mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-16 04:52:39 +02:00
merge master
This commit is contained in:
@@ -26,7 +26,7 @@ body:
|
|||||||
label: Reproduction steps
|
label: Reproduction steps
|
||||||
description: Please provide us with the steps to reproduce the issue if possible. This step makes a big difference if we are going to be able to fix it so be as precise as possible.
|
description: Please provide us with the steps to reproduce the issue if possible. This step makes a big difference if we are going to be able to fix it so be as precise as possible.
|
||||||
placeholder: |
|
placeholder: |
|
||||||
0. Play a Youtube video
|
0. Play a YouTube video
|
||||||
1. Press on Download button
|
1. Press on Download button
|
||||||
2. Select quality 1440p
|
2. Select quality 1440p
|
||||||
3. Grayjay crashes when attempting to download
|
3. Grayjay crashes when attempting to download
|
||||||
@@ -83,7 +83,7 @@ body:
|
|||||||
- "Spotify"
|
- "Spotify"
|
||||||
- "TedTalks"
|
- "TedTalks"
|
||||||
- "Twitch"
|
- "Twitch"
|
||||||
- "Youtube"
|
- "YouTube"
|
||||||
- "Other"
|
- "Other"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -106,3 +106,9 @@
|
|||||||
[submodule "app/src/stable/assets/sources/crunchyroll"]
|
[submodule "app/src/stable/assets/sources/crunchyroll"]
|
||||||
path = app/src/stable/assets/sources/crunchyroll
|
path = app/src/stable/assets/sources/crunchyroll
|
||||||
url = ../plugins/crunchyroll.git
|
url = ../plugins/crunchyroll.git
|
||||||
|
[submodule "app/src/stable/assets/sources/mixcloud"]
|
||||||
|
path = app/src/stable/assets/sources/mixcloud
|
||||||
|
url = ../plugins/mixcloud.git
|
||||||
|
[submodule "app/src/unstable/assets/sources/mixcloud"]
|
||||||
|
path = app/src/unstable/assets/sources/mixcloud
|
||||||
|
url = ../plugins/mixcloud.git
|
||||||
|
|||||||
+11
-5
@@ -39,7 +39,7 @@ protobuf {
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
namespace 'com.futo.platformplayer'
|
namespace 'com.futo.platformplayer'
|
||||||
compileSdk 34
|
compileSdk 36
|
||||||
flavorDimensions "buildType"
|
flavorDimensions "buildType"
|
||||||
productFlavors {
|
productFlavors {
|
||||||
stable {
|
stable {
|
||||||
@@ -97,7 +97,7 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdk 28
|
minSdk 28
|
||||||
targetSdk 34
|
targetSdk 35
|
||||||
versionCode gitVersionCode
|
versionCode gitVersionCode
|
||||||
versionName gitVersionName
|
versionName gitVersionName
|
||||||
|
|
||||||
@@ -119,7 +119,7 @@ android {
|
|||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
signingConfig signingConfigs.debug
|
signingConfig signingConfigs.release
|
||||||
minifyEnabled false
|
minifyEnabled false
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
@@ -154,10 +154,10 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'com.google.dagger:dagger:2.48'
|
//implementation 'com.google.dagger:dagger:2.48'
|
||||||
implementation 'androidx.test:monitor:1.7.2'
|
implementation 'androidx.test:monitor:1.7.2'
|
||||||
implementation 'com.google.android.material:material:1.12.0'
|
implementation 'com.google.android.material:material:1.12.0'
|
||||||
annotationProcessor 'com.google.dagger:dagger-compiler:2.48'
|
//annotationProcessor 'com.google.dagger:dagger-compiler:2.48'
|
||||||
|
|
||||||
//Core
|
//Core
|
||||||
implementation 'androidx.core:core-ktx:1.12.0'
|
implementation 'androidx.core:core-ktx:1.12.0'
|
||||||
@@ -231,4 +231,10 @@ dependencies {
|
|||||||
testImplementation "org.mockito:mockito-core:5.4.0"
|
testImplementation "org.mockito:mockito-core:5.4.0"
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||||
|
|
||||||
|
//Rust casting SDK
|
||||||
|
implementation('org.futo.gitlab.videostreaming.fcast-sdk-jitpack:sender-sdk-minimal:0.3.1') {
|
||||||
|
// Polycentricandroid includes this
|
||||||
|
exclude group: 'net.java.dev.jna'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1022,15 +1022,35 @@
|
|||||||
return x.value
|
return x.value
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
let settingsToUse = __DEV_SETTINGS ?? {};
|
||||||
|
if (true) {
|
||||||
|
for (let setting of this.Plugin?.currentPlugin?.settings) {
|
||||||
|
if (typeof settingsToUse[setting.variable] == "undefined") {
|
||||||
|
switch (setting?.type?.toLowerCase()) {
|
||||||
|
case "boolean":
|
||||||
|
settingsToUse[setting.variable] = setting.default === 'true';
|
||||||
|
break;
|
||||||
|
case "dropdown":
|
||||||
|
let dropDownIndex = parseInt(setting.default);
|
||||||
|
if (dropDownIndex) {
|
||||||
|
settingsToUse[setting.variable] = setting.options[dropDownIndex];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if(name == "enable") {
|
if(name == "enable") {
|
||||||
if(parameterVals.length > 0)
|
if(parameterVals.length > 0)
|
||||||
parameterVals[0] = this.Plugin.currentPlugin;
|
parameterVals[0] = this.Plugin.currentPlugin;
|
||||||
else
|
else
|
||||||
parameterVals.push(this.Plugin.currentPlugin);
|
parameterVals.push(this.Plugin.currentPlugin);
|
||||||
if(parameterVals.length > 1)
|
if(parameterVals.length > 1)
|
||||||
parameterVals[1] = __DEV_SETTINGS;
|
parameterVals[1] = settingsToUse;
|
||||||
else
|
else
|
||||||
parameterVals.push(__DEV_SETTINGS);
|
parameterVals.push(settingsToUse);
|
||||||
}
|
}
|
||||||
|
|
||||||
const func = source[name];
|
const func = source[name];
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ class ScriptException extends Error {
|
|||||||
super(arguments[0]);
|
super(arguments[0]);
|
||||||
this.plugin_type = "ScriptException";
|
this.plugin_type = "ScriptException";
|
||||||
this.message = arguments[0];
|
this.message = arguments[0];
|
||||||
|
this.msg = arguments[0];
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
super(msg);
|
super(msg);
|
||||||
@@ -467,14 +468,20 @@ class AudioUrlWidevineSource extends AudioUrlSource {
|
|||||||
this.getLicenseRequestExecutor = () => {
|
this.getLicenseRequestExecutor = () => {
|
||||||
return {
|
return {
|
||||||
executeRequest: (url, _headers, _method, license_request_data) => {
|
executeRequest: (url, _headers, _method, license_request_data) => {
|
||||||
return http.POST(
|
const response = http.POST(
|
||||||
url,
|
url,
|
||||||
license_request_data,
|
license_request_data,
|
||||||
{ Authorization: `Bearer ${obj.bearerToken}` },
|
{ Authorization: `Bearer ${obj.bearerToken}` },
|
||||||
false,
|
false,
|
||||||
true
|
true
|
||||||
).body
|
);
|
||||||
}
|
|
||||||
|
if (!response.body) {
|
||||||
|
throw new ScriptException("Unable to acquire license key");
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.body;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import com.futo.platformplayer.engine.V8Plugin
|
|||||||
import com.futo.platformplayer.engine.exceptions.ScriptExecutionException
|
import com.futo.platformplayer.engine.exceptions.ScriptExecutionException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Deferred
|
import kotlinx.coroutines.Deferred
|
||||||
@@ -21,7 +22,6 @@ import kotlinx.coroutines.async
|
|||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.selects.SelectClause0
|
import kotlinx.coroutines.selects.SelectClause0
|
||||||
import kotlinx.coroutines.selects.SelectClause1
|
import kotlinx.coroutines.selects.SelectClause1
|
||||||
import java.util.concurrent.CancellationException
|
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.util.concurrent.CountDownLatch
|
||||||
import kotlin.coroutines.AbstractCoroutineContextElement
|
import kotlin.coroutines.AbstractCoroutineContextElement
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
@@ -194,7 +194,6 @@ fun V8ObjectToHashMap(obj: V8ValueObject?): HashMap<String, String> {
|
|||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun <T: V8Value> V8ValuePromise.toV8ValueBlocking(plugin: V8Plugin): T {
|
fun <T: V8Value> V8ValuePromise.toV8ValueBlocking(plugin: V8Plugin): T {
|
||||||
val latch = CountDownLatch(1);
|
val latch = CountDownLatch(1);
|
||||||
var promiseResult: T? = null;
|
var promiseResult: T? = null;
|
||||||
@@ -204,16 +203,19 @@ fun <T: V8Value> V8ValuePromise.toV8ValueBlocking(plugin: V8Plugin): T {
|
|||||||
override fun onFulfilled(p0: V8Value?) {
|
override fun onFulfilled(p0: V8Value?) {
|
||||||
if(p0 is V8ValueError)
|
if(p0 is V8ValueError)
|
||||||
promiseException = ScriptExecutionException(plugin.config, p0.message);
|
promiseException = ScriptExecutionException(plugin.config, p0.message);
|
||||||
else
|
else {
|
||||||
|
if(p0 is V8ValueObject)
|
||||||
|
p0.setWeak();
|
||||||
promiseResult = p0 as T;
|
promiseResult = p0 as T;
|
||||||
|
}
|
||||||
latch.countDown();
|
latch.countDown();
|
||||||
}
|
}
|
||||||
override fun onRejected(p0: V8Value?) {
|
override fun onRejected(p0: V8Value?) {
|
||||||
promiseException = (NotImplementedError("onRejected promise not implemented.."));
|
promiseException = p0?.toException(plugin.config);
|
||||||
latch.countDown();
|
latch.countDown();
|
||||||
}
|
}
|
||||||
override fun onCatch(p0: V8Value?) {
|
override fun onCatch(p0: V8Value?) {
|
||||||
promiseException = (NotImplementedError("onCatch promise not implemented.."));
|
promiseException = p0?.toException(plugin.config);
|
||||||
latch.countDown();
|
latch.countDown();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -223,8 +225,25 @@ fun <T: V8Value> V8ValuePromise.toV8ValueBlocking(plugin: V8Plugin): T {
|
|||||||
promiseException = CancellationException("Cancelled by system");
|
promiseException = CancellationException("Cancelled by system");
|
||||||
latch.countDown();
|
latch.countDown();
|
||||||
}
|
}
|
||||||
plugin.unbusy {
|
//Logger.i("V8", "V8ValueBlocking started (Busy) [" + blockCount + "]" + Thread.currentThread().stackTrace.drop(3)?.firstOrNull()?.toString() + ", " + Thread.currentThread().stackTrace.drop(4)?.firstOrNull()?.toString()+ ", " + Thread.currentThread().stackTrace.drop(5)?.firstOrNull()?.toString());
|
||||||
latch.await();
|
|
||||||
|
|
||||||
|
if(!promise.isPending) {
|
||||||
|
try {
|
||||||
|
Logger.i("V8", "V8Promise resolved synchronously");
|
||||||
|
if(promise.isFulfilled)
|
||||||
|
promiseResult = promise.getResult<T>();
|
||||||
|
else
|
||||||
|
promiseException = promise.getResult<V8Value>().toException(plugin.config);
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
promiseException = ex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
plugin.unbusy {
|
||||||
|
latch.await();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if(promiseException != null)
|
if(promiseException != null)
|
||||||
throw promiseException!!;
|
throw promiseException!!;
|
||||||
@@ -249,12 +268,25 @@ fun <T: V8Value> V8ValuePromise.toV8ValueAsync(plugin: V8Plugin): V8Deferred<T>
|
|||||||
underlyingDef.complete(p0 as T);
|
underlyingDef.complete(p0 as T);
|
||||||
}
|
}
|
||||||
override fun onRejected(p0: V8Value?) {
|
override fun onRejected(p0: V8Value?) {
|
||||||
plugin.resolvePromise(promise);
|
try {
|
||||||
underlyingDef.completeExceptionally(NotImplementedError("onRejected promise not implemented.."));
|
plugin.resolvePromise(promise);
|
||||||
|
val exceptionFound = p0?.toException(plugin.config) ?: NotImplementedError("onRejected promise not implemented..");
|
||||||
|
Logger.i("V8", "Promise rejected, setting exception");
|
||||||
|
underlyingDef.completeExceptionally(CancellationException(exceptionFound.message, exceptionFound));
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e("V8", "Rejection handling failed?" , ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
override fun onCatch(p0: V8Value?) {
|
override fun onCatch(p0: V8Value?) {
|
||||||
plugin.resolvePromise(promise);
|
try {
|
||||||
underlyingDef.completeExceptionally(NotImplementedError("onCatch promise not implemented.."));
|
plugin.resolvePromise(promise);
|
||||||
|
val exceptionFound = p0?.toException(plugin.config) ?: NotImplementedError("onCatch promise not implemented..");
|
||||||
|
underlyingDef.completeExceptionally(CancellationException(exceptionFound.message, exceptionFound));
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e("V8", "Catching handling failed?" , ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -265,6 +297,20 @@ fun <T: V8Value> V8ValuePromise.toV8ValueAsync(plugin: V8Plugin): V8Deferred<T>
|
|||||||
return def;
|
return def;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun V8Value.toException(config: IV8PluginConfig): Throwable {
|
||||||
|
val p0 = this;
|
||||||
|
if(p0 is V8ValueObject) {
|
||||||
|
val pluginType = p0.getOrDefault(config, "plugin_type", "Promise Exception", "")?.let { if(!it.isNullOrBlank()) it + "" else "" }
|
||||||
|
val msg = p0.getOrDefault<String?>(config, "msg", "Promise Exception", null)
|
||||||
|
?: p0.getOrDefault(config, "message", "Promise Exception", "");
|
||||||
|
return Throwable("Promise Failed: " + pluginType + msg);
|
||||||
|
}
|
||||||
|
else if(p0 is V8ValueString)
|
||||||
|
return Throwable("Promise Failed:" + p0.value);
|
||||||
|
else
|
||||||
|
return NotImplementedError("onCatch promise not implemented..");
|
||||||
|
}
|
||||||
|
|
||||||
class V8Deferred<T>(val deferred: Deferred<T>, val estDuration: Int = -1): Deferred<T> by deferred {
|
class V8Deferred<T>(val deferred: Deferred<T>, val estDuration: Int = -1): Deferred<T> by deferred {
|
||||||
|
|
||||||
fun <R> convert(conversion: (result: T)->R): V8Deferred<R>{
|
fun <R> convert(conversion: (result: T)->R): V8Deferred<R>{
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import com.futo.platformplayer.states.StateCache
|
|||||||
import com.futo.platformplayer.states.StateMeta
|
import com.futo.platformplayer.states.StateMeta
|
||||||
import com.futo.platformplayer.states.StatePayment
|
import com.futo.platformplayer.states.StatePayment
|
||||||
import com.futo.platformplayer.states.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
|
import com.futo.platformplayer.states.StateSync
|
||||||
import com.futo.platformplayer.states.StateUpdate
|
import com.futo.platformplayer.states.StateUpdate
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
import com.futo.platformplayer.stores.FragmentedStorageFileJson
|
import com.futo.platformplayer.stores.FragmentedStorageFileJson
|
||||||
@@ -34,6 +35,7 @@ import com.futo.platformplayer.views.fields.DropdownFieldOptionsId
|
|||||||
import com.futo.platformplayer.views.fields.FieldForm
|
import com.futo.platformplayer.views.fields.FieldForm
|
||||||
import com.futo.platformplayer.views.fields.FormField
|
import com.futo.platformplayer.views.fields.FormField
|
||||||
import com.futo.platformplayer.views.fields.FormFieldButton
|
import com.futo.platformplayer.views.fields.FormFieldButton
|
||||||
|
import com.futo.platformplayer.views.fields.FormFieldWarning
|
||||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -201,6 +203,8 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
8 -> "zh";
|
8 -> "zh";
|
||||||
9 -> "ru";
|
9 -> "ru";
|
||||||
10 -> "ar";
|
10 -> "ar";
|
||||||
|
11 -> "it";
|
||||||
|
12 -> "tr";
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -603,6 +607,16 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
else -> 2.0
|
else -> 2.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@AdvancedField
|
||||||
|
@FormField(R.string.shorts_pregenerate, FieldForm.TOGGLE, R.string.shorts_pregenerate_description, 28)
|
||||||
|
var shortsPregenerate: Boolean = false;
|
||||||
|
|
||||||
|
@AdvancedField
|
||||||
|
@FormField(R.string.shorts_fit_video, FieldForm.TOGGLE, R.string.shorts_fit_video_description, 29)
|
||||||
|
@FormFieldWarning(R.string.shorts_fit_video_warning)
|
||||||
|
var shortsFitVideo: Boolean = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.comments, "group", R.string.comments_description, 6)
|
@FormField(R.string.comments, "group", R.string.comments_description, 6)
|
||||||
@@ -705,6 +719,11 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
@Serializable(with = FlexibleBooleanSerializer::class)
|
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||||
var allowLinkLocalIpv4: Boolean = false;
|
var allowLinkLocalIpv4: Boolean = false;
|
||||||
|
|
||||||
|
@AdvancedField
|
||||||
|
@FormField(R.string.experimental_cast, FieldForm.TOGGLE, R.string.experimental_cast_description, 6)
|
||||||
|
@Serializable(with = FlexibleBooleanSerializer::class)
|
||||||
|
var experimentalCasting: Boolean = false
|
||||||
|
|
||||||
/*TODO: Should we have a different casting quality?
|
/*TODO: Should we have a different casting quality?
|
||||||
@FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3)
|
@FormField("Preferred Casting Quality", FieldForm.DROPDOWN, "", 3)
|
||||||
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
@DropdownFieldOptionsId(R.array.preferred_quality_array)
|
||||||
@@ -1087,6 +1106,39 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
|
|
||||||
@FormField(R.string.local_connections, FieldForm.TOGGLE, R.string.local_connections_description, 3)
|
@FormField(R.string.local_connections, FieldForm.TOGGLE, R.string.local_connections_description, 3)
|
||||||
var localConnections: Boolean = true;
|
var localConnections: Boolean = true;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
var syncServerUrl: String? = null;
|
||||||
|
@FormField(R.string.relay_server, FieldForm.READONLYTEXT, -1, 6)
|
||||||
|
val syncServer: String get() = if(syncServerUrl?.isBlank() == true) StateSync.RELAY_SERVER else syncServerUrl ?: StateSync.RELAY_SERVER;
|
||||||
|
|
||||||
|
@AdvancedField
|
||||||
|
@FormField(R.string.configure_sync_server, FieldForm.BUTTON, R.string.configure_sync_server_description, 7)
|
||||||
|
fun configureSyncServer() {
|
||||||
|
SettingsActivity.getActivity()?.let { context ->
|
||||||
|
UIDialogs.showDialog(context, R.drawable.device_sync, false,
|
||||||
|
"Enter the url to your relay server",
|
||||||
|
"Using your own relay server requires a proper setup with portforwarding.\nUse at your own risk.",
|
||||||
|
null,
|
||||||
|
syncServerUrl ?: "",
|
||||||
|
"YourRelayServerDomain.com", 0,
|
||||||
|
UIDialogs.Action("Cancel", {}),
|
||||||
|
UIDialogs.Action("Reset", {
|
||||||
|
syncServerUrl = null;
|
||||||
|
instance.save();
|
||||||
|
context.reloadSettings();
|
||||||
|
UIDialogs.toast("Sync server changes require a restart");
|
||||||
|
}, UIDialogs.ActionStyle.ACCENT),
|
||||||
|
UIDialogs.Action.withInput("Configure", {
|
||||||
|
syncServerUrl = it?.text
|
||||||
|
instance.save();
|
||||||
|
context.reloadSettings();
|
||||||
|
UIDialogs.toast("Sync server changes require a restart");
|
||||||
|
}, UIDialogs.ActionStyle.PRIMARY),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.info, FieldForm.GROUP, -1, 21)
|
@FormField(R.string.info, FieldForm.GROUP, -1, 21)
|
||||||
|
|||||||
@@ -113,8 +113,8 @@ class UIDialogs {
|
|||||||
currentDialog.code,
|
currentDialog.code,
|
||||||
currentDialog.defaultCloseAction,
|
currentDialog.defaultCloseAction,
|
||||||
*currentDialog.actions.map {
|
*currentDialog.actions.map {
|
||||||
return@map Action(it.text, {
|
return@map Action.withInput(it.text, { str ->
|
||||||
it.action();
|
it.invokeAction(str);
|
||||||
multiShowDialog(context, dialogDescriptor.drop(1), finally);
|
multiShowDialog(context, dialogDescriptor.drop(1), finally);
|
||||||
}, it.style);
|
}, it.style);
|
||||||
}.toTypedArray());
|
}.toTypedArray());
|
||||||
@@ -203,7 +203,9 @@ class UIDialogs {
|
|||||||
fun showDialog(context: Context, icon: Int, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action): AlertDialog {
|
fun showDialog(context: Context, icon: Int, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action): AlertDialog {
|
||||||
return showDialog(context, icon, false, text, textDetails, code, defaultCloseAction, *actions);
|
return showDialog(context, icon, false, text, textDetails, code, defaultCloseAction, *actions);
|
||||||
}
|
}
|
||||||
fun showDialog(context: Context, icon: Int, animated: Boolean, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action): AlertDialog {
|
fun showDialog(context: Context, icon: Int, animated: Boolean, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action): AlertDialog
|
||||||
|
= showDialog(context, icon, animated, text, textDetails, code, null, null, defaultCloseAction, *actions);
|
||||||
|
fun showDialog(context: Context, icon: Int, animated: Boolean, text: String, textDetails: String? = null, code: String? = null, input: String?, placeholder: String?, defaultCloseAction: Int, vararg actions: Action): AlertDialog {
|
||||||
val builder = AlertDialog.Builder(context);
|
val builder = AlertDialog.Builder(context);
|
||||||
val view = LayoutInflater.from(context).inflate(R.layout.dialog_multi_button, null);
|
val view = LayoutInflater.from(context).inflate(R.layout.dialog_multi_button, null);
|
||||||
builder.setView(view);
|
builder.setView(view);
|
||||||
@@ -226,6 +228,16 @@ class UIDialogs {
|
|||||||
this.text = textDetails;
|
this.text = textDetails;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
var inputView = view.findViewById<TextView>(R.id.dialog_text_input);
|
||||||
|
inputView.apply {
|
||||||
|
if (input == null && placeholder == null) this.visibility = View.GONE;
|
||||||
|
else {
|
||||||
|
this.text = input ?: "";
|
||||||
|
this.hint = placeholder ?: "";
|
||||||
|
this.visibility = View.VISIBLE;
|
||||||
|
this.textAlignment = View.TEXT_ALIGNMENT_VIEW_START
|
||||||
|
}
|
||||||
|
};
|
||||||
view.findViewById<TextView>(R.id.dialog_text_code).apply {
|
view.findViewById<TextView>(R.id.dialog_text_code).apply {
|
||||||
if (code == null) this.visibility = View.GONE;
|
if (code == null) this.visibility = View.GONE;
|
||||||
else {
|
else {
|
||||||
@@ -250,7 +262,7 @@ class UIDialogs {
|
|||||||
buttonView.textSize = 14f;
|
buttonView.textSize = 14f;
|
||||||
buttonView.typeface = resources.getFont(R.font.inter_regular);
|
buttonView.typeface = resources.getFont(R.font.inter_regular);
|
||||||
buttonView.text = act.text;
|
buttonView.text = act.text;
|
||||||
buttonView.setOnClickListener { act.action(); dialog.dismiss(); };
|
buttonView.setOnClickListener { act.invokeAction(DialogResult(inputView?.text?.toString())); dialog.dismiss(); };
|
||||||
when(act.style) {
|
when(act.style) {
|
||||||
ActionStyle.PRIMARY -> buttonView.setBackgroundResource(R.drawable.background_button_primary);
|
ActionStyle.PRIMARY -> buttonView.setBackgroundResource(R.drawable.background_button_primary);
|
||||||
ActionStyle.ACCENT -> buttonView.setBackgroundResource(R.drawable.background_button_accent);
|
ActionStyle.ACCENT -> buttonView.setBackgroundResource(R.drawable.background_button_accent);
|
||||||
@@ -275,7 +287,7 @@ class UIDialogs {
|
|||||||
};
|
};
|
||||||
dialog.setOnCancelListener {
|
dialog.setOnCancelListener {
|
||||||
if(defaultCloseAction >= 0 && defaultCloseAction < actions.size)
|
if(defaultCloseAction >= 0 && defaultCloseAction < actions.size)
|
||||||
actions[defaultCloseAction].action();
|
actions[defaultCloseAction].invokeAction(DialogResult(inputView?.text?.toString()));
|
||||||
}
|
}
|
||||||
dialog.setOnDismissListener {
|
dialog.setOnDismissListener {
|
||||||
registerDialogClosed(dialog);
|
registerDialogClosed(dialog);
|
||||||
@@ -535,17 +547,36 @@ class UIDialogs {
|
|||||||
}
|
}
|
||||||
class Action {
|
class Action {
|
||||||
val text: String;
|
val text: String;
|
||||||
val action: ()->Unit;
|
val action: ((DialogResult?)->Unit);
|
||||||
val style: ActionStyle;
|
val style: ActionStyle;
|
||||||
var center: Boolean;
|
var center: Boolean;
|
||||||
|
|
||||||
constructor(text: String, action: ()->Unit, style: ActionStyle = ActionStyle.NONE, center: Boolean = false) {
|
constructor(text: String, action: ()->Unit, style: ActionStyle = ActionStyle.NONE, center: Boolean = false) {
|
||||||
|
this.text = text;
|
||||||
|
this.action = { action() };
|
||||||
|
this.style = style;
|
||||||
|
this.center = center;
|
||||||
|
}
|
||||||
|
protected constructor(text: String, action: (DialogResult?)->Unit, style: ActionStyle = ActionStyle.NONE, center: Boolean = false) {
|
||||||
this.text = text;
|
this.text = text;
|
||||||
this.action = action;
|
this.action = action;
|
||||||
this.style = style;
|
this.style = style;
|
||||||
this.center = center;
|
this.center = center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun invokeAction(input: DialogResult? = null) {
|
||||||
|
this.action(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun withInput(text: String, action: (DialogResult?)->Unit, style: ActionStyle = ActionStyle.NONE, center: Boolean = false): Action {
|
||||||
|
return Action(text, action, style, center);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
class DialogResult(
|
||||||
|
val text: String?
|
||||||
|
);
|
||||||
enum class ActionStyle {
|
enum class ActionStyle {
|
||||||
NONE,
|
NONE,
|
||||||
PRIMARY,
|
PRIMARY,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import com.futo.platformplayer.api.media.platforms.js.SourceAuth
|
|||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginAuthConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginAuthConfig
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.matchesDomain
|
||||||
import com.futo.platformplayer.others.LoginWebViewClient
|
import com.futo.platformplayer.others.LoginWebViewClient
|
||||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
@@ -74,9 +75,26 @@ class LoginActivity : AppCompatActivity() {
|
|||||||
finish();
|
finish();
|
||||||
};
|
};
|
||||||
var isFirstLoad = true;
|
var isFirstLoad = true;
|
||||||
|
val loginWarnings = authConfig.loginWarnings?.toMutableList() ?: mutableListOf<SourcePluginAuthConfig.Warning>();
|
||||||
|
val uiMods = authConfig.uiMods?.toMutableList() ?: mutableListOf<SourcePluginAuthConfig.UIMod>();
|
||||||
|
var currentScale = 100;
|
||||||
|
var currentDesktop = false;
|
||||||
webViewClient.onPageLoaded.subscribe { view, url ->
|
webViewClient.onPageLoaded.subscribe { view, url ->
|
||||||
_textUrl.setText(url ?: "");
|
_textUrl.setText(url ?: "");
|
||||||
|
|
||||||
|
if(loginWarnings.size > 0 && url != null) {
|
||||||
|
synchronized(loginWarnings) {
|
||||||
|
val warning = loginWarnings.find { url.matches(it.getRegex()) };
|
||||||
|
if(warning != null) {
|
||||||
|
if(warning.once == true)
|
||||||
|
loginWarnings.remove(warning);
|
||||||
|
UIDialogs.showDialog(this@LoginActivity, R.drawable.ic_warning_yellow, warning.text ?: "", warning.details ?: "", null, 0,
|
||||||
|
UIDialogs.Action("Understood", {
|
||||||
|
}, UIDialogs.ActionStyle.PRIMARY));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if(!isFirstLoad)
|
if(!isFirstLoad)
|
||||||
return@subscribe;
|
return@subscribe;
|
||||||
isFirstLoad = false;
|
isFirstLoad = false;
|
||||||
@@ -86,6 +104,35 @@ class LoginActivity : AppCompatActivity() {
|
|||||||
//TODO: Find most reliable way to wait for page js to finish
|
//TODO: Find most reliable way to wait for page js to finish
|
||||||
view?.evaluateJavascript("setTimeout(()=> document.querySelector(\"${authConfig.loginButton}\")?.click(), 1000)", {});
|
view?.evaluateJavascript("setTimeout(()=> document.querySelector(\"${authConfig.loginButton}\")?.click(), 1000)", {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
var specifiedScale = false;
|
||||||
|
var specifiedDesktop = false;
|
||||||
|
if(uiMods.size > 0 && url != null) {
|
||||||
|
synchronized(uiMods) {
|
||||||
|
val uimod = uiMods.find { url.matches(it.getRegex()) };
|
||||||
|
if(uimod != null) {
|
||||||
|
if(uimod.scale != null) {
|
||||||
|
currentScale =(uimod.scale * 100).toInt();
|
||||||
|
_webView.setInitialScale(currentScale);
|
||||||
|
specifiedScale = true;
|
||||||
|
}
|
||||||
|
if(uimod.desktop != null && uimod.desktop) {
|
||||||
|
_webView.settings.useWideViewPort = true;
|
||||||
|
specifiedDesktop = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(!specifiedScale && currentScale != 100) {
|
||||||
|
currentScale = (100).toInt();
|
||||||
|
_webView.setInitialScale(currentScale);
|
||||||
|
}
|
||||||
|
if(!specifiedDesktop && currentDesktop) {
|
||||||
|
_webView.settings.useWideViewPort = false;
|
||||||
|
currentDesktop = false;
|
||||||
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
_webView.settings.domStorageEnabled = true;
|
_webView.settings.domStorageEnabled = true;
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import com.futo.platformplayer.R
|
|||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
|
import com.futo.platformplayer.api.media.models.video.LocalVideoDetails
|
||||||
import com.futo.platformplayer.casting.StateCasting
|
import com.futo.platformplayer.casting.StateCasting
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.dp
|
import com.futo.platformplayer.dp
|
||||||
@@ -775,7 +776,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
if (targetData != null) {
|
if (targetData != null) {
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
try {
|
try {
|
||||||
handleUrlAll(targetData)
|
handleUrlAll(targetData, intent)
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Unhandled exception in handleUrlAll", e)
|
Logger.e(TAG, "Unhandled exception in handleUrlAll", e)
|
||||||
}
|
}
|
||||||
@@ -786,8 +787,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun handleUrlAll(url: String) {
|
suspend fun handleUrlAll(url: String, openIntent: Intent? = null) {
|
||||||
val uri = Uri.parse(url)
|
val uri = Uri.parse(url)
|
||||||
|
val intent = openIntent ?: this.intent;
|
||||||
when (uri.scheme) {
|
when (uri.scheme) {
|
||||||
"grayjay" -> {
|
"grayjay" -> {
|
||||||
if (url.startsWith("grayjay://license/")) {
|
if (url.startsWith("grayjay://license/")) {
|
||||||
@@ -814,11 +816,11 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
"content" -> {
|
"content" -> {
|
||||||
if (!handleContent(url, intent.type)) {
|
if (!handleContent(url, intent?.type)) {
|
||||||
UIDialogs.showSingleButtonDialog(
|
UIDialogs.showSingleButtonDialog(
|
||||||
this,
|
this,
|
||||||
R.drawable.ic_play,
|
R.drawable.ic_play,
|
||||||
getString(R.string.unknown_content_format) + " [${url}]\n[${intent.type}]",
|
getString(R.string.unknown_content_format) + " [${url}]\n[${intent?.type}]",
|
||||||
"Ok",
|
"Ok",
|
||||||
{ });
|
{ });
|
||||||
}
|
}
|
||||||
@@ -939,6 +941,12 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
} else if (file.lowercase().endsWith(".txt") || mime == "text/plain") {
|
} else if (file.lowercase().endsWith(".txt") || mime == "text/plain") {
|
||||||
return handleUnknownText(String(data));
|
return handleUnknownText(String(data));
|
||||||
}
|
}
|
||||||
|
else if (mime?.let { it.startsWith("video/") || it.startsWith("audio/") } ?: false) {
|
||||||
|
val mediaItem = LocalVideoDetails.fromContent(file, mime);
|
||||||
|
navigateWhenReady(_fragVideoDetail, mediaItem);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1061,7 +1069,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
Logger.i(TAG, "handleFCast");
|
Logger.i(TAG, "handleFCast");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
StateCasting.instance.handleUrl(this, url)
|
StateCasting.instance.handleUrl(url)
|
||||||
return true;
|
return true;
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Log.e(TAG, "Failed to parse FCast URL '${url}'.", e)
|
Log.e(TAG, "Failed to parse FCast URL '${url}'.", e)
|
||||||
|
|||||||
@@ -123,8 +123,6 @@ class LiveChatManager {
|
|||||||
val requestPosition = _position;
|
val requestPosition = _position;
|
||||||
_pager.nextPage(requestPosition.toInt());
|
_pager.nextPage(requestPosition.toInt());
|
||||||
var replayResults = _pager.getResults().filter { it.time > requestPosition || it is LiveEventEmojis };
|
var replayResults = _pager.getResults().filter { it.time > requestPosition || it is LiveEventEmojis };
|
||||||
//TODO: Remove this once dripfeed is done properly
|
|
||||||
replayResults = replayResults.filter{ it.time < requestPosition + 1500 || it is LiveEventEmojis };
|
|
||||||
if(replayResults.size > 0) {
|
if(replayResults.size > 0) {
|
||||||
_eventsPosition = replayResults.maxOf { it.time };
|
_eventsPosition = replayResults.maxOf { it.time };
|
||||||
Logger.i(TAG, "VOD Events last event: " + _eventsPosition);
|
Logger.i(TAG, "VOD Events last event: " + _eventsPosition);
|
||||||
|
|||||||
+17
-3
@@ -2,10 +2,24 @@ package com.futo.platformplayer.api.media.models.streams
|
|||||||
|
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||||
|
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalAudioContentSource
|
||||||
import com.futo.platformplayer.downloads.VideoLocal
|
import com.futo.platformplayer.downloads.VideoLocal
|
||||||
|
|
||||||
|
|
||||||
class LocalVideoUnMuxedSourceDescriptor(private val video: VideoLocal) : VideoUnMuxedSourceDescriptor() {
|
class LocalVideoUnMuxedSourceDescriptor : VideoUnMuxedSourceDescriptor {
|
||||||
override val videoSources: Array<IVideoSource> get() = video.videoSource.toTypedArray();
|
override val videoSources: Array<IVideoSource>;
|
||||||
override val audioSources: Array<IAudioSource> get() = video.audioSource.toTypedArray();
|
override val audioSources: Array<IAudioSource>;
|
||||||
|
|
||||||
|
constructor(video: VideoLocal) {
|
||||||
|
videoSources = video.videoSource.toTypedArray();
|
||||||
|
audioSources = video.audioSource.toTypedArray();
|
||||||
|
}
|
||||||
|
constructor(audio: LocalAudioContentSource) {
|
||||||
|
videoSources = arrayOf()
|
||||||
|
audioSources = arrayOf(audio);
|
||||||
|
}
|
||||||
|
constructor(videoSources: Array<IVideoSource>, audioSources: Array<IAudioSource>) {
|
||||||
|
this.videoSources = videoSources;
|
||||||
|
this.audioSources = audioSources;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+2
-1
@@ -14,7 +14,8 @@ class AudioUrlSource(
|
|||||||
override val language: String = Language.UNKNOWN,
|
override val language: String = Language.UNKNOWN,
|
||||||
override val duration: Long? = null,
|
override val duration: Long? = null,
|
||||||
override var priority: Boolean = false,
|
override var priority: Boolean = false,
|
||||||
override var original: Boolean = false
|
override var original: Boolean = false,
|
||||||
|
var isLocal: Boolean = false
|
||||||
) : IAudioUrlSource, IStreamMetaDataSource{
|
) : IAudioUrlSource, IStreamMetaDataSource{
|
||||||
override var streamMetaData: StreamMetaData? = null;
|
override var streamMetaData: StreamMetaData? = null;
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -14,7 +14,8 @@ open class VideoUrlSource(
|
|||||||
override val codec : String = "",
|
override val codec : String = "",
|
||||||
override val bitrate : Int? = 0,
|
override val bitrate : Int? = 0,
|
||||||
|
|
||||||
override var priority: Boolean = false
|
override var priority: Boolean = false,
|
||||||
|
var isLocal: Boolean = false
|
||||||
) : IVideoUrlSource, IStreamMetaDataSource {
|
) : IVideoUrlSource, IStreamMetaDataSource {
|
||||||
override var streamMetaData: StreamMetaData? = null;
|
override var streamMetaData: StreamMetaData? = null;
|
||||||
|
|
||||||
|
|||||||
+122
@@ -0,0 +1,122 @@
|
|||||||
|
package com.futo.platformplayer.api.media.models.video
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.provider.OpenableColumns
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import com.futo.platformplayer.api.media.IPlatformClient
|
||||||
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
|
import com.futo.platformplayer.api.media.Serializer
|
||||||
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
|
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||||
|
import com.futo.platformplayer.api.media.models.comments.IPlatformComment
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
|
import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker
|
||||||
|
import com.futo.platformplayer.api.media.models.ratings.IRating
|
||||||
|
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.LocalVideoUnMuxedSourceDescriptor
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.AudioUrlSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.LocalAudioSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.LocalVideoSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.SubtitleRawSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.VideoUrlSource
|
||||||
|
import com.futo.platformplayer.api.media.platforms.local.models.LocalVideoMuxedSourceDescriptor
|
||||||
|
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalAudioContentSource
|
||||||
|
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalVideoContentSource
|
||||||
|
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalVideoFileSource
|
||||||
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
|
import com.futo.platformplayer.others.Language
|
||||||
|
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
|
@kotlinx.serialization.Serializable
|
||||||
|
open class LocalVideoDetails(
|
||||||
|
override val id: PlatformID,
|
||||||
|
override val name: String,
|
||||||
|
override val thumbnails: Thumbnails,
|
||||||
|
override val author: PlatformAuthorLink,
|
||||||
|
override val url: String,
|
||||||
|
override val duration: Long,
|
||||||
|
|
||||||
|
val mimeType: String? = null,
|
||||||
|
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
||||||
|
override val datetime: OffsetDateTime?
|
||||||
|
) : IPlatformVideo, IPlatformVideoDetails {
|
||||||
|
final override val contentType: ContentType get() = ContentType.MEDIA;
|
||||||
|
|
||||||
|
override var playbackTime: Long = -1;
|
||||||
|
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
||||||
|
override var playbackDate: OffsetDateTime? = null;
|
||||||
|
|
||||||
|
override val isLive: Boolean get() = false;
|
||||||
|
|
||||||
|
override val dash: IDashManifestSource? get() = null;
|
||||||
|
override val hls: IHLSManifestSource? get() = null;
|
||||||
|
override val live: IVideoSource? get() = null;
|
||||||
|
|
||||||
|
|
||||||
|
override val shareUrl: String = ""
|
||||||
|
override val viewCount: Long = -1
|
||||||
|
override val rating: IRating = RatingLikes(0)
|
||||||
|
override val description: String = "";
|
||||||
|
override val video: IVideoSourceDescriptor = (if(mimeType?.startsWith("audio/") ?: false)
|
||||||
|
(LocalVideoUnMuxedSourceDescriptor(
|
||||||
|
arrayOf(),
|
||||||
|
arrayOf(LocalAudioContentSource(url, mimeType ?: "", name))
|
||||||
|
))
|
||||||
|
else (LocalVideoMuxedSourceDescriptor(
|
||||||
|
LocalVideoContentSource(url, mimeType ?: "", name)
|
||||||
|
))
|
||||||
|
);
|
||||||
|
override val preview: ISerializedVideoSourceDescriptor? = null;
|
||||||
|
|
||||||
|
override val subtitles: List<SubtitleRawSource> = listOf()
|
||||||
|
override val isShort: Boolean = false
|
||||||
|
|
||||||
|
fun toJson() : String {
|
||||||
|
return Json.encodeToString(this);
|
||||||
|
}
|
||||||
|
fun fromJson(str : String) : SerializedPlatformVideoDetails {
|
||||||
|
return Serializer.json.decodeFromString<SerializedPlatformVideoDetails>(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? = null;
|
||||||
|
override fun getPlaybackTracker(): IPlaybackTracker? = null;
|
||||||
|
override fun getContentRecommendations(client: IPlatformClient): IPager<IPlatformContent>? = null;
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromFile(name: String, filePath: String, mimeType: String? = null) : LocalVideoDetails {
|
||||||
|
if(filePath.startsWith("content://"))
|
||||||
|
return fromContent(filePath, mimeType);
|
||||||
|
|
||||||
|
return LocalVideoDetails(PlatformID("FILE", filePath, null, 0, -1),
|
||||||
|
name, Thumbnails(), PlatformAuthorLink.UNKNOWN, filePath, -1, mimeType, null);
|
||||||
|
}
|
||||||
|
fun fromContent(contentUrl: String, mimeType: String? = null) : LocalVideoDetails {
|
||||||
|
var nameToUse = getFileNameFromContentUrl(contentUrl) ?: "File";
|
||||||
|
|
||||||
|
return LocalVideoDetails(PlatformID("FILE", contentUrl, null, 0, -1),
|
||||||
|
nameToUse, Thumbnails(), PlatformAuthorLink.UNKNOWN, contentUrl, -1, mimeType, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("Range")
|
||||||
|
private fun getFileNameFromContentUrl(url: String): String? {
|
||||||
|
val cursor = StateApp.instance.context.contentResolver.query(url.toUri(), null, null, null, null);
|
||||||
|
cursor?.moveToFirst();
|
||||||
|
val fileName = cursor?.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
|
||||||
|
cursor?.close();
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
@@ -3,6 +3,7 @@ package com.futo.platformplayer.api.media.models.video
|
|||||||
import com.futo.platformplayer.api.media.PlatformID
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
import com.futo.platformplayer.api.media.Serializer
|
import com.futo.platformplayer.api.media.Serializer
|
||||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
|
import com.futo.platformplayer.api.media.models.Thumbnail
|
||||||
import com.futo.platformplayer.api.media.models.Thumbnails
|
import com.futo.platformplayer.api.media.models.Thumbnails
|
||||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||||
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||||
|
|||||||
+46
-3
@@ -1,6 +1,10 @@
|
|||||||
package com.futo.platformplayer.api.media.platforms.js
|
package com.futo.platformplayer.api.media.platforms.js
|
||||||
|
|
||||||
@kotlinx.serialization.Serializable
|
import kotlinx.serialization.Contextual
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import java.util.Dictionary
|
||||||
|
|
||||||
|
@Serializable
|
||||||
class SourcePluginAuthConfig(
|
class SourcePluginAuthConfig(
|
||||||
val loginUrl: String,
|
val loginUrl: String,
|
||||||
val completionUrl: String? = null,
|
val completionUrl: String? = null,
|
||||||
@@ -11,5 +15,44 @@ class SourcePluginAuthConfig(
|
|||||||
val userAgent: String? = null,
|
val userAgent: String? = null,
|
||||||
val loginButton: String? = null,
|
val loginButton: String? = null,
|
||||||
val domainHeadersToFind: Map<String, List<String>>? = null,
|
val domainHeadersToFind: Map<String, List<String>>? = null,
|
||||||
val loginWarning: String? = null
|
val loginWarning: String? = null,
|
||||||
) { }
|
val loginWarnings: List<Warning>? = null,
|
||||||
|
val uiMods: List<UIMod>? = null
|
||||||
|
) {
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class Warning(
|
||||||
|
val url: String,
|
||||||
|
val text: String?,
|
||||||
|
val details: String? = null,
|
||||||
|
val once: Boolean? = true
|
||||||
|
) {
|
||||||
|
@Contextual
|
||||||
|
private var _regex: Regex? = null;
|
||||||
|
|
||||||
|
fun getRegex(): Regex {
|
||||||
|
return _regex ?: url.let {
|
||||||
|
val reg = Regex(it);
|
||||||
|
_regex = reg;
|
||||||
|
return reg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Serializable
|
||||||
|
class UIMod(
|
||||||
|
val url: String,
|
||||||
|
val scale: Float?,
|
||||||
|
val desktop: Boolean?
|
||||||
|
) {
|
||||||
|
@Contextual
|
||||||
|
private var _regex: Regex? = null;
|
||||||
|
|
||||||
|
fun getRegex(): Regex {
|
||||||
|
return _regex ?: url.let {
|
||||||
|
val reg = Regex(it);
|
||||||
|
_regex = reg;
|
||||||
|
return reg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-15
@@ -116,25 +116,12 @@ class SourcePluginDescriptor {
|
|||||||
var enableShorts: Boolean? = null;
|
var enableShorts: Boolean? = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.sync, "group", R.string.sync_desc, 3)
|
@FormField(R.string.sync, "group", R.string.sync_desc, 3,"sync")
|
||||||
var sync = Sync();
|
var sync = Sync();
|
||||||
@Serializable
|
@Serializable
|
||||||
class Sync {
|
class Sync {
|
||||||
@FormField(R.string.sync_history, FieldForm.TOGGLE, R.string.sync_history_desc, 1)
|
@FormField(R.string.sync_history, FieldForm.TOGGLE, R.string.sync_history_desc, 1,"syncHistory")
|
||||||
var enableHistorySync: Boolean? = null;
|
var enableHistorySync: Boolean? = null;
|
||||||
|
|
||||||
@FormField(R.string.sync_history, FieldForm.BUTTON, R.string.sync_history_desc, 2)
|
|
||||||
@FormFieldButton()
|
|
||||||
fun syncHistoryNow() {
|
|
||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
|
||||||
val clients = StatePlatform.instance.getEnabledClients();
|
|
||||||
for (client in clients) {
|
|
||||||
if (client is JSClient) {//) && client.descriptor.appSettings.sync.enableHistorySync == true) {
|
|
||||||
StateHistory.instance.syncRemoteHistory(client);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.ratelimit, "group", R.string.ratelimit_description, 4)
|
@FormField(R.string.ratelimit, "group", R.string.ratelimit_description, 4)
|
||||||
|
|||||||
+13
@@ -17,6 +17,7 @@ import com.futo.platformplayer.getOrNull
|
|||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
import com.futo.platformplayer.invokeV8
|
import com.futo.platformplayer.invokeV8
|
||||||
import com.futo.platformplayer.invokeV8Async
|
import com.futo.platformplayer.invokeV8Async
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.others.Language
|
import com.futo.platformplayer.others.Language
|
||||||
import com.futo.platformplayer.states.StateDeveloper
|
import com.futo.platformplayer.states.StateDeveloper
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
@@ -57,12 +58,24 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS
|
|||||||
hasGenerate = _obj.has("generate");
|
hasGenerate = _obj.has("generate");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var _pregenerate: V8Deferred<String?>? = null;
|
||||||
|
fun pregenerateAsync(scope: CoroutineScope): V8Deferred<String?>? {
|
||||||
|
_pregenerate = generateAsync(scope);
|
||||||
|
return _pregenerate;
|
||||||
|
}
|
||||||
|
|
||||||
override fun generateAsync(scope: CoroutineScope): V8Deferred<String?> {
|
override fun generateAsync(scope: CoroutineScope): V8Deferred<String?> {
|
||||||
if(!hasGenerate)
|
if(!hasGenerate)
|
||||||
return V8Deferred(CompletableDeferred(manifest));
|
return V8Deferred(CompletableDeferred(manifest));
|
||||||
if(_obj.isClosed)
|
if(_obj.isClosed)
|
||||||
throw IllegalStateException("Source object already closed");
|
throw IllegalStateException("Source object already closed");
|
||||||
|
|
||||||
|
val pregenerated = _pregenerate;
|
||||||
|
if(pregenerated != null) {
|
||||||
|
Logger.w("JSDashManifestRawAudioSource", "Returning pre-generated audio");
|
||||||
|
return pregenerated;
|
||||||
|
}
|
||||||
|
|
||||||
val plugin = _plugin.getUnderlyingPlugin();
|
val plugin = _plugin.getUnderlyingPlugin();
|
||||||
|
|
||||||
var result: V8Deferred<V8ValueString>? = null;
|
var result: V8Deferred<V8ValueString>? = null;
|
||||||
|
|||||||
+12
@@ -18,6 +18,7 @@ import com.futo.platformplayer.getOrNull
|
|||||||
import com.futo.platformplayer.getOrThrow
|
import com.futo.platformplayer.getOrThrow
|
||||||
import com.futo.platformplayer.invokeV8
|
import com.futo.platformplayer.invokeV8
|
||||||
import com.futo.platformplayer.invokeV8Async
|
import com.futo.platformplayer.invokeV8Async
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateDeveloper
|
import com.futo.platformplayer.states.StateDeveloper
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -65,11 +66,22 @@ open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSo
|
|||||||
hasGenerate = _obj.has("generate");
|
hasGenerate = _obj.has("generate");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var _pregenerate: V8Deferred<String?>? = null;
|
||||||
|
fun pregenerateAsync(scope: CoroutineScope): V8Deferred<String?>? {
|
||||||
|
_pregenerate = generateAsync(scope);
|
||||||
|
return _pregenerate;
|
||||||
|
}
|
||||||
|
|
||||||
override fun generateAsync(scope: CoroutineScope): V8Deferred<String?> {
|
override fun generateAsync(scope: CoroutineScope): V8Deferred<String?> {
|
||||||
if(!hasGenerate)
|
if(!hasGenerate)
|
||||||
return V8Deferred(CompletableDeferred(manifest));
|
return V8Deferred(CompletableDeferred(manifest));
|
||||||
if(_obj.isClosed)
|
if(_obj.isClosed)
|
||||||
throw IllegalStateException("Source object already closed");
|
throw IllegalStateException("Source object already closed");
|
||||||
|
val pregenerated = _pregenerate;
|
||||||
|
if(pregenerated != null) {
|
||||||
|
Logger.w("JSDashManifestRawSource", "Returning pre-generated video");
|
||||||
|
return pregenerated;
|
||||||
|
}
|
||||||
|
|
||||||
val plugin = _plugin.getUnderlyingPlugin();
|
val plugin = _plugin.getUnderlyingPlugin();
|
||||||
|
|
||||||
|
|||||||
+14
-4
@@ -1,13 +1,23 @@
|
|||||||
package com.futo.platformplayer.api.media.platforms.local.models
|
package com.futo.platformplayer.api.media.platforms.local.models
|
||||||
|
|
||||||
import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor
|
import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.LocalVideoSource
|
import com.futo.platformplayer.api.media.models.streams.sources.LocalVideoSource
|
||||||
|
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalVideoContentSource
|
||||||
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalVideoFileSource
|
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalVideoFileSource
|
||||||
import com.futo.platformplayer.downloads.VideoLocal
|
import com.futo.platformplayer.downloads.VideoLocal
|
||||||
|
|
||||||
class LocalVideoMuxedSourceDescriptor(
|
class LocalVideoMuxedSourceDescriptor: VideoMuxedSourceDescriptor {
|
||||||
private val video: LocalVideoFileSource
|
override val videoSources: Array<IVideoSource>;
|
||||||
) : VideoMuxedSourceDescriptor() {
|
|
||||||
override val videoSources: Array<IVideoSource> get() = arrayOf(video);
|
constructor(video: LocalVideoFileSource) {
|
||||||
|
videoSources = arrayOf(video);
|
||||||
|
}
|
||||||
|
constructor(video: LocalVideoContentSource) {
|
||||||
|
videoSources = arrayOf(video);
|
||||||
|
}
|
||||||
|
constructor(videoSources: Array<IVideoSource>) {
|
||||||
|
this.videoSources = videoSources;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
package com.futo.platformplayer.api.media.platforms.local.models.sources
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.provider.MediaStore.Video
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.VideoUrlSource
|
||||||
|
import com.futo.platformplayer.helpers.VideoHelper
|
||||||
|
import com.futo.platformplayer.others.Language
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class LocalAudioContentSource : IAudioSource {
|
||||||
|
|
||||||
|
override val name: String;
|
||||||
|
override val container: String;
|
||||||
|
override val codec: String = ""
|
||||||
|
override val bitrate: Int = 0
|
||||||
|
override val duration: Long;
|
||||||
|
override val priority: Boolean = false;
|
||||||
|
override val language: String = Language.UNKNOWN
|
||||||
|
override val original: Boolean = false;
|
||||||
|
|
||||||
|
var contentUrl: String;
|
||||||
|
|
||||||
|
constructor(contentUrl: String, mime: String, name: String? = null) {
|
||||||
|
this.name = name ?: "File";
|
||||||
|
container = mime;
|
||||||
|
duration = 0;
|
||||||
|
|
||||||
|
this.contentUrl = contentUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
package com.futo.platformplayer.api.media.platforms.local.models.sources
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.provider.MediaStore.Video
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.VideoUrlSource
|
||||||
|
import com.futo.platformplayer.helpers.VideoHelper
|
||||||
|
import com.futo.platformplayer.others.Language
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class LocalAudioFileSource: IAudioSource {
|
||||||
|
|
||||||
|
|
||||||
|
override val name: String;
|
||||||
|
override val container: String;
|
||||||
|
override val codec: String = ""
|
||||||
|
override val bitrate: Int = 0
|
||||||
|
override val duration: Long;
|
||||||
|
override val priority: Boolean = false;
|
||||||
|
override val language: String = Language.UNKNOWN;
|
||||||
|
override val original: Boolean = false;
|
||||||
|
|
||||||
|
var file: File;
|
||||||
|
|
||||||
|
constructor(file: File) {
|
||||||
|
this.file = file;
|
||||||
|
name = file.name;
|
||||||
|
container = VideoHelper.videoExtensionToMimetype(file.extension) ?: "";
|
||||||
|
duration = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
package com.futo.platformplayer.api.media.platforms.local.models.sources
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.provider.MediaStore.Video
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||||
|
import com.futo.platformplayer.api.media.models.streams.sources.VideoUrlSource
|
||||||
|
import com.futo.platformplayer.helpers.VideoHelper
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class LocalVideoContentSource: IVideoSource {
|
||||||
|
|
||||||
|
|
||||||
|
override val name: String;
|
||||||
|
override val width: Int;
|
||||||
|
override val height: Int;
|
||||||
|
override val container: String;
|
||||||
|
override val codec: String = ""
|
||||||
|
override val bitrate: Int = 0
|
||||||
|
override val duration: Long;
|
||||||
|
override val priority: Boolean = false;
|
||||||
|
|
||||||
|
var contentUrl: String;
|
||||||
|
|
||||||
|
constructor(contentUrl: String, mime: String, name: String? = null) {
|
||||||
|
this.name = name ?: "File";
|
||||||
|
width = 0;
|
||||||
|
height = 0;
|
||||||
|
container = mime;
|
||||||
|
duration = 0;
|
||||||
|
this.contentUrl = contentUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
@@ -20,7 +20,10 @@ class LocalVideoFileSource: IVideoSource {
|
|||||||
override val duration: Long;
|
override val duration: Long;
|
||||||
override val priority: Boolean = false;
|
override val priority: Boolean = false;
|
||||||
|
|
||||||
|
var file: File;
|
||||||
|
|
||||||
constructor(file: File) {
|
constructor(file: File) {
|
||||||
|
this.file = file;
|
||||||
name = file.name;
|
name = file.name;
|
||||||
width = 0;
|
width = 0;
|
||||||
height = 0;
|
height = 0;
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@ class MultiDistributionContentPager<T : IPlatformContent> : MultiPager<T> {
|
|||||||
private val dist : HashMap<IPager<T>, Float>;
|
private val dist : HashMap<IPager<T>, Float>;
|
||||||
private val distConsumed : HashMap<IPager<T>, Float>;
|
private val distConsumed : HashMap<IPager<T>, Float>;
|
||||||
|
|
||||||
constructor(pagers : Map<IPager<T>, Float>) : super(pagers.keys.toMutableList()) {
|
constructor(pagers : Map<IPager<T>, Float>, pageSize: Int = 9) : super(pagers.keys.toMutableList(), false, pageSize) {
|
||||||
val distTotal = pagers.values.sum();
|
val distTotal = pagers.values.sum();
|
||||||
dist = HashMap();
|
dist = HashMap();
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import kotlinx.coroutines.launch
|
|||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
class AirPlayCastingDevice : CastingDevice {
|
class AirPlayCastingDevice : CastingDeviceLegacy {
|
||||||
//See for more info: https://nto.github.io/AirPlay
|
//See for more info: https://nto.github.io/AirPlay
|
||||||
|
|
||||||
override val protocol: CastProtocolType get() = CastProtocolType.AIRPLAY;
|
override val protocol: CastProtocolType get() = CastProtocolType.AIRPLAY;
|
||||||
|
|||||||
@@ -2,147 +2,78 @@ package com.futo.platformplayer.casting
|
|||||||
|
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.models.CastingDeviceInfo
|
import com.futo.platformplayer.models.CastingDeviceInfo
|
||||||
import kotlinx.serialization.KSerializer
|
import org.fcast.sender_sdk.Metadata
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
|
||||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
|
||||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
|
||||||
import kotlinx.serialization.encoding.Decoder
|
|
||||||
import kotlinx.serialization.encoding.Encoder
|
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
|
|
||||||
enum class CastConnectionState {
|
|
||||||
DISCONNECTED,
|
|
||||||
CONNECTING,
|
|
||||||
CONNECTED
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable(with = CastProtocolType.CastProtocolTypeSerializer::class)
|
|
||||||
enum class CastProtocolType {
|
|
||||||
CHROMECAST,
|
|
||||||
AIRPLAY,
|
|
||||||
FCAST;
|
|
||||||
|
|
||||||
object CastProtocolTypeSerializer : KSerializer<CastProtocolType> {
|
|
||||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("CastProtocolType", PrimitiveKind.STRING)
|
|
||||||
|
|
||||||
override fun serialize(encoder: Encoder, value: CastProtocolType) {
|
|
||||||
encoder.encodeString(value.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun deserialize(decoder: Decoder): CastProtocolType {
|
|
||||||
val name = decoder.decodeString()
|
|
||||||
return when (name) {
|
|
||||||
"FASTCAST" -> FCAST // Handle the renamed case
|
|
||||||
else -> CastProtocolType.valueOf(name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class CastingDevice {
|
abstract class CastingDevice {
|
||||||
abstract val protocol: CastProtocolType;
|
abstract val isReady: Boolean
|
||||||
abstract val isReady: Boolean;
|
abstract val usedRemoteAddress: InetAddress?
|
||||||
abstract var usedRemoteAddress: InetAddress?;
|
abstract val localAddress: InetAddress?
|
||||||
abstract var localAddress: InetAddress?;
|
abstract val name: String?
|
||||||
abstract val canSetVolume: Boolean;
|
abstract val onConnectionStateChanged: Event1<CastConnectionState>
|
||||||
abstract val canSetSpeed: Boolean;
|
abstract val onPlayChanged: Event1<Boolean>
|
||||||
|
abstract val onTimeChanged: Event1<Double>
|
||||||
|
abstract val onDurationChanged: Event1<Double>
|
||||||
|
abstract val onVolumeChanged: Event1<Double>
|
||||||
|
abstract val onSpeedChanged: Event1<Double>
|
||||||
|
abstract var connectionState: CastConnectionState
|
||||||
|
abstract val protocolType: CastProtocolType
|
||||||
|
abstract var isPlaying: Boolean
|
||||||
|
abstract val expectedCurrentTime: Double
|
||||||
|
abstract var speed: Double
|
||||||
|
abstract var time: Double
|
||||||
|
abstract var duration: Double
|
||||||
|
abstract var volume: Double
|
||||||
|
abstract fun canSetVolume(): Boolean
|
||||||
|
abstract fun canSetSpeed(): Boolean
|
||||||
|
|
||||||
var name: String? = null;
|
@Throws
|
||||||
var isPlaying: Boolean = false
|
abstract fun resumePlayback()
|
||||||
set(value) {
|
|
||||||
val changed = value != field;
|
|
||||||
field = value;
|
|
||||||
if (changed) {
|
|
||||||
onPlayChanged.emit(value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private var lastTimeChangeTime_ms: Long = 0
|
@Throws
|
||||||
var time: Double = 0.0
|
abstract fun pausePlayback()
|
||||||
private set
|
|
||||||
|
|
||||||
protected fun setTime(value: Double, changeTime_ms: Long = System.currentTimeMillis()) {
|
@Throws
|
||||||
if (changeTime_ms > lastTimeChangeTime_ms && value != time) {
|
abstract fun stopPlayback()
|
||||||
time = value
|
|
||||||
lastTimeChangeTime_ms = changeTime_ms
|
|
||||||
onTimeChanged.emit(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var lastDurationChangeTime_ms: Long = 0
|
@Throws
|
||||||
var duration: Double = 0.0
|
abstract fun seekTo(timeSeconds: Double)
|
||||||
private set
|
|
||||||
|
|
||||||
protected fun setDuration(value: Double, changeTime_ms: Long = System.currentTimeMillis()) {
|
@Throws
|
||||||
if (changeTime_ms > lastDurationChangeTime_ms && value != duration) {
|
abstract fun changeVolume(timeSeconds: Double)
|
||||||
duration = value
|
|
||||||
lastDurationChangeTime_ms = changeTime_ms
|
|
||||||
onDurationChanged.emit(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var lastVolumeChangeTime_ms: Long = 0
|
@Throws
|
||||||
var volume: Double = 1.0
|
abstract fun changeSpeed(speed: Double)
|
||||||
private set
|
|
||||||
|
|
||||||
protected fun setVolume(value: Double, changeTime_ms: Long = System.currentTimeMillis()) {
|
@Throws
|
||||||
if (changeTime_ms > lastVolumeChangeTime_ms && value != volume) {
|
abstract fun connect()
|
||||||
volume = value
|
|
||||||
lastVolumeChangeTime_ms = changeTime_ms
|
|
||||||
onVolumeChanged.emit(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var lastSpeedChangeTime_ms: Long = 0
|
@Throws
|
||||||
var speed: Double = 1.0
|
abstract fun disconnect()
|
||||||
private set
|
abstract fun getDeviceInfo(): CastingDeviceInfo
|
||||||
|
abstract fun getAddresses(): List<InetAddress>
|
||||||
|
|
||||||
protected fun setSpeed(value: Double, changeTime_ms: Long = System.currentTimeMillis()) {
|
@Throws
|
||||||
if (changeTime_ms > lastSpeedChangeTime_ms && value != speed) {
|
abstract fun loadVideo(
|
||||||
speed = value
|
streamType: String,
|
||||||
lastSpeedChangeTime_ms = changeTime_ms
|
contentType: String,
|
||||||
onSpeedChanged.emit(value)
|
contentId: String,
|
||||||
}
|
resumePosition: Double,
|
||||||
}
|
duration: Double,
|
||||||
|
speed: Double?,
|
||||||
|
metadata: Metadata?
|
||||||
|
)
|
||||||
|
|
||||||
val expectedCurrentTime: Double
|
@Throws
|
||||||
get() {
|
abstract fun loadContent(
|
||||||
val diff = if (isPlaying) ((System.currentTimeMillis() - lastTimeChangeTime_ms).toDouble() / 1000.0) else 0.0;
|
contentType: String,
|
||||||
return time + diff;
|
content: String,
|
||||||
};
|
resumePosition: Double,
|
||||||
var connectionState: CastConnectionState = CastConnectionState.DISCONNECTED
|
duration: Double,
|
||||||
set(value) {
|
speed: Double?,
|
||||||
val changed = value != field;
|
metadata: Metadata?
|
||||||
field = value;
|
)
|
||||||
|
|
||||||
if (changed) {
|
abstract fun ensureThreadStarted()
|
||||||
onConnectionStateChanged.emit(value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var onConnectionStateChanged = Event1<CastConnectionState>();
|
|
||||||
var onPlayChanged = Event1<Boolean>();
|
|
||||||
var onTimeChanged = Event1<Double>();
|
|
||||||
var onDurationChanged = Event1<Double>();
|
|
||||||
var onVolumeChanged = Event1<Double>();
|
|
||||||
var onSpeedChanged = Event1<Double>();
|
|
||||||
|
|
||||||
abstract fun stopCasting();
|
|
||||||
|
|
||||||
abstract fun seekVideo(timeSeconds: Double);
|
|
||||||
abstract fun stopVideo();
|
|
||||||
abstract fun pauseVideo();
|
|
||||||
abstract fun resumeVideo();
|
|
||||||
abstract fun loadVideo(streamType: String, contentType: String, contentId: String, resumePosition: Double, duration: Double, speed: Double?);
|
|
||||||
abstract fun loadContent(contentType: String, content: String, resumePosition: Double, duration: Double, speed: Double?);
|
|
||||||
open fun changeVolume(volume: Double) { throw NotImplementedError() }
|
|
||||||
open fun changeSpeed(speed: Double) { throw NotImplementedError() }
|
|
||||||
|
|
||||||
abstract fun start();
|
|
||||||
abstract fun stop();
|
|
||||||
|
|
||||||
abstract fun getDeviceInfo(): CastingDeviceInfo;
|
|
||||||
|
|
||||||
abstract fun getAddresses(): List<InetAddress>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,271 @@
|
|||||||
|
package com.futo.platformplayer.casting
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import com.futo.platformplayer.BuildConfig
|
||||||
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.models.CastingDeviceInfo
|
||||||
|
import org.fcast.sender_sdk.ApplicationInfo
|
||||||
|
import org.fcast.sender_sdk.GenericKeyEvent
|
||||||
|
import org.fcast.sender_sdk.GenericMediaEvent
|
||||||
|
import org.fcast.sender_sdk.PlaybackState
|
||||||
|
import org.fcast.sender_sdk.Source
|
||||||
|
import java.net.InetAddress
|
||||||
|
import org.fcast.sender_sdk.CastingDevice as RsCastingDevice;
|
||||||
|
import org.fcast.sender_sdk.DeviceEventHandler as RsDeviceEventHandler;
|
||||||
|
import org.fcast.sender_sdk.DeviceConnectionState
|
||||||
|
import org.fcast.sender_sdk.DeviceFeature
|
||||||
|
import org.fcast.sender_sdk.IpAddr
|
||||||
|
import org.fcast.sender_sdk.LoadRequest
|
||||||
|
import org.fcast.sender_sdk.Metadata
|
||||||
|
import org.fcast.sender_sdk.ProtocolType
|
||||||
|
import org.fcast.sender_sdk.urlFormatIpAddr
|
||||||
|
import java.net.Inet4Address
|
||||||
|
import java.net.Inet6Address
|
||||||
|
|
||||||
|
private fun ipAddrToInetAddress(addr: IpAddr): InetAddress = when (addr) {
|
||||||
|
is IpAddr.V4 -> Inet4Address.getByAddress(
|
||||||
|
byteArrayOf(
|
||||||
|
addr.o1.toByte(),
|
||||||
|
addr.o2.toByte(),
|
||||||
|
addr.o3.toByte(),
|
||||||
|
addr.o4.toByte()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
is IpAddr.V6 -> Inet6Address.getByAddress(
|
||||||
|
byteArrayOf(
|
||||||
|
addr.o1.toByte(),
|
||||||
|
addr.o2.toByte(),
|
||||||
|
addr.o3.toByte(),
|
||||||
|
addr.o4.toByte(),
|
||||||
|
addr.o5.toByte(),
|
||||||
|
addr.o6.toByte(),
|
||||||
|
addr.o7.toByte(),
|
||||||
|
addr.o8.toByte(),
|
||||||
|
addr.o9.toByte(),
|
||||||
|
addr.o10.toByte(),
|
||||||
|
addr.o11.toByte(),
|
||||||
|
addr.o12.toByte(),
|
||||||
|
addr.o13.toByte(),
|
||||||
|
addr.o14.toByte(),
|
||||||
|
addr.o15.toByte(),
|
||||||
|
addr.o16.toByte()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
class CastingDeviceExp(val device: RsCastingDevice) : CastingDevice() {
|
||||||
|
class EventHandler : RsDeviceEventHandler {
|
||||||
|
var onConnectionStateChanged = Event1<DeviceConnectionState>();
|
||||||
|
var onPlayChanged = Event1<Boolean>()
|
||||||
|
var onTimeChanged = Event1<Double>()
|
||||||
|
var onDurationChanged = Event1<Double>()
|
||||||
|
var onVolumeChanged = Event1<Double>()
|
||||||
|
var onSpeedChanged = Event1<Double>()
|
||||||
|
|
||||||
|
override fun connectionStateChanged(state: DeviceConnectionState) {
|
||||||
|
onConnectionStateChanged.emit(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun volumeChanged(volume: Double) {
|
||||||
|
onVolumeChanged.emit(volume)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun timeChanged(time: Double) {
|
||||||
|
onTimeChanged.emit(time)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun playbackStateChanged(state: PlaybackState) {
|
||||||
|
onPlayChanged.emit(state == PlaybackState.PLAYING)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun durationChanged(duration: Double) {
|
||||||
|
onDurationChanged.emit(duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun speedChanged(speed: Double) {
|
||||||
|
onSpeedChanged.emit(speed)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun sourceChanged(source: Source) {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun keyEvent(event: GenericKeyEvent) {
|
||||||
|
// Unreachable
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mediaEvent(event: GenericMediaEvent) {
|
||||||
|
// Unreachable
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun playbackError(message: String) {
|
||||||
|
Logger.e(TAG, "Playback error: $message")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val eventHandler = EventHandler()
|
||||||
|
override val isReady: Boolean
|
||||||
|
get() = device.isReady()
|
||||||
|
override val name: String
|
||||||
|
get() = device.name()
|
||||||
|
override var usedRemoteAddress: InetAddress? = null
|
||||||
|
override var localAddress: InetAddress? = null
|
||||||
|
override fun canSetVolume(): Boolean = device.supportsFeature(DeviceFeature.SET_VOLUME)
|
||||||
|
override fun canSetSpeed(): Boolean = device.supportsFeature(DeviceFeature.SET_SPEED)
|
||||||
|
|
||||||
|
override val onConnectionStateChanged =
|
||||||
|
Event1<CastConnectionState>()
|
||||||
|
override val onPlayChanged: Event1<Boolean>
|
||||||
|
get() = eventHandler.onPlayChanged
|
||||||
|
override val onTimeChanged: Event1<Double>
|
||||||
|
get() = eventHandler.onTimeChanged
|
||||||
|
override val onDurationChanged: Event1<Double>
|
||||||
|
get() = eventHandler.onDurationChanged
|
||||||
|
override val onVolumeChanged: Event1<Double>
|
||||||
|
get() = eventHandler.onVolumeChanged
|
||||||
|
override val onSpeedChanged: Event1<Double>
|
||||||
|
get() = eventHandler.onSpeedChanged
|
||||||
|
|
||||||
|
override fun resumePlayback() = device.resumePlayback()
|
||||||
|
override fun pausePlayback() = device.pausePlayback()
|
||||||
|
override fun stopPlayback() = device.stopPlayback()
|
||||||
|
override fun seekTo(timeSeconds: Double) = device.seek(timeSeconds)
|
||||||
|
override fun changeVolume(newVolume: Double) {
|
||||||
|
device.changeVolume(newVolume)
|
||||||
|
volume = newVolume
|
||||||
|
}
|
||||||
|
override fun changeSpeed(speed: Double) = device.changeSpeed(speed)
|
||||||
|
override fun connect() = device.connect(
|
||||||
|
ApplicationInfo(
|
||||||
|
"Grayjay Android",
|
||||||
|
"${BuildConfig.VERSION_NAME}-${BuildConfig.FLAVOR}",
|
||||||
|
"${Build.MANUFACTURER} ${Build.MODEL}"
|
||||||
|
),
|
||||||
|
eventHandler,
|
||||||
|
1000.toULong()
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun disconnect() = device.disconnect()
|
||||||
|
|
||||||
|
override fun getDeviceInfo(): CastingDeviceInfo {
|
||||||
|
val info = device.getDeviceInfo()
|
||||||
|
return CastingDeviceInfo(
|
||||||
|
info.name,
|
||||||
|
when (info.protocol) {
|
||||||
|
ProtocolType.CHROMECAST -> CastProtocolType.CHROMECAST
|
||||||
|
ProtocolType.F_CAST -> CastProtocolType.FCAST
|
||||||
|
},
|
||||||
|
addresses = info.addresses.map { urlFormatIpAddr(it) }.toTypedArray(),
|
||||||
|
port = info.port.toInt(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAddresses(): List<InetAddress> = device.getAddresses().map {
|
||||||
|
ipAddrToInetAddress(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun loadVideo(
|
||||||
|
streamType: String,
|
||||||
|
contentType: String,
|
||||||
|
contentId: String,
|
||||||
|
resumePosition: Double,
|
||||||
|
duration: Double,
|
||||||
|
speed: Double?,
|
||||||
|
metadata: Metadata?
|
||||||
|
) = device.load(
|
||||||
|
LoadRequest.Video(
|
||||||
|
contentType = contentType,
|
||||||
|
url = contentId,
|
||||||
|
resumePosition = resumePosition,
|
||||||
|
speed = speed,
|
||||||
|
volume = volume,
|
||||||
|
metadata = metadata
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun loadContent(
|
||||||
|
contentType: String,
|
||||||
|
content: String,
|
||||||
|
resumePosition: Double,
|
||||||
|
duration: Double,
|
||||||
|
speed: Double?,
|
||||||
|
metadata: Metadata?
|
||||||
|
) = device.load(
|
||||||
|
LoadRequest.Content(
|
||||||
|
contentType = contentType,
|
||||||
|
content = content,
|
||||||
|
resumePosition = resumePosition,
|
||||||
|
speed = speed,
|
||||||
|
volume = volume,
|
||||||
|
metadata = metadata,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
override var connectionState = CastConnectionState.DISCONNECTED
|
||||||
|
override val protocolType: CastProtocolType
|
||||||
|
get() = when (device.castingProtocol()) {
|
||||||
|
ProtocolType.CHROMECAST -> CastProtocolType.CHROMECAST
|
||||||
|
ProtocolType.F_CAST -> CastProtocolType.FCAST
|
||||||
|
}
|
||||||
|
override var volume: Double = 1.0
|
||||||
|
override var duration: Double = 0.0
|
||||||
|
private var lastTimeChangeTime_ms: Long = 0
|
||||||
|
override var time: Double = 0.0
|
||||||
|
override var speed: Double = 0.0
|
||||||
|
override var isPlaying: Boolean = false
|
||||||
|
|
||||||
|
override val expectedCurrentTime: Double
|
||||||
|
get() {
|
||||||
|
val diff =
|
||||||
|
if (isPlaying) ((System.currentTimeMillis() - lastTimeChangeTime_ms).toDouble() / 1000.0) else 0.0;
|
||||||
|
return time + diff
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
eventHandler.onConnectionStateChanged.subscribe { newState ->
|
||||||
|
when (newState) {
|
||||||
|
is DeviceConnectionState.Connected -> {
|
||||||
|
usedRemoteAddress = ipAddrToInetAddress(newState.usedRemoteAddr)
|
||||||
|
localAddress = ipAddrToInetAddress(newState.localAddr)
|
||||||
|
connectionState = CastConnectionState.CONNECTED
|
||||||
|
onConnectionStateChanged.emit(CastConnectionState.CONNECTED)
|
||||||
|
}
|
||||||
|
|
||||||
|
DeviceConnectionState.Connecting, DeviceConnectionState.Reconnecting -> {
|
||||||
|
connectionState = CastConnectionState.CONNECTING
|
||||||
|
onConnectionStateChanged.emit(CastConnectionState.CONNECTING)
|
||||||
|
}
|
||||||
|
|
||||||
|
DeviceConnectionState.Disconnected -> {
|
||||||
|
connectionState = CastConnectionState.CONNECTING
|
||||||
|
onConnectionStateChanged.emit(CastConnectionState.DISCONNECTED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newState == DeviceConnectionState.Disconnected) {
|
||||||
|
try {
|
||||||
|
Logger.i(TAG, "Stopping device")
|
||||||
|
device.disconnect()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to stop device: $e")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
eventHandler.onPlayChanged.subscribe { isPlaying = it }
|
||||||
|
eventHandler.onTimeChanged.subscribe {
|
||||||
|
lastTimeChangeTime_ms = System.currentTimeMillis()
|
||||||
|
time = it
|
||||||
|
}
|
||||||
|
eventHandler.onDurationChanged.subscribe { duration = it }
|
||||||
|
eventHandler.onVolumeChanged.subscribe { volume = it }
|
||||||
|
eventHandler.onSpeedChanged.subscribe { speed = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun ensureThreadStarted() {}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = "CastingDeviceExp"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
package com.futo.platformplayer.casting
|
||||||
|
|
||||||
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.models.CastingDeviceInfo
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||||
|
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||||
|
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||||
|
import kotlinx.serialization.encoding.Decoder
|
||||||
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
import org.fcast.sender_sdk.Metadata
|
||||||
|
import java.net.InetAddress
|
||||||
|
|
||||||
|
enum class CastConnectionState {
|
||||||
|
DISCONNECTED,
|
||||||
|
CONNECTING,
|
||||||
|
CONNECTED
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable(with = CastProtocolType.CastProtocolTypeSerializer::class)
|
||||||
|
enum class CastProtocolType {
|
||||||
|
CHROMECAST,
|
||||||
|
AIRPLAY,
|
||||||
|
FCAST;
|
||||||
|
|
||||||
|
object CastProtocolTypeSerializer : KSerializer<CastProtocolType> {
|
||||||
|
override val descriptor: SerialDescriptor =
|
||||||
|
PrimitiveSerialDescriptor("CastProtocolType", PrimitiveKind.STRING)
|
||||||
|
|
||||||
|
override fun serialize(encoder: Encoder, value: CastProtocolType) {
|
||||||
|
encoder.encodeString(value.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deserialize(decoder: Decoder): CastProtocolType {
|
||||||
|
val name = decoder.decodeString()
|
||||||
|
return when (name) {
|
||||||
|
"FASTCAST" -> FCAST // Handle the renamed case
|
||||||
|
else -> CastProtocolType.valueOf(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class CastingDeviceLegacy {
|
||||||
|
abstract val protocol: CastProtocolType;
|
||||||
|
abstract val isReady: Boolean;
|
||||||
|
abstract var usedRemoteAddress: InetAddress?;
|
||||||
|
abstract var localAddress: InetAddress?;
|
||||||
|
abstract val canSetVolume: Boolean;
|
||||||
|
abstract val canSetSpeed: Boolean;
|
||||||
|
|
||||||
|
var name: String? = null;
|
||||||
|
var isPlaying: Boolean = false
|
||||||
|
set(value) {
|
||||||
|
val changed = value != field;
|
||||||
|
field = value;
|
||||||
|
if (changed) {
|
||||||
|
onPlayChanged.emit(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private var lastTimeChangeTime_ms: Long = 0
|
||||||
|
var time: Double = 0.0
|
||||||
|
private set
|
||||||
|
|
||||||
|
protected fun setTime(value: Double, changeTime_ms: Long = System.currentTimeMillis()) {
|
||||||
|
if (changeTime_ms > lastTimeChangeTime_ms && value != time) {
|
||||||
|
time = value
|
||||||
|
lastTimeChangeTime_ms = changeTime_ms
|
||||||
|
onTimeChanged.emit(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var lastDurationChangeTime_ms: Long = 0
|
||||||
|
var duration: Double = 0.0
|
||||||
|
private set
|
||||||
|
|
||||||
|
protected fun setDuration(value: Double, changeTime_ms: Long = System.currentTimeMillis()) {
|
||||||
|
if (changeTime_ms > lastDurationChangeTime_ms && value != duration) {
|
||||||
|
duration = value
|
||||||
|
lastDurationChangeTime_ms = changeTime_ms
|
||||||
|
onDurationChanged.emit(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var lastVolumeChangeTime_ms: Long = 0
|
||||||
|
var volume: Double = 1.0
|
||||||
|
private set
|
||||||
|
|
||||||
|
protected fun setVolume(value: Double, changeTime_ms: Long = System.currentTimeMillis()) {
|
||||||
|
if (changeTime_ms > lastVolumeChangeTime_ms && value != volume) {
|
||||||
|
volume = value
|
||||||
|
lastVolumeChangeTime_ms = changeTime_ms
|
||||||
|
onVolumeChanged.emit(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var lastSpeedChangeTime_ms: Long = 0
|
||||||
|
var speed: Double = 1.0
|
||||||
|
private set
|
||||||
|
|
||||||
|
protected fun setSpeed(value: Double, changeTime_ms: Long = System.currentTimeMillis()) {
|
||||||
|
if (changeTime_ms > lastSpeedChangeTime_ms && value != speed) {
|
||||||
|
speed = value
|
||||||
|
lastSpeedChangeTime_ms = changeTime_ms
|
||||||
|
onSpeedChanged.emit(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val expectedCurrentTime: Double
|
||||||
|
get() {
|
||||||
|
val diff =
|
||||||
|
if (isPlaying) ((System.currentTimeMillis() - lastTimeChangeTime_ms).toDouble() / 1000.0) else 0.0;
|
||||||
|
return time + diff;
|
||||||
|
};
|
||||||
|
var connectionState: CastConnectionState = CastConnectionState.DISCONNECTED
|
||||||
|
set(value) {
|
||||||
|
val changed = value != field;
|
||||||
|
field = value;
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
onConnectionStateChanged.emit(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var onConnectionStateChanged = Event1<CastConnectionState>();
|
||||||
|
var onPlayChanged = Event1<Boolean>();
|
||||||
|
var onTimeChanged = Event1<Double>();
|
||||||
|
var onDurationChanged = Event1<Double>();
|
||||||
|
var onVolumeChanged = Event1<Double>();
|
||||||
|
var onSpeedChanged = Event1<Double>();
|
||||||
|
|
||||||
|
abstract fun stopCasting();
|
||||||
|
|
||||||
|
abstract fun seekVideo(timeSeconds: Double);
|
||||||
|
abstract fun stopVideo();
|
||||||
|
abstract fun pauseVideo();
|
||||||
|
abstract fun resumeVideo();
|
||||||
|
abstract fun loadVideo(
|
||||||
|
streamType: String,
|
||||||
|
contentType: String,
|
||||||
|
contentId: String,
|
||||||
|
resumePosition: Double,
|
||||||
|
duration: Double,
|
||||||
|
speed: Double?
|
||||||
|
);
|
||||||
|
|
||||||
|
abstract fun loadContent(
|
||||||
|
contentType: String,
|
||||||
|
content: String,
|
||||||
|
resumePosition: Double,
|
||||||
|
duration: Double,
|
||||||
|
speed: Double?
|
||||||
|
);
|
||||||
|
|
||||||
|
open fun changeVolume(volume: Double) {
|
||||||
|
throw NotImplementedError()
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun changeSpeed(speed: Double) {
|
||||||
|
throw NotImplementedError()
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract fun start();
|
||||||
|
abstract fun stop();
|
||||||
|
|
||||||
|
abstract fun getDeviceInfo(): CastingDeviceInfo;
|
||||||
|
|
||||||
|
abstract fun getAddresses(): List<InetAddress>;
|
||||||
|
}
|
||||||
|
|
||||||
|
class CastingDeviceLegacyWrapper(val inner: CastingDeviceLegacy) : CastingDevice() {
|
||||||
|
override val isReady: Boolean get() = inner.isReady
|
||||||
|
override val usedRemoteAddress: InetAddress? get() = inner.usedRemoteAddress
|
||||||
|
override val localAddress: InetAddress? get() = inner.localAddress
|
||||||
|
override val name: String? get() = inner.name
|
||||||
|
override val onConnectionStateChanged: Event1<CastConnectionState> get() = inner.onConnectionStateChanged
|
||||||
|
override val onPlayChanged: Event1<Boolean> get() = inner.onPlayChanged
|
||||||
|
override val onTimeChanged: Event1<Double> get() = inner.onTimeChanged
|
||||||
|
override val onDurationChanged: Event1<Double> get() = inner.onDurationChanged
|
||||||
|
override val onVolumeChanged: Event1<Double> get() = inner.onVolumeChanged
|
||||||
|
override val onSpeedChanged: Event1<Double> get() = inner.onSpeedChanged
|
||||||
|
override var connectionState: CastConnectionState
|
||||||
|
get() = inner.connectionState
|
||||||
|
set(_) = Unit
|
||||||
|
override val protocolType: CastProtocolType get() = inner.protocol
|
||||||
|
override var isPlaying: Boolean
|
||||||
|
get() = inner.isPlaying
|
||||||
|
set(_) = Unit
|
||||||
|
override val expectedCurrentTime: Double
|
||||||
|
get() = inner.expectedCurrentTime
|
||||||
|
override var speed: Double
|
||||||
|
get() = inner.speed
|
||||||
|
set(_) = Unit
|
||||||
|
override var time: Double
|
||||||
|
get() = inner.time
|
||||||
|
set(_) = Unit
|
||||||
|
override var duration: Double
|
||||||
|
get() = inner.duration
|
||||||
|
set(_) = Unit
|
||||||
|
override var volume: Double
|
||||||
|
get() = inner.volume
|
||||||
|
set(_) = Unit
|
||||||
|
|
||||||
|
override fun canSetVolume(): Boolean = inner.canSetVolume
|
||||||
|
override fun canSetSpeed(): Boolean = inner.canSetSpeed
|
||||||
|
override fun resumePlayback() = inner.resumeVideo()
|
||||||
|
override fun pausePlayback() = inner.pauseVideo()
|
||||||
|
override fun stopPlayback() = inner.stopVideo()
|
||||||
|
override fun seekTo(timeSeconds: Double) = inner.seekVideo(timeSeconds)
|
||||||
|
override fun changeVolume(timeSeconds: Double) = inner.changeVolume(timeSeconds)
|
||||||
|
override fun changeSpeed(speed: Double) = inner.changeSpeed(speed)
|
||||||
|
override fun connect() = inner.start()
|
||||||
|
override fun disconnect() = inner.stop()
|
||||||
|
override fun getDeviceInfo(): CastingDeviceInfo = inner.getDeviceInfo()
|
||||||
|
override fun getAddresses(): List<InetAddress> = inner.getAddresses()
|
||||||
|
override fun loadVideo(
|
||||||
|
streamType: String,
|
||||||
|
contentType: String,
|
||||||
|
contentId: String,
|
||||||
|
resumePosition: Double,
|
||||||
|
duration: Double,
|
||||||
|
speed: Double?,
|
||||||
|
metadata: Metadata?
|
||||||
|
) = inner.loadVideo(streamType, contentType, contentId, resumePosition, duration, speed)
|
||||||
|
|
||||||
|
override fun loadContent(
|
||||||
|
contentType: String,
|
||||||
|
content: String,
|
||||||
|
resumePosition: Double,
|
||||||
|
duration: Double,
|
||||||
|
speed: Double?,
|
||||||
|
metadata: Metadata?
|
||||||
|
) = inner.loadContent(contentType, content, resumePosition, duration, speed)
|
||||||
|
|
||||||
|
override fun ensureThreadStarted() = when (inner) {
|
||||||
|
is FCastCastingDevice -> inner.ensureThreadStarted()
|
||||||
|
is ChromecastCastingDevice -> inner.ensureThreadsStarted()
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,7 +27,7 @@ import javax.net.ssl.SSLSocket
|
|||||||
import javax.net.ssl.TrustManager
|
import javax.net.ssl.TrustManager
|
||||||
import javax.net.ssl.X509TrustManager
|
import javax.net.ssl.X509TrustManager
|
||||||
|
|
||||||
class ChromecastCastingDevice : CastingDevice {
|
class ChromecastCastingDevice : CastingDeviceLegacy {
|
||||||
//See for more info: https://developers.google.com/cast/docs/media/messages
|
//See for more info: https://developers.google.com/cast/docs/media/messages
|
||||||
|
|
||||||
override val protocol: CastProtocolType get() = CastProtocolType.CHROMECAST;
|
override val protocol: CastProtocolType get() = CastProtocolType.CHROMECAST;
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package com.futo.platformplayer.casting
|
|||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.futo.platformplayer.Settings
|
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.casting.models.FCastDecryptedMessage
|
import com.futo.platformplayer.casting.models.FCastDecryptedMessage
|
||||||
import com.futo.platformplayer.casting.models.FCastEncryptedMessage
|
import com.futo.platformplayer.casting.models.FCastEncryptedMessage
|
||||||
@@ -25,7 +24,6 @@ import com.futo.platformplayer.toInetAddress
|
|||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
@@ -34,7 +32,6 @@ import java.io.IOException
|
|||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
import java.net.Inet4Address
|
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import java.net.InetSocketAddress
|
import java.net.InetSocketAddress
|
||||||
import java.net.Socket
|
import java.net.Socket
|
||||||
@@ -72,7 +69,7 @@ enum class Opcode(val value: Byte) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class FCastCastingDevice : CastingDevice {
|
class FCastCastingDevice : CastingDeviceLegacy {
|
||||||
//See for more info: TODO
|
//See for more info: TODO
|
||||||
|
|
||||||
override val protocol: CastProtocolType get() = CastProtocolType.FCAST;
|
override val protocol: CastProtocolType get() = CastProtocolType.FCAST;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,174 @@
|
|||||||
|
package com.futo.platformplayer.casting
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import com.futo.platformplayer.BuildConfig
|
||||||
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.models.CastingDeviceInfo
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import org.fcast.sender_sdk.DeviceInfo as RsDeviceInfo
|
||||||
|
import org.fcast.sender_sdk.ProtocolType
|
||||||
|
import org.fcast.sender_sdk.CastContext
|
||||||
|
import org.fcast.sender_sdk.NsdDeviceDiscoverer
|
||||||
|
|
||||||
|
class StateCastingExp : StateCasting() {
|
||||||
|
private val _context = CastContext()
|
||||||
|
var _deviceDiscoverer: NsdDeviceDiscoverer? = null
|
||||||
|
|
||||||
|
class DiscoveryEventHandler(
|
||||||
|
private val onDeviceAdded: (RsDeviceInfo) -> Unit,
|
||||||
|
private val onDeviceRemoved: (String) -> Unit,
|
||||||
|
private val onDeviceUpdated: (RsDeviceInfo) -> Unit,
|
||||||
|
) : org.fcast.sender_sdk.DeviceDiscovererEventHandler {
|
||||||
|
override fun deviceAvailable(deviceInfo: RsDeviceInfo) {
|
||||||
|
onDeviceAdded(deviceInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deviceChanged(deviceInfo: RsDeviceInfo) {
|
||||||
|
onDeviceUpdated(deviceInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deviceRemoved(deviceName: String) {
|
||||||
|
onDeviceRemoved(deviceName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
org.fcast.sender_sdk.initLogger(org.fcast.sender_sdk.LogLevelFilter.DEBUG)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleUrl(url: String) {
|
||||||
|
try {
|
||||||
|
val foundDeviceInfo = org.fcast.sender_sdk.deviceInfoFromUrl(url)!!
|
||||||
|
val foundDevice = _context.createDeviceFromInfo(foundDeviceInfo)
|
||||||
|
connectDevice(CastingDeviceExp(foundDevice))
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to handle URL: $e")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
val ad = activeDevice ?: return
|
||||||
|
_resumeCastingDevice = ad.getDeviceInfo()
|
||||||
|
Log.i(TAG, "_resumeCastingDevice set to '${ad.name}'")
|
||||||
|
Logger.i(TAG, "Stopping active device because of onStop.")
|
||||||
|
try {
|
||||||
|
ad.disconnect()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to disconnect from device: $e")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun start(context: Context) {
|
||||||
|
if (_started)
|
||||||
|
return
|
||||||
|
_started = true
|
||||||
|
|
||||||
|
Log.i(TAG, "_resumeCastingDevice set null start")
|
||||||
|
_resumeCastingDevice = null
|
||||||
|
|
||||||
|
Logger.i(TAG, "CastingService starting...")
|
||||||
|
|
||||||
|
_castServer.start()
|
||||||
|
enableDeveloper(true)
|
||||||
|
|
||||||
|
Logger.i(TAG, "CastingService started.")
|
||||||
|
|
||||||
|
_deviceDiscoverer = NsdDeviceDiscoverer(
|
||||||
|
context,
|
||||||
|
DiscoveryEventHandler(
|
||||||
|
{ deviceInfo -> // Added
|
||||||
|
Logger.i(TAG, "Device added: ${deviceInfo.name}")
|
||||||
|
val device = _context.createDeviceFromInfo(deviceInfo)
|
||||||
|
val deviceHandle = CastingDeviceExp(device)
|
||||||
|
devices[deviceHandle.device.name()] = deviceHandle
|
||||||
|
invokeInMainScopeIfRequired {
|
||||||
|
onDeviceAdded.emit(deviceHandle)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deviceName -> // Removed
|
||||||
|
invokeInMainScopeIfRequired {
|
||||||
|
if (devices.containsKey(deviceName)) {
|
||||||
|
val device = devices.remove(deviceName)
|
||||||
|
if (device != null) {
|
||||||
|
onDeviceRemoved.emit(device)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deviceInfo -> // Updated
|
||||||
|
Logger.i(TAG, "Device updated: $deviceInfo")
|
||||||
|
val handle = devices[deviceInfo.name]
|
||||||
|
if (handle != null && handle is CastingDeviceExp) {
|
||||||
|
handle.device.setPort(deviceInfo.port)
|
||||||
|
handle.device.setAddresses(deviceInfo.addresses)
|
||||||
|
invokeInMainScopeIfRequired {
|
||||||
|
onDeviceChanged.emit(handle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun stop() {
|
||||||
|
if (!_started) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_started = false
|
||||||
|
|
||||||
|
Logger.i(TAG, "CastingService stopping.")
|
||||||
|
|
||||||
|
_scopeIO.cancel()
|
||||||
|
_scopeMain.cancel()
|
||||||
|
|
||||||
|
Logger.i(TAG, "Stopping active device because StateCasting is being stopped.")
|
||||||
|
val d = activeDevice
|
||||||
|
activeDevice = null
|
||||||
|
try {
|
||||||
|
d?.disconnect()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to disconnect device: $e")
|
||||||
|
}
|
||||||
|
|
||||||
|
_castServer.stop()
|
||||||
|
_castServer.removeAllHandlers()
|
||||||
|
|
||||||
|
Logger.i(TAG, "CastingService stopped.")
|
||||||
|
|
||||||
|
_deviceDiscoverer = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun startUpdateTimeJob(
|
||||||
|
onTimeJobTimeChanged_s: Event1<Long>,
|
||||||
|
setTime: (Long) -> Unit
|
||||||
|
): Job? = null
|
||||||
|
|
||||||
|
override fun deviceFromInfo(deviceInfo: CastingDeviceInfo): CastingDeviceExp {
|
||||||
|
val rsAddrs =
|
||||||
|
deviceInfo.addresses.map { org.fcast.sender_sdk.tryIpAddrFromStr(it) } // Throws!
|
||||||
|
val rsDeviceInfo = RsDeviceInfo(
|
||||||
|
name = deviceInfo.name,
|
||||||
|
protocol = when (deviceInfo.type) {
|
||||||
|
com.futo.platformplayer.casting.CastProtocolType.CHROMECAST -> ProtocolType.CHROMECAST
|
||||||
|
com.futo.platformplayer.casting.CastProtocolType.FCAST -> ProtocolType.F_CAST
|
||||||
|
else -> throw IllegalArgumentException()
|
||||||
|
},
|
||||||
|
addresses = rsAddrs,
|
||||||
|
port = deviceInfo.port.toUShort(),
|
||||||
|
)
|
||||||
|
|
||||||
|
return CastingDeviceExp(_context.createDeviceFromInfo(rsDeviceInfo))
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = "StateCastingExp"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,397 @@
|
|||||||
|
package com.futo.platformplayer.casting
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.net.nsd.NsdManager
|
||||||
|
import android.net.nsd.NsdServiceInfo
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Base64
|
||||||
|
import android.util.Log
|
||||||
|
import com.futo.platformplayer.constructs.Event1
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.models.CastingDeviceInfo
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import java.net.InetAddress
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
|
class StateCastingLegacy : StateCasting() {
|
||||||
|
private var _nsdManager: NsdManager? = null
|
||||||
|
|
||||||
|
private val _discoveryListeners = mapOf(
|
||||||
|
"_googlecast._tcp" to createDiscoveryListener(::addOrUpdateChromeCastDevice),
|
||||||
|
"_airplay._tcp" to createDiscoveryListener(::addOrUpdateAirPlayDevice),
|
||||||
|
"_fastcast._tcp" to createDiscoveryListener(::addOrUpdateFastCastDevice),
|
||||||
|
"_fcast._tcp" to createDiscoveryListener(::addOrUpdateFastCastDevice)
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun handleUrl(url: String) {
|
||||||
|
val uri = Uri.parse(url)
|
||||||
|
if (uri.scheme != "fcast") {
|
||||||
|
throw Exception("Expected scheme to be FCast")
|
||||||
|
}
|
||||||
|
|
||||||
|
val type = uri.host
|
||||||
|
if (type != "r") {
|
||||||
|
throw Exception("Expected type r")
|
||||||
|
}
|
||||||
|
|
||||||
|
val connectionInfo = uri.pathSegments[0]
|
||||||
|
val json =
|
||||||
|
Base64.decode(connectionInfo, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP)
|
||||||
|
.toString(Charsets.UTF_8)
|
||||||
|
val networkConfig = Json.decodeFromString<FCastNetworkConfig>(json)
|
||||||
|
val tcpService = networkConfig.services.first { v -> v.type == 0 }
|
||||||
|
|
||||||
|
val foundInfo = addRememberedDevice(
|
||||||
|
CastingDeviceInfo(
|
||||||
|
name = networkConfig.name,
|
||||||
|
type = CastProtocolType.FCAST,
|
||||||
|
addresses = networkConfig.addresses.toTypedArray(),
|
||||||
|
port = tcpService.port
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
connectDevice(deviceFromInfo(foundInfo))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
val ad = activeDevice ?: return;
|
||||||
|
_resumeCastingDevice = ad.getDeviceInfo()
|
||||||
|
Log.i(TAG, "_resumeCastingDevice set to '${ad.name}'")
|
||||||
|
Logger.i(TAG, "Stopping active device because of onStop.");
|
||||||
|
ad.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun start(context: Context) {
|
||||||
|
if (_started)
|
||||||
|
return;
|
||||||
|
_started = true;
|
||||||
|
|
||||||
|
Log.i(TAG, "_resumeCastingDevice set null start")
|
||||||
|
_resumeCastingDevice = null;
|
||||||
|
|
||||||
|
Logger.i(TAG, "CastingService starting...");
|
||||||
|
|
||||||
|
_castServer.start();
|
||||||
|
enableDeveloper(true);
|
||||||
|
|
||||||
|
Logger.i(TAG, "CastingService started.");
|
||||||
|
|
||||||
|
_nsdManager = context.getSystemService(Context.NSD_SERVICE) as NsdManager
|
||||||
|
startDiscovering()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun startDiscovering() {
|
||||||
|
_nsdManager?.apply {
|
||||||
|
_discoveryListeners.forEach {
|
||||||
|
discoverServices(it.key, NsdManager.PROTOCOL_DNS_SD, it.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun stopDiscovering() {
|
||||||
|
_nsdManager?.apply {
|
||||||
|
_discoveryListeners.forEach {
|
||||||
|
try {
|
||||||
|
stopServiceDiscovery(it.value)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.w(TAG, "Failed to stop service discovery", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun stop() {
|
||||||
|
if (!_started)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_started = false;
|
||||||
|
|
||||||
|
Logger.i(TAG, "CastingService stopping.")
|
||||||
|
|
||||||
|
stopDiscovering()
|
||||||
|
_scopeIO.cancel();
|
||||||
|
_scopeMain.cancel();
|
||||||
|
|
||||||
|
Logger.i(TAG, "Stopping active device because StateCasting is being stopped.")
|
||||||
|
val d = activeDevice;
|
||||||
|
activeDevice = null;
|
||||||
|
d?.disconnect();
|
||||||
|
|
||||||
|
_castServer.stop();
|
||||||
|
_castServer.removeAllHandlers();
|
||||||
|
|
||||||
|
Logger.i(TAG, "CastingService stopped.")
|
||||||
|
|
||||||
|
_nsdManager = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createDiscoveryListener(addOrUpdate: (String, Array<InetAddress>, Int) -> Unit): NsdManager.DiscoveryListener {
|
||||||
|
return object : NsdManager.DiscoveryListener {
|
||||||
|
override fun onDiscoveryStarted(regType: String) {
|
||||||
|
Log.d(TAG, "Service discovery started for $regType")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDiscoveryStopped(serviceType: String) {
|
||||||
|
Log.i(TAG, "Discovery stopped: $serviceType")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceLost(service: NsdServiceInfo) {
|
||||||
|
Log.e(TAG, "service lost: $service")
|
||||||
|
// TODO: Handle service lost, e.g., remove device
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) {
|
||||||
|
Log.e(TAG, "Discovery failed for $serviceType: Error code:$errorCode")
|
||||||
|
try {
|
||||||
|
_nsdManager?.stopServiceDiscovery(this)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.w(TAG, "Failed to stop service discovery", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) {
|
||||||
|
Log.e(TAG, "Stop discovery failed for $serviceType: Error code:$errorCode")
|
||||||
|
try {
|
||||||
|
_nsdManager?.stopServiceDiscovery(this)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.w(TAG, "Failed to stop service discovery", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceFound(service: NsdServiceInfo) {
|
||||||
|
Log.v(TAG, "Service discovery success for ${service.serviceType}: $service")
|
||||||
|
val addresses = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
service.hostAddresses.toTypedArray()
|
||||||
|
} else {
|
||||||
|
arrayOf(service.host)
|
||||||
|
}
|
||||||
|
addOrUpdate(service.serviceName, addresses, service.port)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
_nsdManager?.registerServiceInfoCallback(
|
||||||
|
service,
|
||||||
|
{ it.run() },
|
||||||
|
object : NsdManager.ServiceInfoCallback {
|
||||||
|
override fun onServiceUpdated(serviceInfo: NsdServiceInfo) {
|
||||||
|
Log.v(TAG, "onServiceUpdated: $serviceInfo")
|
||||||
|
addOrUpdate(
|
||||||
|
serviceInfo.serviceName,
|
||||||
|
serviceInfo.hostAddresses.toTypedArray(),
|
||||||
|
serviceInfo.port
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceLost() {
|
||||||
|
Log.v(TAG, "onServiceLost: $service")
|
||||||
|
// TODO: Handle service lost
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceInfoCallbackRegistrationFailed(errorCode: Int) {
|
||||||
|
Log.v(TAG, "onServiceInfoCallbackRegistrationFailed: $errorCode")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceInfoCallbackUnregistered() {
|
||||||
|
Log.v(TAG, "onServiceInfoCallbackUnregistered")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
_nsdManager?.resolveService(service, object : NsdManager.ResolveListener {
|
||||||
|
override fun onResolveFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
|
||||||
|
Log.v(TAG, "Resolve failed: $errorCode")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceResolved(serviceInfo: NsdServiceInfo) {
|
||||||
|
Log.v(TAG, "Resolve Succeeded: $serviceInfo")
|
||||||
|
addOrUpdate(
|
||||||
|
serviceInfo.serviceName,
|
||||||
|
arrayOf(serviceInfo.host),
|
||||||
|
serviceInfo.port
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun startUpdateTimeJob(
|
||||||
|
onTimeJobTimeChanged_s: Event1<Long>,
|
||||||
|
setTime: (Long) -> Unit
|
||||||
|
): Job? {
|
||||||
|
val d = activeDevice;
|
||||||
|
if (d is CastingDeviceLegacyWrapper && (d.inner is AirPlayCastingDevice || d.inner is ChromecastCastingDevice)) {
|
||||||
|
return _scopeMain.launch {
|
||||||
|
while (true) {
|
||||||
|
val device = instance.activeDevice
|
||||||
|
if (device == null || !device.isPlaying) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
delay(1000)
|
||||||
|
val time_ms = (device.expectedCurrentTime * 1000.0).toLong()
|
||||||
|
setTime(time_ms)
|
||||||
|
onTimeJobTimeChanged_s.emit(device.expectedCurrentTime.toLong())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deviceFromInfo(deviceInfo: CastingDeviceInfo): CastingDevice {
|
||||||
|
return CastingDeviceLegacyWrapper(
|
||||||
|
when (deviceInfo.type) {
|
||||||
|
CastProtocolType.CHROMECAST -> {
|
||||||
|
ChromecastCastingDevice(deviceInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
CastProtocolType.AIRPLAY -> {
|
||||||
|
AirPlayCastingDevice(deviceInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
CastProtocolType.FCAST -> {
|
||||||
|
FCastCastingDevice(deviceInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addOrUpdateChromeCastDevice(
|
||||||
|
name: String,
|
||||||
|
addresses: Array<InetAddress>,
|
||||||
|
port: Int
|
||||||
|
) {
|
||||||
|
return addOrUpdateCastDevice(
|
||||||
|
name,
|
||||||
|
deviceFactory = {
|
||||||
|
CastingDeviceLegacyWrapper(
|
||||||
|
ChromecastCastingDevice(
|
||||||
|
name,
|
||||||
|
addresses,
|
||||||
|
port
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
deviceUpdater = { d ->
|
||||||
|
if (d.isReady || d !is CastingDeviceLegacyWrapper || d.inner !is ChromecastCastingDevice) {
|
||||||
|
return@addOrUpdateCastDevice false;
|
||||||
|
}
|
||||||
|
|
||||||
|
val changed =
|
||||||
|
addresses.contentEquals(d.inner.addresses) || d.name != name || d.inner.port != port;
|
||||||
|
if (changed) {
|
||||||
|
d.inner.name = name;
|
||||||
|
d.inner.addresses = addresses;
|
||||||
|
d.inner.port = port;
|
||||||
|
}
|
||||||
|
|
||||||
|
return@addOrUpdateCastDevice changed;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addOrUpdateAirPlayDevice(name: String, addresses: Array<InetAddress>, port: Int) {
|
||||||
|
return addOrUpdateCastDevice(
|
||||||
|
name,
|
||||||
|
deviceFactory = {
|
||||||
|
CastingDeviceLegacyWrapper(
|
||||||
|
AirPlayCastingDevice(
|
||||||
|
name,
|
||||||
|
addresses,
|
||||||
|
port
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
deviceUpdater = { d ->
|
||||||
|
if (d.isReady || d !is CastingDeviceLegacyWrapper || d.inner !is AirPlayCastingDevice) {
|
||||||
|
return@addOrUpdateCastDevice false;
|
||||||
|
}
|
||||||
|
|
||||||
|
val changed =
|
||||||
|
addresses.contentEquals(addresses) || d.name != name || d.inner.port != port;
|
||||||
|
if (changed) {
|
||||||
|
d.inner.name = name;
|
||||||
|
d.inner.port = port;
|
||||||
|
d.inner.addresses = addresses;
|
||||||
|
}
|
||||||
|
|
||||||
|
return@addOrUpdateCastDevice changed;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addOrUpdateFastCastDevice(name: String, addresses: Array<InetAddress>, port: Int) {
|
||||||
|
return addOrUpdateCastDevice(
|
||||||
|
name,
|
||||||
|
deviceFactory = { CastingDeviceLegacyWrapper(FCastCastingDevice(name, addresses, port)) },
|
||||||
|
deviceUpdater = { d ->
|
||||||
|
if (d.isReady || d !is CastingDeviceLegacyWrapper || d.inner !is FCastCastingDevice) {
|
||||||
|
return@addOrUpdateCastDevice false;
|
||||||
|
}
|
||||||
|
|
||||||
|
val changed =
|
||||||
|
addresses.contentEquals(addresses) || d.name != name || d.inner.port != port;
|
||||||
|
if (changed) {
|
||||||
|
d.inner.name = name;
|
||||||
|
d.inner.port = port;
|
||||||
|
d.inner.addresses = addresses;
|
||||||
|
}
|
||||||
|
|
||||||
|
return@addOrUpdateCastDevice changed;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun addOrUpdateCastDevice(
|
||||||
|
name: String,
|
||||||
|
deviceFactory: () -> CastingDevice,
|
||||||
|
deviceUpdater: (device: CastingDevice) -> Boolean
|
||||||
|
) {
|
||||||
|
var invokeEvents: (() -> Unit)? = null;
|
||||||
|
|
||||||
|
synchronized(devices) {
|
||||||
|
val device = devices[name];
|
||||||
|
if (device != null) {
|
||||||
|
val changed = deviceUpdater(device);
|
||||||
|
if (changed) {
|
||||||
|
invokeEvents = {
|
||||||
|
onDeviceChanged.emit(device);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val newDevice = deviceFactory();
|
||||||
|
this.devices[name] = newDevice
|
||||||
|
|
||||||
|
invokeEvents = {
|
||||||
|
onDeviceAdded.emit(newDevice);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
invokeEvents?.let { _scopeMain.launch { it(); }; };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
private data class FCastNetworkConfig(
|
||||||
|
val name: String,
|
||||||
|
val addresses: List<String>,
|
||||||
|
val services: List<FCastService>
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
private data class FCastService(
|
||||||
|
val port: Int,
|
||||||
|
val type: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = "StateCastingLegacy"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,11 +8,13 @@ import android.view.View
|
|||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.casting.CastProtocolType
|
import com.futo.platformplayer.casting.CastProtocolType
|
||||||
import com.futo.platformplayer.casting.StateCasting
|
import com.futo.platformplayer.casting.StateCasting
|
||||||
import com.futo.platformplayer.models.CastingDeviceInfo
|
import com.futo.platformplayer.models.CastingDeviceInfo
|
||||||
import com.futo.platformplayer.toInetAddress
|
import com.futo.platformplayer.toInetAddress
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
|
||||||
|
|
||||||
class CastingAddDialog(context: Context?) : AlertDialog(context) {
|
class CastingAddDialog(context: Context?) : AlertDialog(context) {
|
||||||
@@ -38,7 +40,13 @@ class CastingAddDialog(context: Context?) : AlertDialog(context) {
|
|||||||
_buttonConfirm = findViewById(R.id.button_confirm);
|
_buttonConfirm = findViewById(R.id.button_confirm);
|
||||||
_buttonTutorial = findViewById(R.id.button_tutorial)
|
_buttonTutorial = findViewById(R.id.button_tutorial)
|
||||||
|
|
||||||
ArrayAdapter.createFromResource(context, R.array.casting_device_type_array, R.layout.spinner_item_simple).also { adapter ->
|
val deviceTypeArray = if (Settings.instance.casting.experimentalCasting) {
|
||||||
|
R.array.exp_casting_device_type_array
|
||||||
|
} else {
|
||||||
|
R.array.casting_device_type_array
|
||||||
|
}
|
||||||
|
|
||||||
|
ArrayAdapter.createFromResource(context, deviceTypeArray, R.layout.spinner_item_simple).also { adapter ->
|
||||||
adapter.setDropDownViewResource(R.layout.spinner_dropdownitem_simple);
|
adapter.setDropDownViewResource(R.layout.spinner_dropdownitem_simple);
|
||||||
_spinnerType.adapter = adapter;
|
_spinnerType.adapter = adapter;
|
||||||
};
|
};
|
||||||
@@ -101,7 +109,11 @@ class CastingAddDialog(context: Context?) : AlertDialog(context) {
|
|||||||
|
|
||||||
_textError.visibility = View.GONE;
|
_textError.visibility = View.GONE;
|
||||||
val castingDeviceInfo = CastingDeviceInfo(name, castProtocolType, arrayOf(ip), port.toInt());
|
val castingDeviceInfo = CastingDeviceInfo(name, castProtocolType, arrayOf(ip), port.toInt());
|
||||||
StateCasting.instance.addRememberedDevice(castingDeviceInfo);
|
try {
|
||||||
|
StateCasting.instance.addRememberedDevice(castingDeviceInfo)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to add remembered device: $e")
|
||||||
|
}
|
||||||
performDismiss();
|
performDismiss();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import android.os.Bundle
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.ImageButton
|
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
@@ -18,7 +17,6 @@ import com.futo.platformplayer.R
|
|||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.casting.CastConnectionState
|
import com.futo.platformplayer.casting.CastConnectionState
|
||||||
import com.futo.platformplayer.casting.CastingDevice
|
|
||||||
import com.futo.platformplayer.casting.StateCasting
|
import com.futo.platformplayer.casting.StateCasting
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
@@ -108,15 +106,16 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
|
|||||||
synchronized(StateCasting.instance.devices) {
|
synchronized(StateCasting.instance.devices) {
|
||||||
_devices.addAll(StateCasting.instance.devices.values.mapNotNull { it.name })
|
_devices.addAll(StateCasting.instance.devices.values.mapNotNull { it.name })
|
||||||
}
|
}
|
||||||
|
|
||||||
_rememberedDevices.addAll(StateCasting.instance.getRememberedCastingDeviceNames())
|
_rememberedDevices.addAll(StateCasting.instance.getRememberedCastingDeviceNames())
|
||||||
|
|
||||||
updateUnifiedList()
|
updateUnifiedList()
|
||||||
|
|
||||||
StateCasting.instance.onDeviceAdded.subscribe(this) { d ->
|
StateCasting.instance.onDeviceAdded.subscribe(this) { d ->
|
||||||
val name = d.name
|
val name = d.name
|
||||||
if (name != null)
|
if (name != null) {
|
||||||
_devices.add(name)
|
_devices.add(name)
|
||||||
updateUnifiedList()
|
updateUnifiedList()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StateCasting.instance.onDeviceChanged.subscribe(this) { d ->
|
StateCasting.instance.onDeviceChanged.subscribe(this) { d ->
|
||||||
|
|||||||
@@ -12,12 +12,11 @@ import android.widget.ImageView
|
|||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.casting.AirPlayCastingDevice
|
|
||||||
import com.futo.platformplayer.casting.CastConnectionState
|
import com.futo.platformplayer.casting.CastConnectionState
|
||||||
|
import com.futo.platformplayer.casting.CastProtocolType
|
||||||
import com.futo.platformplayer.casting.CastingDevice
|
import com.futo.platformplayer.casting.CastingDevice
|
||||||
import com.futo.platformplayer.casting.ChromecastCastingDevice
|
|
||||||
import com.futo.platformplayer.casting.FCastCastingDevice
|
|
||||||
import com.futo.platformplayer.casting.StateCasting
|
import com.futo.platformplayer.casting.StateCasting
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.VideoDetailFragment
|
import com.futo.platformplayer.fragment.mainactivity.main.VideoDetailFragment
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
@@ -69,18 +68,18 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
|||||||
|
|
||||||
_buttonPlay = findViewById(R.id.button_play);
|
_buttonPlay = findViewById(R.id.button_play);
|
||||||
_buttonPlay.setOnClickListener {
|
_buttonPlay.setOnClickListener {
|
||||||
StateCasting.instance.activeDevice?.resumeVideo()
|
StateCasting.instance.resumeVideo()
|
||||||
}
|
}
|
||||||
|
|
||||||
_buttonPause = findViewById(R.id.button_pause);
|
_buttonPause = findViewById(R.id.button_pause);
|
||||||
_buttonPause.setOnClickListener {
|
_buttonPause.setOnClickListener {
|
||||||
StateCasting.instance.activeDevice?.pauseVideo()
|
StateCasting.instance.pauseVideo()
|
||||||
}
|
}
|
||||||
|
|
||||||
_buttonStop = findViewById(R.id.button_stop);
|
_buttonStop = findViewById(R.id.button_stop);
|
||||||
_buttonStop.setOnClickListener {
|
_buttonStop.setOnClickListener {
|
||||||
(ownerActivity as MainActivity?)?.getFragment<VideoDetailFragment>()?.closeVideoDetails()
|
(ownerActivity as MainActivity?)?.getFragment<VideoDetailFragment>()?.closeVideoDetails()
|
||||||
StateCasting.instance.activeDevice?.stopVideo()
|
StateCasting.instance.stopVideo()
|
||||||
}
|
}
|
||||||
|
|
||||||
_buttonNext = findViewById(R.id.button_next);
|
_buttonNext = findViewById(R.id.button_next);
|
||||||
@@ -90,7 +89,11 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
|||||||
|
|
||||||
_buttonClose.setOnClickListener { dismiss(); };
|
_buttonClose.setOnClickListener { dismiss(); };
|
||||||
_buttonDisconnect.setOnClickListener {
|
_buttonDisconnect.setOnClickListener {
|
||||||
StateCasting.instance.activeDevice?.stopCasting();
|
try {
|
||||||
|
StateCasting.instance.activeDevice?.disconnect()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Active device failed to disconnect: $e")
|
||||||
|
}
|
||||||
dismiss();
|
dismiss();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -99,12 +102,7 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
|||||||
return@OnChangeListener
|
return@OnChangeListener
|
||||||
}
|
}
|
||||||
|
|
||||||
val activeDevice = StateCasting.instance.activeDevice ?: return@OnChangeListener;
|
StateCasting.instance.videoSeekTo(value.toDouble())
|
||||||
try {
|
|
||||||
activeDevice.seekVideo(value.toDouble());
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.e(TAG, "Failed to change volume.", e);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
//TODO: Check if volume slider is properly hidden in all cases
|
//TODO: Check if volume slider is properly hidden in all cases
|
||||||
@@ -113,14 +111,7 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
|||||||
return@OnChangeListener
|
return@OnChangeListener
|
||||||
}
|
}
|
||||||
|
|
||||||
val activeDevice = StateCasting.instance.activeDevice ?: return@OnChangeListener;
|
StateCasting.instance.changeVolume(value.toDouble())
|
||||||
if (activeDevice.canSetVolume) {
|
|
||||||
try {
|
|
||||||
activeDevice.changeVolume(value.toDouble());
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.e(TAG, "Failed to change volume.", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -172,15 +163,25 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
|||||||
private fun updateDevice() {
|
private fun updateDevice() {
|
||||||
val d = StateCasting.instance.activeDevice ?: return;
|
val d = StateCasting.instance.activeDevice ?: return;
|
||||||
|
|
||||||
if (d is ChromecastCastingDevice) {
|
when (d.protocolType) {
|
||||||
_imageDevice.setImageResource(R.drawable.ic_chromecast);
|
CastProtocolType.CHROMECAST -> {
|
||||||
_textType.text = "Chromecast";
|
_imageDevice.setImageResource(R.drawable.ic_chromecast);
|
||||||
} else if (d is AirPlayCastingDevice) {
|
_textType.text = "Chromecast";
|
||||||
_imageDevice.setImageResource(R.drawable.ic_airplay);
|
}
|
||||||
_textType.text = "AirPlay";
|
CastProtocolType.AIRPLAY -> {
|
||||||
} else if (d is FCastCastingDevice) {
|
_imageDevice.setImageResource(R.drawable.ic_airplay);
|
||||||
_imageDevice.setImageResource(R.drawable.ic_fc);
|
_textType.text = "AirPlay";
|
||||||
_textType.text = "FastCast";
|
}
|
||||||
|
CastProtocolType.FCAST -> {
|
||||||
|
_imageDevice.setImageResource(
|
||||||
|
if (Settings.instance.casting.experimentalCasting) {
|
||||||
|
R.drawable.ic_exp_fc
|
||||||
|
} else {
|
||||||
|
R.drawable.ic_fc
|
||||||
|
}
|
||||||
|
)
|
||||||
|
_textType.text = "FCast";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_textName.text = d.name;
|
_textName.text = d.name;
|
||||||
@@ -192,7 +193,7 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
|||||||
_sliderPosition.value = d.time.toFloat().coerceAtLeast(0.0f).coerceAtMost(dur)
|
_sliderPosition.value = d.time.toFloat().coerceAtLeast(0.0f).coerceAtMost(dur)
|
||||||
_sliderPosition.valueTo = dur
|
_sliderPosition.valueTo = dur
|
||||||
|
|
||||||
if (d.canSetVolume) {
|
if (d.canSetVolume()) {
|
||||||
_layoutVolumeAdjustable.visibility = View.VISIBLE;
|
_layoutVolumeAdjustable.visibility = View.VISIBLE;
|
||||||
_layoutVolumeFixed.visibility = View.GONE;
|
_layoutVolumeFixed.visibility = View.GONE;
|
||||||
} else {
|
} else {
|
||||||
@@ -214,8 +215,7 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
|
|||||||
CastConnectionState.CONNECTED -> {
|
CastConnectionState.CONNECTED -> {
|
||||||
enableControls(interactiveControls)
|
enableControls(interactiveControls)
|
||||||
}
|
}
|
||||||
CastConnectionState.CONNECTING,
|
CastConnectionState.CONNECTING, CastConnectionState.DISCONNECTED -> {
|
||||||
CastConnectionState.DISCONNECTED -> {
|
|
||||||
disableControls(interactiveControls)
|
disableControls(interactiveControls)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -719,7 +719,7 @@ class VideoDownload {
|
|||||||
|
|
||||||
Logger.i(TAG, "Download $name Dash, CueCount: " + foundCues.count().toString());
|
Logger.i(TAG, "Download $name Dash, CueCount: " + foundCues.count().toString());
|
||||||
|
|
||||||
var written = 0;
|
var written: Long = 0;
|
||||||
var indexCounter = 0;
|
var indexCounter = 0;
|
||||||
onProgress(foundCues.count().toLong(), 0, 0);
|
onProgress(foundCues.count().toLong(), 0, 0);
|
||||||
for(cue in foundCues) {
|
for(cue in foundCues) {
|
||||||
@@ -744,7 +744,7 @@ class VideoDownload {
|
|||||||
|
|
||||||
indexCounter++;
|
indexCounter++;
|
||||||
}
|
}
|
||||||
sourceLength = written.toLong();
|
sourceLength = written;
|
||||||
|
|
||||||
Logger.i(TAG, "$name downloadSource Finished");
|
Logger.i(TAG, "$name downloadSource Finished");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -242,10 +242,12 @@ class V8Plugin {
|
|||||||
}
|
}
|
||||||
fun <T> busy(handle: ()->T): T {
|
fun <T> busy(handle: ()->T): T {
|
||||||
_busyLock.lock();
|
_busyLock.lock();
|
||||||
|
//Logger.i(TAG, "Busy Enter [" + _busyLock.holdCount + "]" + Thread.currentThread().stackTrace.drop(3)?.firstOrNull()?.toString() + ", " + Thread.currentThread().stackTrace.drop(4)?.firstOrNull()?.toString() + ", " + Thread.currentThread().stackTrace.drop(5)?.firstOrNull()?.toString())
|
||||||
try {
|
try {
|
||||||
return handle();
|
return handle();
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
|
//Logger.i(TAG, "Busy Leave [" + _busyLock.holdCount + "]" + Thread.currentThread().stackTrace.drop(3)?.firstOrNull()?.toString() + ", " + Thread.currentThread().stackTrace.drop(4)?.firstOrNull()?.toString()+ ", " + Thread.currentThread().stackTrace.drop(5)?.firstOrNull()?.toString())
|
||||||
_busyLock.unlock();
|
_busyLock.unlock();
|
||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -194,7 +194,11 @@ class PackageBridge : V8Package {
|
|||||||
|
|
||||||
val stackTrace = Thread.currentThread().stackTrace;
|
val stackTrace = Thread.currentThread().stackTrace;
|
||||||
val callerMethod = stackTrace.findLast {
|
val callerMethod = stackTrace.findLast {
|
||||||
it.className == JSClient::class.java.name
|
it.className == JSClient::class.java.name &&
|
||||||
|
it.methodName != "isBusy" &&
|
||||||
|
it.methodName != "busy" &&
|
||||||
|
it.methodName != "getCopy" &&
|
||||||
|
it.methodName != "isBusyWith"
|
||||||
}?.methodName ?: "";
|
}?.methodName ?: "";
|
||||||
val session = StateApp.instance.sessionId;
|
val session = StateApp.instance.sessionId;
|
||||||
val pluginId = _plugin.config.id;
|
val pluginId = _plugin.config.id;
|
||||||
|
|||||||
@@ -279,6 +279,14 @@ class HomeFragment : MainFragment() {
|
|||||||
else {
|
else {
|
||||||
view.setToggle(!active);
|
view.setToggle(!active);
|
||||||
}
|
}
|
||||||
|
}, { view, views, enabled ->
|
||||||
|
val toDisable = views.filter { it != view && it.tag == "plugins" };
|
||||||
|
if(!view.isActive)
|
||||||
|
view.handleClick();
|
||||||
|
for(tag in toDisable) {
|
||||||
|
if(tag.isActive)
|
||||||
|
tag.handleClick();
|
||||||
|
}
|
||||||
}).withTag("plugins")
|
}).withTag("plugins")
|
||||||
})
|
})
|
||||||
else listOf())
|
else listOf())
|
||||||
|
|||||||
+105
-505
@@ -1,46 +1,27 @@
|
|||||||
package com.futo.platformplayer.fragment.mainactivity.main
|
package com.futo.platformplayer.fragment.mainactivity.main
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.DialogInterface
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.graphics.drawable.Animatable
|
import android.graphics.drawable.Animatable
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.text.Spanned
|
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.util.TypedValue
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.SoundEffectConstants
|
|
||||||
import android.view.View
|
|
||||||
import android.view.animation.AccelerateInterpolator
|
import android.view.animation.AccelerateInterpolator
|
||||||
import android.view.animation.OvershootInterpolator
|
import android.view.animation.OvershootInterpolator
|
||||||
import android.widget.Button
|
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.core.graphics.drawable.toDrawable
|
|
||||||
import androidx.core.net.toUri
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.media3.common.C
|
import androidx.media3.common.C
|
||||||
import androidx.media3.common.Format
|
import androidx.media3.common.Format
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import com.bumptech.glide.Glide
|
|
||||||
import com.bumptech.glide.request.target.CustomTarget
|
|
||||||
import com.bumptech.glide.request.transition.Transition
|
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.api.media.PlatformID
|
|
||||||
import com.futo.platformplayer.api.media.exceptions.ContentNotAvailableYetException
|
import com.futo.platformplayer.api.media.exceptions.ContentNotAvailableYetException
|
||||||
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
|
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
|
||||||
import com.futo.platformplayer.api.media.models.PlatformAuthorMembershipLink
|
|
||||||
import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
|
|
||||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes
|
import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes
|
||||||
import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
|
||||||
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
|
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
||||||
@@ -54,40 +35,30 @@ import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
|||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.casting.CastConnectionState
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawAudioSource
|
||||||
import com.futo.platformplayer.casting.StateCasting
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawSource
|
||||||
import com.futo.platformplayer.constructs.Event0
|
import com.futo.platformplayer.constructs.Event0
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.constructs.Event3
|
import com.futo.platformplayer.constructs.Event3
|
||||||
import com.futo.platformplayer.constructs.TaskHandler
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
import com.futo.platformplayer.downloads.VideoLocal
|
import com.futo.platformplayer.downloads.VideoLocal
|
||||||
import com.futo.platformplayer.dp
|
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptAgeException
|
import com.futo.platformplayer.engine.exceptions.ScriptAgeException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptException
|
import com.futo.platformplayer.engine.exceptions.ScriptException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
import com.futo.platformplayer.engine.exceptions.ScriptImplementationException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptLoginRequiredException
|
import com.futo.platformplayer.engine.exceptions.ScriptLoginRequiredException
|
||||||
import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
|
import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
|
||||||
import com.futo.platformplayer.exceptions.UnsupportedCastException
|
import com.futo.platformplayer.exceptions.UnsupportedCastException
|
||||||
import com.futo.platformplayer.fixHtmlLinks
|
import com.futo.platformplayer.fragment.mainactivity.special.CommentsModalBottomSheet
|
||||||
import com.futo.platformplayer.getNowDiffSeconds
|
|
||||||
import com.futo.platformplayer.helpers.VideoHelper
|
import com.futo.platformplayer.helpers.VideoHelper
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.selectBestImage
|
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StateMeta
|
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.platformplayer.states.StatePlugins
|
import com.futo.platformplayer.states.StatePlugins
|
||||||
import com.futo.platformplayer.states.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
import com.futo.platformplayer.toHumanBitrate
|
import com.futo.platformplayer.toHumanBitrate
|
||||||
import com.futo.platformplayer.toHumanBytesSize
|
import com.futo.platformplayer.toHumanBytesSize
|
||||||
import com.futo.platformplayer.toHumanNowDiffString
|
import com.futo.platformplayer.views.buttons.ShortsButton
|
||||||
import com.futo.platformplayer.toHumanNumber
|
|
||||||
import com.futo.platformplayer.views.MonetizationView
|
|
||||||
import com.futo.platformplayer.views.comments.AddCommentView
|
|
||||||
import com.futo.platformplayer.views.others.CreatorThumbnail
|
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||||
import com.futo.platformplayer.views.overlays.DescriptionOverlay
|
|
||||||
import com.futo.platformplayer.views.overlays.RepliesOverlay
|
|
||||||
import com.futo.platformplayer.views.overlays.SupportOverlay
|
|
||||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuButtonList
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuButtonList
|
||||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuGroup
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuGroup
|
||||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
|
||||||
@@ -95,20 +66,17 @@ import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
|||||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTitle
|
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTitle
|
||||||
import com.futo.platformplayer.views.pills.OnLikeDislikeUpdatedArgs
|
import com.futo.platformplayer.views.pills.OnLikeDislikeUpdatedArgs
|
||||||
import com.futo.platformplayer.views.platform.PlatformIndicator
|
import com.futo.platformplayer.views.platform.PlatformIndicator
|
||||||
import com.futo.platformplayer.views.segments.CommentsList
|
|
||||||
import com.futo.platformplayer.views.video.FutoShortPlayer
|
import com.futo.platformplayer.views.video.FutoShortPlayer
|
||||||
import com.futo.platformplayer.views.video.FutoVideoPlayerBase
|
import com.futo.platformplayer.views.video.FutoVideoPlayerBase
|
||||||
|
import com.futo.platformplayer.views.video.FutoVideoPlayerBase.Companion.PREFERED_AUDIO_CONTAINERS
|
||||||
|
import com.futo.platformplayer.views.video.FutoVideoPlayerBase.Companion.PREFERED_VIDEO_CONTAINERS
|
||||||
import com.futo.polycentric.core.ApiMethods
|
import com.futo.polycentric.core.ApiMethods
|
||||||
import com.futo.polycentric.core.ContentType
|
import com.futo.polycentric.core.ContentType
|
||||||
import com.futo.polycentric.core.Models
|
import com.futo.polycentric.core.Models
|
||||||
import com.futo.polycentric.core.Opinion
|
import com.futo.polycentric.core.Opinion
|
||||||
import com.futo.polycentric.core.PolycentricProfile
|
|
||||||
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
|
import com.futo.polycentric.core.fullyBackfillServersAnnounceExceptions
|
||||||
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
|
||||||
import com.google.android.material.button.MaterialButton
|
import com.google.android.material.button.MaterialButton
|
||||||
|
//import com.google.android.material.button.MaterialButton
|
||||||
import com.google.protobuf.ByteString
|
import com.google.protobuf.ByteString
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -116,30 +84,29 @@ import userpackage.Protocol
|
|||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
class ShortView : FrameLayout {
|
class ShortView : FrameLayout {
|
||||||
private lateinit var mainFragment: MainFragment
|
private lateinit var fragment: MainFragment
|
||||||
private val player: FutoShortPlayer
|
private val player: FutoShortPlayer
|
||||||
|
|
||||||
private val channelInfo: LinearLayout
|
private val channelInfo: LinearLayout
|
||||||
private val creatorThumbnail: CreatorThumbnail
|
private val creatorThumbnail: CreatorThumbnail
|
||||||
private val channelName: TextView
|
private val channelName: TextView
|
||||||
private val videoTitle: TextView
|
private val videoTitle: TextView
|
||||||
|
private val videoSubtitle: TextView
|
||||||
private val platformIndicator: PlatformIndicator
|
private val platformIndicator: PlatformIndicator
|
||||||
|
|
||||||
|
//TODO: Replace with non-material button
|
||||||
private val backButton: MaterialButton
|
private val backButton: MaterialButton
|
||||||
private val backButtonContainer: ConstraintLayout
|
private val backButtonContainer: ConstraintLayout
|
||||||
|
|
||||||
private val likeContainer: FrameLayout
|
private val likeButton: ShortsButton
|
||||||
private val dislikeContainer: FrameLayout
|
//private val likeCount: TextView
|
||||||
private val likeButton: MaterialButton
|
private val dislikeButton: ShortsButton
|
||||||
private val likeCount: TextView
|
//private val dislikeCount: TextView
|
||||||
private val dislikeButton: MaterialButton
|
|
||||||
private val dislikeCount: TextView
|
|
||||||
|
|
||||||
private val commentsButton: MaterialButton
|
private val commentsButton: ShortsButton
|
||||||
private val shareButton: MaterialButton
|
private val shareButton: ShortsButton
|
||||||
private val refreshButton: MaterialButton
|
private val refreshButton: ShortsButton
|
||||||
private val refreshButtonContainer: View
|
private val qualityButton: ShortsButton
|
||||||
private val qualityButton: MaterialButton
|
|
||||||
|
|
||||||
private val playPauseOverlay: FrameLayout
|
private val playPauseOverlay: FrameLayout
|
||||||
private val playPauseIcon: ImageView
|
private val playPauseIcon: ImageView
|
||||||
@@ -173,18 +140,21 @@ class ShortView : FrameLayout {
|
|||||||
private val onLikeDislikeUpdated = Event1<OnLikeDislikeUpdatedArgs>()
|
private val onLikeDislikeUpdated = Event1<OnLikeDislikeUpdatedArgs>()
|
||||||
private val onVideoUpdated = Event1<IPlatformVideo?>()
|
private val onVideoUpdated = Event1<IPlatformVideo?>()
|
||||||
|
|
||||||
|
//TODO: Replace with non-material UI? Only true dependency on Material left
|
||||||
private val bottomSheet: CommentsModalBottomSheet = CommentsModalBottomSheet()
|
private val bottomSheet: CommentsModalBottomSheet = CommentsModalBottomSheet()
|
||||||
|
|
||||||
var likes: Long = 0
|
var likes: Long = 0
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
likeCount.text = value.toString()
|
likeButton.withPrimaryText(value.toString());
|
||||||
|
//likeCount.text = value.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
var dislikes: Long = 0
|
var dislikes: Long = 0
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
dislikeCount.text = value.toString()
|
dislikeButton.withPrimaryText(value.toString());
|
||||||
|
//dislikeCount.text = value.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(inflater: LayoutInflater, fragment: MainFragment, overlayQualityContainer: FrameLayout) : this(inflater.context) {
|
constructor(inflater: LayoutInflater, fragment: MainFragment, overlayQualityContainer: FrameLayout) : this(inflater.context) {
|
||||||
@@ -194,7 +164,7 @@ class ShortView : FrameLayout {
|
|||||||
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT
|
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT
|
||||||
)
|
)
|
||||||
|
|
||||||
this.mainFragment = fragment
|
this.fragment = fragment
|
||||||
bottomSheet.mainFragment = fragment
|
bottomSheet.mainFragment = fragment
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,19 +187,17 @@ class ShortView : FrameLayout {
|
|||||||
creatorThumbnail = findViewById(R.id.creator_thumbnail)
|
creatorThumbnail = findViewById(R.id.creator_thumbnail)
|
||||||
channelName = findViewById(R.id.channel_name)
|
channelName = findViewById(R.id.channel_name)
|
||||||
videoTitle = findViewById(R.id.video_title)
|
videoTitle = findViewById(R.id.video_title)
|
||||||
|
videoSubtitle = findViewById(R.id.video_subtitle)
|
||||||
platformIndicator = findViewById(R.id.short_platform_indicator)
|
platformIndicator = findViewById(R.id.short_platform_indicator)
|
||||||
backButton = findViewById(R.id.back_button)
|
backButton = findViewById(R.id.back_button)
|
||||||
backButtonContainer = findViewById(R.id.back_button_container)
|
backButtonContainer = findViewById(R.id.back_button_container)
|
||||||
likeContainer = findViewById(R.id.like_container)
|
|
||||||
dislikeContainer = findViewById(R.id.dislike_container)
|
|
||||||
likeButton = findViewById(R.id.like_button)
|
likeButton = findViewById(R.id.like_button)
|
||||||
likeCount = findViewById(R.id.like_count)
|
//likeCount = findViewById(R.id.like_count)
|
||||||
dislikeButton = findViewById(R.id.dislike_button)
|
dislikeButton = findViewById(R.id.dislike_button)
|
||||||
dislikeCount = findViewById(R.id.dislike_count)
|
//dislikeCount = findViewById(R.id.dislike_count)
|
||||||
commentsButton = findViewById(R.id.comments_button)
|
commentsButton = findViewById(R.id.comments_button)
|
||||||
shareButton = findViewById(R.id.share_button)
|
shareButton = findViewById(R.id.share_button)
|
||||||
refreshButton = findViewById(R.id.refresh_button)
|
refreshButton = findViewById(R.id.refresh_button)
|
||||||
refreshButtonContainer = findViewById(R.id.refresh_button_container)
|
|
||||||
qualityButton = findViewById(R.id.quality_button)
|
qualityButton = findViewById(R.id.quality_button)
|
||||||
playPauseOverlay = findViewById(R.id.play_pause_overlay)
|
playPauseOverlay = findViewById(R.id.play_pause_overlay)
|
||||||
playPauseIcon = findViewById(R.id.play_pause_icon)
|
playPauseIcon = findViewById(R.id.play_pause_icon)
|
||||||
@@ -258,48 +226,44 @@ class ShortView : FrameLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onVideoUpdated.subscribe {
|
onVideoUpdated.subscribe {
|
||||||
|
Logger.i(TAG, "Shorts videoUpdated [${it?.name}] (isDetail: ${it is IPlatformVideoDetails}, thumbnail: ${it?.author?.thumbnail})");
|
||||||
videoTitle.text = it?.name
|
videoTitle.text = it?.name
|
||||||
|
videoSubtitle.text = if(it is IPlatformVideoDetails) it?.description; else "";
|
||||||
platformIndicator.setPlatformFromClientID(it?.id?.pluginId)
|
platformIndicator.setPlatformFromClientID(it?.id?.pluginId)
|
||||||
creatorThumbnail.setThumbnail(it?.author?.thumbnail, true)
|
creatorThumbnail.setThumbnail(it?.author?.thumbnail, true)
|
||||||
channelName.text = it?.author?.name
|
channelName.text = it?.author?.name
|
||||||
}
|
}
|
||||||
|
|
||||||
backButton.setOnClickListener {
|
backButton.setOnClickListener {
|
||||||
playSoundEffect(SoundEffectConstants.CLICK)
|
fragment.closeSegment()
|
||||||
mainFragment.closeSegment()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
channelInfo.setOnClickListener {
|
channelInfo.setOnClickListener {
|
||||||
playSoundEffect(SoundEffectConstants.CLICK)
|
fragment.navigate<ChannelFragment>(video?.author)
|
||||||
mainFragment.navigate<ChannelFragment>(video?.author)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
videoTitle.setOnClickListener {
|
videoTitle.setOnClickListener {
|
||||||
playSoundEffect(SoundEffectConstants.CLICK)
|
|
||||||
if (!bottomSheet.isAdded) {
|
if (!bottomSheet.isAdded) {
|
||||||
bottomSheet.show(mainFragment.childFragmentManager, CommentsModalBottomSheet.TAG)
|
bottomSheet.show(fragment.childFragmentManager, CommentsModalBottomSheet.TAG)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
commentsButton.setOnClickListener {
|
commentsButton.onClick.subscribe {
|
||||||
playSoundEffect(SoundEffectConstants.CLICK)
|
|
||||||
if (!bottomSheet.isAdded) {
|
if (!bottomSheet.isAdded) {
|
||||||
bottomSheet.show(mainFragment.childFragmentManager, CommentsModalBottomSheet.TAG)
|
bottomSheet.show(fragment.childFragmentManager, CommentsModalBottomSheet.TAG)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
shareButton.setOnClickListener {
|
shareButton.onClick.subscribe {
|
||||||
playSoundEffect(SoundEffectConstants.CLICK)
|
|
||||||
val url = video?.shareUrl ?: video?.url
|
val url = video?.shareUrl ?: video?.url
|
||||||
mainFragment.startActivity(Intent.createChooser(Intent().apply {
|
fragment.startActivity(Intent.createChooser(Intent().apply {
|
||||||
action = Intent.ACTION_SEND
|
action = Intent.ACTION_SEND
|
||||||
putExtra(Intent.EXTRA_TEXT, url)
|
putExtra(Intent.EXTRA_TEXT, url)
|
||||||
type = "text/plain"
|
type = "text/plain"
|
||||||
}, null))
|
}, null))
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshButton.setOnClickListener {
|
refreshButton.onClick.subscribe {
|
||||||
playSoundEffect(SoundEffectConstants.CLICK)
|
|
||||||
onResetTriggered.emit()
|
onResetTriggered.emit()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,14 +272,12 @@ class ShortView : FrameLayout {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
qualityButton.setOnClickListener {
|
qualityButton.onClick.subscribe {
|
||||||
playSoundEffect(SoundEffectConstants.CLICK)
|
|
||||||
showVideoSettings()
|
showVideoSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
likeButton.setOnClickListener {
|
likeButton.onClick.subscribe {
|
||||||
playSoundEffect(SoundEffectConstants.CLICK)
|
val checked = likeButton.iconId == R.drawable.ic_thumb_up_s // !likeButton.isChecked
|
||||||
val checked = !likeButton.isChecked
|
|
||||||
StatePolycentric.instance.requireLogin(context, context.getString(R.string.please_login_to_like)) {
|
StatePolycentric.instance.requireLogin(context, context.getString(R.string.please_login_to_like)) {
|
||||||
if (checked) {
|
if (checked) {
|
||||||
likes++
|
likes++
|
||||||
@@ -323,24 +285,27 @@ class ShortView : FrameLayout {
|
|||||||
likes--
|
likes--
|
||||||
}
|
}
|
||||||
|
|
||||||
likeButton.isChecked = checked
|
if(checked)
|
||||||
|
likeButton.withIcon(R.drawable.ic_thumb_up_s_filled) //.isChecked = checked
|
||||||
|
else
|
||||||
|
likeButton.withIcon(R.drawable.ic_thumb_up_s)
|
||||||
|
|
||||||
if (dislikeButton.isChecked && checked) {
|
if (dislikeButton.iconId == R.drawable.ic_thumb_down_s_filled && checked) {
|
||||||
dislikeButton.isChecked = false
|
//dislikeButton.isChecked = false
|
||||||
|
dislikeButton.withIcon(R.drawable.ic_thumb_down_s)
|
||||||
dislikes--
|
dislikes--
|
||||||
}
|
}
|
||||||
|
|
||||||
onLikeDislikeUpdated.emit(
|
onLikeDislikeUpdated.emit(
|
||||||
OnLikeDislikeUpdatedArgs(
|
OnLikeDislikeUpdatedArgs(
|
||||||
it, likes, likeButton.isChecked, dislikes, dislikeButton.isChecked
|
it, likes, checked, dislikes, !checked
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dislikeButton.setOnClickListener {
|
dislikeButton.onClick.subscribe {
|
||||||
playSoundEffect(SoundEffectConstants.CLICK)
|
val checked = dislikeButton.iconId == R.drawable.ic_thumb_down_s //!dislikeButton.isChecked
|
||||||
val checked = !dislikeButton.isChecked
|
|
||||||
StatePolycentric.instance.requireLogin(context, context.getString(R.string.please_login_to_like)) {
|
StatePolycentric.instance.requireLogin(context, context.getString(R.string.please_login_to_like)) {
|
||||||
if (checked) {
|
if (checked) {
|
||||||
dislikes++
|
dislikes++
|
||||||
@@ -348,16 +313,21 @@ class ShortView : FrameLayout {
|
|||||||
dislikes--
|
dislikes--
|
||||||
}
|
}
|
||||||
|
|
||||||
dislikeButton.isChecked = checked
|
//dislikeButton.isChecked = checked
|
||||||
|
if(checked)
|
||||||
|
dislikeButton.withIcon(R.drawable.ic_thumb_down_s_filled) //.isChecked = checked
|
||||||
|
else
|
||||||
|
dislikeButton.withIcon(R.drawable.ic_thumb_down_s)
|
||||||
|
|
||||||
if (likeButton.isChecked && checked) {
|
if (likeButton.iconId == R.drawable.ic_thumb_up_s_filled && checked) {
|
||||||
likeButton.isChecked = false
|
//likeButton.isChecked = false
|
||||||
|
likeButton.withIcon(R.drawable.ic_thumb_up_s);
|
||||||
likes--
|
likes--
|
||||||
}
|
}
|
||||||
|
|
||||||
onLikeDislikeUpdated.emit(
|
onLikeDislikeUpdated.emit(
|
||||||
OnLikeDislikeUpdatedArgs(
|
OnLikeDislikeUpdatedArgs(
|
||||||
it, likes, likeButton.isChecked, dislikes, dislikeButton.isChecked
|
it, likes, !checked, dislikes, checked
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -366,11 +336,11 @@ class ShortView : FrameLayout {
|
|||||||
onLikesLoaded.subscribe(tag) { rating, liked, disliked ->
|
onLikesLoaded.subscribe(tag) { rating, liked, disliked ->
|
||||||
likes = rating.likes
|
likes = rating.likes
|
||||||
dislikes = rating.dislikes
|
dislikes = rating.dislikes
|
||||||
likeButton.isChecked = liked
|
//likeButton.isChecked = liked
|
||||||
dislikeButton.isChecked = disliked
|
//dislikeButton.isChecked = disliked
|
||||||
|
|
||||||
dislikeContainer.visibility = VISIBLE
|
dislikeButton.visibility = VISIBLE
|
||||||
likeContainer.visibility = VISIBLE
|
likeButton.visibility = VISIBLE
|
||||||
}
|
}
|
||||||
|
|
||||||
player.onPlaybackStateChanged.subscribe {
|
player.onPlaybackStateChanged.subscribe {
|
||||||
@@ -565,7 +535,7 @@ class ShortView : FrameLayout {
|
|||||||
var toSet: ISubtitleSource? = subtitleSource
|
var toSet: ISubtitleSource? = subtitleSource
|
||||||
if (_lastSubtitleSource == subtitleSource) toSet = null
|
if (_lastSubtitleSource == subtitleSource) toSet = null
|
||||||
|
|
||||||
mainFragment.lifecycleScope.launch(Dispatchers.Main) {
|
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||||
try {
|
try {
|
||||||
player.swapSubtitles(toSet)
|
player.swapSubtitles(toSet)
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
@@ -625,7 +595,7 @@ class ShortView : FrameLayout {
|
|||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
fun setMainFragment(fragment: MainFragment, overlayQualityContainer: FrameLayout) {
|
fun setMainFragment(fragment: MainFragment, overlayQualityContainer: FrameLayout) {
|
||||||
this.mainFragment = fragment
|
this.fragment = fragment
|
||||||
this.bottomSheet.mainFragment = fragment
|
this.bottomSheet.mainFragment = fragment
|
||||||
this.overlayQualityContainer = overlayQualityContainer
|
this.overlayQualityContainer = overlayQualityContainer
|
||||||
}
|
}
|
||||||
@@ -636,10 +606,10 @@ class ShortView : FrameLayout {
|
|||||||
}
|
}
|
||||||
this.video = video
|
this.video = video
|
||||||
|
|
||||||
refreshButtonContainer.visibility = if (isChannelShortsMode) {
|
refreshButton.visibility = if (isChannelShortsMode) {
|
||||||
GONE
|
GONE
|
||||||
} else {
|
} else {
|
||||||
VISIBLE
|
GONE //TODO: Revert?
|
||||||
}
|
}
|
||||||
backButtonContainer.visibility = if (isChannelShortsMode) {
|
backButtonContainer.visibility = if (isChannelShortsMode) {
|
||||||
VISIBLE
|
VISIBLE
|
||||||
@@ -695,8 +665,8 @@ class ShortView : FrameLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun loadLikes(video: IPlatformVideo) {
|
private fun loadLikes(video: IPlatformVideo) {
|
||||||
likeContainer.visibility = GONE
|
likeButton.visibility = GONE
|
||||||
dislikeContainer.visibility = GONE
|
dislikeButton.visibility = GONE
|
||||||
|
|
||||||
loadLikesTask?.cancel()
|
loadLikesTask?.cancel()
|
||||||
loadLikesTask =
|
loadLikesTask =
|
||||||
@@ -735,13 +705,13 @@ class ShortView : FrameLayout {
|
|||||||
args.processHandle.opinion(ref, Opinion.neutral)
|
args.processHandle.opinion(ref, Opinion.neutral)
|
||||||
}
|
}
|
||||||
|
|
||||||
mainFragment.lifecycleScope.launch(Dispatchers.IO) {
|
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
Logger.i(CommentsModalBottomSheet.TAG, "Started backfill")
|
Logger.i(TAG, "Started backfill")
|
||||||
args.processHandle.fullyBackfillServersAnnounceExceptions()
|
args.processHandle.fullyBackfillServersAnnounceExceptions()
|
||||||
Logger.i(CommentsModalBottomSheet.TAG, "Finished backfill")
|
Logger.i(TAG, "Finished backfill")
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(CommentsModalBottomSheet.TAG, "Failed to backfill servers", e)
|
Logger.e(TAG, "Failed to backfill servers", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -763,20 +733,41 @@ class ShortView : FrameLayout {
|
|||||||
|
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
|
||||||
|
Logger.i(TAG, "Shorts loadVideo [${url}]");
|
||||||
|
val timeLoadVideoStart = System.currentTimeMillis();
|
||||||
loadVideoTask = TaskHandler<String, IPlatformVideoDetails>(
|
loadVideoTask = TaskHandler<String, IPlatformVideoDetails>(
|
||||||
StateApp.instance.scopeGetter, {
|
StateApp.instance.scopeGetter, {
|
||||||
val result = StatePlatform.instance.getContentDetails(it).await()
|
val result = StatePlatform.instance.getContentDetails(it).await()
|
||||||
if (result !is IPlatformVideoDetails) throw IllegalStateException("Expected media content, found ${result.contentType}")
|
if (result !is IPlatformVideoDetails) throw IllegalStateException("Expected media content, found ${result.contentType}")
|
||||||
return@TaskHandler result
|
return@TaskHandler result
|
||||||
}).success { result ->
|
}).success { result ->
|
||||||
videoDetails = result
|
val timeLoadVideo = System.currentTimeMillis() - timeLoadVideoStart;
|
||||||
video = result
|
Logger.i(TAG, "Shorts loadVideo [${url}] took ${timeLoadVideo}ms");
|
||||||
|
videoDetails = result
|
||||||
|
video = result
|
||||||
|
|
||||||
bottomSheet.video = result
|
if(Settings.instance.playback.shortsPregenerate)
|
||||||
|
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
if(result != null) {
|
||||||
|
val prefVid = VideoHelper.selectBestVideoSource(result.video, Settings.instance.playback.getCurrentPreferredQualityPixelCount(), PREFERED_VIDEO_CONTAINERS);
|
||||||
|
val prefAud = VideoHelper.selectBestAudioSource(result.video, PREFERED_AUDIO_CONTAINERS, Settings.instance.playback.getPrimaryLanguage(context));
|
||||||
|
|
||||||
setLoading(false)
|
if(prefVid != null && prefVid is JSDashManifestRawSource) {
|
||||||
|
Logger.i(TAG, "Shorts pregenerating video (${result.name})");
|
||||||
|
prefVid.pregenerateAsync(fragment.lifecycleScope);
|
||||||
|
}
|
||||||
|
if(prefAud != null && prefAud is JSDashManifestRawAudioSource) {
|
||||||
|
Logger.i(TAG, "Shorts pregenerating audio (${result.name})");
|
||||||
|
prefAud.pregenerateAsync(fragment.lifecycleScope);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (playWhenReady) playVideo()
|
bottomSheet.video = result
|
||||||
|
|
||||||
|
setLoading(false)
|
||||||
|
|
||||||
|
if (playWhenReady) playVideo()
|
||||||
}.exception<NoPlatformClientException> {
|
}.exception<NoPlatformClientException> {
|
||||||
Logger.w(TAG, "exception<NoPlatformClientException>", it)
|
Logger.w(TAG, "exception<NoPlatformClientException>", it)
|
||||||
UIDialogs.showDialog(
|
UIDialogs.showDialog(
|
||||||
@@ -799,7 +790,7 @@ class ShortView : FrameLayout {
|
|||||||
UIDialogs.showSingleButtonDialog(context, R.drawable.ic_schedule, "Video is available in ${it.availableWhen}.", "Close") { }
|
UIDialogs.showSingleButtonDialog(context, R.drawable.ic_schedule, "Video is available in ${it.availableWhen}.", "Close") { }
|
||||||
}.exception<ScriptImplementationException> {
|
}.exception<ScriptImplementationException> {
|
||||||
Logger.w(TAG, "exception<ScriptImplementationException>", it)
|
Logger.w(TAG, "exception<ScriptImplementationException>", it)
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video_scriptimplementationexception), it, { loadVideo(url) }, null, mainFragment)
|
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video_scriptimplementationexception), it, { loadVideo(url) }, null, fragment)
|
||||||
}.exception<ScriptAgeException> {
|
}.exception<ScriptAgeException> {
|
||||||
Logger.w(TAG, "exception<ScriptAgeException>", it)
|
Logger.w(TAG, "exception<ScriptAgeException>", it)
|
||||||
UIDialogs.showDialog(
|
UIDialogs.showDialog(
|
||||||
@@ -812,10 +803,10 @@ class ShortView : FrameLayout {
|
|||||||
)
|
)
|
||||||
}.exception<ScriptException> {
|
}.exception<ScriptException> {
|
||||||
Logger.w(TAG, "exception<ScriptException>", it)
|
Logger.w(TAG, "exception<ScriptException>", it)
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video_scriptexception), it, { loadVideo(url) }, null, mainFragment)
|
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video_scriptexception), it, { loadVideo(url) }, null, fragment)
|
||||||
}.exception<Throwable> {
|
}.exception<Throwable> {
|
||||||
Logger.w(ChannelFragment.TAG, "Failed to load video.", it)
|
Logger.w(ChannelFragment.TAG, "Failed to load video.", it)
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video), it, { loadVideo(url) }, null, mainFragment)
|
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video), it, { loadVideo(url) }, null, fragment)
|
||||||
}
|
}
|
||||||
|
|
||||||
loadVideoTask?.run(url)
|
loadVideoTask?.run(url)
|
||||||
@@ -849,6 +840,7 @@ class ShortView : FrameLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val thumbnail = videoDetails.thumbnails.getHQThumbnail()
|
val thumbnail = videoDetails.thumbnails.getHQThumbnail()
|
||||||
|
/*
|
||||||
if (videoSource == null && !thumbnail.isNullOrBlank()) Glide.with(context).asBitmap()
|
if (videoSource == null && !thumbnail.isNullOrBlank()) Glide.with(context).asBitmap()
|
||||||
.load(thumbnail).into(object : CustomTarget<Bitmap>() {
|
.load(thumbnail).into(object : CustomTarget<Bitmap>() {
|
||||||
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
|
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
|
||||||
@@ -860,8 +852,9 @@ class ShortView : FrameLayout {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
else player.setArtwork(null)
|
else player.setArtwork(null)
|
||||||
|
*/
|
||||||
|
|
||||||
mainFragment.lifecycleScope.launch(Dispatchers.Main) {
|
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||||
try {
|
try {
|
||||||
player.setSource(videoSource, audioSource, play = true, keepSubtitles = false, resume = resumePositionMs > 0)
|
player.setSource(videoSource, audioSource, play = true, keepSubtitles = false, resume = resumePositionMs > 0)
|
||||||
if (subtitleSource != null) player.swapSubtitles(subtitleSource)
|
if (subtitleSource != null) player.swapSubtitles(subtitleSource)
|
||||||
@@ -887,397 +880,4 @@ class ShortView : FrameLayout {
|
|||||||
const val TAG = "VideoDetailView"
|
const val TAG = "VideoDetailView"
|
||||||
}
|
}
|
||||||
|
|
||||||
class CommentsModalBottomSheet : BottomSheetDialogFragment() {
|
|
||||||
var mainFragment: MainFragment? = null
|
|
||||||
|
|
||||||
private lateinit var containerContent: FrameLayout
|
|
||||||
private lateinit var containerContentMain: LinearLayout
|
|
||||||
private lateinit var containerContentReplies: RepliesOverlay
|
|
||||||
private lateinit var containerContentDescription: DescriptionOverlay
|
|
||||||
private lateinit var containerContentSupport: SupportOverlay
|
|
||||||
|
|
||||||
private lateinit var title: TextView
|
|
||||||
private lateinit var subTitle: TextView
|
|
||||||
private lateinit var channelName: TextView
|
|
||||||
private lateinit var channelMeta: TextView
|
|
||||||
private lateinit var creatorThumbnail: CreatorThumbnail
|
|
||||||
private lateinit var channelButton: LinearLayout
|
|
||||||
private lateinit var monetization: MonetizationView
|
|
||||||
private lateinit var platform: PlatformIndicator
|
|
||||||
private lateinit var textLikes: TextView
|
|
||||||
private lateinit var textDislikes: TextView
|
|
||||||
private lateinit var layoutRating: LinearLayout
|
|
||||||
private lateinit var imageDislikeIcon: ImageView
|
|
||||||
private lateinit var imageLikeIcon: ImageView
|
|
||||||
|
|
||||||
private lateinit var description: TextView
|
|
||||||
private lateinit var descriptionContainer: LinearLayout
|
|
||||||
private lateinit var descriptionViewMore: TextView
|
|
||||||
|
|
||||||
private lateinit var commentsList: CommentsList
|
|
||||||
private lateinit var addCommentView: AddCommentView
|
|
||||||
|
|
||||||
private var polycentricProfile: PolycentricProfile? = null
|
|
||||||
|
|
||||||
private lateinit var buttonPolycentric: Button
|
|
||||||
private lateinit var buttonPlatform: Button
|
|
||||||
|
|
||||||
private var tabIndex: Int? = null
|
|
||||||
|
|
||||||
private var contentOverlayView: View? = null
|
|
||||||
|
|
||||||
lateinit var video: IPlatformVideoDetails
|
|
||||||
|
|
||||||
private lateinit var behavior: BottomSheetBehavior<FrameLayout>
|
|
||||||
|
|
||||||
private val _taskLoadPolycentricProfile =
|
|
||||||
TaskHandler<PlatformID, PolycentricProfile?>(StateApp.instance.scopeGetter, { ApiMethods.getPolycentricProfileByClaim(ApiMethods.SERVER, ApiMethods.FUTO_TRUST_ROOT, it.claimFieldType.toLong(), it.claimType.toLong(), it.value!!) }).success { setPolycentricProfile(it, animate = true) }
|
|
||||||
.exception<Throwable> {
|
|
||||||
Logger.w(TAG, "Failed to load claims.", it)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateDialog(
|
|
||||||
savedInstanceState: Bundle?,
|
|
||||||
): Dialog {
|
|
||||||
val bottomSheetDialog =
|
|
||||||
BottomSheetDialog(requireContext(), R.style.Custom_BottomSheetDialog_Theme)
|
|
||||||
bottomSheetDialog.setContentView(R.layout.modal_comments)
|
|
||||||
|
|
||||||
behavior = bottomSheetDialog.behavior
|
|
||||||
|
|
||||||
// TODO figure out how to not need all of these non null assertions
|
|
||||||
containerContent = bottomSheetDialog.findViewById(R.id.content_container)!!
|
|
||||||
containerContentMain = bottomSheetDialog.findViewById(R.id.videodetail_container_main)!!
|
|
||||||
containerContentReplies =
|
|
||||||
bottomSheetDialog.findViewById(R.id.videodetail_container_replies)!!
|
|
||||||
containerContentDescription =
|
|
||||||
bottomSheetDialog.findViewById(R.id.videodetail_container_description)!!
|
|
||||||
containerContentSupport =
|
|
||||||
bottomSheetDialog.findViewById(R.id.videodetail_container_support)!!
|
|
||||||
|
|
||||||
title = bottomSheetDialog.findViewById(R.id.videodetail_title)!!
|
|
||||||
subTitle = bottomSheetDialog.findViewById(R.id.videodetail_meta)!!
|
|
||||||
channelName = bottomSheetDialog.findViewById(R.id.videodetail_channel_name)!!
|
|
||||||
channelMeta = bottomSheetDialog.findViewById(R.id.videodetail_channel_meta)!!
|
|
||||||
creatorThumbnail = bottomSheetDialog.findViewById(R.id.creator_thumbnail)!!
|
|
||||||
channelButton = bottomSheetDialog.findViewById(R.id.videodetail_channel_button)!!
|
|
||||||
monetization = bottomSheetDialog.findViewById(R.id.monetization)!!
|
|
||||||
platform = bottomSheetDialog.findViewById(R.id.videodetail_platform)!!
|
|
||||||
layoutRating = bottomSheetDialog.findViewById(R.id.layout_rating)!!
|
|
||||||
textDislikes = bottomSheetDialog.findViewById(R.id.text_dislikes)!!
|
|
||||||
textLikes = bottomSheetDialog.findViewById(R.id.text_likes)!!
|
|
||||||
imageLikeIcon = bottomSheetDialog.findViewById(R.id.image_like_icon)!!
|
|
||||||
imageDislikeIcon = bottomSheetDialog.findViewById(R.id.image_dislike_icon)!!
|
|
||||||
|
|
||||||
description = bottomSheetDialog.findViewById(R.id.videodetail_description)!!
|
|
||||||
descriptionContainer =
|
|
||||||
bottomSheetDialog.findViewById(R.id.videodetail_description_container)!!
|
|
||||||
descriptionViewMore =
|
|
||||||
bottomSheetDialog.findViewById(R.id.videodetail_description_view_more)!!
|
|
||||||
|
|
||||||
addCommentView = bottomSheetDialog.findViewById(R.id.add_comment_view)!!
|
|
||||||
commentsList = bottomSheetDialog.findViewById(R.id.comments_list)!!
|
|
||||||
buttonPolycentric = bottomSheetDialog.findViewById(R.id.button_polycentric)!!
|
|
||||||
buttonPlatform = bottomSheetDialog.findViewById(R.id.button_platform)!!
|
|
||||||
|
|
||||||
commentsList.onAuthorClick.subscribe { c ->
|
|
||||||
if (c !is PolycentricPlatformComment) {
|
|
||||||
return@subscribe
|
|
||||||
}
|
|
||||||
val id = c.author.id.value
|
|
||||||
|
|
||||||
Logger.i(TAG, "onAuthorClick: $id")
|
|
||||||
if (id != null && id.startsWith("polycentric://")) {
|
|
||||||
val navUrl = "https://harbor.social/" + id.substring("polycentric://".length)
|
|
||||||
mainFragment!!.startActivity(Intent(Intent.ACTION_VIEW, navUrl.toUri()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
commentsList.onRepliesClick.subscribe { c ->
|
|
||||||
val replyCount = c.replyCount ?: 0
|
|
||||||
var metadata = ""
|
|
||||||
if (replyCount > 0) {
|
|
||||||
metadata += "$replyCount " + requireContext().getString(R.string.replies)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (c is PolycentricPlatformComment) {
|
|
||||||
var parentComment: PolycentricPlatformComment = c
|
|
||||||
containerContentReplies.load(tabIndex!! != 0, metadata, c.contextUrl, c.reference, c, { StatePolycentric.instance.getCommentPager(c.contextUrl, c.reference) }, {
|
|
||||||
val newComment = parentComment.cloneWithUpdatedReplyCount(
|
|
||||||
(parentComment.replyCount ?: 0) + 1
|
|
||||||
)
|
|
||||||
commentsList.replaceComment(parentComment, newComment)
|
|
||||||
parentComment = newComment
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
containerContentReplies.load(tabIndex!! != 0, metadata, null, null, c, { StatePlatform.instance.getSubComments(c) })
|
|
||||||
}
|
|
||||||
animateOpenOverlayView(containerContentReplies)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (StatePolycentric.instance.enabled) {
|
|
||||||
buttonPolycentric.setOnClickListener {
|
|
||||||
setTabIndex(0)
|
|
||||||
StateMeta.instance.setLastCommentSection(0)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
buttonPolycentric.visibility = GONE
|
|
||||||
}
|
|
||||||
|
|
||||||
buttonPlatform.setOnClickListener {
|
|
||||||
setTabIndex(1)
|
|
||||||
StateMeta.instance.setLastCommentSection(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
val ref = Models.referenceFromBuffer(video.url.toByteArray())
|
|
||||||
addCommentView.setContext(video.url, ref)
|
|
||||||
|
|
||||||
if (Settings.instance.comments.recommendationsDefault && !Settings.instance.comments.hideRecommendations) {
|
|
||||||
setTabIndex(2, true)
|
|
||||||
} else {
|
|
||||||
when (Settings.instance.comments.defaultCommentSection) {
|
|
||||||
0 -> if (Settings.instance.other.polycentricEnabled) setTabIndex(0, true) else setTabIndex(1, true)
|
|
||||||
1 -> setTabIndex(1, true)
|
|
||||||
2 -> setTabIndex(StateMeta.instance.getLastCommentSection(), true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
containerContentDescription.onClose.subscribe { animateCloseOverlayView() }
|
|
||||||
containerContentReplies.onClose.subscribe { animateCloseOverlayView() }
|
|
||||||
|
|
||||||
descriptionViewMore.setOnClickListener {
|
|
||||||
animateOpenOverlayView(containerContentDescription)
|
|
||||||
}
|
|
||||||
|
|
||||||
updateDescriptionUI(video.description.fixHtmlLinks())
|
|
||||||
|
|
||||||
val dp5 =
|
|
||||||
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics)
|
|
||||||
val dp2 =
|
|
||||||
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2f, resources.displayMetrics)
|
|
||||||
|
|
||||||
//UI
|
|
||||||
title.text = video.name
|
|
||||||
channelName.text = video.author.name
|
|
||||||
if (video.author.subscribers != null) {
|
|
||||||
channelMeta.text = if ((video.author.subscribers
|
|
||||||
?: 0) > 0
|
|
||||||
) video.author.subscribers!!.toHumanNumber() + " " + requireContext().getString(R.string.subscribers) else ""
|
|
||||||
(channelName.layoutParams as MarginLayoutParams).setMargins(
|
|
||||||
0, (dp5 * -1).toInt(), 0, 0
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
channelMeta.text = ""
|
|
||||||
(channelName.layoutParams as MarginLayoutParams).setMargins(0, (dp2).toInt(), 0, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
video.author.let {
|
|
||||||
if (it is PlatformAuthorMembershipLink && !it.membershipUrl.isNullOrEmpty()) monetization.setPlatformMembership(video.id.pluginId, it.membershipUrl)
|
|
||||||
else monetization.setPlatformMembership(null, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
val subTitleSegments: ArrayList<String> = ArrayList()
|
|
||||||
if (video.viewCount > 0) subTitleSegments.add("${video.viewCount.toHumanNumber()} ${if (video.isLive) requireContext().getString(R.string.watching_now) else requireContext().getString(R.string.views)}")
|
|
||||||
if (video.datetime != null) {
|
|
||||||
val diff = video.datetime?.getNowDiffSeconds() ?: 0
|
|
||||||
val ago = video.datetime?.toHumanNowDiffString(true)
|
|
||||||
if (diff >= 0) subTitleSegments.add("$ago ago")
|
|
||||||
else subTitleSegments.add("available in $ago")
|
|
||||||
}
|
|
||||||
|
|
||||||
platform.setPlatformFromClientID(video.id.pluginId)
|
|
||||||
subTitle.text = subTitleSegments.joinToString(" • ")
|
|
||||||
creatorThumbnail.setThumbnail(video.author.thumbnail, false)
|
|
||||||
|
|
||||||
setPolycentricProfile(null, animate = false)
|
|
||||||
_taskLoadPolycentricProfile.run(video.author.id)
|
|
||||||
|
|
||||||
when (video.rating) {
|
|
||||||
is RatingLikeDislikes -> {
|
|
||||||
val r = video.rating as RatingLikeDislikes
|
|
||||||
layoutRating.visibility = VISIBLE
|
|
||||||
|
|
||||||
textLikes.visibility = VISIBLE
|
|
||||||
imageLikeIcon.visibility = VISIBLE
|
|
||||||
textLikes.text = r.likes.toHumanNumber()
|
|
||||||
|
|
||||||
imageDislikeIcon.visibility = VISIBLE
|
|
||||||
textDislikes.visibility = VISIBLE
|
|
||||||
textDislikes.text = r.dislikes.toHumanNumber()
|
|
||||||
}
|
|
||||||
|
|
||||||
is RatingLikes -> {
|
|
||||||
val r = video.rating as RatingLikes
|
|
||||||
layoutRating.visibility = VISIBLE
|
|
||||||
|
|
||||||
textLikes.visibility = VISIBLE
|
|
||||||
imageLikeIcon.visibility = VISIBLE
|
|
||||||
textLikes.text = r.likes.toHumanNumber()
|
|
||||||
|
|
||||||
imageDislikeIcon.visibility = GONE
|
|
||||||
textDislikes.visibility = GONE
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
layoutRating.visibility = GONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
monetization.onSupportTap.subscribe {
|
|
||||||
containerContentSupport.setPolycentricProfile(polycentricProfile)
|
|
||||||
animateOpenOverlayView(containerContentSupport)
|
|
||||||
}
|
|
||||||
|
|
||||||
monetization.onStoreTap.subscribe {
|
|
||||||
polycentricProfile?.systemState?.store?.let {
|
|
||||||
try {
|
|
||||||
val uri = it.toUri()
|
|
||||||
val intent = Intent(Intent.ACTION_VIEW)
|
|
||||||
intent.data = uri
|
|
||||||
requireContext().startActivity(intent)
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.e(TAG, "Failed to open URI: '${it}'.", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
monetization.onUrlTap.subscribe {
|
|
||||||
mainFragment!!.navigate<BrowserFragment>(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
addCommentView.onCommentAdded.subscribe {
|
|
||||||
commentsList.addComment(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
channelButton.setOnClickListener {
|
|
||||||
mainFragment!!.navigate<ChannelFragment>(video.author)
|
|
||||||
}
|
|
||||||
|
|
||||||
return bottomSheetDialog
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDismiss(dialog: DialogInterface) {
|
|
||||||
super.onDismiss(dialog)
|
|
||||||
animateCloseOverlayView()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setPolycentricProfile(profile: PolycentricProfile?, animate: Boolean) {
|
|
||||||
polycentricProfile = profile
|
|
||||||
|
|
||||||
val dp35 = 35.dp(requireContext().resources)
|
|
||||||
val avatar = profile?.systemState?.avatar?.selectBestImage(dp35 * dp35)
|
|
||||||
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) }
|
|
||||||
|
|
||||||
if (avatar != null) {
|
|
||||||
creatorThumbnail.setThumbnail(avatar, animate)
|
|
||||||
} else {
|
|
||||||
creatorThumbnail.setThumbnail(video.author.thumbnail, animate)
|
|
||||||
creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto())
|
|
||||||
}
|
|
||||||
|
|
||||||
val username = profile?.systemState?.username
|
|
||||||
if (username != null) {
|
|
||||||
channelName.text = username
|
|
||||||
}
|
|
||||||
|
|
||||||
monetization.setPolycentricProfile(profile)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setTabIndex(index: Int?, forceReload: Boolean = false) {
|
|
||||||
Logger.i(TAG, "setTabIndex (index: ${index}, forceReload: ${forceReload})")
|
|
||||||
val changed = tabIndex != index || forceReload
|
|
||||||
if (!changed) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tabIndex = index
|
|
||||||
buttonPlatform.setTextColor(resources.getColor(if (index == 1) R.color.white else R.color.gray_ac, null))
|
|
||||||
buttonPolycentric.setTextColor(resources.getColor(if (index == 0) R.color.white else R.color.gray_ac, null))
|
|
||||||
|
|
||||||
when (index) {
|
|
||||||
null -> {
|
|
||||||
addCommentView.visibility = GONE
|
|
||||||
commentsList.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
0 -> {
|
|
||||||
addCommentView.visibility = VISIBLE
|
|
||||||
fetchPolycentricComments()
|
|
||||||
}
|
|
||||||
|
|
||||||
1 -> {
|
|
||||||
addCommentView.visibility = GONE
|
|
||||||
fetchComments()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun fetchComments() {
|
|
||||||
Logger.i(TAG, "fetchComments")
|
|
||||||
video.let {
|
|
||||||
commentsList.load(true) { StatePlatform.instance.getComments(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun fetchPolycentricComments() {
|
|
||||||
Logger.i(TAG, "fetchPolycentricComments")
|
|
||||||
val video = video
|
|
||||||
val idValue = video.id.value
|
|
||||||
if (video.url.isEmpty()) {
|
|
||||||
Logger.w(TAG, "Failed to fetch polycentric comments because url was null")
|
|
||||||
commentsList.clear()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val ref = Models.referenceFromBuffer(video.url.toByteArray())
|
|
||||||
val extraBytesRef = idValue?.let { if (it.isNotEmpty()) it.toByteArray() else null }
|
|
||||||
commentsList.load(false) { StatePolycentric.instance.getCommentPager(video.url, ref, listOfNotNull(extraBytesRef)); }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateDescriptionUI(text: Spanned) {
|
|
||||||
containerContentDescription.load(text)
|
|
||||||
description.text = text
|
|
||||||
|
|
||||||
if (description.text.isNotEmpty()) descriptionContainer.visibility = VISIBLE
|
|
||||||
else descriptionContainer.visibility = GONE
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun animateOpenOverlayView(view: View) {
|
|
||||||
if (contentOverlayView != null) {
|
|
||||||
Logger.e(TAG, "Content overlay already open")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
behavior.isDraggable = false
|
|
||||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
|
||||||
|
|
||||||
val animHeight = containerContentMain.height
|
|
||||||
|
|
||||||
view.translationY = animHeight.toFloat()
|
|
||||||
view.visibility = VISIBLE
|
|
||||||
|
|
||||||
view.animate().setDuration(300).translationY(0f).withEndAction {
|
|
||||||
contentOverlayView = view
|
|
||||||
}.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun animateCloseOverlayView() {
|
|
||||||
val curView = contentOverlayView
|
|
||||||
if (curView == null) {
|
|
||||||
Logger.e(TAG, "No content overlay open")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
behavior.isDraggable = true
|
|
||||||
|
|
||||||
val animHeight = contentOverlayView!!.height
|
|
||||||
|
|
||||||
curView.animate().setDuration(300).translationY(animHeight.toFloat()).withEndAction {
|
|
||||||
curView.visibility = GONE
|
|
||||||
contentOverlayView = null
|
|
||||||
}.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val TAG = "ModalBottomSheet"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+28
-17
@@ -11,6 +11,7 @@ import android.widget.FrameLayout
|
|||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import androidx.annotation.OptIn
|
import androidx.annotation.OptIn
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.viewpager2.widget.ViewPager2
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
@@ -25,6 +26,9 @@ import com.futo.platformplayer.logging.Logger
|
|||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.platformplayer.views.buttons.BigButton
|
import com.futo.platformplayer.views.buttons.BigButton
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.system.measureTimeMillis
|
||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
class ShortsFragment : MainFragment() {
|
class ShortsFragment : MainFragment() {
|
||||||
@@ -35,6 +39,7 @@ class ShortsFragment : MainFragment() {
|
|||||||
private var loadPagerTask: TaskHandler<ShortsFragment, IPager<IPlatformVideo>>? = null
|
private var loadPagerTask: TaskHandler<ShortsFragment, IPager<IPlatformVideo>>? = null
|
||||||
private var nextPageTask: TaskHandler<ShortsFragment, List<IPlatformVideo>>? = null
|
private var nextPageTask: TaskHandler<ShortsFragment, List<IPlatformVideo>>? = null
|
||||||
|
|
||||||
|
//TODO: Reduce number of pagers (1, or at most 2)
|
||||||
private var mainShortsPager: IPager<IPlatformVideo>? = null
|
private var mainShortsPager: IPager<IPlatformVideo>? = null
|
||||||
private val mainShorts: MutableList<IPlatformVideo> = mutableListOf()
|
private val mainShorts: MutableList<IPlatformVideo> = mutableListOf()
|
||||||
|
|
||||||
@@ -58,6 +63,7 @@ class ShortsFragment : MainFragment() {
|
|||||||
private var customViewAdapter: CustomViewAdapter? = null
|
private var customViewAdapter: CustomViewAdapter? = null
|
||||||
|
|
||||||
// we just completely reset the data structure so we want to tell the adapter that
|
// we just completely reset the data structure so we want to tell the adapter that
|
||||||
|
//TODO: Move most of this logic to ShortsView
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
override fun onShownWithView(parameter: Any?, isBack: Boolean) {
|
||||||
(activity as MainActivity?)?.getFragment<VideoDetailFragment>()?.closeVideoDetails()
|
(activity as MainActivity?)?.getFragment<VideoDetailFragment>()?.closeVideoDetails()
|
||||||
@@ -118,7 +124,6 @@ class ShortsFragment : MainFragment() {
|
|||||||
overlayQualityContainer = view.findViewById(R.id.shorts_quality_overview)
|
overlayQualityContainer = view.findViewById(R.id.shorts_quality_overview)
|
||||||
|
|
||||||
sourcesButton.onClick.subscribe {
|
sourcesButton.onClick.subscribe {
|
||||||
sourcesButton.playSoundEffect(SoundEffectConstants.CLICK)
|
|
||||||
navigate<SourcesFragment>()
|
navigate<SourcesFragment>()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,7 +150,7 @@ class ShortsFragment : MainFragment() {
|
|||||||
|
|
||||||
this.customViewAdapter = customViewAdapter
|
this.customViewAdapter = customViewAdapter
|
||||||
|
|
||||||
if (loadPagerTask == null && currentShorts.isEmpty()) {
|
if (loadPagerTask == null) {// && currentShorts.isEmpty()) {
|
||||||
loadPager()
|
loadPager()
|
||||||
|
|
||||||
loadPagerTask!!.success {
|
loadPagerTask!!.success {
|
||||||
@@ -207,28 +212,29 @@ class ShortsFragment : MainFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun nextPage() {
|
private fun nextPage() {
|
||||||
nextPageTask?.cancel()
|
Logger.i(TAG, "ShortsFragment nextPage");
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val nextPageTask =
|
try {
|
||||||
TaskHandler<ShortsFragment, List<IPlatformVideo>>(StateApp.instance.scopeGetter, {
|
val time = measureTimeMillis {
|
||||||
currentShortsPager!!.nextPage()
|
currentShortsPager!!.nextPage();
|
||||||
|
}
|
||||||
return@TaskHandler currentShortsPager!!.getResults()
|
val newVideos = currentShortsPager!!.getResults();
|
||||||
}).success { newVideos ->
|
|
||||||
val prevCount = customViewAdapter!!.itemCount
|
val prevCount = customViewAdapter!!.itemCount
|
||||||
|
Logger.i(TAG, "Shorts nextPage took ${time}ms, ${prevCount}-${prevCount + newVideos.size}, hasMore: ${currentShortsPager?.hasMorePages()}");
|
||||||
currentShorts.addAll(newVideos)
|
currentShorts.addAll(newVideos)
|
||||||
if (isChannelShortsMode) {
|
if (isChannelShortsMode) {
|
||||||
channelShorts.addAll(newVideos)
|
channelShorts.addAll(newVideos)
|
||||||
} else {
|
} else {
|
||||||
mainShorts.addAll(newVideos)
|
mainShorts.addAll(newVideos)
|
||||||
}
|
}
|
||||||
customViewAdapter!!.notifyItemRangeInserted(prevCount, newVideos.size)
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
customViewAdapter!!.notifyItemRangeInserted(prevCount, newVideos.size)
|
||||||
|
}
|
||||||
nextPageTask = null
|
nextPageTask = null
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
Logger.e(TAG, "Shorts Failed to call nextPage", ex);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
nextPageTask.run(this)
|
|
||||||
|
|
||||||
this.nextPageTask = nextPageTask
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// we just completely reset the data structure so we want to tell the adapter that
|
// we just completely reset the data structure so we want to tell the adapter that
|
||||||
@@ -236,12 +242,16 @@ class ShortsFragment : MainFragment() {
|
|||||||
private fun loadPager() {
|
private fun loadPager() {
|
||||||
loadPagerTask?.cancel()
|
loadPagerTask?.cancel()
|
||||||
|
|
||||||
|
Logger.i(TAG, "Shorts loadPage");
|
||||||
|
var loadPageStart = System.currentTimeMillis();
|
||||||
val loadPagerTask =
|
val loadPagerTask =
|
||||||
TaskHandler<ShortsFragment, IPager<IPlatformVideo>>(StateApp.instance.scopeGetter, {
|
TaskHandler<ShortsFragment, IPager<IPlatformVideo>>(StateApp.instance.scopeGetter, {
|
||||||
val pager = StatePlatform.instance.getShorts()
|
val pager = StatePlatform.instance.getShorts();
|
||||||
|
|
||||||
return@TaskHandler pager
|
return@TaskHandler pager
|
||||||
}).success { pager ->
|
}).success { pager ->
|
||||||
|
val timeLoadPage = System.currentTimeMillis() - loadPageStart;
|
||||||
|
Logger.i(TAG, "Shorts loadPage took ${timeLoadPage}ms");
|
||||||
mainShorts.clear()
|
mainShorts.clear()
|
||||||
mainShorts.addAll(pager.getResults())
|
mainShorts.addAll(pager.getResults())
|
||||||
mainShortsPager = pager
|
mainShortsPager = pager
|
||||||
@@ -259,7 +269,7 @@ class ShortsFragment : MainFragment() {
|
|||||||
loadPagerTask = null
|
loadPagerTask = null
|
||||||
}.exception<Throwable> { err ->
|
}.exception<Throwable> { err ->
|
||||||
val message = "Unable to load shorts $err"
|
val message = "Unable to load shorts $err"
|
||||||
Logger.i(TAG, message)
|
Logger.w(TAG, message, err)
|
||||||
if (context != null) {
|
if (context != null) {
|
||||||
UIDialogs.showDialog(
|
UIDialogs.showDialog(
|
||||||
requireContext(), R.drawable.ic_sources, message, null, null, 0, UIDialogs.Action(
|
requireContext(), R.drawable.ic_sources, message, null, null, 0, UIDialogs.Action(
|
||||||
@@ -329,6 +339,7 @@ class ShortsFragment : MainFragment() {
|
|||||||
|
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
override fun onBindViewHolder(holder: CustomViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: CustomViewHolder, position: Int) {
|
||||||
|
Logger.i(TAG, "Shorts change (position: ${position}): ${videos[position].name} (${videos[position].id.value})")
|
||||||
holder.shortView.changeVideo(videos[position], isChannelShortsMode())
|
holder.shortView.changeVideo(videos[position], isChannelShortsMode())
|
||||||
|
|
||||||
if (position == itemCount - 1) {
|
if (position == itemCount - 1) {
|
||||||
|
|||||||
+45
@@ -25,6 +25,7 @@ import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
|||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StateDeveloper
|
import com.futo.platformplayer.states.StateDeveloper
|
||||||
|
import com.futo.platformplayer.states.StateHistory
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.platformplayer.states.StatePlugins
|
import com.futo.platformplayer.states.StatePlugins
|
||||||
import com.futo.platformplayer.views.buttons.BigButton
|
import com.futo.platformplayer.views.buttons.BigButton
|
||||||
@@ -152,6 +153,50 @@ class SourceDetailFragment : MainFragment() {
|
|||||||
if(field is View)
|
if(field is View)
|
||||||
field.isVisible = false;
|
field.isVisible = false;
|
||||||
}
|
}
|
||||||
|
if(!source.capabilities.hasGetUserHistory || !source.isLoggedIn) {
|
||||||
|
val field = _settingsAppForm.findField("sync");
|
||||||
|
if(field is View)
|
||||||
|
field.isVisible = false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
val field = _settingsAppForm.findField("syncHistory");
|
||||||
|
field?.onChanged?.subscribe { field, new, old ->
|
||||||
|
if(old != new && new == true && StatePlatform.instance.isClientEnabled(config.id)) {
|
||||||
|
UIDialogs.showDialog(context, R.drawable.ic_sources, "Would you like to sync now?",
|
||||||
|
"This will attempt to update your history from the platform, when this setting is enabled, it is done during startup.", null, 0,
|
||||||
|
UIDialogs.Action("No", {
|
||||||
|
|
||||||
|
}),
|
||||||
|
UIDialogs.Action("Yes", {
|
||||||
|
UIDialogs.showDialogProgress(context, {
|
||||||
|
it.setText("Importing history..");
|
||||||
|
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val client = StatePlatform.instance.getClient(config.id);
|
||||||
|
if (client != null && client is JSClient) {
|
||||||
|
val count = StateHistory.instance.syncRemoteHistory(client);
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
it.hide();
|
||||||
|
if(count > 0)
|
||||||
|
UIDialogs.showDialogOk(context, R.drawable.ic_pair_success, "Imported ${count} history items");
|
||||||
|
else
|
||||||
|
UIDialogs.showDialogOk(context, R.drawable.ic_help, "Imported no history items");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
UIDialogs.appToast("Sync History failed due to:\n" + ex.message);
|
||||||
|
it.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, UIDialogs.ActionStyle.PRIMARY));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
_settingsAppForm.onChanged.clear();
|
_settingsAppForm.onChanged.clear();
|
||||||
_settingsAppForm.onChanged.subscribe { field, value ->
|
_settingsAppForm.onChanged.subscribe { field, value ->
|
||||||
_settingsAppChanged = true;
|
_settingsAppChanged = true;
|
||||||
|
|||||||
+3
-3
@@ -437,7 +437,7 @@ class VideoDetailFragment() : MainFragment() {
|
|||||||
|
|
||||||
fun onUserLeaveHint() {
|
fun onUserLeaveHint() {
|
||||||
val viewDetail = _viewDetail;
|
val viewDetail = _viewDetail;
|
||||||
Logger.i(TAG, "onUserLeaveHint preventPictureInPicture=${viewDetail?.preventPictureInPicture} isCasting=${StateCasting.instance.isCasting} isBackgroundPictureInPicture=${Settings.instance.playback.isBackgroundPictureInPicture()} allowBackground=${viewDetail?.allowBackground}");
|
Logger.i(TAG, "onUserLeaveHint preventPictureInPicture=${viewDetail?.preventPictureInPicture} isCasting=${StateCasting.instance.isCasting} isBackgroundPictureInPicture=${Settings.instance.playback.isBackgroundPictureInPicture()} allowBackground=${viewDetail?.isAudioOnlyUserAction}");
|
||||||
|
|
||||||
if (viewDetail === null) {
|
if (viewDetail === null) {
|
||||||
return
|
return
|
||||||
@@ -446,7 +446,7 @@ class VideoDetailFragment() : MainFragment() {
|
|||||||
if (viewDetail.shouldEnterPictureInPicture) {
|
if (viewDetail.shouldEnterPictureInPicture) {
|
||||||
_leavingPiP = false
|
_leavingPiP = false
|
||||||
}
|
}
|
||||||
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.S && viewDetail.preventPictureInPicture == false && !StateCasting.instance.isCasting && Settings.instance.playback.isBackgroundPictureInPicture() && !viewDetail.allowBackground) {
|
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.S && viewDetail.preventPictureInPicture == false && !StateCasting.instance.isCasting && Settings.instance.playback.isBackgroundPictureInPicture() && !viewDetail.isAudioOnlyUserAction) {
|
||||||
val params = _viewDetail?.getPictureInPictureParams();
|
val params = _viewDetail?.getPictureInPictureParams();
|
||||||
if(params != null) {
|
if(params != null) {
|
||||||
Logger.i(TAG, "enterPictureInPictureMode")
|
Logger.i(TAG, "enterPictureInPictureMode")
|
||||||
@@ -526,7 +526,7 @@ class VideoDetailFragment() : MainFragment() {
|
|||||||
|
|
||||||
private fun stopIfRequired() {
|
private fun stopIfRequired() {
|
||||||
var shouldStop = true;
|
var shouldStop = true;
|
||||||
if (_viewDetail?.allowBackground == true) {
|
if (_viewDetail?.isAudioOnlyUserAction == true) {
|
||||||
shouldStop = false;
|
shouldStop = false;
|
||||||
} else if (Settings.instance.playback.isBackgroundPictureInPicture() && !_leavingPiP) {
|
} else if (Settings.instance.playback.isBackgroundPictureInPicture() && !_leavingPiP) {
|
||||||
shouldStop = false;
|
shouldStop = false;
|
||||||
|
|||||||
+49
-27
@@ -10,7 +10,6 @@ import android.content.Intent
|
|||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Rect
|
|
||||||
import android.graphics.drawable.Animatable
|
import android.graphics.drawable.Animatable
|
||||||
import android.graphics.drawable.BitmapDrawable
|
import android.graphics.drawable.BitmapDrawable
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
@@ -51,7 +50,6 @@ import com.futo.platformplayer.Settings
|
|||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.UISlideOverlays
|
import com.futo.platformplayer.UISlideOverlays
|
||||||
import com.futo.platformplayer.activities.MainActivity
|
import com.futo.platformplayer.activities.MainActivity
|
||||||
import com.futo.platformplayer.activities.SyncShowPairingCodeActivity.Companion.activity
|
|
||||||
import com.futo.platformplayer.api.media.IPluginSourced
|
import com.futo.platformplayer.api.media.IPluginSourced
|
||||||
import com.futo.platformplayer.api.media.LiveChatManager
|
import com.futo.platformplayer.api.media.LiveChatManager
|
||||||
import com.futo.platformplayer.api.media.PlatformID
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
@@ -82,7 +80,6 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
|||||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSVideo
|
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.JSVideoDetails
|
import com.futo.platformplayer.api.media.platforms.js.models.JSVideoDetails
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource
|
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
@@ -326,7 +323,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
val onEnterPictureInPicture = Event0();
|
val onEnterPictureInPicture = Event0();
|
||||||
val onVideoChanged = Event2<Int, Int>()
|
val onVideoChanged = Event2<Int, Int>()
|
||||||
|
|
||||||
var allowBackground: Boolean = false
|
var isAudioOnlyUserAction: Boolean = false
|
||||||
private set(value) {
|
private set(value) {
|
||||||
if (field != value) {
|
if (field != value) {
|
||||||
field = value
|
field = value
|
||||||
@@ -338,7 +335,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
get() = !preventPictureInPicture &&
|
get() = !preventPictureInPicture &&
|
||||||
!StateCasting.instance.isCasting &&
|
!StateCasting.instance.isCasting &&
|
||||||
Settings.instance.playback.isBackgroundPictureInPicture() &&
|
Settings.instance.playback.isBackgroundPictureInPicture() &&
|
||||||
!allowBackground &&
|
!isAudioOnlyUserAction &&
|
||||||
isPlaying
|
isPlaying
|
||||||
|
|
||||||
val onShouldEnterPictureInPictureChanged = Event0();
|
val onShouldEnterPictureInPictureChanged = Event0();
|
||||||
@@ -579,9 +576,8 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
if(chapter?.type == ChapterType.SKIPPABLE) {
|
if(chapter?.type == ChapterType.SKIPPABLE) {
|
||||||
_layoutSkip.visibility = VISIBLE;
|
_layoutSkip.visibility = VISIBLE;
|
||||||
} else if(chapter?.type == ChapterType.SKIP || chapter?.type == ChapterType.SKIPONCE) {
|
} else if(chapter?.type == ChapterType.SKIP || chapter?.type == ChapterType.SKIPONCE) {
|
||||||
val ad = StateCasting.instance.activeDevice
|
if (StateCasting.instance.activeDevice != null) {
|
||||||
if (ad != null) {
|
StateCasting.instance.videoSeekTo(chapter.timeEnd)
|
||||||
ad.seekVideo(chapter.timeEnd)
|
|
||||||
} else {
|
} else {
|
||||||
_player.seekTo((chapter.timeEnd * 1000).toLong());
|
_player.seekTo((chapter.timeEnd * 1000).toLong());
|
||||||
}
|
}
|
||||||
@@ -764,7 +760,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
MediaControlReceiver.onBackgroundReceived.subscribe(this) {
|
MediaControlReceiver.onBackgroundReceived.subscribe(this) {
|
||||||
Logger.i(TAG, "MediaControlReceiver.onBackgroundReceived")
|
Logger.i(TAG, "MediaControlReceiver.onBackgroundReceived")
|
||||||
_player.switchToAudioMode(video);
|
_player.switchToAudioMode(video);
|
||||||
allowBackground = true;
|
isAudioOnlyUserAction = true;
|
||||||
StateApp.instance.contextOrNull?.let {
|
StateApp.instance.contextOrNull?.let {
|
||||||
try {
|
try {
|
||||||
if (it is MainActivity) {
|
if (it is MainActivity) {
|
||||||
@@ -889,7 +885,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
if (ad != null) {
|
if (ad != null) {
|
||||||
val currentChapter = _cast.getCurrentChapter((ad.time * 1000).toLong());
|
val currentChapter = _cast.getCurrentChapter((ad.time * 1000).toLong());
|
||||||
if(currentChapter?.type == ChapterType.SKIPPABLE) {
|
if(currentChapter?.type == ChapterType.SKIPPABLE) {
|
||||||
ad.seekVideo(currentChapter.timeEnd);
|
StateCasting.instance.videoSeekTo(currentChapter.timeEnd);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val currentChapter = _player.getCurrentChapter(_player.position);
|
val currentChapter = _player.getCurrentChapter(_player.position);
|
||||||
@@ -1008,15 +1004,26 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
_slideUpOverlay?.hide();
|
_slideUpOverlay?.hide();
|
||||||
|
} else if(video is JSVideoDetails && (video as JSVideoDetails).hasVODEvents())
|
||||||
|
RoundButton(context, R.drawable.ic_chat, context.getString(R.string.vod_chat), TAG_VODCHAT) {
|
||||||
|
video?.let {
|
||||||
|
try {
|
||||||
|
loadVODChat(it);
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to reopen vod chat", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_slideUpOverlay?.hide();
|
||||||
} else null,
|
} else null,
|
||||||
if (!isLimitedVersion) RoundButton(context, R.drawable.ic_screen_share, if (allowBackground) context.getString(R.string.background_revert) else context.getString(R.string.background), TAG_BACKGROUND) {
|
if (!isLimitedVersion) RoundButton(context, R.drawable.ic_screen_share, if (isAudioOnlyUserAction) context.getString(R.string.background_revert) else context.getString(R.string.background), TAG_BACKGROUND) {
|
||||||
if (!allowBackground) {
|
if (!isAudioOnlyUserAction) {
|
||||||
_player.switchToAudioMode(video);
|
_player.switchToAudioMode(video);
|
||||||
allowBackground = true;
|
isAudioOnlyUserAction = true;
|
||||||
it.text.text = resources.getString(R.string.background_revert);
|
it.text.text = resources.getString(R.string.background_revert);
|
||||||
} else {
|
} else {
|
||||||
_player.switchToVideoMode();
|
_player.switchToVideoMode();
|
||||||
allowBackground = false;
|
isAudioOnlyUserAction = false;
|
||||||
it.text.text = resources.getString(R.string.background);
|
it.text.text = resources.getString(R.string.background);
|
||||||
}
|
}
|
||||||
_slideUpOverlay?.hide();
|
_slideUpOverlay?.hide();
|
||||||
@@ -1132,19 +1139,23 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
|
|
||||||
|
|
||||||
//Lifecycle
|
//Lifecycle
|
||||||
|
var isLoginStop = false; //TODO: This is a bit jank, but easiest solution for now without reworking flow. (Alternatively, fix MainActivity getting stopped/disposing video)
|
||||||
fun onResume() {
|
fun onResume() {
|
||||||
Logger.v(TAG, "onResume");
|
Logger.v(TAG, "onResume");
|
||||||
_onPauseCalled = false;
|
_onPauseCalled = false;
|
||||||
|
|
||||||
|
val wasLoginCall = isLoginStop;
|
||||||
|
isLoginStop = false;
|
||||||
|
|
||||||
Logger.i(TAG, "_video: ${video?.name ?: "no video"}");
|
Logger.i(TAG, "_video: ${video?.name ?: "no video"}");
|
||||||
Logger.i(TAG, "_didStop: $_didStop");
|
Logger.i(TAG, "_didStop: $_didStop");
|
||||||
|
|
||||||
//Recover cancelled loads
|
//Recover cancelled loads
|
||||||
if(video == null) {
|
if(video == null) {
|
||||||
val t = (lastPositionMilliseconds / 1000.0f).roundToLong();
|
val t = (lastPositionMilliseconds / 1000.0f).roundToLong();
|
||||||
if(_searchVideo != null)
|
if(_searchVideo != null && !wasLoginCall)
|
||||||
setVideoOverview(_searchVideo!!, true, t);
|
setVideoOverview(_searchVideo!!, true, t);
|
||||||
else if(_url != null)
|
else if(_url != null && !wasLoginCall)
|
||||||
setVideo(_url!!, t, _playWhenReady);
|
setVideo(_url!!, t, _playWhenReady);
|
||||||
}
|
}
|
||||||
else if(_didStop) {
|
else if(_didStop) {
|
||||||
@@ -1156,11 +1167,14 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
|
|
||||||
if(_player.isAudioMode) {
|
if(_player.isAudioMode) {
|
||||||
//Requested behavior to leave it in audio mode. leaving it commented if it causes issues, revert?
|
//Requested behavior to leave it in audio mode. leaving it commented if it causes issues, revert?
|
||||||
if(!allowBackground) {
|
if(!isAudioOnlyUserAction) {
|
||||||
_player.switchToVideoMode();
|
_player.switchToVideoMode();
|
||||||
allowBackground = false;
|
isAudioOnlyUserAction = false;
|
||||||
_buttonPins.getButtonByTag(TAG_BACKGROUND)?.text?.text = resources.getString(R.string.background);
|
_buttonPins.getButtonByTag(TAG_BACKGROUND)?.text?.text = resources.getString(R.string.background);
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
_buttonPins.getButtonByTag(TAG_BACKGROUND)?.text?.text = resources.getString(R.string.video);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if(!_player.isFitMode && !_player.isFullScreen && !fragment.isInPictureInPicture)
|
if(!_player.isFitMode && !_player.isFullScreen && !fragment.isInPictureInPicture)
|
||||||
_player.fitHeight();
|
_player.fitHeight();
|
||||||
@@ -1176,7 +1190,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
if(StateCasting.instance.isCasting)
|
if(StateCasting.instance.isCasting)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if(allowBackground)
|
if(isAudioOnlyUserAction)
|
||||||
StatePlayer.instance.startOrUpdateMediaSession(context, video);
|
StatePlayer.instance.startOrUpdateMediaSession(context, video);
|
||||||
else {
|
else {
|
||||||
when (Settings.instance.playback.backgroundPlay) {
|
when (Settings.instance.playback.backgroundPlay) {
|
||||||
@@ -1184,7 +1198,6 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
1 -> {
|
1 -> {
|
||||||
if(!(video?.isLive ?: false)) {
|
if(!(video?.isLive ?: false)) {
|
||||||
_player.switchToAudioMode(video);
|
_player.switchToAudioMode(video);
|
||||||
allowBackground = true;
|
|
||||||
}
|
}
|
||||||
StatePlayer.instance.startOrUpdateMediaSession(context, video);
|
StatePlayer.instance.startOrUpdateMediaSession(context, video);
|
||||||
}
|
}
|
||||||
@@ -1207,6 +1220,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_taskLoadVideo.cancel();
|
_taskLoadVideo.cancel();
|
||||||
handleStop();
|
handleStop();
|
||||||
_didStop = true;
|
_didStop = true;
|
||||||
|
onShouldEnterPictureInPictureChanged.emit()
|
||||||
Logger.i(TAG, "_didStop set to true");
|
Logger.i(TAG, "_didStop set to true");
|
||||||
|
|
||||||
StatePlayer.instance.rotationLock = false;
|
StatePlayer.instance.rotationLock = false;
|
||||||
@@ -1971,10 +1985,10 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
|
|
||||||
if (isLimitedVersion && _player.isAudioMode) {
|
if (isLimitedVersion && _player.isAudioMode) {
|
||||||
_player.switchToVideoMode()
|
_player.switchToVideoMode()
|
||||||
allowBackground = false;
|
isAudioOnlyUserAction = false;
|
||||||
} else {
|
} else {
|
||||||
val thumbnail = video.thumbnails.getHQThumbnail();
|
val thumbnail = video.thumbnails.getHQThumbnail();
|
||||||
if ((videoSource == null || _player.isAudioMode) && !thumbnail.isNullOrBlank())
|
if ((videoSource == null) && !thumbnail.isNullOrBlank()) // || _player.isAudioMode
|
||||||
Glide.with(context).asBitmap().load(thumbnail)
|
Glide.with(context).asBitmap().load(thumbnail)
|
||||||
.into(object: CustomTarget<Bitmap>() {
|
.into(object: CustomTarget<Bitmap>() {
|
||||||
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
|
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
|
||||||
@@ -2353,11 +2367,11 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
?.distinct()
|
?.distinct()
|
||||||
?.toList() ?: listOf() else audioSources?.toList() ?: listOf();
|
?.toList() ?: listOf() else audioSources?.toList() ?: listOf();
|
||||||
|
|
||||||
val canSetSpeed = !_isCasting || StateCasting.instance.activeDevice?.canSetSpeed == true
|
val canSetSpeed = !_isCasting || StateCasting.instance.activeDevice?.canSetSpeed() == true
|
||||||
val currentPlaybackRate = if (_isCasting) StateCasting.instance.activeDevice?.speed else _player.getPlaybackRate()
|
val currentPlaybackRate = if (_isCasting) StateCasting.instance.activeDevice?.speed else _player.getPlaybackRate()
|
||||||
val qualityPlaybackSpeedTitle = if (canSetSpeed) SlideUpMenuTitle(this.context).apply { setTitle(context.getString(R.string.playback_rate) + " (${String.format("%.2f", currentPlaybackRate)})"); } else null;
|
val qualityPlaybackSpeedTitle = if (canSetSpeed) SlideUpMenuTitle(this.context).apply { setTitle(context.getString(R.string.playback_rate) + " (${String.format("%.2f", currentPlaybackRate)})"); } else null;
|
||||||
_overlay_quality_selector = SlideUpMenuOverlay(this.context, _overlay_quality_container, context.getString(
|
_overlay_quality_selector = SlideUpMenuOverlay(this.context, _overlay_quality_container, context.getString(
|
||||||
R.string.quality), null, true,
|
R.string.quality), null, true,
|
||||||
qualityPlaybackSpeedTitle,
|
qualityPlaybackSpeedTitle,
|
||||||
if (canSetSpeed) SlideUpMenuButtonList(this.context, null, "playback_rate").apply {
|
if (canSetSpeed) SlideUpMenuButtonList(this.context, null, "playback_rate").apply {
|
||||||
val playbackSpeeds = Settings.instance.playback.getPlaybackSpeeds();
|
val playbackSpeeds = Settings.instance.playback.getPlaybackSpeeds();
|
||||||
@@ -2378,7 +2392,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
val newPlaybackSpeed = playbackSpeedString.toDouble();
|
val newPlaybackSpeed = playbackSpeedString.toDouble();
|
||||||
if (_isCasting) {
|
if (_isCasting) {
|
||||||
val ad = StateCasting.instance.activeDevice ?: return@subscribe
|
val ad = StateCasting.instance.activeDevice ?: return@subscribe
|
||||||
if (!ad.canSetSpeed) {
|
if (!ad.canSetSpeed()) {
|
||||||
return@subscribe
|
return@subscribe
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2502,6 +2516,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
if (!StateCasting.instance.resumeVideo()) {
|
if (!StateCasting.instance.resumeVideo()) {
|
||||||
_player.play();
|
_player.play();
|
||||||
}
|
}
|
||||||
|
onShouldEnterPictureInPictureChanged.emit()
|
||||||
|
|
||||||
//TODO: This was needed because handleLowerVolume was done.
|
//TODO: This was needed because handleLowerVolume was done.
|
||||||
//_player.setVolume(1.0f);
|
//_player.setVolume(1.0f);
|
||||||
@@ -2518,6 +2533,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
if (!StateCasting.instance.pauseVideo()) {
|
if (!StateCasting.instance.pauseVideo()) {
|
||||||
_player.pause();
|
_player.pause();
|
||||||
}
|
}
|
||||||
|
onShouldEnterPictureInPictureChanged.emit()
|
||||||
}
|
}
|
||||||
private fun handleSeek(ms: Long) {
|
private fun handleSeek(ms: Long) {
|
||||||
Logger.i(TAG, "handleSeek(ms=$ms)")
|
Logger.i(TAG, "handleSeek(ms=$ms)")
|
||||||
@@ -3264,8 +3280,13 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
val id = e.config.let { if(it is SourcePluginConfig) it.id else null };
|
val id = e.config.let { if(it is SourcePluginConfig) it.id else null };
|
||||||
val didLogin = if(id == null)
|
val didLogin = if(id == null)
|
||||||
false
|
false
|
||||||
else StatePlugins.instance.loginPlugin(context, id) {
|
else {
|
||||||
fetchVideo();
|
isLoginStop = true;
|
||||||
|
StatePlugins.instance.loginPlugin(context, id) {
|
||||||
|
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
fetchVideo();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if(!didLogin)
|
if(!didLogin)
|
||||||
UIDialogs.showDialogOk(context, R.drawable.ic_error_pred, "Failed to login");
|
UIDialogs.showDialogOk(context, R.drawable.ic_error_pred, "Failed to login");
|
||||||
@@ -3443,6 +3464,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
const val TAG_SHARE = "share";
|
const val TAG_SHARE = "share";
|
||||||
const val TAG_OVERLAY = "overlay";
|
const val TAG_OVERLAY = "overlay";
|
||||||
const val TAG_LIVECHAT = "livechat";
|
const val TAG_LIVECHAT = "livechat";
|
||||||
|
const val TAG_VODCHAT = "vodchat";
|
||||||
const val TAG_CHAPTERS = "chapters";
|
const val TAG_CHAPTERS = "chapters";
|
||||||
const val TAG_OPEN = "open";
|
const val TAG_OPEN = "open";
|
||||||
const val TAG_SEND_TO_DEVICE = "send_to_device";
|
const val TAG_SEND_TO_DEVICE = "send_to_device";
|
||||||
|
|||||||
+454
@@ -0,0 +1,454 @@
|
|||||||
|
package com.futo.platformplayer.fragment.mainactivity.special
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.content.DialogInterface
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.Spanned
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup.MarginLayoutParams
|
||||||
|
import android.widget.Button
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.FrameLayout.GONE
|
||||||
|
import android.widget.FrameLayout.VISIBLE
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.Settings
|
||||||
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
|
import com.futo.platformplayer.api.media.models.PlatformAuthorMembershipLink
|
||||||
|
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
|
||||||
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||||
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
|
import com.futo.platformplayer.dp
|
||||||
|
import com.futo.platformplayer.fixHtmlLinks
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.BrowserFragment
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.ChannelFragment
|
||||||
|
import com.futo.platformplayer.fragment.mainactivity.main.MainFragment
|
||||||
|
import com.futo.platformplayer.getNowDiffSeconds
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.selectBestImage
|
||||||
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
import com.futo.platformplayer.states.StateMeta
|
||||||
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
|
import com.futo.platformplayer.toHumanNowDiffString
|
||||||
|
import com.futo.platformplayer.toHumanNumber
|
||||||
|
import com.futo.platformplayer.views.MonetizationView
|
||||||
|
import com.futo.platformplayer.views.comments.AddCommentView
|
||||||
|
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||||
|
import com.futo.platformplayer.views.overlays.DescriptionOverlay
|
||||||
|
import com.futo.platformplayer.views.overlays.RepliesOverlay
|
||||||
|
import com.futo.platformplayer.views.overlays.SupportOverlay
|
||||||
|
import com.futo.platformplayer.views.platform.PlatformIndicator
|
||||||
|
import com.futo.platformplayer.views.segments.CommentsList
|
||||||
|
import com.futo.polycentric.core.ApiMethods
|
||||||
|
import com.futo.polycentric.core.Models
|
||||||
|
import com.futo.polycentric.core.PolycentricProfile
|
||||||
|
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||||
|
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
|
|
||||||
|
|
||||||
|
class CommentsModalBottomSheet : BottomSheetDialogFragment() {
|
||||||
|
var mainFragment: MainFragment? = null
|
||||||
|
|
||||||
|
private lateinit var containerContent: FrameLayout
|
||||||
|
private lateinit var containerContentMain: LinearLayout
|
||||||
|
private lateinit var containerContentReplies: RepliesOverlay
|
||||||
|
private lateinit var containerContentDescription: DescriptionOverlay
|
||||||
|
private lateinit var containerContentSupport: SupportOverlay
|
||||||
|
|
||||||
|
private lateinit var title: TextView
|
||||||
|
private lateinit var subTitle: TextView
|
||||||
|
private lateinit var channelName: TextView
|
||||||
|
private lateinit var channelMeta: TextView
|
||||||
|
private lateinit var creatorThumbnail: CreatorThumbnail
|
||||||
|
private lateinit var channelButton: LinearLayout
|
||||||
|
private lateinit var monetization: MonetizationView
|
||||||
|
private lateinit var platform: PlatformIndicator
|
||||||
|
private lateinit var textLikes: TextView
|
||||||
|
private lateinit var textDislikes: TextView
|
||||||
|
private lateinit var layoutRating: LinearLayout
|
||||||
|
private lateinit var imageDislikeIcon: ImageView
|
||||||
|
private lateinit var imageLikeIcon: ImageView
|
||||||
|
|
||||||
|
private lateinit var description: TextView
|
||||||
|
private lateinit var descriptionContainer: LinearLayout
|
||||||
|
private lateinit var descriptionViewMore: TextView
|
||||||
|
|
||||||
|
private lateinit var commentsList: CommentsList
|
||||||
|
private lateinit var addCommentView: AddCommentView
|
||||||
|
|
||||||
|
private var polycentricProfile: PolycentricProfile? = null
|
||||||
|
|
||||||
|
private lateinit var buttonPolycentric: Button
|
||||||
|
private lateinit var buttonPlatform: Button
|
||||||
|
|
||||||
|
private var tabIndex: Int? = null
|
||||||
|
|
||||||
|
private var contentOverlayView: View? = null
|
||||||
|
|
||||||
|
lateinit var video: IPlatformVideoDetails
|
||||||
|
|
||||||
|
private lateinit var behavior: BottomSheetBehavior<FrameLayout>
|
||||||
|
|
||||||
|
private val _taskLoadPolycentricProfile =
|
||||||
|
TaskHandler<PlatformID, PolycentricProfile?>(StateApp.instance.scopeGetter, { ApiMethods.getPolycentricProfileByClaim(
|
||||||
|
ApiMethods.SERVER, ApiMethods.FUTO_TRUST_ROOT, it.claimFieldType.toLong(), it.claimType.toLong(), it.value!!) }).success { setPolycentricProfile(it, animate = true) }
|
||||||
|
.exception<Throwable> {
|
||||||
|
Logger.w(TAG, "Failed to load claims.", it)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(
|
||||||
|
savedInstanceState: Bundle?,
|
||||||
|
): Dialog {
|
||||||
|
val bottomSheetDialog =
|
||||||
|
BottomSheetDialog(requireContext(), R.style.Custom_BottomSheetDialog_Theme)
|
||||||
|
bottomSheetDialog.setContentView(R.layout.modal_comments)
|
||||||
|
|
||||||
|
behavior = bottomSheetDialog.behavior
|
||||||
|
|
||||||
|
// TODO figure out how to not need all of these non null assertions
|
||||||
|
containerContent = bottomSheetDialog.findViewById(R.id.content_container)!!
|
||||||
|
containerContentMain = bottomSheetDialog.findViewById(R.id.videodetail_container_main)!!
|
||||||
|
containerContentReplies =
|
||||||
|
bottomSheetDialog.findViewById(R.id.videodetail_container_replies)!!
|
||||||
|
containerContentDescription =
|
||||||
|
bottomSheetDialog.findViewById(R.id.videodetail_container_description)!!
|
||||||
|
containerContentSupport =
|
||||||
|
bottomSheetDialog.findViewById(R.id.videodetail_container_support)!!
|
||||||
|
|
||||||
|
title = bottomSheetDialog.findViewById(R.id.videodetail_title)!!
|
||||||
|
subTitle = bottomSheetDialog.findViewById(R.id.videodetail_meta)!!
|
||||||
|
channelName = bottomSheetDialog.findViewById(R.id.videodetail_channel_name)!!
|
||||||
|
channelMeta = bottomSheetDialog.findViewById(R.id.videodetail_channel_meta)!!
|
||||||
|
creatorThumbnail = bottomSheetDialog.findViewById(R.id.creator_thumbnail)!!
|
||||||
|
channelButton = bottomSheetDialog.findViewById(R.id.videodetail_channel_button)!!
|
||||||
|
monetization = bottomSheetDialog.findViewById(R.id.monetization)!!
|
||||||
|
platform = bottomSheetDialog.findViewById(R.id.videodetail_platform)!!
|
||||||
|
layoutRating = bottomSheetDialog.findViewById(R.id.layout_rating)!!
|
||||||
|
textDislikes = bottomSheetDialog.findViewById(R.id.text_dislikes)!!
|
||||||
|
textLikes = bottomSheetDialog.findViewById(R.id.text_likes)!!
|
||||||
|
imageLikeIcon = bottomSheetDialog.findViewById(R.id.image_like_icon)!!
|
||||||
|
imageDislikeIcon = bottomSheetDialog.findViewById(R.id.image_dislike_icon)!!
|
||||||
|
|
||||||
|
description = bottomSheetDialog.findViewById(R.id.videodetail_description)!!
|
||||||
|
descriptionContainer =
|
||||||
|
bottomSheetDialog.findViewById(R.id.videodetail_description_container)!!
|
||||||
|
descriptionViewMore =
|
||||||
|
bottomSheetDialog.findViewById(R.id.videodetail_description_view_more)!!
|
||||||
|
|
||||||
|
addCommentView = bottomSheetDialog.findViewById(R.id.add_comment_view)!!
|
||||||
|
commentsList = bottomSheetDialog.findViewById(R.id.comments_list)!!
|
||||||
|
buttonPolycentric = bottomSheetDialog.findViewById(R.id.button_polycentric)!!
|
||||||
|
buttonPlatform = bottomSheetDialog.findViewById(R.id.button_platform)!!
|
||||||
|
|
||||||
|
commentsList.onAuthorClick.subscribe { c ->
|
||||||
|
if (c !is PolycentricPlatformComment) {
|
||||||
|
return@subscribe
|
||||||
|
}
|
||||||
|
val id = c.author.id.value
|
||||||
|
|
||||||
|
Logger.i(TAG, "onAuthorClick: $id")
|
||||||
|
if (id != null && id.startsWith("polycentric://")) {
|
||||||
|
val navUrl = "https://harbor.social/" + id.substring("polycentric://".length)
|
||||||
|
mainFragment!!.startActivity(Intent(Intent.ACTION_VIEW, navUrl.toUri()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
commentsList.onRepliesClick.subscribe { c ->
|
||||||
|
val replyCount = c.replyCount ?: 0
|
||||||
|
var metadata = ""
|
||||||
|
if (replyCount > 0) {
|
||||||
|
metadata += "$replyCount " + requireContext().getString(R.string.replies)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c is PolycentricPlatformComment) {
|
||||||
|
var parentComment: PolycentricPlatformComment = c
|
||||||
|
containerContentReplies.load(tabIndex!! != 0, metadata, c.contextUrl, c.reference, c, { StatePolycentric.instance.getCommentPager(c.contextUrl, c.reference) }, {
|
||||||
|
val newComment = parentComment.cloneWithUpdatedReplyCount(
|
||||||
|
(parentComment.replyCount ?: 0) + 1
|
||||||
|
)
|
||||||
|
commentsList.replaceComment(parentComment, newComment)
|
||||||
|
parentComment = newComment
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
containerContentReplies.load(tabIndex!! != 0, metadata, null, null, c, { StatePlatform.instance.getSubComments(c) })
|
||||||
|
}
|
||||||
|
animateOpenOverlayView(containerContentReplies)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StatePolycentric.instance.enabled) {
|
||||||
|
buttonPolycentric.setOnClickListener {
|
||||||
|
setTabIndex(0)
|
||||||
|
StateMeta.instance.setLastCommentSection(0)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
buttonPolycentric.visibility = GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
buttonPlatform.setOnClickListener {
|
||||||
|
setTabIndex(1)
|
||||||
|
StateMeta.instance.setLastCommentSection(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
val ref = Models.referenceFromBuffer(video.url.toByteArray())
|
||||||
|
addCommentView.setContext(video.url, ref)
|
||||||
|
|
||||||
|
if (Settings.instance.comments.recommendationsDefault && !Settings.instance.comments.hideRecommendations) {
|
||||||
|
setTabIndex(2, true)
|
||||||
|
} else {
|
||||||
|
when (Settings.instance.comments.defaultCommentSection) {
|
||||||
|
0 -> if (Settings.instance.other.polycentricEnabled) setTabIndex(0, true) else setTabIndex(1, true)
|
||||||
|
1 -> setTabIndex(1, true)
|
||||||
|
2 -> setTabIndex(StateMeta.instance.getLastCommentSection(), true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
containerContentDescription.onClose.subscribe { animateCloseOverlayView() }
|
||||||
|
containerContentReplies.onClose.subscribe { animateCloseOverlayView() }
|
||||||
|
|
||||||
|
descriptionViewMore.setOnClickListener {
|
||||||
|
animateOpenOverlayView(containerContentDescription)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDescriptionUI(video.description.fixHtmlLinks())
|
||||||
|
|
||||||
|
val dp5 =
|
||||||
|
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics)
|
||||||
|
val dp2 =
|
||||||
|
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2f, resources.displayMetrics)
|
||||||
|
|
||||||
|
//UI
|
||||||
|
title.text = video.name
|
||||||
|
channelName.text = video.author.name
|
||||||
|
if (video.author.subscribers != null) {
|
||||||
|
channelMeta.text = if ((video.author.subscribers
|
||||||
|
?: 0) > 0
|
||||||
|
) video.author.subscribers!!.toHumanNumber() + " " + requireContext().getString(R.string.subscribers) else ""
|
||||||
|
(channelName.layoutParams as MarginLayoutParams).setMargins(
|
||||||
|
0, (dp5 * -1).toInt(), 0, 0
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
channelMeta.text = ""
|
||||||
|
(channelName.layoutParams as MarginLayoutParams).setMargins(0, (dp2).toInt(), 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
video.author.let {
|
||||||
|
if (it is PlatformAuthorMembershipLink && !it.membershipUrl.isNullOrEmpty()) monetization.setPlatformMembership(video.id.pluginId, it.membershipUrl)
|
||||||
|
else monetization.setPlatformMembership(null, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
val subTitleSegments: ArrayList<String> = ArrayList()
|
||||||
|
if (video.viewCount > 0) subTitleSegments.add("${video.viewCount.toHumanNumber()} ${if (video.isLive) requireContext().getString(
|
||||||
|
R.string.watching_now) else requireContext().getString(R.string.views)}")
|
||||||
|
if (video.datetime != null) {
|
||||||
|
val diff = video.datetime?.getNowDiffSeconds() ?: 0
|
||||||
|
val ago = video.datetime?.toHumanNowDiffString(true)
|
||||||
|
if (diff >= 0) subTitleSegments.add("$ago ago")
|
||||||
|
else subTitleSegments.add("available in $ago")
|
||||||
|
}
|
||||||
|
|
||||||
|
platform.setPlatformFromClientID(video.id.pluginId)
|
||||||
|
subTitle.text = subTitleSegments.joinToString(" • ")
|
||||||
|
creatorThumbnail.setThumbnail(video.author.thumbnail, false)
|
||||||
|
|
||||||
|
setPolycentricProfile(null, animate = false)
|
||||||
|
_taskLoadPolycentricProfile.run(video.author.id)
|
||||||
|
|
||||||
|
when (video.rating) {
|
||||||
|
is RatingLikeDislikes -> {
|
||||||
|
val r = video.rating as RatingLikeDislikes
|
||||||
|
layoutRating.visibility = VISIBLE
|
||||||
|
|
||||||
|
textLikes.visibility = VISIBLE
|
||||||
|
imageLikeIcon.visibility = VISIBLE
|
||||||
|
textLikes.text = r.likes.toHumanNumber()
|
||||||
|
|
||||||
|
imageDislikeIcon.visibility = VISIBLE
|
||||||
|
textDislikes.visibility = VISIBLE
|
||||||
|
textDislikes.text = r.dislikes.toHumanNumber()
|
||||||
|
}
|
||||||
|
|
||||||
|
is RatingLikes -> {
|
||||||
|
val r = video.rating as RatingLikes
|
||||||
|
layoutRating.visibility = VISIBLE
|
||||||
|
|
||||||
|
textLikes.visibility = VISIBLE
|
||||||
|
imageLikeIcon.visibility = VISIBLE
|
||||||
|
textLikes.text = r.likes.toHumanNumber()
|
||||||
|
|
||||||
|
imageDislikeIcon.visibility = GONE
|
||||||
|
textDislikes.visibility = GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
layoutRating.visibility = GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
monetization.onSupportTap.subscribe {
|
||||||
|
containerContentSupport.setPolycentricProfile(polycentricProfile)
|
||||||
|
animateOpenOverlayView(containerContentSupport)
|
||||||
|
}
|
||||||
|
|
||||||
|
monetization.onStoreTap.subscribe {
|
||||||
|
polycentricProfile?.systemState?.store?.let {
|
||||||
|
try {
|
||||||
|
val uri = it.toUri()
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW)
|
||||||
|
intent.data = uri
|
||||||
|
requireContext().startActivity(intent)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to open URI: '${it}'.", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
monetization.onUrlTap.subscribe {
|
||||||
|
mainFragment!!.navigate<BrowserFragment>(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
addCommentView.onCommentAdded.subscribe {
|
||||||
|
commentsList.addComment(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
channelButton.setOnClickListener {
|
||||||
|
mainFragment!!.navigate<ChannelFragment>(video.author)
|
||||||
|
}
|
||||||
|
|
||||||
|
return bottomSheetDialog
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDismiss(dialog: DialogInterface) {
|
||||||
|
super.onDismiss(dialog)
|
||||||
|
animateCloseOverlayView()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setPolycentricProfile(profile: PolycentricProfile?, animate: Boolean) {
|
||||||
|
polycentricProfile = profile
|
||||||
|
|
||||||
|
val dp35 = 35.dp(requireContext().resources)
|
||||||
|
val avatar = profile?.systemState?.avatar?.selectBestImage(dp35 * dp35)
|
||||||
|
?.let { it.toURLInfoSystemLinkUrl(profile.system.toProto(), it.process, profile.systemState.servers.toList()) }
|
||||||
|
|
||||||
|
if (avatar != null) {
|
||||||
|
creatorThumbnail.setThumbnail(avatar, animate)
|
||||||
|
} else {
|
||||||
|
creatorThumbnail.setThumbnail(video.author.thumbnail, animate)
|
||||||
|
creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto())
|
||||||
|
}
|
||||||
|
|
||||||
|
val username = profile?.systemState?.username
|
||||||
|
if (username != null) {
|
||||||
|
channelName.text = username
|
||||||
|
}
|
||||||
|
|
||||||
|
monetization.setPolycentricProfile(profile)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setTabIndex(index: Int?, forceReload: Boolean = false) {
|
||||||
|
Logger.i(TAG, "setTabIndex (index: ${index}, forceReload: ${forceReload})")
|
||||||
|
val changed = tabIndex != index || forceReload
|
||||||
|
if (!changed) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tabIndex = index
|
||||||
|
buttonPlatform.setTextColor(resources.getColor(if (index == 1) R.color.white else R.color.gray_ac, null))
|
||||||
|
buttonPolycentric.setTextColor(resources.getColor(if (index == 0) R.color.white else R.color.gray_ac, null))
|
||||||
|
|
||||||
|
when (index) {
|
||||||
|
null -> {
|
||||||
|
addCommentView.visibility = GONE
|
||||||
|
commentsList.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
0 -> {
|
||||||
|
addCommentView.visibility = VISIBLE
|
||||||
|
fetchPolycentricComments()
|
||||||
|
}
|
||||||
|
|
||||||
|
1 -> {
|
||||||
|
addCommentView.visibility = GONE
|
||||||
|
fetchComments()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fetchComments() {
|
||||||
|
Logger.i(TAG, "fetchComments")
|
||||||
|
video.let {
|
||||||
|
commentsList.load(true) { StatePlatform.instance.getComments(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fetchPolycentricComments() {
|
||||||
|
Logger.i(TAG, "fetchPolycentricComments")
|
||||||
|
val video = video
|
||||||
|
val idValue = video.id.value
|
||||||
|
if (video.url.isEmpty()) {
|
||||||
|
Logger.w(TAG, "Failed to fetch polycentric comments because url was null")
|
||||||
|
commentsList.clear()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val ref = Models.referenceFromBuffer(video.url.toByteArray())
|
||||||
|
val extraBytesRef = idValue?.let { if (it.isNotEmpty()) it.toByteArray() else null }
|
||||||
|
commentsList.load(false) { StatePolycentric.instance.getCommentPager(video.url, ref, listOfNotNull(extraBytesRef)); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateDescriptionUI(text: Spanned) {
|
||||||
|
containerContentDescription.load(text)
|
||||||
|
description.text = text
|
||||||
|
|
||||||
|
if (description.text.isNotEmpty()) descriptionContainer.visibility = VISIBLE
|
||||||
|
else descriptionContainer.visibility = GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun animateOpenOverlayView(view: View) {
|
||||||
|
if (contentOverlayView != null) {
|
||||||
|
Logger.e(TAG, "Content overlay already open")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
behavior.isDraggable = false
|
||||||
|
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||||
|
|
||||||
|
val animHeight = containerContentMain.height
|
||||||
|
|
||||||
|
view.translationY = animHeight.toFloat()
|
||||||
|
view.visibility = VISIBLE
|
||||||
|
|
||||||
|
view.animate().setDuration(300).translationY(0f).withEndAction {
|
||||||
|
contentOverlayView = view
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun animateCloseOverlayView() {
|
||||||
|
val curView = contentOverlayView
|
||||||
|
if (curView == null) {
|
||||||
|
Logger.e(TAG, "No content overlay open")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
behavior.isDraggable = true
|
||||||
|
|
||||||
|
val animHeight = contentOverlayView!!.height
|
||||||
|
|
||||||
|
curView.animate().setDuration(300).translationY(animHeight.toFloat()).withEndAction {
|
||||||
|
curView.visibility = GONE
|
||||||
|
contentOverlayView = null
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "ModalBottomSheet"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,16 +3,9 @@ package com.futo.platformplayer.models
|
|||||||
import com.futo.platformplayer.casting.CastProtocolType
|
import com.futo.platformplayer.casting.CastProtocolType
|
||||||
|
|
||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
class CastingDeviceInfo {
|
class CastingDeviceInfo(
|
||||||
var name: String;
|
var name: String,
|
||||||
var type: CastProtocolType;
|
var type: CastProtocolType,
|
||||||
var addresses: Array<String>;
|
var addresses: Array<String>,
|
||||||
var port: Int;
|
var port: Int
|
||||||
|
)
|
||||||
constructor(name: String, type: CastProtocolType, addresses: Array<String>, port: Int) {
|
|
||||||
this.name = name;
|
|
||||||
this.type = type;
|
|
||||||
this.addresses = addresses;
|
|
||||||
this.port = port;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -136,8 +136,12 @@ class StateApp {
|
|||||||
return _scope;
|
return _scope;
|
||||||
}
|
}
|
||||||
val scope: CoroutineScope get() {
|
val scope: CoroutineScope get() {
|
||||||
val thisScope = scopeOrNull
|
val thisScope = scopeOrNull;
|
||||||
?: throw IllegalStateException("Attempted to use a global lifetime scope while MainActivity is no longer available");
|
if(thisScope == null) {
|
||||||
|
//throw IllegalStateException("Attempted to use a global lifetime scope while MainActivity is no longer available");
|
||||||
|
Logger.w(TAG, "Attempted to use a global lifetime scope while MainActivity is no longer available, USING GLOBAL SCOPE");
|
||||||
|
return GlobalScope;
|
||||||
|
}
|
||||||
return thisScope;
|
return thisScope;
|
||||||
}
|
}
|
||||||
val scopeGetter: ()->CoroutineScope get() {
|
val scopeGetter: ()->CoroutineScope get() {
|
||||||
|
|||||||
@@ -194,17 +194,18 @@ class StateHistory {
|
|||||||
_remoteHistoryDatesStore.save();
|
_remoteHistoryDatesStore.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
fun syncRemoteHistory(plugin: JSClient) {
|
fun syncRemoteHistory(plugin: JSClient): Int {
|
||||||
if (plugin.capabilities.hasGetUserHistory &&
|
if (plugin.capabilities.hasGetUserHistory &&
|
||||||
plugin.isLoggedIn) {
|
plugin.isLoggedIn) {
|
||||||
Logger.i(TAG, "Syncing remote history for plugin [${plugin.name}]");
|
Logger.i(TAG, "Syncing remote history for plugin [${plugin.name}]");
|
||||||
|
|
||||||
val hist = StatePlatform.instance.getUserHistory(plugin.id);
|
val hist = StatePlatform.instance.getUserHistory(plugin.id);
|
||||||
|
|
||||||
syncRemoteHistory(plugin.id, hist, 100, 3);
|
return syncRemoteHistory(plugin.id, hist, 100, 3);
|
||||||
}
|
}
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
fun syncRemoteHistory(pluginId: String, videos: IPager<IPlatformContent>, maxVideos: Int, maxPages: Int) {
|
fun syncRemoteHistory(pluginId: String, videos: IPager<IPlatformContent>, maxVideos: Int, maxPages: Int): Int {
|
||||||
val lastDate = _remoteHistoryDatesStore.get(pluginId) ?: OffsetDateTime.MIN;
|
val lastDate = _remoteHistoryDatesStore.get(pluginId) ?: OffsetDateTime.MIN;
|
||||||
val maxVideosCount = if(maxVideos <= 0) 500 else maxVideos;
|
val maxVideosCount = if(maxVideos <= 0) 500 else maxVideos;
|
||||||
val maxPageCount = if(maxPages <= 0) 3 else maxPages;
|
val maxPageCount = if(maxPages <= 0) 3 else maxPages;
|
||||||
@@ -272,12 +273,14 @@ class StateHistory {
|
|||||||
}
|
}
|
||||||
catch(ex: Throwable){}
|
catch(ex: Throwable){}
|
||||||
}
|
}
|
||||||
|
return updated;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch(ex: Throwable) {
|
catch(ex: Throwable) {
|
||||||
val plugin = if(pluginId != StateDeveloper.DEV_ID) StatePlugins.instance.getPlugin(pluginId) else null;
|
val plugin = if(pluginId != StateDeveloper.DEV_ID) StatePlugins.instance.getPlugin(pluginId) else null;
|
||||||
Logger.e(TAG, "Sync Remote History failed for [${plugin?.config?.name}] due to: " + ex.message)
|
Logger.e(TAG, "Sync Remote History failed for [${plugin?.config?.name}] due to: " + ex.message)
|
||||||
}
|
}
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -177,16 +177,11 @@ class StatePlatform {
|
|||||||
}
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
|
var toDisables = mutableListOf<IPlatformClient>();
|
||||||
var enabled: Array<String>;
|
var enabled: Array<String>;
|
||||||
synchronized(_clientsLock) {
|
synchronized(_clientsLock) {
|
||||||
for(e in _enabledClients) {
|
for(e in _enabledClients) {
|
||||||
try {
|
toDisables.add(e);
|
||||||
e.disable();
|
|
||||||
onSourceDisabled.emit(e);
|
|
||||||
}
|
|
||||||
catch(ex: Throwable) {
|
|
||||||
UIDialogs.appToast(ToastView.Toast("If this happens often, please inform the developers on Github", false, null, "Plugin [${e.name}] failed to disable"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_enabledClients.clear();
|
_enabledClients.clear();
|
||||||
@@ -236,6 +231,18 @@ class StatePlatform {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
selectClients(*enabled);
|
selectClients(*enabled);
|
||||||
|
|
||||||
|
for(toDisable in toDisables) {
|
||||||
|
launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
toDisable.disable();
|
||||||
|
onSourceDisabled.emit(toDisable);
|
||||||
|
}
|
||||||
|
catch(ex: Throwable) {
|
||||||
|
Logger.e(TAG, "FAILED TO DISABLE CLIENT [${toDisable?.name}] AFTER UpdateAvailableClients", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -348,11 +355,11 @@ class StatePlatform {
|
|||||||
StateApp.instance.handleCaptchaException(c, ex);
|
StateApp.instance.handleCaptchaException(c, ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var toDisable: IPlatformClient? = null;
|
||||||
synchronized(_clientsLock) {
|
synchronized(_clientsLock) {
|
||||||
if (_enabledClients.contains(client)) {
|
if (_enabledClients.contains(client)) {
|
||||||
_enabledClients.remove(client);
|
_enabledClients.remove(client);
|
||||||
client.disable();
|
toDisable = client;
|
||||||
onSourceDisabled.emit(client);
|
|
||||||
newClient.initialize();
|
newClient.initialize();
|
||||||
_enabledClients.add(newClient);
|
_enabledClients.add(newClient);
|
||||||
}
|
}
|
||||||
@@ -360,6 +367,18 @@ class StatePlatform {
|
|||||||
_availableClients.removeIf { it.id == id };
|
_availableClients.removeIf { it.id == id };
|
||||||
_availableClients.add(newClient);
|
_availableClients.add(newClient);
|
||||||
}
|
}
|
||||||
|
if(toDisable != null) {
|
||||||
|
launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
toDisable?.disable();
|
||||||
|
onSourceDisabled.emit(client);
|
||||||
|
}
|
||||||
|
catch (ex: Throwable) {
|
||||||
|
Logger.e(TAG, "FAILED TO DISABLE CLIENT [${toDisable?.name}] AFTER RELOAD", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
afterReload?.invoke();
|
afterReload?.invoke();
|
||||||
return@withContext newClient;
|
return@withContext newClient;
|
||||||
};
|
};
|
||||||
@@ -500,7 +519,7 @@ class StatePlatform {
|
|||||||
.toList()
|
.toList()
|
||||||
.associateWith { 1f };
|
.associateWith { 1f };
|
||||||
|
|
||||||
val pager = MultiDistributionContentPager(pages);
|
val pager = MultiDistributionContentPager(pages, 2);
|
||||||
pager.initialize();
|
pager.initialize();
|
||||||
return pager;
|
return pager;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -179,8 +179,9 @@ class StatePlugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StateApp.instance.scope.launch(Dispatchers.IO) {
|
StateApp.instance.scope.launch(Dispatchers.IO) {
|
||||||
StatePlatform.instance.reloadClient(context, id);
|
StatePlatform.instance.reloadClient(context, id) {
|
||||||
afterLogin.invoke();
|
afterLogin.invoke();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return true;
|
return true;
|
||||||
@@ -401,18 +402,25 @@ class StatePlugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val icon = config.absoluteIconUrl?.let { absIconUrl ->
|
val icon = config.absoluteIconUrl?.let { absIconUrl ->
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
it.setText("Saving plugin...");
|
|
||||||
it.setProgress(0.75);
|
|
||||||
}
|
|
||||||
val iconResp = client.get(absIconUrl);
|
val iconResp = client.get(absIconUrl);
|
||||||
if(iconResp.isOk)
|
if(iconResp.isOk)
|
||||||
return@let iconResp.body?.byteStream()?.use { it.readBytes() };
|
return@let iconResp.body?.byteStream()?.use { it.readBytes() };
|
||||||
return@let null;
|
return@let null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
it.setText("Saving plugin...");
|
||||||
|
it.setProgress(0.75);
|
||||||
|
}
|
||||||
|
|
||||||
val installEx = StatePlugins.instance.createPlugin(config, script, icon, reinstall);
|
val installEx = StatePlugins.instance.createPlugin(config, script, icon, reinstall);
|
||||||
if(installEx != null)
|
if(installEx != null)
|
||||||
throw installEx;
|
throw installEx;
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
it.setText("Reloading available plugins...");
|
||||||
|
it.setProgress(0.9);
|
||||||
|
}
|
||||||
StatePlatform.instance.updateAvailableClients(context);
|
StatePlatform.instance.updateAvailableClients(context);
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
@@ -475,6 +483,7 @@ class StatePlugins {
|
|||||||
delay(500);
|
delay(500);
|
||||||
|
|
||||||
val client = ManagedHttpClient();
|
val client = ManagedHttpClient();
|
||||||
|
client.setTimeout(10000);
|
||||||
try {
|
try {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
onProgress.invoke("Validating script", 0.25);
|
onProgress.invoke("Validating script", 0.25);
|
||||||
@@ -489,14 +498,14 @@ class StatePlugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val icon = config.absoluteIconUrl?.let { absIconUrl ->
|
val icon = config.absoluteIconUrl?.let { absIconUrl ->
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
onProgress.invoke("Saving plugin", 0.75);
|
|
||||||
}
|
|
||||||
val iconResp = client.get(absIconUrl);
|
val iconResp = client.get(absIconUrl);
|
||||||
if (iconResp.isOk)
|
if (iconResp.isOk)
|
||||||
return@let iconResp.body?.byteStream()?.use { it.readBytes() };
|
return@let iconResp.body?.byteStream()?.use { it.readBytes() };
|
||||||
return@let null;
|
return@let null;
|
||||||
}
|
}
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
onProgress.invoke("Saving plugin", 0.75);
|
||||||
|
}
|
||||||
val installEx = StatePlugins.instance.createPlugin(config, script, icon, true);
|
val installEx = StatePlugins.instance.createPlugin(config, script, icon, true);
|
||||||
if (installEx != null)
|
if (installEx != null)
|
||||||
throw installEx;
|
throw installEx;
|
||||||
@@ -520,9 +529,7 @@ class StatePlugins {
|
|||||||
if(id == StateDeveloper.DEV_ID)
|
if(id == StateDeveloper.DEV_ID)
|
||||||
throw IllegalStateException("Attempted to retrieve a persistent developer plugin, this is not allowed");
|
throw IllegalStateException("Attempted to retrieve a persistent developer plugin, this is not allowed");
|
||||||
|
|
||||||
synchronized(_plugins) {
|
return _plugins.findItem { it.config.id == id };
|
||||||
return _plugins.findItem { it.config.id == id };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
fun getPlugins(): List<SourcePluginDescriptor> {
|
fun getPlugins(): List<SourcePluginDescriptor> {
|
||||||
return _plugins.getItems();
|
return _plugins.getItems();
|
||||||
@@ -531,12 +538,10 @@ class StatePlugins {
|
|||||||
|
|
||||||
fun deletePlugin(id: String) {
|
fun deletePlugin(id: String) {
|
||||||
synchronized(_pluginScripts) {
|
synchronized(_pluginScripts) {
|
||||||
synchronized(_plugins) {
|
_pluginScripts.deleteFile(id);
|
||||||
_pluginScripts.deleteFile(id);
|
val plugins = _plugins.findItems { it.config.id == id };
|
||||||
val plugins = _plugins.findItems { it.config.id == id };
|
for(plugin in plugins)
|
||||||
for(plugin in plugins)
|
_plugins.delete(plugin);
|
||||||
_plugins.delete(plugin);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun createPlugin(config: SourcePluginConfig, script: String, icon: ByteArray? = null, reinstall: Boolean = false, flags: List<String> = listOf()) : Throwable? {
|
fun createPlugin(config: SourcePluginConfig, script: String, icon: ByteArray? = null, reinstall: Boolean = false, flags: List<String> = listOf()) : Throwable? {
|
||||||
|
|||||||
@@ -57,9 +57,12 @@ class StateSync {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var relayServerUrl = Settings.instance.synchronization.syncServer;
|
||||||
|
Logger.i(TAG, "Relay used: ${relayServerUrl}");
|
||||||
|
|
||||||
syncService = SyncService(
|
syncService = SyncService(
|
||||||
SERVICE_NAME,
|
SERVICE_NAME,
|
||||||
RELAY_SERVER,
|
relayServerUrl,
|
||||||
RELAY_PUBLIC_KEY,
|
RELAY_PUBLIC_KEY,
|
||||||
APP_ID,
|
APP_ID,
|
||||||
StoreBasedSyncDatabaseProvider(),
|
StoreBasedSyncDatabaseProvider(),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.content.Context
|
|||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
|
import androidx.core.view.children
|
||||||
import androidx.lifecycle.findViewTreeLifecycleOwner
|
import androidx.lifecycle.findViewTreeLifecycleOwner
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
@@ -28,6 +29,8 @@ import kotlinx.coroutines.launch
|
|||||||
class ToggleBar : LinearLayout {
|
class ToggleBar : LinearLayout {
|
||||||
private val _tagsContainer: LinearLayout;
|
private val _tagsContainer: LinearLayout;
|
||||||
|
|
||||||
|
private var allowLongPress: Boolean = false;
|
||||||
|
|
||||||
override fun onAttachedToWindow() {
|
override fun onAttachedToWindow() {
|
||||||
super.onAttachedToWindow();
|
super.onAttachedToWindow();
|
||||||
}
|
}
|
||||||
@@ -48,12 +51,31 @@ class ToggleBar : LinearLayout {
|
|||||||
for(button in buttons) {
|
for(button in buttons) {
|
||||||
_tagsContainer.addView(ToggleTagView(context).apply {
|
_tagsContainer.addView(ToggleTagView(context).apply {
|
||||||
if(button.icon > 0)
|
if(button.icon > 0)
|
||||||
this.setInfo(button.icon, button.name, button.isActive, button.isButton);
|
this.setInfo(button.icon, button.name, button.isActive, button.isButton, button.tag);
|
||||||
else if(button.iconVariable != null)
|
else if(button.iconVariable != null)
|
||||||
this.setInfo(button.iconVariable, button.name, button.isActive, button.isButton);
|
this.setInfo(button.iconVariable, button.name, button.isActive, button.isButton, button.tag);
|
||||||
else
|
else
|
||||||
this.setInfo(button.name, button.isActive, button.isButton);
|
this.setInfo(button.name, button.isActive, button.isButton, button.tag);
|
||||||
this.onClick.subscribe({ view, enabled -> button.action(view, enabled); });
|
this.onClick.subscribe({ view, enabled -> button.action(view, enabled); });
|
||||||
|
if(allowLongPress) {
|
||||||
|
this.onLongClick.subscribe({ view, enabled ->
|
||||||
|
for (tagView in _tagsContainer.children.filter { it is ToggleTagView }) {
|
||||||
|
if (tagView != view && tagView is ToggleTagView && !tagView.isButton) {
|
||||||
|
if (enabled && !tagView.isActive) {
|
||||||
|
tagView.handleClick();
|
||||||
|
} else if (!enabled && tagView.isActive) {
|
||||||
|
tagView.handleClick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else if(button.actionLong != null) {
|
||||||
|
this.onLongClick.subscribe({ view, enabled ->
|
||||||
|
val tags = _tagsContainer.children.filter { it is ToggleTagView }.map { it as ToggleTagView }.toList();
|
||||||
|
button.actionLong!!(view, tags, enabled);
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -63,16 +85,18 @@ class ToggleBar : LinearLayout {
|
|||||||
val icon: Int;
|
val icon: Int;
|
||||||
val iconVariable: ImageVariable?;
|
val iconVariable: ImageVariable?;
|
||||||
val action: (ToggleTagView, Boolean)->Unit;
|
val action: (ToggleTagView, Boolean)->Unit;
|
||||||
|
val actionLong: ((ToggleTagView, List<ToggleTagView>, Boolean) -> Unit)?;
|
||||||
val isActive: Boolean;
|
val isActive: Boolean;
|
||||||
var isButton: Boolean = false
|
var isButton: Boolean = false
|
||||||
private set;
|
private set;
|
||||||
var tag: String? = null;
|
var tag: String? = null;
|
||||||
|
|
||||||
constructor(name: String, icon: ImageVariable?, isActive: Boolean = false, action: (ToggleTagView, Boolean)->Unit) {
|
constructor(name: String, icon: ImageVariable?, isActive: Boolean = false, action: (ToggleTagView, Boolean)->Unit, actionLong: ((ToggleTagView, List<ToggleTagView>, Boolean)->Unit)? = null) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.icon = 0;
|
this.icon = 0;
|
||||||
this.iconVariable = icon;
|
this.iconVariable = icon;
|
||||||
this.action = action;
|
this.action = action;
|
||||||
|
this.actionLong = actionLong;
|
||||||
this.isActive = isActive;
|
this.isActive = isActive;
|
||||||
}
|
}
|
||||||
constructor(name: String, icon: Int, isActive: Boolean = false, action: (ToggleTagView, Boolean)->Unit) {
|
constructor(name: String, icon: Int, isActive: Boolean = false, action: (ToggleTagView, Boolean)->Unit) {
|
||||||
@@ -80,6 +104,7 @@ class ToggleBar : LinearLayout {
|
|||||||
this.icon = icon;
|
this.icon = icon;
|
||||||
this.iconVariable = null;
|
this.iconVariable = null;
|
||||||
this.action = action;
|
this.action = action;
|
||||||
|
this.actionLong = null;
|
||||||
this.isActive = isActive;
|
this.isActive = isActive;
|
||||||
}
|
}
|
||||||
constructor(name: String, isActive: Boolean = false, action: (ToggleTagView, Boolean)->Unit) {
|
constructor(name: String, isActive: Boolean = false, action: (ToggleTagView, Boolean)->Unit) {
|
||||||
@@ -87,6 +112,7 @@ class ToggleBar : LinearLayout {
|
|||||||
this.icon = 0;
|
this.icon = 0;
|
||||||
this.iconVariable = null;
|
this.iconVariable = null;
|
||||||
this.action = action;
|
this.action = action;
|
||||||
|
this.actionLong = null;
|
||||||
this.isActive = isActive;
|
this.isActive = isActive;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,21 +4,19 @@ import android.graphics.drawable.Animatable
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.LinearLayout
|
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.casting.AirPlayCastingDevice
|
import com.futo.platformplayer.Settings
|
||||||
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.casting.CastConnectionState
|
import com.futo.platformplayer.casting.CastConnectionState
|
||||||
|
import com.futo.platformplayer.casting.CastProtocolType
|
||||||
import com.futo.platformplayer.casting.CastingDevice
|
import com.futo.platformplayer.casting.CastingDevice
|
||||||
import com.futo.platformplayer.casting.ChromecastCastingDevice
|
|
||||||
import com.futo.platformplayer.casting.FCastCastingDevice
|
|
||||||
import com.futo.platformplayer.casting.StateCasting
|
import com.futo.platformplayer.casting.StateCasting
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.constructs.Event2
|
import com.futo.platformplayer.logging.Logger
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import com.futo.platformplayer.UIDialogs
|
|
||||||
|
|
||||||
class DeviceViewHolder : ViewHolder {
|
class DeviceViewHolder : ViewHolder {
|
||||||
private val _layoutDevice: FrameLayout;
|
private val _layoutDevice: FrameLayout;
|
||||||
@@ -56,16 +54,18 @@ class DeviceViewHolder : ViewHolder {
|
|||||||
|
|
||||||
val connect = {
|
val connect = {
|
||||||
device?.let { dev ->
|
device?.let { dev ->
|
||||||
if (dev.isReady) {
|
try {
|
||||||
StateCasting.instance.activeDevice?.stopCasting()
|
if (dev.isReady) {
|
||||||
StateCasting.instance.connectDevice(dev)
|
StateCasting.instance.activeDevice?.stopPlayback()
|
||||||
onConnect.emit(dev)
|
StateCasting.instance.connectDevice(dev)
|
||||||
} else {
|
onConnect.emit(dev)
|
||||||
try {
|
} else {
|
||||||
view.context?.let { UIDialogs.toast(it, "Device not ready, may be offline") }
|
view.context?.let {
|
||||||
} catch (e: Throwable) {
|
UIDialogs.toast(it, "Device not ready, may be offline")
|
||||||
//Ignored
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to connect: $e")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,15 +81,25 @@ class DeviceViewHolder : ViewHolder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun bind(d: CastingDevice, isOnlineDevice: Boolean, isPinnedDevice: Boolean) {
|
fun bind(d: CastingDevice, isOnlineDevice: Boolean, isPinnedDevice: Boolean) {
|
||||||
if (d is ChromecastCastingDevice) {
|
when (d.protocolType) {
|
||||||
_imageDevice.setImageResource(R.drawable.ic_chromecast);
|
CastProtocolType.CHROMECAST -> {
|
||||||
_textType.text = "Chromecast";
|
_imageDevice.setImageResource(R.drawable.ic_chromecast);
|
||||||
} else if (d is AirPlayCastingDevice) {
|
_textType.text = "Chromecast";
|
||||||
_imageDevice.setImageResource(R.drawable.ic_airplay);
|
}
|
||||||
_textType.text = "AirPlay";
|
CastProtocolType.AIRPLAY -> {
|
||||||
} else if (d is FCastCastingDevice) {
|
_imageDevice.setImageResource(R.drawable.ic_airplay);
|
||||||
_imageDevice.setImageResource(R.drawable.ic_fc);
|
_textType.text = "AirPlay";
|
||||||
_textType.text = "FCast";
|
}
|
||||||
|
CastProtocolType.FCAST -> {
|
||||||
|
_imageDevice.setImageResource(
|
||||||
|
if (Settings.instance.casting.experimentalCasting) {
|
||||||
|
R.drawable.ic_exp_fc
|
||||||
|
} else {
|
||||||
|
R.drawable.ic_fc
|
||||||
|
}
|
||||||
|
)
|
||||||
|
_textType.text = "FCast";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_textName.text = d.name;
|
_textName.text = d.name;
|
||||||
@@ -136,4 +146,8 @@ class DeviceViewHolder : ViewHolder {
|
|||||||
|
|
||||||
device = d;
|
device = d;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = "DeviceViewHolder"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
package com.futo.platformplayer.views.buttons
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.UIDialogs
|
||||||
|
import com.futo.platformplayer.constructs.Event0
|
||||||
|
import com.google.android.material.imageview.ShapeableImageView
|
||||||
|
import com.google.android.material.shape.ShapeAppearanceModel
|
||||||
|
|
||||||
|
class ShortsButton : LinearLayout {
|
||||||
|
private val _root: LinearLayout;
|
||||||
|
private val _icon: ImageView;
|
||||||
|
private val _textPrimary: TextView;
|
||||||
|
val onClick = Event0();
|
||||||
|
|
||||||
|
var iconId: Int? = null;
|
||||||
|
|
||||||
|
constructor(context : Context, text: String, icon: Int, action: ()->Unit) : super(context) {
|
||||||
|
inflate(context, R.layout.view_shorts_button, this);
|
||||||
|
_icon = findViewById(R.id.button_icon);
|
||||||
|
_textPrimary = findViewById(R.id.button_text);
|
||||||
|
_root = findViewById(R.id.root);
|
||||||
|
|
||||||
|
withPrimaryText(text);
|
||||||
|
withIcon(icon);
|
||||||
|
|
||||||
|
_root.apply {
|
||||||
|
isClickable = true;
|
||||||
|
setOnClickListener {
|
||||||
|
if(!isEnabled)
|
||||||
|
return@setOnClickListener;
|
||||||
|
action();
|
||||||
|
onClick.emit();
|
||||||
|
UIDialogs.toast("Clicked button: " + _textPrimary.text);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
|
||||||
|
inflate(context, R.layout.view_shorts_button, this);
|
||||||
|
_icon = findViewById(R.id.image_icon);
|
||||||
|
_textPrimary = findViewById(R.id.text_title);
|
||||||
|
_root = findViewById(R.id.root);
|
||||||
|
_root.apply {
|
||||||
|
isClickable = true;
|
||||||
|
setOnClickListener {
|
||||||
|
if(!isEnabled)
|
||||||
|
return@setOnClickListener;
|
||||||
|
onClick.emit();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
val attrArr = context.obtainStyledAttributes(attrs, R.styleable.ShortsButton, 0, 0);
|
||||||
|
val attrIconRef = attrArr.getResourceId(R.styleable.ShortsButton_buttonIcon_s, -1);
|
||||||
|
val attrText = attrArr.getText(R.styleable.ShortsButton_buttonText_s) ?: "";
|
||||||
|
attrArr.recycle()
|
||||||
|
|
||||||
|
withIcon(attrIconRef);
|
||||||
|
withPrimaryText(attrText.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
fun withMargin(bottom: Int, side: Int = 0): ShortsButton {
|
||||||
|
setPadding(side, 0, side, bottom)
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
fun withPrimaryText(text: String): ShortsButton {
|
||||||
|
_textPrimary.text = text;
|
||||||
|
|
||||||
|
if(text.isNullOrBlank())
|
||||||
|
_textPrimary.visibility = View.GONE;
|
||||||
|
else
|
||||||
|
_textPrimary.visibility = View.VISIBLE;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
fun withIcon(resourceId: Int): ShortsButton {
|
||||||
|
if (resourceId != -1) {
|
||||||
|
_icon.visibility = View.VISIBLE;
|
||||||
|
_icon.setImageResource(resourceId);
|
||||||
|
} else
|
||||||
|
_icon.visibility = View.GONE;
|
||||||
|
_icon.scaleType = ImageView.ScaleType.CENTER_CROP;
|
||||||
|
iconId = resourceId;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun withIcon(bitmap: Bitmap): ShortsButton {
|
||||||
|
_icon.visibility = View.VISIBLE;
|
||||||
|
_icon.setImageBitmap(bitmap);
|
||||||
|
iconId = -1;
|
||||||
|
|
||||||
|
_icon.scaleType = ImageView.ScaleType.CENTER_CROP;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setButtonEnabled(enabled: Boolean) {
|
||||||
|
if(enabled) {
|
||||||
|
alpha = 1f;
|
||||||
|
isEnabled = true;
|
||||||
|
isClickable = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
alpha = 0.5f;
|
||||||
|
isEnabled = false;
|
||||||
|
isClickable = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,12 +2,7 @@ package com.futo.platformplayer.views.casting
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.FrameLayout
|
|
||||||
import android.widget.ImageButton
|
|
||||||
import android.widget.LinearLayout
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
|
|||||||
@@ -21,14 +21,13 @@ import com.futo.platformplayer.R
|
|||||||
import com.futo.platformplayer.Settings
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.api.media.models.chapters.IChapter
|
import com.futo.platformplayer.api.media.models.chapters.IChapter
|
||||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||||
import com.futo.platformplayer.casting.AirPlayCastingDevice
|
import com.futo.platformplayer.casting.CastConnectionState
|
||||||
import com.futo.platformplayer.casting.ChromecastCastingDevice
|
|
||||||
import com.futo.platformplayer.casting.StateCasting
|
import com.futo.platformplayer.casting.StateCasting
|
||||||
import com.futo.platformplayer.constructs.Event0
|
import com.futo.platformplayer.constructs.Event0
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.constructs.Event2
|
import com.futo.platformplayer.constructs.Event2
|
||||||
import com.futo.platformplayer.formatDuration
|
import com.futo.platformplayer.formatDuration
|
||||||
import com.futo.platformplayer.states.StateHistory
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StatePlayer
|
import com.futo.platformplayer.states.StatePlayer
|
||||||
import com.futo.platformplayer.views.TargetTapLoaderView
|
import com.futo.platformplayer.views.TargetTapLoaderView
|
||||||
import com.futo.platformplayer.views.behavior.GestureControlView
|
import com.futo.platformplayer.views.behavior.GestureControlView
|
||||||
@@ -36,7 +35,6 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class CastView : ConstraintLayout {
|
class CastView : ConstraintLayout {
|
||||||
@@ -99,19 +97,30 @@ class CastView : ConstraintLayout {
|
|||||||
val d = StateCasting.instance.activeDevice ?: return@subscribe;
|
val d = StateCasting.instance.activeDevice ?: return@subscribe;
|
||||||
_speedHoldWasPlaying = d.isPlaying
|
_speedHoldWasPlaying = d.isPlaying
|
||||||
_speedHoldPrevRate = d.speed
|
_speedHoldPrevRate = d.speed
|
||||||
if (d.canSetSpeed)
|
try {
|
||||||
d.changeSpeed(Settings.instance.playback.getHoldPlaybackSpeed())
|
if (d.canSetSpeed()) {
|
||||||
d.resumeVideo()
|
d.changeSpeed(Settings.instance.playback.getHoldPlaybackSpeed())
|
||||||
|
}
|
||||||
|
d.resumePlayback()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to change playback speed to hold playback speed: $e")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_gestureControlView.onSpeedHoldEnd.subscribe {
|
_gestureControlView.onSpeedHoldEnd.subscribe {
|
||||||
val d = StateCasting.instance.activeDevice ?: return@subscribe;
|
try {
|
||||||
if (!_speedHoldWasPlaying) d.pauseVideo()
|
val d = StateCasting.instance.activeDevice ?: return@subscribe;
|
||||||
d.changeSpeed(_speedHoldPrevRate)
|
if (!_speedHoldWasPlaying) {
|
||||||
|
d.pausePlayback()
|
||||||
|
}
|
||||||
|
d.changeSpeed(_speedHoldPrevRate)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Logger.e(TAG, "Failed to change playback speed to previous hold playback speed: $e")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_gestureControlView.onSeek.subscribe {
|
_gestureControlView.onSeek.subscribe {
|
||||||
val d = StateCasting.instance.activeDevice ?: return@subscribe;
|
val d = StateCasting.instance.activeDevice ?: return@subscribe;
|
||||||
StateCasting.instance.videoSeekTo(d.expectedCurrentTime + it / 1000);
|
StateCasting.instance.videoSeekTo( d.expectedCurrentTime + it / 1000);
|
||||||
};
|
};
|
||||||
|
|
||||||
_buttonLoop.setOnClickListener {
|
_buttonLoop.setOnClickListener {
|
||||||
@@ -220,22 +229,9 @@ class CastView : ConstraintLayout {
|
|||||||
stopTimeJob()
|
stopTimeJob()
|
||||||
|
|
||||||
if(isPlaying) {
|
if(isPlaying) {
|
||||||
val d = StateCasting.instance.activeDevice;
|
StateCasting.instance.startUpdateTimeJob(
|
||||||
if (d is AirPlayCastingDevice || d is ChromecastCastingDevice) {
|
onTimeJobTimeChanged_s
|
||||||
_updateTimeJob = _scope.launch {
|
) { setTime(it) }
|
||||||
while (true) {
|
|
||||||
val device = StateCasting.instance.activeDevice;
|
|
||||||
if (device == null || !device.isPlaying) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
delay(1000);
|
|
||||||
val time_ms = (device.expectedCurrentTime * 1000.0).toLong()
|
|
||||||
setTime(time_ms);
|
|
||||||
onTimeJobTimeChanged_s.emit(device.expectedCurrentTime.toLong())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_inPictureInPicture) {
|
if (!_inPictureInPicture) {
|
||||||
_buttonPause.visibility = View.VISIBLE;
|
_buttonPause.visibility = View.VISIBLE;
|
||||||
@@ -333,4 +329,8 @@ class CastView : ConstraintLayout {
|
|||||||
_loaderGame.visibility = View.VISIBLE
|
_loaderGame.visibility = View.VISIBLE
|
||||||
_loaderGame.startLoader(expectedDurationMs.toLong())
|
_loaderGame.startLoader(expectedDurationMs.toLong())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = "CastView";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,7 @@ import com.futo.platformplayer.constructs.Event2
|
|||||||
import com.futo.platformplayer.constructs.Event3
|
import com.futo.platformplayer.constructs.Event3
|
||||||
import java.lang.reflect.Field
|
import java.lang.reflect.Field
|
||||||
|
|
||||||
@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY)
|
@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY, AnnotationTarget.FUNCTION)
|
||||||
@Retention(AnnotationRetention.RUNTIME)
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
annotation class AdvancedField();
|
annotation class AdvancedField();
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import com.futo.platformplayer.R
|
|||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.getDataLinkFromUrl
|
import com.futo.platformplayer.getDataLinkFromUrl
|
||||||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||||
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.views.IdenticonView
|
import com.futo.platformplayer.views.IdenticonView
|
||||||
import userpackage.Protocol
|
import userpackage.Protocol
|
||||||
|
|
||||||
@@ -82,14 +83,14 @@ class CreatorThumbnail : ConstraintLayout {
|
|||||||
Glide.with(_imageChannelThumbnail)
|
Glide.with(_imageChannelThumbnail)
|
||||||
.load(url)
|
.load(url)
|
||||||
.placeholder(R.drawable.placeholder_channel_thumbnail)
|
.placeholder(R.drawable.placeholder_channel_thumbnail)
|
||||||
.diskCacheStrategy(DiskCacheStrategy.DATA)
|
.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
|
||||||
.crossfade()
|
.crossfade()
|
||||||
.into(_imageChannelThumbnail);
|
.into(_imageChannelThumbnail)
|
||||||
} else {
|
} else {
|
||||||
Glide.with(_imageChannelThumbnail)
|
Glide.with(_imageChannelThumbnail)
|
||||||
.load(url)
|
.load(url)
|
||||||
.placeholder(R.drawable.placeholder_channel_thumbnail)
|
.placeholder(R.drawable.placeholder_channel_thumbnail)
|
||||||
.diskCacheStrategy(DiskCacheStrategy.DATA)
|
.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
|
||||||
.into(_imageChannelThumbnail);
|
.into(_imageChannelThumbnail);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,29 @@ class RadioGroupView : FlexboxLayout {
|
|||||||
radioView.layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
|
radioView.layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
|
||||||
radioView.setInfo(option.first, initiallySelectedOptions.contains(option.second));
|
radioView.setInfo(option.first, initiallySelectedOptions.contains(option.second));
|
||||||
radioView.setPadding(_padding_px, _padding_px, _padding_px, _padding_px);
|
radioView.setPadding(_padding_px, _padding_px, _padding_px, _padding_px);
|
||||||
|
if(multiSelect)
|
||||||
|
radioView.onLongClick.subscribe {
|
||||||
|
val selected = !radioView.selected;
|
||||||
|
if (selected) {
|
||||||
|
selectedOptions.clear();
|
||||||
|
for(v in radioViews)
|
||||||
|
v.setIsSelected(true);
|
||||||
|
selectedOptions.addAll(options.map { it.second });
|
||||||
|
} else {
|
||||||
|
if(atLeastOne) {
|
||||||
|
for(v in radioViews)
|
||||||
|
v.setIsSelected(false);
|
||||||
|
selectedOptions.clear();
|
||||||
|
selectedOptions.add(option.second);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
for(v in radioViews)
|
||||||
|
v.setIsSelected(false);
|
||||||
|
selectedOptions.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onSelectedChange.emit(selectedOptions);
|
||||||
|
}
|
||||||
radioView.onClick.subscribe {
|
radioView.onClick.subscribe {
|
||||||
val selected = !radioView.selected;
|
val selected = !radioView.selected;
|
||||||
if (selected) {
|
if (selected) {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class RadioView : LinearLayout {
|
|||||||
|
|
||||||
val selected get() = _selected;
|
val selected get() = _selected;
|
||||||
var onClick = Event0();
|
var onClick = Event0();
|
||||||
|
var onLongClick = Event0();
|
||||||
var onSelectedChange = Event1<Boolean>();
|
var onSelectedChange = Event1<Boolean>();
|
||||||
|
|
||||||
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
|
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
|
||||||
@@ -32,6 +33,13 @@ class RadioView : LinearLayout {
|
|||||||
setIsSelected(!_selected)
|
setIsSelected(!_selected)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
_root.setOnLongClickListener {
|
||||||
|
onLongClick.emit();
|
||||||
|
if (_handleClick) {
|
||||||
|
setIsSelected(!_selected)
|
||||||
|
}
|
||||||
|
return@setOnLongClickListener true;
|
||||||
|
}
|
||||||
|
|
||||||
_root.setBackgroundResource(R.drawable.background_radio_unselected);
|
_root.setBackgroundResource(R.drawable.background_radio_unselected);
|
||||||
_textTag.setTextColor(ContextCompat.getColor(context, R.color.gray_67));
|
_textTag.setTextColor(ContextCompat.getColor(context, R.color.gray_67));
|
||||||
|
|||||||
@@ -23,12 +23,16 @@ class ToggleTagView : LinearLayout {
|
|||||||
private var _text: String = "";
|
private var _text: String = "";
|
||||||
private var _image: ImageView;
|
private var _image: ImageView;
|
||||||
|
|
||||||
|
var tag: String? = null
|
||||||
|
private set;
|
||||||
|
|
||||||
var isActive: Boolean = false
|
var isActive: Boolean = false
|
||||||
private set;
|
private set;
|
||||||
var isButton: Boolean = false
|
var isButton: Boolean = false
|
||||||
private set;
|
private set;
|
||||||
|
|
||||||
var onClick = Event2<ToggleTagView, Boolean>();
|
var onClick = Event2<ToggleTagView, Boolean>();
|
||||||
|
var onLongClick = Event2<ToggleTagView, Boolean>();
|
||||||
|
|
||||||
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
|
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
|
||||||
LayoutInflater.from(context).inflate(R.layout.view_toggle_tag, this, true);
|
LayoutInflater.from(context).inflate(R.layout.view_toggle_tag, this, true);
|
||||||
@@ -36,10 +40,25 @@ class ToggleTagView : LinearLayout {
|
|||||||
_textTag = findViewById(R.id.text_tag);
|
_textTag = findViewById(R.id.text_tag);
|
||||||
_image = findViewById(R.id.image_tag);
|
_image = findViewById(R.id.image_tag);
|
||||||
_root.setOnClickListener {
|
_root.setOnClickListener {
|
||||||
if(!isButton)
|
handleClick();
|
||||||
setToggle(!isActive);
|
|
||||||
onClick.emit(this, isActive);
|
|
||||||
}
|
}
|
||||||
|
_root.setOnLongClickListener {
|
||||||
|
if(onLongClick.hasListeners())
|
||||||
|
onLongClick.emit(this, isActive);
|
||||||
|
else {
|
||||||
|
if(!isButton) {
|
||||||
|
setToggle(!isActive);
|
||||||
|
}
|
||||||
|
onClick.emit(this, isActive);
|
||||||
|
}
|
||||||
|
return@setOnLongClickListener true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun handleClick() {
|
||||||
|
if(!isButton)
|
||||||
|
setToggle(!isActive);
|
||||||
|
onClick.emit(this, isActive);
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setToggle(isActive: Boolean) {
|
fun setToggle(isActive: Boolean) {
|
||||||
@@ -70,9 +89,10 @@ class ToggleTagView : LinearLayout {
|
|||||||
_image.visibility = View.VISIBLE;
|
_image.visibility = View.VISIBLE;
|
||||||
_textTag.visibility = if(!toggle.name.isNullOrEmpty()) View.VISIBLE else View.GONE;
|
_textTag.visibility = if(!toggle.name.isNullOrEmpty()) View.VISIBLE else View.GONE;
|
||||||
this.isButton = isButton;
|
this.isButton = isButton;
|
||||||
|
tag = toggle.tag;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setInfo(imageResource: Int, text: String, isActive: Boolean, isButton: Boolean = false) {
|
fun setInfo(imageResource: Int, text: String, isActive: Boolean, isButton: Boolean = false, tag: String? = null) {
|
||||||
_text = text;
|
_text = text;
|
||||||
_textTag.text = text;
|
_textTag.text = text;
|
||||||
setToggle(isActive);
|
setToggle(isActive);
|
||||||
@@ -80,8 +100,9 @@ class ToggleTagView : LinearLayout {
|
|||||||
_image.visibility = View.VISIBLE;
|
_image.visibility = View.VISIBLE;
|
||||||
_textTag.visibility = if(!text.isNullOrEmpty()) View.VISIBLE else View.GONE;
|
_textTag.visibility = if(!text.isNullOrEmpty()) View.VISIBLE else View.GONE;
|
||||||
this.isButton = isButton;
|
this.isButton = isButton;
|
||||||
|
this.tag = tag;
|
||||||
}
|
}
|
||||||
fun setInfo(image: ImageVariable, text: String, isActive: Boolean, isButton: Boolean = false) {
|
fun setInfo(image: ImageVariable, text: String, isActive: Boolean, isButton: Boolean = false, tag: String? = null) {
|
||||||
_text = text;
|
_text = text;
|
||||||
_textTag.text = text;
|
_textTag.text = text;
|
||||||
setToggle(isActive);
|
setToggle(isActive);
|
||||||
@@ -89,13 +110,15 @@ class ToggleTagView : LinearLayout {
|
|||||||
_image.visibility = View.VISIBLE;
|
_image.visibility = View.VISIBLE;
|
||||||
_textTag.visibility = if(!text.isNullOrEmpty()) View.VISIBLE else View.GONE;
|
_textTag.visibility = if(!text.isNullOrEmpty()) View.VISIBLE else View.GONE;
|
||||||
this.isButton = isButton;
|
this.isButton = isButton;
|
||||||
|
this.tag = tag;
|
||||||
}
|
}
|
||||||
fun setInfo(text: String, isActive: Boolean, isButton: Boolean = false) {
|
fun setInfo(text: String, isActive: Boolean, isButton: Boolean = false, tag: String? = null) {
|
||||||
_image.visibility = View.GONE;
|
_image.visibility = View.GONE;
|
||||||
_text = text;
|
_text = text;
|
||||||
_textTag.text = text;
|
_textTag.text = text;
|
||||||
_textTag.visibility = if(!text.isNullOrEmpty()) View.VISIBLE else View.GONE;
|
_textTag.visibility = if(!text.isNullOrEmpty()) View.VISIBLE else View.GONE;
|
||||||
setToggle(isActive);
|
setToggle(isActive);
|
||||||
this.isButton = isButton;
|
this.isButton = isButton;
|
||||||
|
this.tag = tag;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,14 +6,17 @@ import android.graphics.drawable.Drawable
|
|||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.animation.LinearInterpolator
|
import android.view.animation.LinearInterpolator
|
||||||
|
import androidx.annotation.Dimension
|
||||||
import androidx.annotation.OptIn
|
import androidx.annotation.OptIn
|
||||||
import androidx.media3.common.PlaybackParameters
|
import androidx.media3.common.PlaybackParameters
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
import androidx.media3.ui.AspectRatioFrameLayout
|
||||||
import androidx.media3.ui.DefaultTimeBar
|
import androidx.media3.ui.DefaultTimeBar
|
||||||
import androidx.media3.ui.PlayerView
|
import androidx.media3.ui.PlayerView
|
||||||
import androidx.media3.ui.TimeBar
|
import androidx.media3.ui.TimeBar
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StatePlayer
|
import com.futo.platformplayer.states.StatePlayer
|
||||||
@@ -65,6 +68,13 @@ class FutoShortPlayer(context: Context, attrs: AttributeSet? = null) :
|
|||||||
videoView = findViewById(R.id.short_player_view)
|
videoView = findViewById(R.id.short_player_view)
|
||||||
progressBar = findViewById(R.id.short_player_progress_bar)
|
progressBar = findViewById(R.id.short_player_progress_bar)
|
||||||
|
|
||||||
|
if(Settings.instance.playback.shortsFitVideo)
|
||||||
|
videoView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT;
|
||||||
|
else
|
||||||
|
videoView.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM;
|
||||||
|
|
||||||
|
videoView.subtitleView?.setFixedTextSize(Dimension.SP, 18F);
|
||||||
|
|
||||||
if (!isInEditMode) {
|
if (!isInEditMode) {
|
||||||
player = StatePlayer.instance.getShortPlayerOrCreate(context)
|
player = StatePlayer.instance.getShortPlayerOrCreate(context)
|
||||||
player.player.repeatMode = Player.REPEAT_MODE_ONE
|
player.player.repeatMode = Player.REPEAT_MODE_ONE
|
||||||
|
|||||||
@@ -907,11 +907,14 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
|||||||
|
|
||||||
override fun switchToVideoMode() {
|
override fun switchToVideoMode() {
|
||||||
super.switchToVideoMode()
|
super.switchToVideoMode()
|
||||||
setArtwork(null)
|
//setArtwork(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun switchToAudioMode(video: IPlatformVideoDetails?) {
|
override fun switchToAudioMode(video: IPlatformVideoDetails?) {
|
||||||
super.switchToAudioMode(video)
|
super.switchToAudioMode(video)
|
||||||
|
|
||||||
|
//This causes issues, and is in general confusing, needs improvements
|
||||||
|
/*
|
||||||
val thumbnail = video?.thumbnails?.getHQThumbnail()
|
val thumbnail = video?.thumbnails?.getHQThumbnail()
|
||||||
if (!thumbnail.isNullOrBlank()) {
|
if (!thumbnail.isNullOrBlank()) {
|
||||||
Glide.with(context).asBitmap().load(thumbnail)
|
Glide.with(context).asBitmap().load(thumbnail)
|
||||||
@@ -928,5 +931,6 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,6 +64,10 @@ 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.JSHLSManifestAudioSource
|
||||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource
|
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.JSVideoUrlRangeSource
|
||||||
|
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalAudioContentSource
|
||||||
|
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalAudioFileSource
|
||||||
|
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalVideoContentSource
|
||||||
|
import com.futo.platformplayer.api.media.platforms.local.models.sources.LocalVideoFileSource
|
||||||
import com.futo.platformplayer.api.media.structures.IPager
|
import com.futo.platformplayer.api.media.structures.IPager
|
||||||
import com.futo.platformplayer.constructs.Event0
|
import com.futo.platformplayer.constructs.Event0
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
@@ -480,6 +484,8 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
|||||||
is IHLSManifestSource -> { swapVideoSourceHLS(videoSource); true; }
|
is IHLSManifestSource -> { swapVideoSourceHLS(videoSource); true; }
|
||||||
is IVideoUrlWidevineSource -> { swapVideoSourceUrlWidevine(videoSource); true; }
|
is IVideoUrlWidevineSource -> { swapVideoSourceUrlWidevine(videoSource); true; }
|
||||||
is IVideoUrlSource -> { swapVideoSourceUrl(videoSource); true; }
|
is IVideoUrlSource -> { swapVideoSourceUrl(videoSource); true; }
|
||||||
|
is LocalVideoFileSource -> { swapVideoSourceLocalFile(videoSource); true; }
|
||||||
|
is LocalVideoContentSource -> { swapVideoSourceLocalContent(videoSource); true; }
|
||||||
null -> { _lastVideoMediaSource = null; true;}
|
null -> { _lastVideoMediaSource = null; true;}
|
||||||
else -> throw IllegalArgumentException("Unsupported video source [${videoSource.javaClass.simpleName}]");
|
else -> throw IllegalArgumentException("Unsupported video source [${videoSource.javaClass.simpleName}]");
|
||||||
}
|
}
|
||||||
@@ -496,6 +502,8 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
|||||||
is JSDashManifestRawAudioSource -> swapAudioSourceDashRaw(audioSource, play, resume, swapId);
|
is JSDashManifestRawAudioSource -> swapAudioSourceDashRaw(audioSource, play, resume, swapId);
|
||||||
is IAudioUrlWidevineSource -> { swapAudioSourceUrlWidevine(audioSource); true; }
|
is IAudioUrlWidevineSource -> { swapAudioSourceUrlWidevine(audioSource); true; }
|
||||||
is IAudioUrlSource -> { swapAudioSourceUrl(audioSource); true; }
|
is IAudioUrlSource -> { swapAudioSourceUrl(audioSource); true; }
|
||||||
|
is LocalAudioFileSource -> { swapAudioSourceLocalFile(audioSource); true; }
|
||||||
|
is LocalAudioContentSource -> { swapAudioSourceLocalContent(audioSource); true; }
|
||||||
null -> { _lastAudioMediaSource = null; true; }
|
null -> { _lastAudioMediaSource = null; true; }
|
||||||
else -> throw IllegalArgumentException("Unsupported video source [${audioSource.javaClass.simpleName}]");
|
else -> throw IllegalArgumentException("Unsupported video source [${audioSource.javaClass.simpleName}]");
|
||||||
}
|
}
|
||||||
@@ -514,6 +522,23 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
|||||||
.createMediaSource(MediaItem.fromUri(Uri.fromFile(file)));
|
.createMediaSource(MediaItem.fromUri(Uri.fromFile(file)));
|
||||||
}
|
}
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
|
private fun swapVideoSourceLocalFile(videoSource: LocalVideoFileSource) {
|
||||||
|
Logger.i(TAG, "Loading VideoSource [Local]");
|
||||||
|
val file = videoSource.file;
|
||||||
|
if(!file.exists())
|
||||||
|
throw IllegalArgumentException("File for this video does not exist");
|
||||||
|
_lastVideoMediaSource = ProgressiveMediaSource.Factory(DefaultDataSource.Factory(context))
|
||||||
|
.createMediaSource(MediaItem.fromUri(Uri.fromFile(file)));
|
||||||
|
}
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
|
private fun swapVideoSourceLocalContent(videoSource: LocalVideoContentSource) {
|
||||||
|
Logger.i(TAG, "Loading VideoSource [Local]");
|
||||||
|
if(!videoSource.contentUrl.startsWith("content://"))
|
||||||
|
throw IllegalArgumentException("Not a content uri");
|
||||||
|
_lastVideoMediaSource = ProgressiveMediaSource.Factory(DefaultDataSource.Factory(context))
|
||||||
|
.createMediaSource(MediaItem.fromUri(videoSource.contentUrl));
|
||||||
|
}
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
private fun swapVideoSourceUrlRange(videoSource: JSVideoUrlRangeSource) {
|
private fun swapVideoSourceUrlRange(videoSource: JSVideoUrlRangeSource) {
|
||||||
Logger.i(TAG, "Loading JSVideoUrlRangeSource");
|
Logger.i(TAG, "Loading JSVideoUrlRangeSource");
|
||||||
if(videoSource.hasItag) {
|
if(videoSource.hasItag) {
|
||||||
@@ -707,6 +732,23 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
|||||||
.createMediaSource(MediaItem.fromUri(Uri.fromFile(file)));
|
.createMediaSource(MediaItem.fromUri(Uri.fromFile(file)));
|
||||||
}
|
}
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
|
private fun swapAudioSourceLocalFile(audioSource: LocalAudioFileSource) {
|
||||||
|
Logger.i(TAG, "Loading VideoSource [Local]");
|
||||||
|
val file = audioSource.file;
|
||||||
|
if(!file.exists())
|
||||||
|
throw IllegalArgumentException("File for this video does not exist");
|
||||||
|
_lastAudioMediaSource = ProgressiveMediaSource.Factory(DefaultDataSource.Factory(context))
|
||||||
|
.createMediaSource(MediaItem.fromUri(Uri.fromFile(file)));
|
||||||
|
}
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
|
private fun swapAudioSourceLocalContent(audioSource: LocalAudioContentSource) {
|
||||||
|
Logger.i(TAG, "Loading VideoSource [Local]");
|
||||||
|
if(!audioSource.contentUrl.startsWith("content://"))
|
||||||
|
throw IllegalArgumentException("Not a content uri");
|
||||||
|
_lastAudioMediaSource = ProgressiveMediaSource.Factory(DefaultDataSource.Factory(context))
|
||||||
|
.createMediaSource(MediaItem.fromUri(audioSource.contentUrl));
|
||||||
|
}
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
private fun swapAudioSourceUrlRange(audioSource: JSAudioUrlRangeSource) {
|
private fun swapAudioSourceUrlRange(audioSource: JSAudioUrlRangeSource) {
|
||||||
Logger.i(TAG, "Loading JSAudioUrlRangeSource");
|
Logger.i(TAG, "Loading JSAudioUrlRangeSource");
|
||||||
if(audioSource.hasItag) {
|
if(audioSource.hasItag) {
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960"
|
||||||
|
android:autoMirrored="true">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:strokeColor="#222"
|
||||||
|
android:strokeWidth="20"
|
||||||
|
android:pathData="M240,560L720,560L720,480L240,480L240,560ZM240,440L720,440L720,360L240,360L240,440ZM240,320L720,320L720,240L240,240L240,320ZM880,880L720,720L160,720Q127,720 103.5,696.5Q80,673 80,640L80,160Q80,127 103.5,103.5Q127,80 160,80L800,80Q833,80 856.5,103.5Q880,127 880,160L880,880ZM160,640L754,640L800,685L800,160Q800,160 800,160Q800,160 800,160L160,160Q160,160 160,160Q160,160 160,160L160,640Q160,640 160,640Q160,640 160,640ZM160,640Q160,640 160,640Q160,640 160,640L160,160Q160,160 160,160Q160,160 160,160L160,160Q160,160 160,160Q160,160 160,160L160,640Z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="111.96dp"
|
||||||
|
android:height="114.46dp"
|
||||||
|
android:viewportWidth="111.96"
|
||||||
|
android:viewportHeight="114.46">
|
||||||
|
<path
|
||||||
|
android:pathData="m84.76,5.58c2.06,-2.06 0.6,-5.58 -2.31,-5.58H3.27C1.46,-0 -0,1.46 -0,3.27V82.45c0,2.91 3.52,4.37 5.58,2.31L20.37,69.98c0.61,-0.61 0.96,-1.45 0.96,-2.31V24.6c0,-1.81 1.46,-3.27 3.27,-3.27h43.07c0.87,0 1.7,-0.34 2.31,-0.96z"
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<path
|
||||||
|
android:pathData="m45.68,73.5v17.18c0,0.68 -0.55,1.23 -1.23,1.23L27.56,91.9c-0.68,0 -1.23,-0.55 -1.23,-1.23v-17.18c0,-0.68 0.55,-1.23 1.23,-1.23h16.89c0.68,0 1.23,0.55 1.23,1.23zM45.68,51.16v17.18c0,0.68 -0.55,1.23 -1.23,1.23L27.56,69.57c-0.68,0 -1.23,-0.55 -1.23,-1.23L26.33,51.16c0,-0.68 0.55,-1.23 1.23,-1.23h16.89c0.68,0 1.23,0.55 1.23,1.23zM67.73,51.16v17.18c0,0.68 -0.55,1.23 -1.23,1.23L49.61,69.57c-0.68,0 -1.23,-0.55 -1.23,-1.23L48.38,51.16c0,-0.68 0.55,-1.23 1.23,-1.23h16.89c0.68,0 1.23,0.55 1.23,1.23zM67.73,28.83v17.18c0,0.68 -0.55,1.23 -1.23,1.23L49.61,47.24c-0.68,0 -1.23,-0.55 -1.23,-1.23L48.38,28.83c0,-0.68 0.55,-1.23 1.23,-1.23h16.89c0.68,0 1.23,0.55 1.23,1.23zM89.77,51.16v17.18c0,0.68 -0.55,1.23 -1.23,1.23h-16.89c-0.68,0 -1.23,-0.55 -1.23,-1.23L70.43,51.16c0,-0.68 0.55,-1.23 1.23,-1.23h16.89c0.68,0 1.23,0.55 1.23,1.23zM67.73,73.5v17.18c0,0.68 -0.55,1.23 -1.23,1.23L49.61,91.9c-0.68,0 -1.23,-0.55 -1.23,-1.23v-17.18c0,-0.68 0.55,-1.23 1.23,-1.23h16.89c0.68,0 1.23,0.55 1.23,1.23zM89.77,28.83v17.18c0,0.68 -0.55,1.23 -1.23,1.23h-16.89c-0.68,0 -1.23,-0.55 -1.23,-1.23L70.43,28.83c0,-0.68 0.55,-1.23 1.23,-1.23h16.89c0.68,0 1.23,0.55 1.23,1.23zM45.68,28.83v17.18c0,0.68 -0.55,1.23 -1.23,1.23L27.56,47.24c-0.68,0 -1.23,-0.55 -1.23,-1.23L26.33,28.83c0,-0.68 0.55,-1.23 1.23,-1.23h16.89c0.68,0 1.23,0.55 1.23,1.23zM45.68,95.83v17.18c0,0.68 -0.55,1.23 -1.23,1.23L27.56,114.24c-0.68,0 -1.23,-0.55 -1.23,-1.23L26.33,95.83c0,-0.68 0.55,-1.23 1.23,-1.23h16.89c0.68,0 1.23,0.55 1.23,1.23zM111.77,28.83v17.18c0,0.68 -0.55,1.23 -1.23,1.23L93.65,47.24c-0.68,0 -1.23,-0.55 -1.23,-1.23L92.43,28.83c0,-0.68 0.55,-1.23 1.23,-1.23L110.55,27.6c0.68,0 1.23,0.55 1.23,1.23z"
|
||||||
|
android:strokeWidth="0"
|
||||||
|
android:fillColor="#ffffff"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:strokeColor="#222"
|
||||||
|
android:strokeWidth="20"
|
||||||
|
android:pathData="M370,880L354,752Q341,747 329.5,740Q318,733 307,725L188,775L78,585L181,507Q180,500 180,493.5Q180,487 180,480Q180,473 180,466.5Q180,460 181,453L78,375L188,185L307,235Q318,227 330,220Q342,213 354,208L370,80L590,80L606,208Q619,213 630.5,220Q642,227 653,235L772,185L882,375L779,453Q780,460 780,466.5Q780,473 780,480Q780,487 780,493.5Q780,500 778,507L881,585L771,775L653,725Q642,733 630,740Q618,747 606,752L590,880L370,880ZM440,800L519,800L533,694Q564,686 590.5,670.5Q617,655 639,633L738,674L777,606L691,541Q696,527 698,511.5Q700,496 700,480Q700,464 698,448.5Q696,433 691,419L777,354L738,286L639,328Q617,305 590.5,289.5Q564,274 533,266L520,160L441,160L427,266Q396,274 369.5,289.5Q343,305 321,327L222,286L183,354L269,418Q264,433 262,448Q260,463 260,480Q260,496 262,511Q264,526 269,541L183,606L222,674L321,632Q343,655 369.5,670.5Q396,686 427,694L440,800ZM482,620Q540,620 581,579Q622,538 622,480Q622,422 581,381Q540,340 482,340Q423,340 382.5,381Q342,422 342,480Q342,538 382.5,579Q423,620 482,620ZM480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480Z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:strokeColor="#222"
|
||||||
|
android:strokeWidth="20"
|
||||||
|
android:pathData="M680,880Q630,880 595,845Q560,810 560,760Q560,754 563,732L282,568Q266,583 245,591.5Q224,600 200,600Q150,600 115,565Q80,530 80,480Q80,430 115,395Q150,360 200,360Q224,360 245,368.5Q266,377 282,392L563,228Q561,221 560.5,214.5Q560,208 560,200Q560,150 595,115Q630,80 680,80Q730,80 765,115Q800,150 800,200Q800,250 765,285Q730,320 680,320Q656,320 635,311.5Q614,303 598,288L317,452Q319,459 319.5,465.5Q320,472 320,480Q320,488 319.5,494.5Q319,501 317,508L598,672Q614,657 635,648.5Q656,640 680,640Q730,640 765,675Q800,710 800,760Q800,810 765,845Q730,880 680,880ZM680,800Q697,800 708.5,788.5Q720,777 720,760Q720,743 708.5,731.5Q697,720 680,720Q663,720 651.5,731.5Q640,743 640,760Q640,777 651.5,788.5Q663,800 680,800ZM200,520Q217,520 228.5,508.5Q240,497 240,480Q240,463 228.5,451.5Q217,440 200,440Q183,440 171.5,451.5Q160,463 160,480Q160,497 171.5,508.5Q183,520 200,520ZM680,240Q697,240 708.5,228.5Q720,217 720,200Q720,183 708.5,171.5Q697,160 680,160Q663,160 651.5,171.5Q640,183 640,200Q640,217 651.5,228.5Q663,240 680,240ZM680,760Q680,760 680,760Q680,760 680,760Q680,760 680,760Q680,760 680,760Q680,760 680,760Q680,760 680,760Q680,760 680,760Q680,760 680,760ZM200,480Q200,480 200,480Q200,480 200,480Q200,480 200,480Q200,480 200,480Q200,480 200,480Q200,480 200,480Q200,480 200,480Q200,480 200,480ZM680,200Q680,200 680,200Q680,200 680,200Q680,200 680,200Q680,200 680,200Q680,200 680,200Q680,200 680,200Q680,200 680,200Q680,200 680,200Z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:strokeColor="#222"
|
||||||
|
android:strokeWidth="20"
|
||||||
|
android:pathData="M240,120L680,120L680,640L400,920L350,870Q343,863 338.5,851Q334,839 334,828L334,814L378,640L120,640Q88,640 64,616Q40,592 40,560L40,480Q40,473 42,465Q44,457 46,450L166,168Q175,148 196,134Q217,120 240,120ZM600,200L240,200Q240,200 240,200Q240,200 240,200L120,480L120,560Q120,560 120,560Q120,560 120,560L480,560L426,780L600,606L600,200ZM600,606L600,606L600,560L600,560Q600,560 600,560Q600,560 600,560L600,480L600,200Q600,200 600,200Q600,200 600,200L600,200L600,606ZM680,640L680,560L800,560L800,200L680,200L680,120L880,120L880,640L680,640Z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960">
|
||||||
|
<path
|
||||||
|
android:fillColor="@color/colorPrimary"
|
||||||
|
android:pathData="M240,120L640,120L640,640L360,920L310,870Q303,863 298.5,851Q294,839 294,828L294,814L338,640L120,640Q88,640 64,616Q40,592 40,560L40,480Q40,473 41.5,465Q43,457 46,450L166,168Q175,148 196,134Q217,120 240,120ZM720,640L720,120L880,120L880,640L720,640Z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:strokeColor="#222"
|
||||||
|
android:strokeWidth="20"
|
||||||
|
android:pathData="M720,840L280,840L280,320L560,40L610,90Q617,97 621.5,109Q626,121 626,132L626,146L582,320L840,320Q872,320 896,344Q920,368 920,400L920,480Q920,487 918,495Q916,503 914,510L794,792Q785,812 764,826Q743,840 720,840ZM360,760L720,760Q720,760 720,760Q720,760 720,760L840,480L840,400Q840,400 840,400Q840,400 840,400L480,400L534,180L360,354L360,760ZM360,354L360,354L360,400L360,400Q360,400 360,400Q360,400 360,400L360,480L360,760Q360,760 360,760Q360,760 360,760L360,760L360,354ZM280,320L280,400L160,400L160,760L280,760L280,840L80,840L80,320L280,320Z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960">
|
||||||
|
<path
|
||||||
|
android:fillColor="@color/colorPrimary"
|
||||||
|
android:pathData="M720,840L320,840L320,320L600,40L650,90Q657,97 661.5,109Q666,121 666,132L666,146L622,320L840,320Q872,320 896,344Q920,368 920,400L920,480Q920,487 918.5,495Q917,503 914,510L794,792Q785,812 764,826Q743,840 720,840ZM240,320L240,840L80,840L80,320L240,320Z"/>
|
||||||
|
</vector>
|
||||||
@@ -63,6 +63,16 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
tools:ignore="HardcodedText" />
|
tools:ignore="HardcodedText" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/dialog_text_input"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginLeft="10dp"
|
||||||
|
android:layout_marginRight="10dp"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
/>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/dialog_buttons"
|
android:id="@+id/dialog_buttons"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|||||||
@@ -129,6 +129,19 @@
|
|||||||
android:text=""
|
android:text=""
|
||||||
android:textColor="@android:color/white"
|
android:textColor="@android:color/white"
|
||||||
android:textSize="14sp" />
|
android:textSize="14sp" />
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/video_subtitle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="4dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:paddingHorizontal="4dp"
|
||||||
|
android:shadowColor="@android:color/black"
|
||||||
|
android:shadowRadius="8"
|
||||||
|
android:text=""
|
||||||
|
android:textColor="#CCC"
|
||||||
|
android:textSize="14sp" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<!-- Buttons section -->
|
<!-- Buttons section -->
|
||||||
@@ -143,341 +156,88 @@
|
|||||||
app:layout_constraintEnd_toEndOf="parent">
|
app:layout_constraintEnd_toEndOf="parent">
|
||||||
|
|
||||||
<!-- Like button -->
|
<!-- Like button -->
|
||||||
<FrameLayout
|
<com.futo.platformplayer.views.buttons.ShortsButton
|
||||||
android:id="@+id/like_container"
|
android:id="@+id/like_button"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center_horizontal"
|
android:layout_marginBottom="10dp"
|
||||||
android:layout_marginBottom="12dp"
|
android:checkable="true"
|
||||||
android:visibility="gone">
|
android:contentDescription="@string/cd_image_like_icon"
|
||||||
|
app:backgroundTint="@color/transparent"
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
app:buttonIcon_s="@drawable/ic_thumb_up_s"
|
||||||
android:layout_width="wrap_content"
|
app:iconSize="24dp"
|
||||||
android:layout_height="wrap_content"
|
app:iconTint="@android:color/white"
|
||||||
android:layout_gravity="center_horizontal">
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
<ImageView
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
android:layout_width="0dp"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
android:layout_height="0dp"
|
app:rippleColor="@color/ripple"
|
||||||
android:importantForAccessibility="no"
|
app:toggleCheckedStateOnClick="false" />
|
||||||
android:src="@drawable/button_shadow"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@id/like_button"
|
|
||||||
app:layout_constraintEnd_toEndOf="@id/like_button"
|
|
||||||
app:layout_constraintStart_toStartOf="@id/like_button"
|
|
||||||
app:layout_constraintTop_toTopOf="@id/like_button"
|
|
||||||
app:tint="@color/black"
|
|
||||||
tools:ignore="ImageContrastCheck" />
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/like_button"
|
|
||||||
style="@style/Widget.Material3.Button.IconButton"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginBottom="8dp"
|
|
||||||
android:checkable="true"
|
|
||||||
android:contentDescription="@string/cd_image_like_icon"
|
|
||||||
app:backgroundTint="@color/transparent"
|
|
||||||
app:icon="@drawable/thumb_up_selector"
|
|
||||||
app:iconSize="24dp"
|
|
||||||
app:iconTint="@android:color/white"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:rippleColor="@color/ripple"
|
|
||||||
app:toggleCheckedStateOnClick="false" />
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/like_count"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="bottom|center_horizontal"
|
|
||||||
android:paddingHorizontal="4dp"
|
|
||||||
android:shadowColor="@android:color/black"
|
|
||||||
android:shadowRadius="8"
|
|
||||||
android:textColor="@android:color/white"
|
|
||||||
android:textSize="12sp" />
|
|
||||||
</FrameLayout>
|
|
||||||
|
|
||||||
<!-- Dislike button -->
|
<!-- Dislike button -->
|
||||||
<FrameLayout
|
<com.futo.platformplayer.views.buttons.ShortsButton
|
||||||
android:id="@+id/dislike_container"
|
android:id="@+id/dislike_button"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center_horizontal"
|
android:layout_marginBottom="20dp"
|
||||||
android:layout_marginBottom="12dp"
|
android:checkable="true"
|
||||||
android:visibility="gone">
|
android:contentDescription="@string/cd_image_dislike_icon"
|
||||||
|
app:backgroundTint="@color/transparent"
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
app:buttonIcon_s="@drawable/ic_thumb_down_s"
|
||||||
android:layout_width="wrap_content"
|
app:iconSize="24dp"
|
||||||
android:layout_height="wrap_content"
|
app:iconTint="@android:color/white"
|
||||||
android:layout_gravity="center_horizontal">
|
app:rippleColor="@color/ripple"
|
||||||
|
app:toggleCheckedStateOnClick="false" />
|
||||||
<ImageView
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:importantForAccessibility="no"
|
|
||||||
android:src="@drawable/button_shadow"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@id/dislike_button"
|
|
||||||
app:layout_constraintEnd_toEndOf="@id/dislike_button"
|
|
||||||
app:layout_constraintStart_toStartOf="@id/dislike_button"
|
|
||||||
app:layout_constraintTop_toTopOf="@id/dislike_button"
|
|
||||||
app:tint="@color/black"
|
|
||||||
tools:ignore="ImageContrastCheck" />
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/dislike_button"
|
|
||||||
style="@style/Widget.Material3.Button.IconButton"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginBottom="8dp"
|
|
||||||
android:checkable="true"
|
|
||||||
android:contentDescription="@string/cd_image_dislike_icon"
|
|
||||||
app:backgroundTint="@color/transparent"
|
|
||||||
app:icon="@drawable/thumb_down_selector"
|
|
||||||
app:iconSize="24dp"
|
|
||||||
app:iconTint="@android:color/white"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:rippleColor="@color/ripple"
|
|
||||||
app:toggleCheckedStateOnClick="false" />
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/dislike_count"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="bottom|center_horizontal"
|
|
||||||
android:paddingHorizontal="4dp"
|
|
||||||
android:shadowColor="@android:color/black"
|
|
||||||
android:shadowRadius="8"
|
|
||||||
android:textColor="@android:color/white"
|
|
||||||
android:textSize="12sp" />
|
|
||||||
</FrameLayout>
|
|
||||||
|
|
||||||
<!-- Comments button -->
|
<!-- Comments button -->
|
||||||
<FrameLayout
|
<com.futo.platformplayer.views.buttons.ShortsButton
|
||||||
|
android:id="@+id/comments_button"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center_horizontal"
|
android:layout_marginBottom="20dp"
|
||||||
android:layout_marginBottom="12dp">
|
android:contentDescription="@string/comments"
|
||||||
|
app:buttonIcon_s="@drawable/ic_comment_s"
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
app:buttonText_s=""
|
||||||
android:layout_width="wrap_content"
|
app:iconSize="24dp"
|
||||||
android:layout_height="wrap_content"
|
app:iconTint="@android:color/white"
|
||||||
android:layout_gravity="center_horizontal">
|
app:rippleColor="@color/ripple" />
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:importantForAccessibility="no"
|
|
||||||
android:src="@drawable/button_shadow"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@id/comments_button"
|
|
||||||
app:layout_constraintEnd_toEndOf="@id/comments_button"
|
|
||||||
app:layout_constraintStart_toStartOf="@id/comments_button"
|
|
||||||
app:layout_constraintTop_toTopOf="@id/comments_button"
|
|
||||||
app:tint="@color/black"
|
|
||||||
tools:ignore="ImageContrastCheck" />
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/comments_button"
|
|
||||||
style="@style/Widget.Material3.Button.IconButton"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginBottom="8dp"
|
|
||||||
android:contentDescription="@string/comments"
|
|
||||||
app:icon="@drawable/desktop_comments"
|
|
||||||
app:iconSize="24dp"
|
|
||||||
app:iconTint="@android:color/white"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:rippleColor="@color/ripple" />
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="bottom|center_horizontal"
|
|
||||||
android:importantForAccessibility="no"
|
|
||||||
android:paddingHorizontal="4dp"
|
|
||||||
android:shadowColor="@android:color/black"
|
|
||||||
android:shadowRadius="8"
|
|
||||||
android:text="@string/comments"
|
|
||||||
android:textColor="@android:color/white"
|
|
||||||
android:textSize="12sp"
|
|
||||||
tools:ignore="TextContrastCheck" />
|
|
||||||
</FrameLayout>
|
|
||||||
|
|
||||||
<!-- Share button -->
|
<!-- Share button -->
|
||||||
<FrameLayout
|
<com.futo.platformplayer.views.buttons.ShortsButton
|
||||||
|
android:id="@+id/share_button"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center_horizontal"
|
android:layout_marginBottom="20dp"
|
||||||
android:layout_marginBottom="12dp">
|
android:contentDescription="@string/share"
|
||||||
|
app:buttonIcon_s="@drawable/ic_share_s"
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
app:iconSize="24dp"
|
||||||
android:layout_width="wrap_content"
|
app:iconTint="@android:color/white"
|
||||||
android:layout_height="wrap_content"
|
app:rippleColor="@color/ripple" />
|
||||||
android:layout_gravity="center_horizontal">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:importantForAccessibility="no"
|
|
||||||
android:src="@drawable/button_shadow"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@id/share_button"
|
|
||||||
app:layout_constraintEnd_toEndOf="@id/share_button"
|
|
||||||
app:layout_constraintStart_toStartOf="@id/share_button"
|
|
||||||
app:layout_constraintTop_toTopOf="@id/share_button"
|
|
||||||
app:tint="@color/black"
|
|
||||||
tools:ignore="ImageContrastCheck" />
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/share_button"
|
|
||||||
style="@style/Widget.Material3.Button.IconButton"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginBottom="8dp"
|
|
||||||
android:contentDescription="@string/share"
|
|
||||||
app:icon="@drawable/desktop_share"
|
|
||||||
app:iconSize="24dp"
|
|
||||||
app:iconTint="@android:color/white"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:rippleColor="@color/ripple" />
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="bottom|center_horizontal"
|
|
||||||
android:importantForAccessibility="no"
|
|
||||||
android:paddingHorizontal="4dp"
|
|
||||||
android:shadowColor="@android:color/black"
|
|
||||||
android:shadowRadius="8"
|
|
||||||
android:text="@string/share"
|
|
||||||
android:textColor="@android:color/white"
|
|
||||||
android:textSize="12sp"
|
|
||||||
tools:ignore="TextContrastCheck" />
|
|
||||||
</FrameLayout>
|
|
||||||
|
|
||||||
<!-- Refresh button -->
|
<!-- Refresh button -->
|
||||||
<FrameLayout
|
<com.futo.platformplayer.views.buttons.ShortsButton
|
||||||
android:id="@+id/refresh_button_container"
|
android:id="@+id/refresh_button"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center_horizontal"
|
android:layout_gravity="center_horizontal"
|
||||||
android:layout_marginBottom="12dp">
|
android:layout_marginBottom="20dp"
|
||||||
|
android:contentDescription="@string/refresh"
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
app:buttonIcon_s="@drawable/ic_refresh"
|
||||||
android:layout_width="wrap_content"
|
app:iconSize="24dp"
|
||||||
android:layout_height="wrap_content"
|
app:iconTint="@android:color/white"
|
||||||
android:layout_gravity="center_horizontal">
|
app:rippleColor="@color/ripple" />
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:importantForAccessibility="no"
|
|
||||||
android:src="@drawable/button_shadow"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@id/refresh_button"
|
|
||||||
app:layout_constraintEnd_toEndOf="@id/refresh_button"
|
|
||||||
app:layout_constraintStart_toStartOf="@id/refresh_button"
|
|
||||||
app:layout_constraintTop_toTopOf="@id/refresh_button"
|
|
||||||
app:tint="@color/black"
|
|
||||||
tools:ignore="ImageContrastCheck" />
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/refresh_button"
|
|
||||||
style="@style/Widget.Material3.Button.IconButton"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center_horizontal"
|
|
||||||
android:layout_marginBottom="8dp"
|
|
||||||
android:contentDescription="@string/refresh"
|
|
||||||
app:icon="@drawable/desktop_refresh"
|
|
||||||
app:iconSize="24dp"
|
|
||||||
app:iconTint="@android:color/white"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:rippleColor="@color/ripple" />
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="bottom|center_horizontal"
|
|
||||||
android:importantForAccessibility="no"
|
|
||||||
android:paddingHorizontal="4dp"
|
|
||||||
android:shadowColor="@android:color/black"
|
|
||||||
android:shadowRadius="8"
|
|
||||||
android:text="@string/refresh"
|
|
||||||
android:textColor="@android:color/white"
|
|
||||||
android:textSize="12sp"
|
|
||||||
tools:ignore="TextContrastCheck" />
|
|
||||||
</FrameLayout>
|
|
||||||
|
|
||||||
<!-- Quality/More button -->
|
<!-- Quality/More button -->
|
||||||
<FrameLayout
|
<com.futo.platformplayer.views.buttons.ShortsButton
|
||||||
|
android:id="@+id/quality_button"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center_horizontal">
|
android:layout_marginBottom="10dp"
|
||||||
|
android:contentDescription="@string/quality"
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
app:buttonIcon_s="@drawable/ic_settings_s"
|
||||||
android:layout_width="wrap_content"
|
app:iconSize="24dp"
|
||||||
android:layout_height="wrap_content"
|
app:iconTint="@android:color/white"
|
||||||
android:layout_gravity="center_horizontal">
|
app:rippleColor="@color/ripple" />
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:importantForAccessibility="no"
|
|
||||||
android:src="@drawable/button_shadow"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@id/quality_button"
|
|
||||||
app:layout_constraintEnd_toEndOf="@id/quality_button"
|
|
||||||
app:layout_constraintStart_toStartOf="@id/quality_button"
|
|
||||||
app:layout_constraintTop_toTopOf="@id/quality_button"
|
|
||||||
app:tint="@color/black"
|
|
||||||
tools:ignore="ImageContrastCheck" />
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/quality_button"
|
|
||||||
style="@style/Widget.Material3.Button.IconButton"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginBottom="8dp"
|
|
||||||
android:contentDescription="@string/quality"
|
|
||||||
app:icon="@drawable/desktop_gear"
|
|
||||||
app:iconSize="24dp"
|
|
||||||
app:iconTint="@android:color/white"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:rippleColor="@color/ripple" />
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="bottom|center_horizontal"
|
|
||||||
android:importantForAccessibility="no"
|
|
||||||
android:paddingHorizontal="4dp"
|
|
||||||
android:shadowColor="@android:color/black"
|
|
||||||
android:shadowRadius="8"
|
|
||||||
android:text="@string/quality"
|
|
||||||
android:textColor="@android:color/white"
|
|
||||||
android:textSize="12sp"
|
|
||||||
tools:ignore="TextContrastCheck" />
|
|
||||||
</FrameLayout>
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
android:layout_above="@+id/short_player_progress_bar"
|
android:layout_above="@+id/short_player_progress_bar"
|
||||||
android:background="@color/black"
|
android:background="@color/black"
|
||||||
app:default_artwork="@drawable/placeholder_video_thumbnail"
|
app:default_artwork="@drawable/placeholder_video_thumbnail"
|
||||||
app:resize_mode="fit"
|
app:resize_mode="zoom"
|
||||||
app:show_buffering="when_playing"
|
app:show_buffering="when_playing"
|
||||||
app:use_artwork="true"
|
app:use_artwork="true"
|
||||||
app:use_controller="false" />
|
app:use_controller="false" />
|
||||||
@@ -17,9 +17,9 @@
|
|||||||
<androidx.media3.ui.DefaultTimeBar
|
<androidx.media3.ui.DefaultTimeBar
|
||||||
android:id="@+id/short_player_progress_bar"
|
android:id="@+id/short_player_progress_bar"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="6dp"
|
android:layout_height="3dp"
|
||||||
android:layout_alignParentBottom="true"
|
android:layout_alignParentBottom="true"
|
||||||
app:bar_height="6dp"
|
app:bar_height="3dp"
|
||||||
app:buffered_color="#DDEEEEEE"
|
app:buffered_color="#DDEEEEEE"
|
||||||
app:played_color="@color/colorPrimary"
|
app:played_color="@color/colorPrimary"
|
||||||
app:scrubber_disabled_size="0dp"
|
app:scrubber_disabled_size="0dp"
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingBottom="5dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:background="@color/transparent"
|
||||||
|
android:id="@+id/root"
|
||||||
|
android:paddingTop="3dp"
|
||||||
|
android:paddingLeft="10dp"
|
||||||
|
android:paddingRight="10dp">
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/image_icon"
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:src="@drawable/ic_qr" />
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="14dp"
|
||||||
|
android:autoSizeTextType="uniform"
|
||||||
|
android:fontFamily="@font/inter_light"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:text="" />
|
||||||
|
</LinearLayout>
|
||||||
@@ -704,4 +704,19 @@
|
|||||||
<item>Newest</item>
|
<item>Newest</item>
|
||||||
<item>Oldest</item>
|
<item>Oldest</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
<string-array name="app_languages">
|
||||||
|
<item>النظام</item>
|
||||||
|
<item>الإنجليزية (EN)</item>
|
||||||
|
<item>الألمانية (DE)</item>
|
||||||
|
<item>الإسبانية (ES)</item>
|
||||||
|
<item>البرتغالية (PT)</item>
|
||||||
|
<item>الفرنسية (FR)</item>
|
||||||
|
<item>اليابانية (JA)</item>
|
||||||
|
<item>الكورية (KO)</item>
|
||||||
|
<item>الصينية (ZH)</item>
|
||||||
|
<item>الروسية (RU)</item>
|
||||||
|
<item>العربية (AR)</item>
|
||||||
|
<item>الإيطالية (IT)</item>
|
||||||
|
<item>التركية (TR)</item>
|
||||||
|
</string-array>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -706,4 +706,19 @@
|
|||||||
<item>Newest</item>
|
<item>Newest</item>
|
||||||
<item>Oldest</item>
|
<item>Oldest</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
<string-array name="app_languages">
|
||||||
|
<item>System</item>
|
||||||
|
<item>Englisch (EN)</item>
|
||||||
|
<item>Deutsch (DE)</item>
|
||||||
|
<item>Spanisch (ES)</item>
|
||||||
|
<item>Portugiesisch (PT)</item>
|
||||||
|
<item>Französisch (FR)</item>
|
||||||
|
<item>Japanisch (JA)</item>
|
||||||
|
<item>Koreanisch (KO)</item>
|
||||||
|
<item>Chinesisch (ZH)</item>
|
||||||
|
<item>Russisch (RU)</item>
|
||||||
|
<item>Arabisch (AR)</item>
|
||||||
|
<item>Italienisch (IT)</item>
|
||||||
|
<item>Türkisch (TR)</item>
|
||||||
|
</string-array>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -716,4 +716,19 @@
|
|||||||
<item>Newest</item>
|
<item>Newest</item>
|
||||||
<item>Oldest</item>
|
<item>Oldest</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
<string-array name="app_languages">
|
||||||
|
<item>Sistema</item>
|
||||||
|
<item>Inglés (EN)</item>
|
||||||
|
<item>Alemán (DE)</item>
|
||||||
|
<item>Español (ES)</item>
|
||||||
|
<item>Portugués (PT)</item>
|
||||||
|
<item>Francés (FR)</item>
|
||||||
|
<item>Japonés (JA)</item>
|
||||||
|
<item>Coreano (KO)</item>
|
||||||
|
<item>Chino (ZH)</item>
|
||||||
|
<item>Ruso (RU)</item>
|
||||||
|
<item>Árabe (AR)</item>
|
||||||
|
<item>Italiano (IT)</item>
|
||||||
|
<item>Turco (TR)</item>
|
||||||
|
</string-array>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -714,4 +714,19 @@
|
|||||||
<item>Newest</item>
|
<item>Newest</item>
|
||||||
<item>Oldest</item>
|
<item>Oldest</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
<string-array name="app_languages">
|
||||||
|
<item>Système</item>
|
||||||
|
<item>Anglais (EN)</item>
|
||||||
|
<item>Allemand (DE)</item>
|
||||||
|
<item>Espagnol (ES)</item>
|
||||||
|
<item>Portugais (PT)</item>
|
||||||
|
<item>Français (FR)</item>
|
||||||
|
<item>Japonais (JA)</item>
|
||||||
|
<item>Coréen (KO)</item>
|
||||||
|
<item>Chinois (ZH)</item>
|
||||||
|
<item>Russe (RU)</item>
|
||||||
|
<item>Arabe (AR)</item>
|
||||||
|
<item>Italien (IT)</item>
|
||||||
|
<item>Turc (TR)</item>
|
||||||
|
</string-array>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -704,4 +704,19 @@
|
|||||||
<item>Newest</item>
|
<item>Newest</item>
|
||||||
<item>Oldest</item>
|
<item>Oldest</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
<string-array name="app_languages">
|
||||||
|
<item>システム</item>
|
||||||
|
<item>英語 (EN)</item>
|
||||||
|
<item>ドイツ語 (DE)</item>
|
||||||
|
<item>スペイン語 (ES)</item>
|
||||||
|
<item>ポルトガル語 (PT)</item>
|
||||||
|
<item>フランス語 (FR)</item>
|
||||||
|
<item>日本語 (JA)</item>
|
||||||
|
<item>韓国語 (KO)</item>
|
||||||
|
<item>中国語 (ZH)</item>
|
||||||
|
<item>ロシア語 (RU)</item>
|
||||||
|
<item>アラビア語 (AR)</item>
|
||||||
|
<item>イタリア語 (IT)</item>
|
||||||
|
<item>トルコ語 (TR)</item>
|
||||||
|
</string-array>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -704,4 +704,19 @@
|
|||||||
<item>Newest</item>
|
<item>Newest</item>
|
||||||
<item>Oldest</item>
|
<item>Oldest</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
<string-array name="app_languages">
|
||||||
|
<item>시스템</item>
|
||||||
|
<item>영어 (EN)</item>
|
||||||
|
<item>독일어 (DE)</item>
|
||||||
|
<item>스페인어 (ES)</item>
|
||||||
|
<item>포르투갈어 (PT)</item>
|
||||||
|
<item>프랑스어 (FR)</item>
|
||||||
|
<item>일본어 (JA)</item>
|
||||||
|
<item>한국어 (KO)</item>
|
||||||
|
<item>중국어 (ZH)</item>
|
||||||
|
<item>러시아어 (RU)</item>
|
||||||
|
<item>아랍어 (AR)</item>
|
||||||
|
<item>이탈리아어 (IT)</item>
|
||||||
|
<item>터키어 (TR)</item>
|
||||||
|
</string-array>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -704,4 +704,19 @@
|
|||||||
<item>Newest</item>
|
<item>Newest</item>
|
||||||
<item>Oldest</item>
|
<item>Oldest</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
<string-array name="app_languages">
|
||||||
|
<item>Sistema</item>
|
||||||
|
<item>Inglês (EN)</item>
|
||||||
|
<item>Alemão (DE)</item>
|
||||||
|
<item>Espanhol (ES)</item>
|
||||||
|
<item>Português (PT)</item>
|
||||||
|
<item>Francês (FR)</item>
|
||||||
|
<item>Japonês (JA)</item>
|
||||||
|
<item>Coreano (KO)</item>
|
||||||
|
<item>Chinês (ZH)</item>
|
||||||
|
<item>Russo (RU)</item>
|
||||||
|
<item>Árabe (AR)</item>
|
||||||
|
<item>Italiano (IT)</item>
|
||||||
|
<item>Turco (TR)</item>
|
||||||
|
</string-array>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -704,4 +704,19 @@
|
|||||||
<item>Newest</item>
|
<item>Newest</item>
|
||||||
<item>Oldest</item>
|
<item>Oldest</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
<string-array name="app_languages">
|
||||||
|
<item>Система</item>
|
||||||
|
<item>Английский (EN)</item>
|
||||||
|
<item>Немецкий (DE)</item>
|
||||||
|
<item>Испанский (ES)</item>
|
||||||
|
<item>Португальский (PT)</item>
|
||||||
|
<item>Французский (FR)</item>
|
||||||
|
<item>Японский (JA)</item>
|
||||||
|
<item>Корейский (KO)</item>
|
||||||
|
<item>Китайский (ZH)</item>
|
||||||
|
<item>Русский (RU)</item>
|
||||||
|
<item>Арабский (AR)</item>
|
||||||
|
<item>Итальянский (IT)</item>
|
||||||
|
<item>Турецкий (TR)</item>
|
||||||
|
</string-array>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -704,4 +704,19 @@
|
|||||||
<item>Newest</item>
|
<item>Newest</item>
|
||||||
<item>Oldest</item>
|
<item>Oldest</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
<string-array name="app_languages">
|
||||||
|
<item>系统</item>
|
||||||
|
<item>英语 (EN)</item>
|
||||||
|
<item>德语 (DE)</item>
|
||||||
|
<item>西班牙语 (ES)</item>
|
||||||
|
<item>葡萄牙语 (PT)</item>
|
||||||
|
<item>法语 (FR)</item>
|
||||||
|
<item>日语 (JA)</item>
|
||||||
|
<item>韩语 (KO)</item>
|
||||||
|
<item>中文 (ZH)</item>
|
||||||
|
<item>俄语 (RU)</item>
|
||||||
|
<item>阿拉伯语 (AR)</item>
|
||||||
|
<item>意大利语 (IT)</item>
|
||||||
|
<item>土耳其语 (TR)</item>
|
||||||
|
</string-array>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<declare-styleable name="ShortsButton">
|
||||||
|
<attr name="buttonIcon_s" format="reference" />
|
||||||
|
<attr name="buttonText_s" format="string" />
|
||||||
|
</declare-styleable>
|
||||||
|
</resources>
|
||||||
@@ -82,6 +82,8 @@
|
|||||||
<string name="allow_ipv6_description">If casting over IPV6 is allowed, can cause issues on some networks</string>
|
<string name="allow_ipv6_description">If casting over IPV6 is allowed, can cause issues on some networks</string>
|
||||||
<string name="allow_ipv4">Allow Link Local IPV4</string>
|
<string name="allow_ipv4">Allow Link Local IPV4</string>
|
||||||
<string name="allow_ipv4_description">If casting over IPV4 link local is allowed, can cause issues on some networks</string>
|
<string name="allow_ipv4_description">If casting over IPV4 link local is allowed, can cause issues on some networks</string>
|
||||||
|
<string name="experimental_cast">Experimental</string>
|
||||||
|
<string name="experimental_cast_description">Use experimental casting backend (requires restart)</string>
|
||||||
<string name="discover">Discover</string>
|
<string name="discover">Discover</string>
|
||||||
<string name="find_new_video_sources_to_add">Find new video sources to add</string>
|
<string name="find_new_video_sources_to_add">Find new video sources to add</string>
|
||||||
<string name="these_sources_have_been_disabled">These sources have been disabled</string>
|
<string name="these_sources_have_been_disabled">These sources have been disabled</string>
|
||||||
@@ -247,6 +249,7 @@
|
|||||||
<string name="membership">Membership</string>
|
<string name="membership">Membership</string>
|
||||||
<string name="store">Store</string>
|
<string name="store">Store</string>
|
||||||
<string name="live_chat">Live Chat</string>
|
<string name="live_chat">Live Chat</string>
|
||||||
|
<string name="vod_chat">VOD Chat</string>
|
||||||
<string name="remove">Remove</string>
|
<string name="remove">Remove</string>
|
||||||
<string name="space_videos">Videos</string>
|
<string name="space_videos">Videos</string>
|
||||||
<string name="playlist">Playlist</string>
|
<string name="playlist">Playlist</string>
|
||||||
@@ -337,6 +340,8 @@
|
|||||||
<string name="test_background_worker">Test Background Worker</string>
|
<string name="test_background_worker">Test Background Worker</string>
|
||||||
<string name="test_background_worker_description"></string>
|
<string name="test_background_worker_description"></string>
|
||||||
<string name="clear_payment">Clear Payment</string>
|
<string name="clear_payment">Clear Payment</string>
|
||||||
|
<string name="configure_sync_server">Configure Sync Server</string>
|
||||||
|
<string name="configure_sync_server_description">Allows you to change the Sync Server to a self-hosted one.</string>
|
||||||
<string name="clears_cookies_when_you_log_out">Clears cookies when you log out</string>
|
<string name="clears_cookies_when_you_log_out">Clears cookies when you log out</string>
|
||||||
<string name="clears_in_app_browser_cookies">Clears in-app browser cookies</string>
|
<string name="clears_in_app_browser_cookies">Clears in-app browser cookies</string>
|
||||||
<string name="configure_browsing_behavior">Configure browsing behavior</string>
|
<string name="configure_browsing_behavior">Configure browsing behavior</string>
|
||||||
@@ -435,6 +440,11 @@
|
|||||||
<string name="allow_full_screen_portrait">Allow full-screen portrait when watching horizontal videos</string>
|
<string name="allow_full_screen_portrait">Allow full-screen portrait when watching horizontal videos</string>
|
||||||
<string name="delete_watchlist_on_finish">Delete from WatchLater when watched</string>
|
<string name="delete_watchlist_on_finish">Delete from WatchLater when watched</string>
|
||||||
<string name="delete_watchlist_on_finish_description">After you leave a video that you mostly watched, it will be removed from watch later.</string>
|
<string name="delete_watchlist_on_finish_description">After you leave a video that you mostly watched, it will be removed from watch later.</string>
|
||||||
|
<string name="shorts_pregenerate">Pre-generate shorts sources</string>
|
||||||
|
<string name="shorts_pregenerate_description">Generates short sources (when applicable) one video ahead</string>
|
||||||
|
<string name="shorts_fit_video">Fit Shorts Video</string>
|
||||||
|
<string name="shorts_fit_video_description">Will scale the video to fit the view, instead of filling the view properly.</string>
|
||||||
|
<string name="shorts_fit_video_warning">This setting will require you to reboot Grayjay.</string>
|
||||||
<string name="seek_offset">Seek duration</string>
|
<string name="seek_offset">Seek duration</string>
|
||||||
<string name="min_playback_speed">Minimum Playback Speed</string>
|
<string name="min_playback_speed">Minimum Playback Speed</string>
|
||||||
<string name="min_playback_speed_description">Minimum Available Speed</string>
|
<string name="min_playback_speed_description">Minimum Available Speed</string>
|
||||||
@@ -470,6 +480,7 @@
|
|||||||
<string name="number_of_concurrent_threads_to_multiply_download_speeds_from_throttled_sources">Number of concurrent threads to multiply download speeds from throttled sources</string>
|
<string name="number_of_concurrent_threads_to_multiply_download_speeds_from_throttled_sources">Number of concurrent threads to multiply download speeds from throttled sources</string>
|
||||||
<string name="payment">Payment</string>
|
<string name="payment">Payment</string>
|
||||||
<string name="payment_status">Payment Status</string>
|
<string name="payment_status">Payment Status</string>
|
||||||
|
<string name="relay_server">Sync Relay Server</string>
|
||||||
<string name="bypass_rotation_prevention">Bypass Rotation Prevention</string>
|
<string name="bypass_rotation_prevention">Bypass Rotation Prevention</string>
|
||||||
<string name="playlist_delete_confirmation">Playlist Delete Confirmation</string>
|
<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="playlist_delete_confirmation_description">Show confirmation dialog when deleting media from a playlist</string>
|
||||||
@@ -1061,6 +1072,8 @@
|
|||||||
<item>Chinese (ZH)</item>
|
<item>Chinese (ZH)</item>
|
||||||
<item>Russian (RU)</item>
|
<item>Russian (RU)</item>
|
||||||
<item>Arabic (AR)</item>
|
<item>Arabic (AR)</item>
|
||||||
|
<item>Italian (IT)</item>
|
||||||
|
<item>Turkish (TR)</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="player_background_behavior">
|
<string-array name="player_background_behavior">
|
||||||
<item>None</item>
|
<item>None</item>
|
||||||
@@ -1105,6 +1118,10 @@
|
|||||||
<item>ChromeCast</item>
|
<item>ChromeCast</item>
|
||||||
<item>AirPlay</item>
|
<item>AirPlay</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
<string-array name="exp_casting_device_type_array" translatable="false">
|
||||||
|
<item>FCast</item>
|
||||||
|
<item>ChromeCast</item>
|
||||||
|
</string-array>
|
||||||
<string-array name="log_levels">
|
<string-array name="log_levels">
|
||||||
<item>None</item>
|
<item>None</item>
|
||||||
<item>Error</item>
|
<item>Error</item>
|
||||||
|
|||||||
Submodule
+1
Submodule app/src/stable/assets/sources/mixcloud added at 0bbe4c63f4
@@ -16,7 +16,8 @@
|
|||||||
"89ae4889-0420-4d16-ad6c-19c776b28f99": "sources/apple-podcasts/ApplePodcastsConfig.json",
|
"89ae4889-0420-4d16-ad6c-19c776b28f99": "sources/apple-podcasts/ApplePodcastsConfig.json",
|
||||||
"8d029a7f-5507-4e36-8bd8-c19a3b77d383": "sources/tedtalks/TedTalksConfig.json",
|
"8d029a7f-5507-4e36-8bd8-c19a3b77d383": "sources/tedtalks/TedTalksConfig.json",
|
||||||
"273b6523-5438-44e2-9f5d-78e0325a8fd9": "sources/curiositystream/CuriosityStreamConfig.json",
|
"273b6523-5438-44e2-9f5d-78e0325a8fd9": "sources/curiositystream/CuriosityStreamConfig.json",
|
||||||
"9bb33039-8580-48d4-9849-21319ae845a4": "sources/crunchyroll/CrunchyrollConfig.json"
|
"9bb33039-8580-48d4-9849-21319ae845a4": "sources/crunchyroll/CrunchyrollConfig.json",
|
||||||
|
"84331338-b045-419c-88e4-c86036f4cbf5": "sources/mixcloud/MixcloudConfig.json"
|
||||||
},
|
},
|
||||||
"SOURCES_EMBEDDED_DEFAULT": [
|
"SOURCES_EMBEDDED_DEFAULT": [
|
||||||
"35ae969a-a7db-11ed-afa1-0242ac120002"
|
"35ae969a-a7db-11ed-afa1-0242ac120002"
|
||||||
|
|||||||
@@ -8,6 +8,16 @@
|
|||||||
<receiver android:name=".receivers.InstallReceiver" />
|
<receiver android:name=".receivers.InstallReceiver" />
|
||||||
|
|
||||||
<activity android:name=".activities.MainActivity" android:launchMode="singleInstance">
|
<activity android:name=".activities.MainActivity" android:launchMode="singleInstance">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
<category android:name="android.intent.category.OPENABLE" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<data android:mimeType="video/*" />
|
||||||
|
<data android:mimeType="audio/*" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
<intent-filter android:autoVerify="true">
|
<intent-filter android:autoVerify="true">
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||||
|
|||||||
Submodule
+1
Submodule app/src/unstable/assets/sources/mixcloud added at 0bbe4c63f4
@@ -16,7 +16,8 @@
|
|||||||
"89ae4889-0420-4d16-ad6c-19c776b28f99": "sources/apple-podcasts/ApplePodcastsConfig.json",
|
"89ae4889-0420-4d16-ad6c-19c776b28f99": "sources/apple-podcasts/ApplePodcastsConfig.json",
|
||||||
"8d029a7f-5507-4e36-8bd8-c19a3b77d383": "sources/tedtalks/TedTalksConfig.json",
|
"8d029a7f-5507-4e36-8bd8-c19a3b77d383": "sources/tedtalks/TedTalksConfig.json",
|
||||||
"273b6523-5438-44e2-9f5d-78e0325a8fd9": "sources/curiositystream/CuriosityStreamConfig.json",
|
"273b6523-5438-44e2-9f5d-78e0325a8fd9": "sources/curiositystream/CuriosityStreamConfig.json",
|
||||||
"9bb33039-8580-48d4-9849-21319ae845a4": "sources/crunchyroll/CrunchyrollConfig.json"
|
"9bb33039-8580-48d4-9849-21319ae845a4": "sources/crunchyroll/CrunchyrollConfig.json",
|
||||||
|
"84331338-b045-419c-88e4-c86036f4cbf5": "sources/mixcloud/MixcloudConfig.json"
|
||||||
},
|
},
|
||||||
"SOURCES_EMBEDDED_DEFAULT": [
|
"SOURCES_EMBEDDED_DEFAULT": [
|
||||||
"35ae969a-a7db-11ed-afa1-0242ac120002"
|
"35ae969a-a7db-11ed-afa1-0242ac120002"
|
||||||
|
|||||||
Reference in New Issue
Block a user