Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay

This commit is contained in:
Koen 2024-01-04 13:38:37 +01:00
commit f4cb1719e0
15 changed files with 254 additions and 102 deletions

View File

@ -37,6 +37,7 @@ import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateBackup import com.futo.platformplayer.states.StateBackup
import com.futo.platformplayer.stores.v2.ManagedStore import com.futo.platformplayer.stores.v2.ManagedStore
import com.futo.platformplayer.views.ToastView
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -398,13 +399,28 @@ class UIDialogs {
StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) { StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
try { try {
StateApp.withContext { StateApp.withContext {
Toast.makeText(it, text, if (long) Toast.LENGTH_LONG else Toast.LENGTH_SHORT).show(); toast(it, text, long);
} }
} catch (e: Throwable) { } catch (e: Throwable) {
Logger.e(TAG, "Failed to show toast.", e); Logger.e(TAG, "Failed to show toast.", e);
} }
} }
} }
fun appToast(text: String, long: Boolean = false) {
appToast(ToastView.Toast(text, long))
}
fun appToastError(text: String, long: Boolean) {
StateApp.withContext {
appToast(ToastView.Toast(text, long, it.getColor(R.color.pastel_red)));
};
}
fun appToast(toast: ToastView.Toast) {
StateApp.withContext {
if(it is MainActivity) {
it.showAppToast(toast);
}
}
}
fun showClickableToast(context: Context, text: String, onClick: () -> Unit, isLongDuration: Boolean = false) { fun showClickableToast(context: Context, text: String, onClick: () -> Unit, isLongDuration: Boolean = false) {
//TODO: Is not actually clickable... //TODO: Is not actually clickable...

View File

@ -343,7 +343,7 @@ class UISlideOverlays {
videoSources.filter { it is IVideoUrlSource && it.isDownloadable() }.asIterable(), videoSources.filter { it is IVideoUrlSource && it.isDownloadable() }.asIterable(),
Settings.instance.downloads.getDefaultVideoQualityPixels(), Settings.instance.downloads.getDefaultVideoQualityPixels(),
FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS
) as IVideoUrlSource; ) as IVideoUrlSource?;
} }
if (audioSources != null) { if (audioSources != null) {

View File

@ -1,7 +1,6 @@
package com.futo.platformplayer.activities package com.futo.platformplayer.activities
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.NotificationManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
@ -24,6 +23,7 @@ import androidx.core.content.ContextCompat
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.WindowInsetsControllerCompat
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentContainerView import androidx.fragment.app.FragmentContainerView
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
@ -45,6 +45,7 @@ import com.futo.platformplayer.states.*
import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.FragmentedStorage
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.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.* import kotlinx.coroutines.*
@ -54,6 +55,7 @@ import java.io.PrintWriter
import java.io.StringWriter import java.io.StringWriter
import java.lang.reflect.InvocationTargetException import java.lang.reflect.InvocationTargetException
import java.util.* import java.util.*
import java.util.concurrent.ConcurrentLinkedQueue
class MainActivity : AppCompatActivity, IWithResultLauncher { class MainActivity : AppCompatActivity, IWithResultLauncher {
@ -65,6 +67,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
lateinit var rootView : MotionLayout; lateinit var rootView : MotionLayout;
private lateinit var _overlayContainer: FrameLayout; private lateinit var _overlayContainer: FrameLayout;
private lateinit var _toastView: ToastView;
//Segment Containers //Segment Containers
private lateinit var _fragContainerTopBar: FragmentContainerView; private lateinit var _fragContainerTopBar: FragmentContainerView;
@ -207,7 +210,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_fragContainerVideoDetail = findViewById(R.id.fragment_overlay); _fragContainerVideoDetail = findViewById(R.id.fragment_overlay);
_fragContainerOverlay = findViewById(R.id.fragment_overlay_container); _fragContainerOverlay = findViewById(R.id.fragment_overlay_container);
_overlayContainer = findViewById(R.id.overlay_container); _overlayContainer = findViewById(R.id.overlay_container);
//_overlayContainer.visibility = View.GONE; _toastView = findViewById(R.id.toast_view);
//Initialize fragments //Initialize fragments
@ -478,21 +481,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
} }
_isVisible = true; _isVisible = true;
val videoToOpen = StateSaved.instance.videoToOpen;
if (_wasStopped) {
_wasStopped = false;
if (videoToOpen != null && _fragVideoDetail.state == VideoDetailFragment.State.CLOSED) {
Logger.i(TAG, "onResume videoToOpen=$videoToOpen");
if (StatePlatform.instance.hasEnabledVideoClient(videoToOpen.url)) {
navigate(_fragVideoDetail, UrlVideoWithTime(videoToOpen.url, videoToOpen.timeSeconds, false));
_fragVideoDetail.maximizeVideoDetail(true);
}
StateSaved.instance.setVideoToOpenNonBlocking(null);
}
}
} }
override fun onPause() { override fun onPause() {
@ -864,7 +852,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
_orientationManager.disable(); _orientationManager.disable();
StateApp.instance.mainAppDestroyed(this); StateApp.instance.mainAppDestroyed(this);
StateSaved.instance.setVideoToOpenBlocking(null);
} }
inline fun <reified T> isFragmentActive(): Boolean { inline fun <reified T> isFragmentActive(): Boolean {
@ -1052,6 +1039,43 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
} }
} }
private val _toastQueue = ConcurrentLinkedQueue<ToastView.Toast>();
private var _toastJob: Job? = null;
fun showAppToast(toast: ToastView.Toast) {
synchronized(_toastQueue) {
_toastQueue.add(toast);
if(_toastJob?.isActive != true)
_toastJob = lifecycleScope.launch(Dispatchers.Default) {
launchAppToastJob();
};
}
}
private suspend fun launchAppToastJob() {
Logger.i(TAG, "Starting appToast loop");
while(!_toastQueue.isEmpty()) {
val toast = _toastQueue.poll() ?: continue;
Logger.i(TAG, "Showing next toast (${toast.msg})");
lifecycleScope.launch(Dispatchers.Main) {
if (!_toastView.isVisible) {
Logger.i(TAG, "First showing toast");
_toastView.setToast(toast);
_toastView.show(true);
} else {
_toastView.setToastAnimated(toast);
}
}
if(toast.long)
delay(5000);
else
delay(3000);
}
Logger.i(TAG, "Ending appToast loop");
lifecycleScope.launch(Dispatchers.Main) {
_toastView.hide(true) {
};
}
}
//TODO: Only calls last handler due to missing request codes on ActivityResultLaunchers. //TODO: Only calls last handler due to missing request codes on ActivityResultLaunchers.

