mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-04-30 22:54:30 +02:00
Merge
This commit is contained in:
commit
e989590c08
@ -6,6 +6,7 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.text.Layout
|
||||||
import android.text.method.ScrollingMovementMethod
|
import android.text.method.ScrollingMovementMethod
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
@ -198,7 +199,6 @@ class UIDialogs {
|
|||||||
dialog.show();
|
dialog.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun showDialog(context: Context, icon: Int, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action) {
|
fun showDialog(context: Context, icon: Int, text: String, textDetails: String? = null, code: String? = null, defaultCloseAction: Int, vararg actions: Action) {
|
||||||
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);
|
||||||
@ -216,16 +216,18 @@ class UIDialogs {
|
|||||||
view.findViewById<TextView>(R.id.dialog_text_details).apply {
|
view.findViewById<TextView>(R.id.dialog_text_details).apply {
|
||||||
if (textDetails == null)
|
if (textDetails == null)
|
||||||
this.visibility = View.GONE;
|
this.visibility = View.GONE;
|
||||||
else
|
else {
|
||||||
this.text = textDetails;
|
this.text = textDetails;
|
||||||
|
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)
|
if (code == null) this.visibility = View.GONE;
|
||||||
this.visibility = View.GONE;
|
|
||||||
else {
|
else {
|
||||||
this.text = code;
|
this.text = code;
|
||||||
this.movementMethod = ScrollingMovementMethod.getInstance();
|
this.movementMethod = ScrollingMovementMethod.getInstance();
|
||||||
this.visibility = View.VISIBLE;
|
this.visibility = View.VISIBLE;
|
||||||
|
this.textAlignment = View.TEXT_ALIGNMENT_VIEW_START
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
view.findViewById<LinearLayout>(R.id.dialog_buttons).apply {
|
view.findViewById<LinearLayout>(R.id.dialog_buttons).apply {
|
||||||
|
@ -32,6 +32,7 @@ import java.io.IOException
|
|||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.ByteOrder
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.ThreadLocalRandom
|
import java.util.concurrent.ThreadLocalRandom
|
||||||
|
|
||||||
@ -272,3 +273,9 @@ fun <T> findNewIndex(originalArr: List<T>, newArr: List<T>, item: T): Int{
|
|||||||
else
|
else
|
||||||
return newIndex;
|
return newIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun ByteBuffer.toUtf8String(): String {
|
||||||
|
val remainingBytes = ByteArray(remaining())
|
||||||
|
get(remainingBytes)
|
||||||
|
return String(remainingBytes, Charsets.UTF_8)
|
||||||
|
}
|
||||||
|
@ -7,7 +7,6 @@ import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
|||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.net.wifi.WifiManager
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.StrictMode
|
import android.os.StrictMode
|
||||||
import android.os.StrictMode.VmPolicy
|
import android.os.StrictMode.VmPolicy
|
||||||
@ -33,6 +32,7 @@ import com.futo.platformplayer.BuildConfig
|
|||||||
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.http.ManagedHttpClient
|
||||||
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.fragment.mainactivity.bottombar.MenuBottomBarFragment
|
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
|
||||||
@ -81,6 +81,7 @@ import com.futo.platformplayer.states.StatePlayer
|
|||||||
import com.futo.platformplayer.states.StatePlaylists
|
import com.futo.platformplayer.states.StatePlaylists
|
||||||
import com.futo.platformplayer.states.StateSubscriptions
|
import com.futo.platformplayer.states.StateSubscriptions
|
||||||
import com.futo.platformplayer.stores.FragmentedStorage
|
import com.futo.platformplayer.stores.FragmentedStorage
|
||||||
|
import com.futo.platformplayer.stores.StringStorage
|
||||||
import com.futo.platformplayer.stores.SubscriptionStorage
|
import com.futo.platformplayer.stores.SubscriptionStorage
|
||||||
import com.futo.platformplayer.stores.v2.ManagedStore
|
import com.futo.platformplayer.stores.v2.ManagedStore
|
||||||
import com.futo.platformplayer.views.ToastView
|
import com.futo.platformplayer.views.ToastView
|
||||||
@ -88,11 +89,14 @@ import com.futo.polycentric.core.ApiMethods
|
|||||||
import com.google.gson.JsonParser
|
import com.google.gson.JsonParser
|
||||||
import com.google.zxing.integration.android.IntentIntegrator
|
import com.google.zxing.integration.android.IntentIntegrator
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.PrintWriter
|
import java.io.PrintWriter
|
||||||
@ -102,7 +106,6 @@ import java.util.LinkedList
|
|||||||
import java.util.Queue
|
import java.util.Queue
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue
|
import java.util.concurrent.ConcurrentLinkedQueue
|
||||||
|
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity, IWithResultLauncher {
|
class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||||
|
|
||||||
//TODO: Move to dimensions
|
//TODO: Move to dimensions
|
||||||
@ -256,7 +259,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
setContentView(R.layout.activity_main);
|
setContentView(R.layout.activity_main);
|
||||||
setNavigationBarColorAndIcons();
|
setNavigationBarColorAndIcons();
|
||||||
if (Settings.instance.playback.allowVideoToGoUnderCutout)
|
if (Settings.instance.playback.allowVideoToGoUnderCutout)
|
||||||
window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
window.attributes.layoutInDisplayCutoutMode =
|
||||||
|
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
||||||
|
|
||||||
runBlocking {
|
runBlocking {
|
||||||
StatePlatform.instance.updateAvailableClients(this@MainActivity);
|
StatePlatform.instance.updateAvailableClients(this@MainActivity);
|
||||||
@ -331,9 +335,11 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
};
|
};
|
||||||
_fragVideoDetail.onTransitioning.subscribe {
|
_fragVideoDetail.onTransitioning.subscribe {
|
||||||
if (it || _fragVideoDetail.state != VideoDetailFragment.State.MINIMIZED)
|
if (it || _fragVideoDetail.state != VideoDetailFragment.State.MINIMIZED)
|
||||||
_fragContainerOverlay.elevation = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 15f, resources.displayMetrics);
|
_fragContainerOverlay.elevation =
|
||||||
|
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 15f, resources.displayMetrics);
|
||||||
else
|
else
|
||||||
_fragContainerOverlay.elevation = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics);
|
_fragContainerOverlay.elevation =
|
||||||
|
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics);
|
||||||
}
|
}
|
||||||
|
|
||||||
_fragVideoDetail.onCloseEvent.subscribe {
|
_fragVideoDetail.onCloseEvent.subscribe {
|
||||||
@ -353,8 +359,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
if (it) {
|
if (it) {
|
||||||
_buttonIncognito.elevation = 99f;
|
_buttonIncognito.elevation = 99f;
|
||||||
_buttonIncognito.alpha = 1f;
|
_buttonIncognito.alpha = 1f;
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
_buttonIncognito.elevation = -99f;
|
_buttonIncognito.elevation = -99f;
|
||||||
_buttonIncognito.alpha = 0f;
|
_buttonIncognito.alpha = 0f;
|
||||||
}
|
}
|
||||||
@ -362,14 +367,16 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
_buttonIncognito.setOnClickListener {
|
_buttonIncognito.setOnClickListener {
|
||||||
if (!StateApp.instance.privateMode)
|
if (!StateApp.instance.privateMode)
|
||||||
return@setOnClickListener;
|
return@setOnClickListener;
|
||||||
UIDialogs.showDialog(this, R.drawable.ic_disabled_visible_purple, "Disable Privacy Mode",
|
UIDialogs.showDialog(
|
||||||
|
this, R.drawable.ic_disabled_visible_purple, "Disable Privacy Mode",
|
||||||
"Do you want to disable privacy mode? New videos will be tracked again.", null, 0,
|
"Do you want to disable privacy mode? New videos will be tracked again.", null, 0,
|
||||||
UIDialogs.Action("Cancel", {
|
UIDialogs.Action("Cancel", {
|
||||||
StateApp.instance.setPrivacyMode(true);
|
StateApp.instance.setPrivacyMode(true);
|
||||||
}, UIDialogs.ActionStyle.NONE),
|
}, UIDialogs.ActionStyle.NONE),
|
||||||
UIDialogs.Action("Disable", {
|
UIDialogs.Action("Disable", {
|
||||||
StateApp.instance.setPrivacyMode(false);
|
StateApp.instance.setPrivacyMode(false);
|
||||||
}, UIDialogs.ActionStyle.DANGEROUS));
|
}, UIDialogs.ActionStyle.DANGEROUS)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
_fragVideoDetail.onFullscreenChanged.subscribe {
|
_fragVideoDetail.onFullscreenChanged.subscribe {
|
||||||
Logger.i(TAG, "onFullscreenChanged ${it}");
|
Logger.i(TAG, "onFullscreenChanged ${it}");
|
||||||
@ -377,13 +384,11 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
if (it) {
|
if (it) {
|
||||||
_buttonIncognito.elevation = -99f;
|
_buttonIncognito.elevation = -99f;
|
||||||
_buttonIncognito.alpha = 0f;
|
_buttonIncognito.alpha = 0f;
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
if (StateApp.instance.privateMode) {
|
if (StateApp.instance.privateMode) {
|
||||||
_buttonIncognito.elevation = 99f;
|
_buttonIncognito.elevation = 99f;
|
||||||
_buttonIncognito.alpha = 1f;
|
_buttonIncognito.alpha = 1f;
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
_buttonIncognito.elevation = -99f;
|
_buttonIncognito.elevation = -99f;
|
||||||
_buttonIncognito.alpha = 0f;
|
_buttonIncognito.alpha = 0f;
|
||||||
}
|
}
|
||||||
@ -448,7 +453,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
fragCurrent = _fragMainHome;
|
fragCurrent = _fragMainHome;
|
||||||
|
|
||||||
val defaultTab = Settings.instance.tabs.mapNotNull {
|
val defaultTab = Settings.instance.tabs.mapNotNull {
|
||||||
val buttonDefinition = MenuBottomBarFragment.buttonDefinitions.firstOrNull { bd -> it.id == bd.id };
|
val buttonDefinition =
|
||||||
|
MenuBottomBarFragment.buttonDefinitions.firstOrNull { bd -> it.id == bd.id };
|
||||||
if (buttonDefinition == null) {
|
if (buttonDefinition == null) {
|
||||||
return@mapNotNull null;
|
return@mapNotNull null;
|
||||||
} else {
|
} else {
|
||||||
@ -507,7 +513,8 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
|
|
||||||
//startActivity(Intent(this, TestActivity::class.java));
|
//startActivity(Intent(this, TestActivity::class.java));
|
||||||
|
|
||||||
val sharedPreferences = getSharedPreferences("GrayjayFirstBoot", Context.MODE_PRIVATE)
|
val sharedPreferences =
|
||||||
|
getSharedPreferences("GrayjayFirstBoot", Context.MODE_PRIVATE)
|
||||||
val isFirstBoot = sharedPreferences.getBoolean("IsFirstBoot", true)
|
val isFirstBoot = sharedPreferences.getBoolean("IsFirstBoot", true)
|
||||||
if (isFirstBoot) {
|
if (isFirstBoot) {
|
||||||
UIDialogs.showConfirmationDialog(this, getString(R.string.do_you_want_to_see_the_tutorials_you_can_find_them_at_any_time_through_the_more_button), {
|
UIDialogs.showConfirmationDialog(this, getString(R.string.do_you_want_to_see_the_tutorials_you_can_find_them_at_any_time_through_the_more_button), {
|
||||||
@ -516,6 +523,64 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
|
|
||||||
sharedPreferences.edit().putBoolean("IsFirstBoot", false).apply()
|
sharedPreferences.edit().putBoolean("IsFirstBoot", false).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val submissionStatus = FragmentedStorage.get<StringStorage>("subscriptionSubmissionStatus")
|
||||||
|
|
||||||
|
val numSubscriptions = StateSubscriptions.instance.getSubscriptionCount()
|
||||||
|
|
||||||
|
val subscriptionsThreshold = 20
|
||||||
|
|
||||||
|
if (
|
||||||
|
submissionStatus.value == ""
|
||||||
|
&& StateApp.instance.getCurrentNetworkState() != StateApp.NetworkState.DISCONNECTED
|
||||||
|
&& numSubscriptions >= subscriptionsThreshold
|
||||||
|
) {
|
||||||
|
|
||||||
|
UIDialogs.showDialog(
|
||||||
|
this,
|
||||||
|
R.drawable.ic_internet,
|
||||||
|
getString(R.string.contribute_personal_subscriptions_list),
|
||||||
|
getString(R.string.contribute_personal_subscriptions_list_description),
|
||||||
|
null,
|
||||||
|
0,
|
||||||
|
UIDialogs.Action("Cancel", {
|
||||||
|
submissionStatus.setAndSave("dismissed")
|
||||||
|
}, UIDialogs.ActionStyle.NONE),
|
||||||
|
UIDialogs.Action("Upload", {
|
||||||
|
submissionStatus.setAndSave("submitted")
|
||||||
|
|
||||||
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
@Serializable
|
||||||
|
data class CreatorInfo(val pluginId: String, val url: String)
|
||||||
|
|
||||||
|
val subscriptions =
|
||||||
|
StateSubscriptions.instance.getSubscriptions().map { original ->
|
||||||
|
CreatorInfo(
|
||||||
|
pluginId = original.channel.id.pluginId ?: "",
|
||||||
|
url = original.channel.url
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val json = Json.encodeToString(subscriptions)
|
||||||
|
|
||||||
|
val url = "https://data.grayjay.app/donate-subscription-list"
|
||||||
|
val client = ManagedHttpClient();
|
||||||
|
val headers = hashMapOf(
|
||||||
|
"Content-Type" to "application/json"
|
||||||
|
)
|
||||||
|
try {
|
||||||
|
val response = client.post(url, json, headers)
|
||||||
|
// if it failed retry one time
|
||||||
|
if (!response.isOk) {
|
||||||
|
client.post(url, json, headers)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Logger.i(TAG, "Failed to submit subscription list.", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, UIDialogs.ActionStyle.PRIMARY)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -589,9 +654,11 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
|
|
||||||
when (intent.action) {
|
when (intent.action) {
|
||||||
Intent.ACTION_SEND -> {
|
Intent.ACTION_SEND -> {
|
||||||
targetData = intent.getStringExtra(Intent.EXTRA_STREAM) ?: intent.getStringExtra(Intent.EXTRA_TEXT);
|
targetData = intent.getStringExtra(Intent.EXTRA_STREAM)
|
||||||
|
?: intent.getStringExtra(Intent.EXTRA_TEXT);
|
||||||
Logger.i(TAG, "Share Received: " + targetData);
|
Logger.i(TAG, "Share Received: " + targetData);
|
||||||
}
|
}
|
||||||
|
|
||||||
Intent.ACTION_VIEW -> {
|
Intent.ACTION_VIEW -> {
|
||||||
targetData = intent.dataString
|
targetData = intent.dataString
|
||||||
|
|
||||||
@ -599,18 +666,22 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
Logger.i(TAG, "View Received: " + targetData);
|
Logger.i(TAG, "View Received: " + targetData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"VIDEO" -> {
|
"VIDEO" -> {
|
||||||
val url = intent.getStringExtra("VIDEO");
|
val url = intent.getStringExtra("VIDEO");
|
||||||
navigate(_fragVideoDetail, url);
|
navigate(_fragVideoDetail, url);
|
||||||
}
|
}
|
||||||
|
|
||||||
"IMPORT_OPTIONS" -> {
|
"IMPORT_OPTIONS" -> {
|
||||||
UIDialogs.showImportOptionsDialog(this);
|
UIDialogs.showImportOptionsDialog(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
"ACTION" -> {
|
"ACTION" -> {
|
||||||
val action = intent.getStringExtra("ACTION");
|
val action = intent.getStringExtra("ACTION");
|
||||||
StateDeveloper.instance.testState = "TestPlayback";
|
StateDeveloper.instance.testState = "TestPlayback";
|
||||||
StateDeveloper.instance.testPlayback();
|
StateDeveloper.instance.testPlayback();
|
||||||
}
|
}
|
||||||
|
|
||||||
"TAB" -> {
|
"TAB" -> {
|
||||||
when (intent.getStringExtra("TAB")) {
|
when (intent.getStringExtra("TAB")) {
|
||||||
"Sources" -> {
|
"Sources" -> {
|
||||||
@ -642,8 +713,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
handleUrlAll(targetData)
|
handleUrlAll(targetData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} catch (ex: Throwable) {
|
||||||
catch(ex: Throwable) {
|
|
||||||
UIDialogs.showGeneralErrorDialog(this, getString(R.string.failed_to_handle_file), ex);
|
UIDialogs.showGeneralErrorDialog(this, getString(R.string.failed_to_handle_file), ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -653,32 +723,28 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
when (uri.scheme) {
|
when (uri.scheme) {
|
||||||
"grayjay" -> {
|
"grayjay" -> {
|
||||||
if (url.startsWith("grayjay://license/")) {
|
if (url.startsWith("grayjay://license/")) {
|
||||||
if(StatePayment.instance.setPaymentLicenseUrl(url))
|
if (StatePayment.instance.setPaymentLicenseUrl(url)) {
|
||||||
{
|
|
||||||
UIDialogs.showDialogOk(this, R.drawable.ic_check, getString(R.string.your_license_key_has_been_set_an_app_restart_might_be_required));
|
UIDialogs.showDialogOk(this, R.drawable.ic_check, getString(R.string.your_license_key_has_been_set_an_app_restart_might_be_required));
|
||||||
|
|
||||||
if (fragCurrent is BuyFragment)
|
if (fragCurrent is BuyFragment)
|
||||||
closeSegment(fragCurrent);
|
closeSegment(fragCurrent);
|
||||||
}
|
} else
|
||||||
else
|
|
||||||
UIDialogs.toast(getString(R.string.invalid_license_format));
|
UIDialogs.toast(getString(R.string.invalid_license_format));
|
||||||
|
|
||||||
}
|
} else if (url.startsWith("grayjay://plugin/")) {
|
||||||
else if(url.startsWith("grayjay://plugin/")) {
|
|
||||||
val intent = Intent(this, AddSourceActivity::class.java).apply {
|
val intent = Intent(this, AddSourceActivity::class.java).apply {
|
||||||
data = Uri.parse(url.substring("grayjay://plugin/".length));
|
data = Uri.parse(url.substring("grayjay://plugin/".length));
|
||||||
};
|
};
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
}
|
} else if (url.startsWith("grayjay://video/")) {
|
||||||
else if(url.startsWith("grayjay://video/")) {
|
|
||||||
val videoUrl = url.substring("grayjay://video/".length);
|
val videoUrl = url.substring("grayjay://video/".length);
|
||||||
navigate(_fragVideoDetail, videoUrl);
|
navigate(_fragVideoDetail, videoUrl);
|
||||||
}
|
} else if (url.startsWith("grayjay://channel/")) {
|
||||||
else if(url.startsWith("grayjay://channel/")) {
|
|
||||||
val channelUrl = url.substring("grayjay://channel/".length);
|
val channelUrl = url.substring("grayjay://channel/".length);
|
||||||
navigate(_fragMainChannel, channelUrl);
|
navigate(_fragMainChannel, channelUrl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"content" -> {
|
"content" -> {
|
||||||
if (!handleContent(url, intent.type)) {
|
if (!handleContent(url, intent.type)) {
|
||||||
UIDialogs.showSingleButtonDialog(
|
UIDialogs.showSingleButtonDialog(
|
||||||
@ -689,6 +755,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
{ });
|
{ });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"file" -> {
|
"file" -> {
|
||||||
if (!handleFile(url)) {
|
if (!handleFile(url)) {
|
||||||
UIDialogs.showSingleButtonDialog(
|
UIDialogs.showSingleButtonDialog(
|
||||||
@ -699,6 +766,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
{ });
|
{ });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"polycentric" -> {
|
"polycentric" -> {
|
||||||
if (!handlePolycentric(url)) {
|
if (!handlePolycentric(url)) {
|
||||||
UIDialogs.showSingleButtonDialog(
|
UIDialogs.showSingleButtonDialog(
|
||||||
@ -709,6 +777,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
{ });
|
{ });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"fcast" -> {
|
"fcast" -> {
|
||||||
if (!handleFCast(url)) {
|
if (!handleFCast(url)) {
|
||||||
UIDialogs.showSingleButtonDialog(
|
UIDialogs.showSingleButtonDialog(
|
||||||
@ -719,6 +788,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
{ });
|
{ });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
if (!handleUrl(url)) {
|
if (!handleUrl(url)) {
|
||||||
UIDialogs.showSingleButtonDialog(
|
UIDialogs.showSingleButtonDialog(
|
||||||
@ -768,6 +838,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
return@withContext false;
|
return@withContext false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleContent(file: String, mime: String? = null): Boolean {
|
fun handleContent(file: String, mime: String? = null): Boolean {
|
||||||
Logger.i(TAG, "handleContent(url=$file)");
|
Logger.i(TAG, "handleContent(url=$file)");
|
||||||
|
|
||||||
@ -778,14 +849,14 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
return handleUnknownJson(recon);
|
return handleUnknownJson(recon);
|
||||||
|
|
||||||
var reconLines = Json.decodeFromString<List<String>>(recon);
|
var reconLines = Json.decodeFromString<List<String>>(recon);
|
||||||
val cacheStr = reconLines.find { it.startsWith("__CACHE:") }?.substring("__CACHE:".length);
|
val cacheStr =
|
||||||
|
reconLines.find { it.startsWith("__CACHE:") }?.substring("__CACHE:".length);
|
||||||
reconLines = reconLines.filter { !it.startsWith("__CACHE:") }; //TODO: constant prefix
|
reconLines = reconLines.filter { !it.startsWith("__CACHE:") }; //TODO: constant prefix
|
||||||
var cache: ImportCache? = null;
|
var cache: ImportCache? = null;
|
||||||
try {
|
try {
|
||||||
if (cacheStr != null)
|
if (cacheStr != null)
|
||||||
cache = Json.decodeFromString(cacheStr);
|
cache = Json.decodeFromString(cacheStr);
|
||||||
}
|
} catch (ex: Throwable) {
|
||||||
catch(ex: Throwable) {
|
|
||||||
Logger.e(TAG, "Failed to deserialize cache");
|
Logger.e(TAG, "Failed to deserialize cache");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -794,16 +865,15 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
Logger.i(TAG, "Opened shared playlist reconstruction\n${recon}");
|
Logger.i(TAG, "Opened shared playlist reconstruction\n${recon}");
|
||||||
handleReconstruction(recon, cache);
|
handleReconstruction(recon, cache);
|
||||||
return true;
|
return true;
|
||||||
}
|
} else if (file.lowercase().endsWith(".zip") || mime == "application/zip") {
|
||||||
else if(file.lowercase().endsWith(".zip") || mime == "application/zip") {
|
|
||||||
StateBackup.importZipBytes(this, lifecycleScope, data);
|
StateBackup.importZipBytes(this, lifecycleScope, data);
|
||||||
return true;
|
return true;
|
||||||
}
|
} 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));
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleFile(file: String): Boolean {
|
fun handleFile(file: String): Boolean {
|
||||||
Logger.i(TAG, "handleFile(url=$file)");
|
Logger.i(TAG, "handleFile(url=$file)");
|
||||||
if (file.lowercase().endsWith(".json")) {
|
if (file.lowercase().endsWith(".json")) {
|
||||||
@ -812,14 +882,14 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
return handleUnknownJson(recon);
|
return handleUnknownJson(recon);
|
||||||
|
|
||||||
var reconLines = Json.decodeFromString<List<String>>(recon);
|
var reconLines = Json.decodeFromString<List<String>>(recon);
|
||||||
val cacheStr = reconLines.find { it.startsWith("__CACHE:") }?.substring("__CACHE:".length);
|
val cacheStr =
|
||||||
|
reconLines.find { it.startsWith("__CACHE:") }?.substring("__CACHE:".length);
|
||||||
reconLines = reconLines.filter { !it.startsWith("__CACHE:") }; //TODO: constant prefix
|
reconLines = reconLines.filter { !it.startsWith("__CACHE:") }; //TODO: constant prefix
|
||||||
var cache: ImportCache? = null;
|
var cache: ImportCache? = null;
|
||||||
try {
|
try {
|
||||||
if (cacheStr != null)
|
if (cacheStr != null)
|
||||||
cache = Json.decodeFromString(cacheStr);
|
cache = Json.decodeFromString(cacheStr);
|
||||||
}
|
} catch (ex: Throwable) {
|
||||||
catch(ex: Throwable) {
|
|
||||||
Logger.e(TAG, "Failed to deserialize cache");
|
Logger.e(TAG, "Failed to deserialize cache");
|
||||||
}
|
}
|
||||||
recon = reconLines.joinToString("\n");
|
recon = reconLines.joinToString("\n");
|
||||||
@ -827,16 +897,15 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
Logger.i(TAG, "Opened shared playlist reconstruction\n${recon}");
|
Logger.i(TAG, "Opened shared playlist reconstruction\n${recon}");
|
||||||
handleReconstruction(recon, cache);
|
handleReconstruction(recon, cache);
|
||||||
return true;
|
return true;
|
||||||
}
|
} else if (file.lowercase().endsWith(".zip")) {
|
||||||
else if(file.lowercase().endsWith(".zip")) {
|
|
||||||
StateBackup.importZipBytes(this, lifecycleScope, readSharedFile(file));
|
StateBackup.importZipBytes(this, lifecycleScope, readSharedFile(file));
|
||||||
return true;
|
return true;
|
||||||
}
|
} else if (file.lowercase().endsWith(".txt")) {
|
||||||
else if(file.lowercase().endsWith(".txt")) {
|
|
||||||
return handleUnknownText(String(readSharedFile(file)));
|
return handleUnknownText(String(readSharedFile(file)));
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleReconstruction(recon: String, cache: ImportCache? = null) {
|
fun handleReconstruction(recon: String, cache: ImportCache? = null) {
|
||||||
val type = ManagedStore.getReconstructionIdentifier(recon);
|
val type = ManagedStore.getReconstructionIdentifier(recon);
|
||||||
val store: ManagedStore<*> = when (type) {
|
val store: ManagedStore<*> = when (type) {
|
||||||
@ -848,7 +917,9 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
};
|
};
|
||||||
|
|
||||||
val name = when (type) {
|
val name = when (type) {
|
||||||
"Playlist" -> recon.split("\n").filter { !it.startsWith(ManagedStore.RECONSTRUCTION_HEADER_OPERATOR) }.firstOrNull() ?: type;
|
"Playlist" -> recon.split("\n")
|
||||||
|
.filter { !it.startsWith(ManagedStore.RECONSTRUCTION_HEADER_OPERATOR) }
|
||||||
|
.firstOrNull() ?: type;
|
||||||
else -> type
|
else -> type
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -867,13 +938,13 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
navigate(_fragImportSubscriptions, lines);
|
navigate(_fragImportSubscriptions, lines);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
} catch (ex: Throwable) {
|
||||||
catch(ex: Throwable) {
|
|
||||||
Logger.e(TAG, ex.message, ex);
|
Logger.e(TAG, ex.message, ex);
|
||||||
UIDialogs.showGeneralErrorDialog(this, getString(R.string.failed_to_parse_text_file), ex);
|
UIDialogs.showGeneralErrorDialog(this, getString(R.string.failed_to_parse_text_file), ex);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleUnknownJson(json: String): Boolean {
|
fun handleUnknownJson(json: String): Boolean {
|
||||||
|
|
||||||
val context = this;
|
val context = this;
|
||||||
@ -885,8 +956,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
return false;//throw IllegalArgumentException("Invalid NewPipe json structure found");
|
return false;//throw IllegalArgumentException("Invalid NewPipe json structure found");
|
||||||
|
|
||||||
StateBackup.importNewPipeSubs(this, newPipeSubsParsed);
|
StateBackup.importNewPipeSubs(this, newPipeSubsParsed);
|
||||||
}
|
} catch (ex: Exception) {
|
||||||
catch(ex: Exception) {
|
|
||||||
Logger.e(TAG, ex.message, ex);
|
Logger.e(TAG, ex.message, ex);
|
||||||
UIDialogs.showGeneralErrorDialog(context, getString(R.string.failed_to_parse_newpipe_subscriptions), ex);
|
UIDialogs.showGeneralErrorDialog(context, getString(R.string.failed_to_parse_newpipe_subscriptions), ex);
|
||||||
}
|
}
|
||||||
@ -1005,7 +1075,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fragCurrent.onHide();
|
fragCurrent.onHide();
|
||||||
|
|
||||||
if (segment.isMainView) {
|
if (segment.isMainView) {
|
||||||
@ -1017,8 +1086,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
.replace(R.id.fragment_top_bar, segment.topBar as Fragment);
|
.replace(R.id.fragment_top_bar, segment.topBar as Fragment);
|
||||||
fragCurrent.topBar?.onHide();
|
fragCurrent.topBar?.onHide();
|
||||||
}
|
}
|
||||||
}
|
} else if (fragCurrent.topBar != null)
|
||||||
else if(fragCurrent.topBar != null)
|
|
||||||
transaction.hide(fragCurrent.topBar as Fragment);
|
transaction.hide(fragCurrent.topBar as Fragment);
|
||||||
|
|
||||||
transaction = transaction.replace(R.id.fragment_main, segment);
|
transaction = transaction.replace(R.id.fragment_main, segment);
|
||||||
@ -1026,8 +1094,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
if (segment.hasBottomBar) {
|
if (segment.hasBottomBar) {
|
||||||
if (!fragCurrent.hasBottomBar)
|
if (!fragCurrent.hasBottomBar)
|
||||||
transaction = transaction.show(_fragBotBarMenu);
|
transaction = transaction.show(_fragBotBarMenu);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
if (fragCurrent.hasBottomBar)
|
if (fragCurrent.hasBottomBar)
|
||||||
transaction = transaction.hide(_fragBotBarMenu);
|
transaction = transaction.hide(_fragBotBarMenu);
|
||||||
}
|
}
|
||||||
@ -1130,12 +1197,18 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
if (fragCurrent.hasBottomBar)
|
if (fragCurrent.hasBottomBar)
|
||||||
paddingBottom += HEIGHT_MENU_DP;
|
paddingBottom += HEIGHT_MENU_DP;
|
||||||
|
|
||||||
_fragContainerOverlay.setPadding(0,0,0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, paddingBottom - HEIGHT_MENU_DP, resources.displayMetrics).toInt());
|
_fragContainerOverlay.setPadding(
|
||||||
|
0, 0, 0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, paddingBottom - HEIGHT_MENU_DP, resources.displayMetrics)
|
||||||
|
.toInt()
|
||||||
|
);
|
||||||
|
|
||||||
if (_fragVideoDetail.state == VideoDetailFragment.State.MINIMIZED)
|
if (_fragVideoDetail.state == VideoDetailFragment.State.MINIMIZED)
|
||||||
paddingBottom += HEIGHT_VIDEO_MINIMIZED_DP;
|
paddingBottom += HEIGHT_VIDEO_MINIMIZED_DP;
|
||||||
|
|
||||||
_fragContainerMain.setPadding(0,0,0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, paddingBottom, resources.displayMetrics).toInt());
|
_fragContainerMain.setPadding(
|
||||||
|
0, 0, 0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, paddingBottom, resources.displayMetrics)
|
||||||
|
.toInt()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -1151,14 +1224,18 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
ContextCompat.checkSelfPermission(this, notifPermission) == PackageManager.PERMISSION_GRANTED -> {
|
ContextCompat.checkSelfPermission(this, notifPermission) == PackageManager.PERMISSION_GRANTED -> {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ActivityCompat.shouldShowRequestPermissionRationale(this, notifPermission) -> {
|
ActivityCompat.shouldShowRequestPermissionRationale(this, notifPermission) -> {
|
||||||
UIDialogs.showDialog(this, R.drawable.ic_notifications, "Notifications Required",
|
UIDialogs.showDialog(
|
||||||
|
this, R.drawable.ic_notifications, "Notifications Required",
|
||||||
reason, null, 0,
|
reason, null, 0,
|
||||||
UIDialogs.Action("Cancel", {}),
|
UIDialogs.Action("Cancel", {}),
|
||||||
UIDialogs.Action("Enable", {
|
UIDialogs.Action("Enable", {
|
||||||
requestPermissionLauncher.launch(notifPermission);
|
requestPermissionLauncher.launch(notifPermission);
|
||||||
}, UIDialogs.ActionStyle.PRIMARY));
|
}, UIDialogs.ActionStyle.PRIMARY)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
requestPermissionLauncher.launch(notifPermission);
|
requestPermissionLauncher.launch(notifPermission);
|
||||||
}
|
}
|
||||||
@ -1176,6 +1253,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun launchAppToastJob() {
|
private suspend fun launchAppToastJob() {
|
||||||
Logger.i(TAG, "Starting appToast loop");
|
Logger.i(TAG, "Starting appToast loop");
|
||||||
while (!_toastQueue.isEmpty()) {
|
while (!_toastQueue.isEmpty()) {
|
||||||
@ -1208,14 +1286,15 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
private var resultLauncherMap = mutableMapOf<Int, (ActivityResult) -> Unit>();
|
private var resultLauncherMap = mutableMapOf<Int, (ActivityResult) -> Unit>();
|
||||||
private var requestCode: Int? = -1;
|
private var requestCode: Int? = -1;
|
||||||
private val resultLauncher: ActivityResultLauncher<Intent> = registerForActivityResult(
|
private val resultLauncher: ActivityResultLauncher<Intent> = registerForActivityResult(
|
||||||
ActivityResultContracts.StartActivityForResult()) {
|
ActivityResultContracts.StartActivityForResult()
|
||||||
result: ActivityResult ->
|
) { result: ActivityResult ->
|
||||||
val handler = synchronized(resultLauncherMap) {
|
val handler = synchronized(resultLauncherMap) {
|
||||||
resultLauncherMap.remove(requestCode);
|
resultLauncherMap.remove(requestCode);
|
||||||
}
|
}
|
||||||
if (handler != null)
|
if (handler != null)
|
||||||
handler(result);
|
handler(result);
|
||||||
};
|
};
|
||||||
|
|
||||||
override fun launchForResult(intent: Intent, code: Int, handler: (ActivityResult) -> Unit) {
|
override fun launchForResult(intent: Intent, code: Int, handler: (ActivityResult) -> Unit) {
|
||||||
synchronized(resultLauncherMap) {
|
synchronized(resultLauncherMap) {
|
||||||
resultLauncherMap[code] = handler;
|
resultLauncherMap[code] = handler;
|
||||||
@ -1234,6 +1313,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||||
return sourcesIntent;
|
return sourcesIntent;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getVideoIntent(context: Context, videoUrl: String): Intent {
|
fun getVideoIntent(context: Context, videoUrl: String): Intent {
|
||||||
val sourcesIntent = Intent(context, MainActivity::class.java);
|
val sourcesIntent = Intent(context, MainActivity::class.java);
|
||||||
sourcesIntent.action = "VIDEO";
|
sourcesIntent.action = "VIDEO";
|
||||||
@ -1241,6 +1321,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
|||||||
sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
sourcesIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||||
return sourcesIntent;
|
return sourcesIntent;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getActionIntent(context: Context, action: String): Intent {
|
fun getActionIntent(context: Context, action: String): Intent {
|
||||||
val sourcesIntent = Intent(context, MainActivity::class.java);
|
val sourcesIntent = Intent(context, MainActivity::class.java);
|
||||||
sourcesIntent.action = "ACTION";
|
sourcesIntent.action = "ACTION";
|
||||||
|
@ -88,7 +88,8 @@ class DashBuilder : XMLBuilder {
|
|||||||
fun withRepresentationOnDemand(id: String, subtitleSource: ISubtitleSource, subtitleUrl: String) {
|
fun withRepresentationOnDemand(id: String, subtitleSource: ISubtitleSource, subtitleUrl: String) {
|
||||||
withRepresentation(id, mapOf(
|
withRepresentation(id, mapOf(
|
||||||
Pair("mimeType", subtitleSource.format ?: "text/vtt"),
|
Pair("mimeType", subtitleSource.format ?: "text/vtt"),
|
||||||
Pair("startWithSAP", "1"),
|
Pair("default", "true"),
|
||||||
|
Pair("lang", "en"),
|
||||||
Pair("bandwidth", "1000")
|
Pair("bandwidth", "1000")
|
||||||
)) {
|
)) {
|
||||||
it.withBaseURL(subtitleUrl)
|
it.withBaseURL(subtitleUrl)
|
||||||
@ -151,7 +152,7 @@ class DashBuilder : XMLBuilder {
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
//TODO: Verify if & really should be replaced like this?
|
//TODO: Verify if & really should be replaced like this?
|
||||||
it.withRepresentationOnDemand("1", subtitleSource, subtitleUrl.replace("&", "&"))
|
it.withRepresentationOnDemand("caption_en", subtitleSource, subtitleUrl.replace("&", "&"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//Video
|
//Video
|
||||||
@ -164,7 +165,7 @@ class DashBuilder : XMLBuilder {
|
|||||||
Pair("subsegmentStartsWithSAP", "1")
|
Pair("subsegmentStartsWithSAP", "1")
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
it.withRepresentationOnDemand("1", vidSource, vidUrl.replace("&", "&"));
|
it.withRepresentationOnDemand("2", vidSource, vidUrl.replace("&", "&"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1352,7 +1352,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
}
|
}
|
||||||
catch(ex: Throwable) {
|
catch(ex: Throwable) {
|
||||||
Logger.e(TAG, "Playback tracker failed", ex);
|
Logger.e(TAG, "Playback tracker failed", ex);
|
||||||
if(me.video?.isLive == true) withContext(Dispatchers.Main) {
|
if(me.video?.isLive == true || ex.message?.contains("Unable to resolve host") == true) withContext(Dispatchers.Main) {
|
||||||
UIDialogs.toast(context, context.getString(R.string.failed_to_get_playback_tracker));
|
UIDialogs.toast(context, context.getString(R.string.failed_to_get_playback_tracker));
|
||||||
};
|
};
|
||||||
else withContext(Dispatchers.Main) {
|
else withContext(Dispatchers.Main) {
|
||||||
@ -2823,6 +2823,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
Logger.w(ChannelFragment.TAG, "Failed to load video.", it);
|
Logger.w(ChannelFragment.TAG, "Failed to load video.", it);
|
||||||
|
|
||||||
|
if(!(it.message?.contains("Unable to resolve host") ?: false && nextVideo())){
|
||||||
handleErrorOrCall {
|
handleErrorOrCall {
|
||||||
_retryCount = 0;
|
_retryCount = 0;
|
||||||
_retryJob?.cancel();
|
_retryJob?.cancel();
|
||||||
@ -2831,6 +2832,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
_liveTryJob = null;
|
_liveTryJob = null;
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video), it, ::fetchVideo, null, fragment);
|
UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_video), it, ::fetchVideo, null, fragment);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else TaskHandler(IPlatformVideoDetails::class.java, {fragment.lifecycleScope});
|
} else TaskHandler(IPlatformVideoDetails::class.java, {fragment.lifecycleScope});
|
||||||
|
|
||||||
private val _taskLoadRecommendations = TaskHandler<String?, IPager<IPlatformContent>?>(StateApp.instance.scopeGetter, {
|
private val _taskLoadRecommendations = TaskHandler<String?, IPager<IPlatformContent>?>(StateApp.instance.scopeGetter, {
|
||||||
|
@ -26,6 +26,7 @@ import com.futo.platformplayer.stores.FragmentedStorage
|
|||||||
import com.futo.platformplayer.stores.SearchHistoryStorage
|
import com.futo.platformplayer.stores.SearchHistoryStorage
|
||||||
|
|
||||||
class SearchTopBarFragment : TopFragment() {
|
class SearchTopBarFragment : TopFragment() {
|
||||||
|
@Suppress("PrivatePropertyName")
|
||||||
private val TAG = "SearchTopBarFragment"
|
private val TAG = "SearchTopBarFragment"
|
||||||
|
|
||||||
private var _editSearch: EditText? = null;
|
private var _editSearch: EditText? = null;
|
||||||
@ -191,29 +192,32 @@ class SearchTopBarFragment : TopFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun onDone() {
|
private fun onDone() {
|
||||||
val editSearch = _editSearch;
|
val editSearch = _editSearch
|
||||||
if (editSearch != null) {
|
if (editSearch != null) {
|
||||||
val text = editSearch.text.toString();
|
val text = editSearch.text.toString()
|
||||||
if (text.length < 3) {
|
if (text.isEmpty()) {
|
||||||
UIDialogs.toast(getString(R.string.please_use_at_least_3_characters));
|
UIDialogs.toast(getString(R.string.please_use_at_least_1_character))
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
editSearch.clearFocus();
|
editSearch.clearFocus()
|
||||||
_inputMethodManager?.hideSoftInputFromWindow(editSearch.windowToken, 0);
|
_inputMethodManager?.hideSoftInputFromWindow(editSearch.windowToken, 0)
|
||||||
|
|
||||||
if (Settings.instance.search.searchHistory) {
|
if (Settings.instance.search.searchHistory) {
|
||||||
val storage = FragmentedStorage.get<SearchHistoryStorage>();
|
val storage = FragmentedStorage.get<SearchHistoryStorage>()
|
||||||
storage.add(text);
|
storage.add(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_searchType == SearchType.CREATOR) {
|
if (_searchType == SearchType.CREATOR) {
|
||||||
onSearch.emit(text);
|
onSearch.emit(text)
|
||||||
} else {
|
} else {
|
||||||
onSearch.emit(text);
|
onSearch.emit(text)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Logger.w(TAG, "Unexpected condition happened where done is edit search is null but done is triggered.");
|
Logger.w(
|
||||||
|
TAG,
|
||||||
|
"Unexpected condition happened where done is edit search is null but done is triggered."
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -182,13 +182,14 @@ class HLS {
|
|||||||
|
|
||||||
private fun parseAttributes(content: String): Map<String, String> {
|
private fun parseAttributes(content: String): Map<String, String> {
|
||||||
val attributes = mutableMapOf<String, String>()
|
val attributes = mutableMapOf<String, String>()
|
||||||
val attributePairs = content.substringAfter(":").splitToSequence(',')
|
val maybeAttributePairs = content.substringAfter(":").splitToSequence(',')
|
||||||
|
|
||||||
var currentPair = StringBuilder()
|
var currentPair = StringBuilder()
|
||||||
for (pair in attributePairs) {
|
for (pair in maybeAttributePairs) {
|
||||||
currentPair.append(pair)
|
currentPair.append(pair)
|
||||||
if (currentPair.count { it == '\"' } % 2 == 0) { // Check if the number of quotes is even
|
if (currentPair.count { it == '\"' } % 2 == 0) { // Check if the number of quotes is even
|
||||||
val (key, value) = currentPair.toString().split('=')
|
val key = currentPair.toString().substringBefore("=")
|
||||||
|
val value = currentPair.toString().substringAfter("=")
|
||||||
attributes[key.trim()] = value.trim().removeSurrounding("\"")
|
attributes[key.trim()] = value.trim().removeSurrounding("\"")
|
||||||
currentPair = StringBuilder() // Reset for the next attribute
|
currentPair = StringBuilder() // Reset for the next attribute
|
||||||
} else {
|
} else {
|
||||||
|
@ -537,7 +537,7 @@ class StatePlatform {
|
|||||||
else getSortedEnabledClient().filter { if (it is JSClient) it.enableInSearch else true };
|
else getSortedEnabledClient().filter { if (it is JSClient) it.enableInSearch else true };
|
||||||
|
|
||||||
clients.parallelStream().forEach {
|
clients.parallelStream().forEach {
|
||||||
val searchCapabilities = it.getSearchCapabilities();
|
val searchCapabilities = it.getSearchChannelContentsCapabilities();
|
||||||
val mappedFilters = filters.map { pair -> Pair(pair.key, pair.value.map { v -> searchCapabilities.filters.first { g -> g.idOrName == pair.key }.filters.first { f -> f.idOrName == v }.value }) }.toMap();
|
val mappedFilters = filters.map { pair -> Pair(pair.key, pair.value.map { v -> searchCapabilities.filters.first { g -> g.idOrName == pair.key }.filters.first { f -> f.idOrName == v }.value }) }.toMap();
|
||||||
|
|
||||||
if (it.isChannelUrl(channelUrl)) {
|
if (it.isChannelUrl(channelUrl)) {
|
||||||
|
@ -284,12 +284,16 @@ class StateSync {
|
|||||||
return@SyncSocketSession
|
return@SyncSocketSession
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.i(TAG, "Handshake complete with ${s.remotePublicKey}")
|
Logger.i(TAG, "Handshake complete with (LocalPublicKey = ${s.localPublicKey}, RemotePublicKey = ${s.remotePublicKey})")
|
||||||
|
|
||||||
synchronized(_sessions) {
|
synchronized(_sessions) {
|
||||||
session = _sessions[s.remotePublicKey]
|
session = _sessions[s.remotePublicKey]
|
||||||
if (session == null) {
|
if (session == null) {
|
||||||
session = SyncSession(remotePublicKey, onAuthorized = {
|
session = SyncSession(remotePublicKey, onAuthorized = { it, isNewlyAuthorized, isNewSession ->
|
||||||
|
if (!isNewSession) {
|
||||||
|
return@SyncSession
|
||||||
|
}
|
||||||
|
|
||||||
Logger.i(TAG, "${s.remotePublicKey} authorized")
|
Logger.i(TAG, "${s.remotePublicKey} authorized")
|
||||||
synchronized(_lastAddressStorage) {
|
synchronized(_lastAddressStorage) {
|
||||||
_lastAddressStorage.setAndSave(remotePublicKey, s.remoteAddress)
|
_lastAddressStorage.setAndSave(remotePublicKey, s.remoteAddress)
|
||||||
@ -358,6 +362,16 @@ class StateSync {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
val publicKey = session!!.remotePublicKey
|
||||||
|
session!!.unauthorize(s)
|
||||||
|
session!!.close()
|
||||||
|
|
||||||
|
synchronized(_sessions) {
|
||||||
|
_sessions.remove(publicKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.i(TAG, "Connection unauthorized for ${remotePublicKey} because not authorized and not on pairing activity to ask")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
//Responder does not need to check because already approved
|
//Responder does not need to check because already approved
|
||||||
|
@ -23,6 +23,7 @@ import com.futo.platformplayer.sync.models.SyncPlaylistsPackage
|
|||||||
import com.futo.platformplayer.sync.models.SyncSubscriptionGroupsPackage
|
import com.futo.platformplayer.sync.models.SyncSubscriptionGroupsPackage
|
||||||
import com.futo.platformplayer.sync.models.SyncSubscriptionsPackage
|
import com.futo.platformplayer.sync.models.SyncSubscriptionsPackage
|
||||||
import com.futo.platformplayer.sync.models.SyncWatchLaterPackage
|
import com.futo.platformplayer.sync.models.SyncWatchLaterPackage
|
||||||
|
import com.futo.platformplayer.toUtf8String
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
@ -32,6 +33,7 @@ import java.nio.ByteBuffer
|
|||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.time.ZoneOffset
|
import java.time.ZoneOffset
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
interface IAuthorizable {
|
interface IAuthorizable {
|
||||||
val isAuthorized: Boolean
|
val isAuthorized: Boolean
|
||||||
@ -41,13 +43,16 @@ class SyncSession : IAuthorizable {
|
|||||||
private val _socketSessions: MutableList<SyncSocketSession> = mutableListOf()
|
private val _socketSessions: MutableList<SyncSocketSession> = mutableListOf()
|
||||||
private var _authorized: Boolean = false
|
private var _authorized: Boolean = false
|
||||||
private var _remoteAuthorized: Boolean = false
|
private var _remoteAuthorized: Boolean = false
|
||||||
private val _onAuthorized: (session: SyncSession) -> Unit
|
private val _onAuthorized: (session: SyncSession, isNewlyAuthorized: Boolean, isNewSession: Boolean) -> Unit
|
||||||
private val _onUnauthorized: (session: SyncSession) -> Unit
|
private val _onUnauthorized: (session: SyncSession) -> Unit
|
||||||
private val _onClose: (session: SyncSession) -> Unit
|
private val _onClose: (session: SyncSession) -> Unit
|
||||||
private val _onConnectedChanged: (session: SyncSession, connected: Boolean) -> Unit
|
private val _onConnectedChanged: (session: SyncSession, connected: Boolean) -> Unit
|
||||||
val remotePublicKey: String
|
val remotePublicKey: String
|
||||||
override val isAuthorized get() = _authorized && _remoteAuthorized
|
override val isAuthorized get() = _authorized && _remoteAuthorized
|
||||||
private var _wasAuthorized = false
|
private var _wasAuthorized = false
|
||||||
|
private val _id = UUID.randomUUID()
|
||||||
|
private var _remoteId: UUID? = null
|
||||||
|
private var _lastAuthorizedRemoteId: UUID? = null
|
||||||
|
|
||||||
var connected: Boolean = false
|
var connected: Boolean = false
|
||||||
private set(v) {
|
private set(v) {
|
||||||
@ -57,7 +62,7 @@ class SyncSession : IAuthorizable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(remotePublicKey: String, onAuthorized: (session: SyncSession) -> Unit, onUnauthorized: (session: SyncSession) -> Unit, onConnectedChanged: (session: SyncSession, connected: Boolean) -> Unit, onClose: (session: SyncSession) -> Unit) {
|
constructor(remotePublicKey: String, onAuthorized: (session: SyncSession, isNewlyAuthorized: Boolean, isNewSession: Boolean) -> Unit, onUnauthorized: (session: SyncSession) -> Unit, onConnectedChanged: (session: SyncSession, connected: Boolean) -> Unit, onClose: (session: SyncSession) -> Unit) {
|
||||||
this.remotePublicKey = remotePublicKey
|
this.remotePublicKey = remotePublicKey
|
||||||
_onAuthorized = onAuthorized
|
_onAuthorized = onAuthorized
|
||||||
_onUnauthorized = onUnauthorized
|
_onUnauthorized = onUnauthorized
|
||||||
@ -79,7 +84,8 @@ class SyncSession : IAuthorizable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun authorize(socketSession: SyncSocketSession) {
|
fun authorize(socketSession: SyncSocketSession) {
|
||||||
socketSession.send(Opcode.NOTIFY_AUTHORIZED.value)
|
Logger.i(TAG, "Sent AUTHORIZED with session id $_id")
|
||||||
|
socketSession.send(Opcode.NOTIFY_AUTHORIZED.value, 0u, ByteBuffer.wrap(_id.toString().toByteArray()))
|
||||||
_authorized = true
|
_authorized = true
|
||||||
checkAuthorized()
|
checkAuthorized()
|
||||||
}
|
}
|
||||||
@ -97,9 +103,13 @@ class SyncSession : IAuthorizable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun checkAuthorized() {
|
private fun checkAuthorized() {
|
||||||
if (!_wasAuthorized && isAuthorized) {
|
if (isAuthorized) {
|
||||||
|
val isNewlyAuthorized = !_wasAuthorized;
|
||||||
|
val isNewSession = _lastAuthorizedRemoteId != _remoteId;
|
||||||
|
Logger.i(TAG, "onAuthorized (isNewlyAuthorized = $isNewlyAuthorized, isNewSession = $isNewSession)");
|
||||||
|
_onAuthorized.invoke(this, !_wasAuthorized, _lastAuthorizedRemoteId != _remoteId)
|
||||||
_wasAuthorized = true
|
_wasAuthorized = true
|
||||||
_onAuthorized.invoke(this)
|
_lastAuthorizedRemoteId = _remoteId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -128,12 +138,19 @@ class SyncSession : IAuthorizable {
|
|||||||
|
|
||||||
when (opcode) {
|
when (opcode) {
|
||||||
Opcode.NOTIFY_AUTHORIZED.value -> {
|
Opcode.NOTIFY_AUTHORIZED.value -> {
|
||||||
|
val str = data.toUtf8String()
|
||||||
|
_remoteId = if (data.remaining() >= 0) UUID.fromString(str) else UUID.fromString("00000000-0000-0000-0000-000000000000")
|
||||||
_remoteAuthorized = true
|
_remoteAuthorized = true
|
||||||
|
Logger.i(TAG, "Received AUTHORIZED with session id $_remoteId")
|
||||||
checkAuthorized()
|
checkAuthorized()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
Opcode.NOTIFY_UNAUTHORIZED.value -> {
|
Opcode.NOTIFY_UNAUTHORIZED.value -> {
|
||||||
|
_remoteId = null
|
||||||
|
_lastAuthorizedRemoteId = null
|
||||||
_remoteAuthorized = false
|
_remoteAuthorized = false
|
||||||
_onUnauthorized(this)
|
_onUnauthorized(this)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
//TODO: Handle any kind of packet (that is not necessarily authorized)
|
//TODO: Handle any kind of packet (that is not necessarily authorized)
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ import com.futo.platformplayer.noise.protocol.HandshakeState
|
|||||||
import com.futo.platformplayer.states.StateSync
|
import com.futo.platformplayer.states.StateSync
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.nio.ByteOrder
|
import java.nio.ByteOrder
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
class SyncSocketSession {
|
class SyncSocketSession {
|
||||||
enum class Opcode(val value: UByte) {
|
enum class Opcode(val value: UByte) {
|
||||||
|
@ -726,7 +726,7 @@
|
|||||||
<string name="not_yet_available_retrying_in_time_s">Not yet available, retrying in {time}s</string>
|
<string name="not_yet_available_retrying_in_time_s">Not yet available, retrying in {time}s</string>
|
||||||
<string name="failed_to_retry_for_live_stream">Failed to retry for live stream</string>
|
<string name="failed_to_retry_for_live_stream">Failed to retry for live stream</string>
|
||||||
<string name="this_app_is_in_development_please_submit_bug_reports_and_understand_that_many_features_are_incomplete">This app is in development. Please submit bug reports and understand that many features are incomplete.</string>
|
<string name="this_app_is_in_development_please_submit_bug_reports_and_understand_that_many_features_are_incomplete">This app is in development. Please submit bug reports and understand that many features are incomplete.</string>
|
||||||
<string name="please_use_at_least_3_characters">Please use at least 3 characters</string>
|
<string name="please_use_at_least_1_character">Please use at least 1 character</string>
|
||||||
<string name="are_you_sure_you_want_to_delete_this_video">Are you sure you want to delete this video?</string>
|
<string name="are_you_sure_you_want_to_delete_this_video">Are you sure you want to delete this video?</string>
|
||||||
<string name="tap_to_open">Tap to open</string>
|
<string name="tap_to_open">Tap to open</string>
|
||||||
<string name="update_available_exclamation">Update available!</string>
|
<string name="update_available_exclamation">Update available!</string>
|
||||||
@ -810,6 +810,8 @@
|
|||||||
<string name="scroll_to_top">Scroll to top</string>
|
<string name="scroll_to_top">Scroll to top</string>
|
||||||
<string name="disable_battery_optimization">Disable Battery Optimization</string>
|
<string name="disable_battery_optimization">Disable Battery Optimization</string>
|
||||||
<string name="click_to_go_to_battery_optimization_settings_disabling_battery_optimization_will_prevent_the_os_from_killing_media_sessions">Click to go to battery optimization settings. Disabling battery optimization will prevent the OS from killing media sessions.</string>
|
<string name="click_to_go_to_battery_optimization_settings_disabling_battery_optimization_will_prevent_the_os_from_killing_media_sessions">Click to go to battery optimization settings. Disabling battery optimization will prevent the OS from killing media sessions.</string>
|
||||||
|
<string name="contribute_personal_subscriptions_list">Contribute Personal Subscriptions List</string>
|
||||||
|
<string name="contribute_personal_subscriptions_list_description">\nWould you liked to contribute your current creator subscriptions list to FUTO?\n\nThe data will be handled according to the Grayjay privacy policy. That is the list will be anonymized and stored without any reference to whomever the list of creators belonged to.\n\nThe intention is for Grayjay and FUTO to use these data to build a cross platform creator recommendation system to make it easier to find new creators you might like from within Grayjay.</string>
|
||||||
<string name="cd_cast_button">Cast button</string>
|
<string name="cd_cast_button">Cast button</string>
|
||||||
<string name="cd_incognito_button">Incognito button</string>
|
<string name="cd_incognito_button">Incognito button</string>
|
||||||
<string name="cd_creator_thumbnail">Creator thumbnail</string>
|
<string name="cd_creator_thumbnail">Creator thumbnail</string>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user