View File

@ -10,6 +10,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.futo.platformplayer.* import com.futo.platformplayer.*
import com.futo.platformplayer.activities.MainActivity import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.api.media.IPlatformClient
import com.futo.platformplayer.api.media.models.contents.ContentType 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.contents.IPlatformContent
import com.futo.platformplayer.api.media.models.video.IPlatformVideo import com.futo.platformplayer.api.media.models.video.IPlatformVideo
@ -427,7 +428,7 @@ class SubscriptionsFeedFragment : MainFragment() {
context?.let { context?.let {
fragment.lifecycleScope.launch(Dispatchers.Main) { fragment.lifecycleScope.launch(Dispatchers.Main) {
try { try {
if (exs.size <= 8) { if (exs.size <= 3) {
for (ex in exs) { for (ex in exs) {
var toShow = ex; var toShow = ex;
var channel: String? = null; var channel: String? = null;
@ -437,12 +438,11 @@ class SubscriptionsFeedFragment : MainFragment() {
} }
Logger.e(TAG, "Channel [${channel}] failed", ex); Logger.e(TAG, "Channel [${channel}] failed", ex);
if (toShow is PluginException) if (toShow is PluginException)
UIDialogs.toast( UIDialogs.appToast(
it,
context.getString(R.string.plugin_pluginname_failed_message).replace("{pluginName}", toShow.config.name).replace("{message}", toShow.message ?: "") context.getString(R.string.plugin_pluginname_failed_message).replace("{pluginName}", toShow.config.name).replace("{message}", toShow.message ?: "")
); );
else else
UIDialogs.toast(it, ex.message ?: ""); UIDialogs.appToast(ex.message ?: "");
} }
} }
else { else {
@ -453,7 +453,7 @@ class SubscriptionsFeedFragment : MainFragment() {
.map { it!! } .map { it!! }
.toList(); .toList();
for(distinctPluginFail in failedPlugins) for(distinctPluginFail in failedPlugins)
UIDialogs.toast(it, context.getString(R.string.plugin_pluginname_failed_message).replace("{pluginName}", distinctPluginFail.config.name).replace("{message}", distinctPluginFail.message ?: "")); UIDialogs.appToast(context.getString(R.string.plugin_pluginname_failed_message).replace("{pluginName}", distinctPluginFail.config.name).replace("{message}", distinctPluginFail.message ?: ""));
} }
} catch (e: Throwable) { } catch (e: Throwable) {
Logger.e(TAG, "Failed to handle exceptions", e) Logger.e(TAG, "Failed to handle exceptions", e)

View File

@ -25,8 +25,6 @@ import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.models.PlatformVideoWithTime import com.futo.platformplayer.models.PlatformVideoWithTime
import com.futo.platformplayer.models.UrlVideoWithTime import com.futo.platformplayer.models.UrlVideoWithTime
import com.futo.platformplayer.states.StatePlayer import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.states.StateSaved
import com.futo.platformplayer.states.VideoToOpen
import com.futo.platformplayer.views.containers.SingleViewTouchableMotionLayout import com.futo.platformplayer.views.containers.SingleViewTouchableMotionLayout
class VideoDetailFragment : MainFragment { class VideoDetailFragment : MainFragment {
@ -372,11 +370,6 @@ class VideoDetailFragment : MainFragment {
Logger.v(TAG, "shouldStop: $shouldStop"); Logger.v(TAG, "shouldStop: $shouldStop");
if(shouldStop) { if(shouldStop) {
_viewDetail?.let {
val v = it.video ?: return@let;
StateSaved.instance.setVideoToOpenBlocking(VideoToOpen(v.url, (it.lastPositionMilliseconds / 1000.0f).toLong()));
}
_viewDetail?.onStop(); _viewDetail?.onStop();
StateCasting.instance.onStop(); StateCasting.instance.onStop();
Logger.v(TAG, "called onStop() shouldStop: $shouldStop"); Logger.v(TAG, "called onStop() shouldStop: $shouldStop");

View File

@ -149,6 +149,8 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
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.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import userpackage.Protocol import userpackage.Protocol
import java.time.OffsetDateTime import java.time.OffsetDateTime
@ -853,14 +855,19 @@ class VideoDetailView : ConstraintLayout {
} }
} }
} }
private val _historyIndexLock = Mutex(false);
suspend fun getHistoryIndex(video: IPlatformVideo): DBHistory.Index = withContext(Dispatchers.IO){ suspend fun getHistoryIndex(video: IPlatformVideo): DBHistory.Index = withContext(Dispatchers.IO){
val current = _historyIndex; _historyIndexLock.withLock {
if(current == null || current.url != video.url) { val current = _historyIndex;
val index = StateHistory.instance.getHistoryByVideo(video, true)!!; if(current == null || current.url != video.url) {
_historyIndex = index; val index = StateHistory.instance.getHistoryByVideo(video, true)!!;
return@withContext index; _historyIndex = index;
return@withContext index;
}
return@withContext current;
} }
return@withContext current;
} }
@ -1121,7 +1128,7 @@ class VideoDetailView : ConstraintLayout {
switchContentView(_container_content_main); switchContentView(_container_content_main);
} }
@OptIn(ExperimentalCoroutinesApi::class) //@OptIn(ExperimentalCoroutinesApi::class)
fun setVideoDetails(videoDetail: IPlatformVideoDetails, newVideo: Boolean = false) { fun setVideoDetails(videoDetail: IPlatformVideoDetails, newVideo: Boolean = false) {
Logger.i(TAG, "setVideoDetails (${videoDetail.name})") Logger.i(TAG, "setVideoDetails (${videoDetail.name})")

View File

@ -143,7 +143,7 @@ class MediaPlaybackService : Service() {
override fun onDestroy() { override fun onDestroy() {
Logger.v(TAG, "onDestroy"); Logger.v(TAG, "onDestroy");
_instance = null; _instance = null;
MediaControlReceiver.onCloseReceived.emit(); MediaControlReceiver.onPauseReceived.emit();
super.onDestroy(); super.onDestroy();
} }

View File

@ -5,6 +5,7 @@ import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.graphics.Color
import android.media.AudioManager import android.media.AudioManager
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.Network import android.net.Network
@ -38,6 +39,7 @@ import com.futo.platformplayer.receivers.AudioNoisyReceiver
import com.futo.platformplayer.services.DownloadService import com.futo.platformplayer.services.DownloadService
import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.v2.ManagedStore import com.futo.platformplayer.stores.v2.ManagedStore
import com.futo.platformplayer.views.ToastView
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.io.File import java.io.File
import java.time.OffsetDateTime import java.time.OffsetDateTime
@ -380,8 +382,6 @@ class StateApp {
Logger.i(TAG, "MainApp Starting: Initializing [Polycentric]"); Logger.i(TAG, "MainApp Starting: Initializing [Polycentric]");
StatePolycentric.instance.load(context); StatePolycentric.instance.load(context);
Logger.i(TAG, "MainApp Starting: Initializing [Saved]");
StateSaved.instance.load();
Logger.i(TAG, "MainApp Starting: Initializing [Connectivity]"); Logger.i(TAG, "MainApp Starting: Initializing [Connectivity]");
displayMetrics = context.resources.displayMetrics; displayMetrics = context.resources.displayMetrics;
@ -568,19 +568,36 @@ class StateApp {
StateAnnouncement.instance.deleteAnnouncement("plugin-update") StateAnnouncement.instance.deleteAnnouncement("plugin-update")
scopeOrNull?.launch(Dispatchers.IO) { scopeOrNull?.launch(Dispatchers.IO) {
val updateAvailableCount = StatePlatform.instance.checkForUpdates() val updateAvailable = StatePlatform.instance.checkForUpdates()
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
if (updateAvailableCount > 0) { if (updateAvailable.isNotEmpty()) {
UIDialogs.appToast(
ToastView.Toast(updateAvailable
.map { " - " + it.name }
.joinToString("\n"),
true,
null,
"Plugin updates available"
));
StateAnnouncement.instance.registerAnnouncement( StateAnnouncement.instance.registerAnnouncement(
"plugin-update", "plugin-update",
"Plugin updates available", "Plugin updates available",
"There are $updateAvailableCount plugin updates available.", "There are ${updateAvailable.size} plugin updates available.",
AnnouncementType.SESSION_RECURRING AnnouncementType.SESSION_RECURRING
) )
} }
} }
} }
/*
UIDialogs.appToast("This is a test", false);
UIDialogs.appToast("This is a test 2", false);
UIDialogs.appToastError("This is a test 3 (Error)", false);
UIDialogs.appToast(ToastView.Toast("This is a test 4, with title", false, Color.WHITE, "Test title"));
UIDialogs.appToast("This is a test 5 Long text\nWith enters\nasdh asfh fds h rwe h fxh sdfh sdf h dsfh sdf hasdfhsdhg ads as", true);
*/
} }
fun mainAppStartedWithExternalFiles(context: Context) { fun mainAppStartedWithExternalFiles(context: Context) {

View File

@ -941,8 +941,8 @@ class StatePlatform {
} }
} }
suspend fun checkForUpdates(): Int = withContext(Dispatchers.IO) { suspend fun checkForUpdates(): List<SourcePluginConfig> = withContext(Dispatchers.IO) {
var updateAvailableCount = 0 var configs = mutableListOf<SourcePluginConfig>()
val updatesAvailableFor = hashSetOf<String>() val updatesAvailableFor = hashSetOf<String>()
for (availableClient in getAvailableClients()) { for (availableClient in getAvailableClients()) {
if (availableClient !is JSClient) { if (availableClient !is JSClient) {
@ -950,13 +950,13 @@ class StatePlatform {
} }
if (checkForUpdates(availableClient.config)) { if (checkForUpdates(availableClient.config)) {
updateAvailableCount++ configs.add(availableClient.config);
updatesAvailableFor.add(availableClient.config.id) updatesAvailableFor.add(availableClient.config.id)
} }
} }
_updatesAvailableMap = updatesAvailableFor _updatesAvailableMap = updatesAvailableFor
return@withContext updateAvailableCount return@withContext configs;
} }
fun clearUpdateAvailable(c: SourcePluginConfig) { fun clearUpdateAvailable(c: SourcePluginConfig) {

View File

@ -1,52 +0,0 @@
package com.futo.platformplayer.states
import com.futo.platformplayer.api.media.Serializer
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.StringStorage
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
@kotlinx.serialization.Serializable
data class VideoToOpen(val url: String, val timeSeconds: Long);
class StateSaved {
var videoToOpen: VideoToOpen? = null;
private val _videoToOpen = FragmentedStorage.get<StringStorage>("videoToOpen")
fun load() {
val videoToOpenString = _videoToOpen.value;
if (videoToOpenString.isNotEmpty()) {
try {
val v = Serializer.json.decodeFromString<VideoToOpen>(videoToOpenString);
videoToOpen = v;
} catch (e: Throwable) {
Logger.w(TAG, "Failed to load video to open", e)
}
}
Logger.i(TAG, "loaded videoToOpen=$videoToOpen");
}
fun setVideoToOpenNonBlocking(v: VideoToOpen? = null) {
Logger.i(TAG, "set videoToOpen=$v");
videoToOpen = v;
_videoToOpen.setAndSave(if (v != null) Serializer.json.encodeToString(v) else "");
}
fun setVideoToOpenBlocking(v: VideoToOpen? = null) {
Logger.i(TAG, "set videoToOpen=$v");
videoToOpen = v;
_videoToOpen.setAndSaveBlocking(if (v != null) Serializer.json.encodeToString(v) else "");
}
companion object {
const val TAG = "StateSaved"
val instance: StateSaved = StateSaved()
}
}

View File

@ -55,7 +55,7 @@ abstract class SubscriptionsTaskFetchAlgorithm(
val clientCacheCount = clientTasks.value.size - clientTaskCount; val clientCacheCount = clientTasks.value.size - clientTaskCount;
val limit = clientTasks.key.getSubscriptionRateLimit(); val limit = clientTasks.key.getSubscriptionRateLimit();
if(clientCacheCount > 0 && clientTaskCount > 0 && limit != null && clientTaskCount >= limit && StateApp.instance.contextOrNull?.let { it is MainActivity && it.isFragmentActive<SubscriptionsFeedFragment>() } == true) { if(clientCacheCount > 0 && clientTaskCount > 0 && limit != null && clientTaskCount >= limit && StateApp.instance.contextOrNull?.let { it is MainActivity && it.isFragmentActive<SubscriptionsFeedFragment>() } == true) {
UIDialogs.toast("[${clientTasks.key.name}] only updating ${clientTaskCount} most urgent channels (rqs). (${clientCacheCount} cached)"); UIDialogs.appToast("[${clientTasks.key.name}] only updating ${clientTaskCount} most urgent channels (rqs). (${clientCacheCount} cached)");
} }
} }

View File

@ -0,0 +1,91 @@
package com.futo.platformplayer.views
import android.content.Context
import android.graphics.Color
import android.util.AttributeSet
import android.widget.LinearLayout
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isVisible
import com.futo.platformplayer.R
import com.futo.platformplayer.dp
import com.futo.platformplayer.logging.Logger
class ToastView : LinearLayout {
private val root: LinearLayout;
private val title: TextView;
private val text: TextView;
init {
inflate(context, R.layout.toast, this);
root = findViewById(R.id.root);
title = findViewById(R.id.title);
text = findViewById(R.id.text);
}
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
setToast(ToastView.Toast("", false))
root.visibility = GONE;
}
fun hide(animate: Boolean, onFinished: (()->Unit)? = null) {
Logger.i("MainActivity", "Hiding toast");
if(!animate) {
root.visibility = GONE;
alpha = 0f;
onFinished?.invoke();
}
else {
animate()
.alpha(0f)
.setDuration(700)
.translationY(20.dp(context.resources).toFloat())
.withEndAction { root.visibility = GONE; onFinished?.invoke(); }
.start();
}
}
fun show(animate: Boolean) {
Logger.i("MainActivity", "Showing toast");
if(!animate) {
root.visibility = VISIBLE;
alpha = 1f;
}
else {
alpha = 0f;
root.visibility = VISIBLE;
translationY = 20.dp(context.resources).toFloat();
animate()
.alpha(1f)
.setDuration(700)
.translationY(0f)
.start();
}
}
fun setToast(toast: Toast) {
if(toast.title.isNullOrEmpty())
title.isVisible = false;
else {
title.text = toast.title;
title.isVisible = true;
}
text.text = toast.msg;
if(toast.color != null)
text.setTextColor(toast.color);
else
text.setTextColor(Color.WHITE);
}
fun setToastAnimated(toast: Toast) {
hide(true) {
setToast(toast);
show(true);
};
}
class Toast(
val msg: String,
val long: Boolean,
val color: Int? = null,
val title: String? = null
);
}

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#EE202020" />
<corners android:radius="10dp" />
<padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
</shape>

View File

@ -70,4 +70,13 @@
android:visibility="gone" android:visibility="gone"
android:elevation="15dp"> android:elevation="15dp">
</FrameLayout> </FrameLayout>
<com.futo.platformplayer.views.ToastView
android:id="@+id/toast_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="50dp"
android:elevation="30dp"
app:layout_constraintLeft_toLeftOf="@id/fragment_main"
app:layout_constraintRight_toRightOf="@id/fragment_main"
app:layout_constraintBottom_toBottomOf="@id/fragment_main" />
</androidx.constraintlayout.motion.widget.MotionLayout> </androidx.constraintlayout.motion.widget.MotionLayout>

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:toolNs="http://schemas.android.com/tools"
android:orientation="vertical"
android:id="@+id/root"
android:padding="10dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/background_toast"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:paddingLeft="15dp"
android:paddingRight="15dp"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:orientation="vertical">
<TextView
android:id="@+id/title"
android:textColor="@color/white"
toolNs:text="Some Title"
android:fontFamily="@font/inter_bold"
android:textSize="15dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/text"
android:textColor="@color/white"
android:textSize="14dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/inter_light"
toolNs:text="This is a test" />
</LinearLayout>
</LinearLayout>