diff --git a/app/src/main/java/com/futo/platformplayer/UIDialogs.kt b/app/src/main/java/com/futo/platformplayer/UIDialogs.kt
index a3399d2a80885395bb59408d8ed1d10938c26bf0..0ebf26bd490101fa212040f3260f6b8959cdd0ca 100644
--- a/app/src/main/java/com/futo/platformplayer/UIDialogs.kt
+++ b/app/src/main/java/com/futo/platformplayer/UIDialogs.kt
@@ -37,6 +37,7 @@ import com.futo.platformplayer.logging.Logger
 import com.futo.platformplayer.states.StateApp
 import com.futo.platformplayer.states.StateBackup
 import com.futo.platformplayer.stores.v2.ManagedStore
+import com.futo.platformplayer.views.ToastView
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
@@ -398,13 +399,28 @@ class UIDialogs {
             StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
                 try {
                     StateApp.withContext {
-                        Toast.makeText(it, text, if (long) Toast.LENGTH_LONG else Toast.LENGTH_SHORT).show();
+                        toast(it, text, long);
                     }
                 } catch (e: Throwable) {
                     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) {
             //TODO: Is not actually clickable...
diff --git a/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt b/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt
index f846cd9710a1d8c38aa16e237f05bbfb67b2524d..01f2f48b12fe2868525b280e6b2b5a1bd982f701 100644
--- a/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt
+++ b/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt
@@ -343,7 +343,7 @@ class UISlideOverlays {
                     videoSources.filter { it is IVideoUrlSource && it.isDownloadable() }.asIterable(),
                     Settings.instance.downloads.getDefaultVideoQualityPixels(),
                     FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS
-                ) as IVideoUrlSource;
+                ) as IVideoUrlSource?;
             }
 
             if (audioSources != null) {
diff --git a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt
index 294c811f1fb8e39fb79937e12cb6299714a2d275..665b7624c7f397c31a470f8a8a8956180a5c9f24 100644
--- a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt
+++ b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt
@@ -1,7 +1,6 @@
 package com.futo.platformplayer.activities
 
 import android.annotation.SuppressLint
-import android.app.NotificationManager
 import android.content.Context
 import android.content.Intent
 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.WindowInsetsCompat
 import androidx.core.view.WindowInsetsControllerCompat
+import androidx.core.view.isVisible
 import androidx.fragment.app.Fragment
 import androidx.fragment.app.FragmentContainerView
 import androidx.lifecycle.Lifecycle
@@ -45,6 +45,7 @@ import com.futo.platformplayer.states.*
 import com.futo.platformplayer.stores.FragmentedStorage
 import com.futo.platformplayer.stores.SubscriptionStorage
 import com.futo.platformplayer.stores.v2.ManagedStore
+import com.futo.platformplayer.views.ToastView
 import com.google.gson.JsonParser
 import com.google.zxing.integration.android.IntentIntegrator
 import kotlinx.coroutines.*
@@ -54,6 +55,7 @@ import java.io.PrintWriter
 import java.io.StringWriter
 import java.lang.reflect.InvocationTargetException
 import java.util.*
+import java.util.concurrent.ConcurrentLinkedQueue
 
 class MainActivity : AppCompatActivity, IWithResultLauncher {
 
@@ -65,6 +67,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
     lateinit var rootView : MotionLayout;
 
     private lateinit var _overlayContainer: FrameLayout;
+    private lateinit var _toastView: ToastView;
 
     //Segment Containers
     private lateinit var _fragContainerTopBar: FragmentContainerView;
@@ -207,7 +210,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
         _fragContainerVideoDetail = findViewById(R.id.fragment_overlay);
         _fragContainerOverlay = findViewById(R.id.fragment_overlay_container);
         _overlayContainer = findViewById(R.id.overlay_container);
-        //_overlayContainer.visibility = View.GONE;
+        _toastView = findViewById(R.id.toast_view);
 
         //Initialize fragments
 
@@ -478,21 +481,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
         }
 
         _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() {
@@ -864,7 +852,6 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
         _orientationManager.disable();
 
         StateApp.instance.mainAppDestroyed(this);
-        StateSaved.instance.setVideoToOpenBlocking(null);
     }
 
     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.
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt
index 3881b92aea754bd8b19426550ca9bec448336f47..88fc76dc6382c7f6ea39558967de533a74a2ef39 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt
@@ -10,6 +10,7 @@ import androidx.lifecycle.lifecycleScope
 import androidx.recyclerview.widget.LinearLayoutManager
 import com.futo.platformplayer.*
 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.IPlatformContent
 import com.futo.platformplayer.api.media.models.video.IPlatformVideo
@@ -427,7 +428,7 @@ class SubscriptionsFeedFragment : MainFragment() {
             context?.let {
                 fragment.lifecycleScope.launch(Dispatchers.Main) {
                     try {
-                        if (exs.size <= 8) {
+                        if (exs.size <= 3) {
                             for (ex in exs) {
                                 var toShow = ex;
                                 var channel: String? = null;
@@ -437,12 +438,11 @@ class SubscriptionsFeedFragment : MainFragment() {
                                 }
                                 Logger.e(TAG, "Channel [${channel}] failed", ex);
                                 if (toShow is PluginException)
-                                    UIDialogs.toast(
-                                        it,
+                                    UIDialogs.appToast(
                                         context.getString(R.string.plugin_pluginname_failed_message).replace("{pluginName}", toShow.config.name).replace("{message}", toShow.message ?: "")
                                     );
                                 else
-                                    UIDialogs.toast(it, ex.message ?: "");
+                                    UIDialogs.appToast(ex.message ?: "");
                             }
                         }
                         else {
@@ -453,7 +453,7 @@ class SubscriptionsFeedFragment : MainFragment() {
                                 .map { it!! }
                                 .toList();
                             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) {
                         Logger.e(TAG, "Failed to handle exceptions", e)
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailFragment.kt
index cb0abbed83cdce530c2b5ce0334c72109b52b79d..487832333ec0270d9e2a25cbce0587fbba056591 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailFragment.kt
@@ -25,8 +25,6 @@ import com.futo.platformplayer.logging.Logger
 import com.futo.platformplayer.models.PlatformVideoWithTime
 import com.futo.platformplayer.models.UrlVideoWithTime
 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
 
 class VideoDetailFragment : MainFragment {
@@ -372,11 +370,6 @@ class VideoDetailFragment : MainFragment {
 
         Logger.v(TAG, "shouldStop: $shouldStop");
         if(shouldStop) {
-            _viewDetail?.let {
-                val v = it.video ?: return@let;
-                StateSaved.instance.setVideoToOpenBlocking(VideoToOpen(v.url, (it.lastPositionMilliseconds / 1000.0f).toLong()));
-            }
-
             _viewDetail?.onStop();
             StateCasting.instance.onStop();
             Logger.v(TAG, "called onStop() shouldStop: $shouldStop");
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt
index 7d7d58540f41e5a91c2279ee6ddcf76ba71dcfb3..695edab30ab3159c1317e3773338d9ddca528a8c 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt
@@ -149,6 +149,8 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
 import kotlinx.coroutines.withContext
 import userpackage.Protocol
 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){
-        val current = _historyIndex;
-        if(current == null || current.url != video.url) {
-            val index = StateHistory.instance.getHistoryByVideo(video, true)!!;
-            _historyIndex = index;
-            return@withContext index;
+        _historyIndexLock.withLock {
+            val current = _historyIndex;
+            if(current == null || current.url != video.url) {
+                val index = StateHistory.instance.getHistoryByVideo(video, true)!!;
+                _historyIndex = index;
+                return@withContext index;
+            }
+            return@withContext current;
         }
-        return@withContext current;
     }
 
 
@@ -1121,7 +1128,7 @@ class VideoDetailView : ConstraintLayout {
 
         switchContentView(_container_content_main);
     }
-    @OptIn(ExperimentalCoroutinesApi::class)
+    //@OptIn(ExperimentalCoroutinesApi::class)
     fun setVideoDetails(videoDetail: IPlatformVideoDetails, newVideo: Boolean = false) {
         Logger.i(TAG, "setVideoDetails (${videoDetail.name})")
 
diff --git a/app/src/main/java/com/futo/platformplayer/services/MediaPlaybackService.kt b/app/src/main/java/com/futo/platformplayer/services/MediaPlaybackService.kt
index f6fb28798de4935c74b4fae44c99069f13571ac4..77125de8fd140f7ba7eb9aeb4114c6a9417dbc47 100644
--- a/app/src/main/java/com/futo/platformplayer/services/MediaPlaybackService.kt
+++ b/app/src/main/java/com/futo/platformplayer/services/MediaPlaybackService.kt
@@ -143,7 +143,7 @@ class MediaPlaybackService : Service() {
     override fun onDestroy() {
         Logger.v(TAG, "onDestroy");
         _instance = null;
-        MediaControlReceiver.onCloseReceived.emit();
+        MediaControlReceiver.onPauseReceived.emit();
         super.onDestroy();
     }
 
diff --git a/app/src/main/java/com/futo/platformplayer/states/StateApp.kt b/app/src/main/java/com/futo/platformplayer/states/StateApp.kt
index 14c75210a2fcbc9d8c721ed01cf94b392dc85b7b..36551de65da1be903b4c3b68cfc0ffe55aa4b52c 100644
--- a/app/src/main/java/com/futo/platformplayer/states/StateApp.kt
+++ b/app/src/main/java/com/futo/platformplayer/states/StateApp.kt
@@ -5,6 +5,7 @@ import android.app.Activity
 import android.content.Context
 import android.content.Intent
 import android.content.IntentFilter
+import android.graphics.Color
 import android.media.AudioManager
 import android.net.ConnectivityManager
 import android.net.Network
@@ -38,6 +39,7 @@ import com.futo.platformplayer.receivers.AudioNoisyReceiver
 import com.futo.platformplayer.services.DownloadService
 import com.futo.platformplayer.stores.FragmentedStorage
 import com.futo.platformplayer.stores.v2.ManagedStore
+import com.futo.platformplayer.views.ToastView
 import kotlinx.coroutines.*
 import java.io.File
 import java.time.OffsetDateTime
@@ -380,8 +382,6 @@ class StateApp {
 
         Logger.i(TAG, "MainApp Starting: Initializing [Polycentric]");
         StatePolycentric.instance.load(context);
-        Logger.i(TAG, "MainApp Starting: Initializing [Saved]");
-        StateSaved.instance.load();
 
         Logger.i(TAG, "MainApp Starting: Initializing [Connectivity]");
         displayMetrics = context.resources.displayMetrics;
@@ -568,19 +568,36 @@ class StateApp {
         StateAnnouncement.instance.deleteAnnouncement("plugin-update")
 
         scopeOrNull?.launch(Dispatchers.IO) {
-            val updateAvailableCount = StatePlatform.instance.checkForUpdates()
+            val updateAvailable = StatePlatform.instance.checkForUpdates()
 
             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(
                         "plugin-update",
                         "Plugin updates available",
-                        "There are $updateAvailableCount plugin updates available.",
+                        "There are ${updateAvailable.size} plugin updates available.",
                         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) {
diff --git a/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt b/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt
index fe06e78d6c41d79c53f03eb8dbdfbf1386d2e4fb..5c8f77c511575e9b6d75a7cef70ff23fa864e3a4 100644
--- a/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt
+++ b/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt
@@ -941,8 +941,8 @@ class StatePlatform {
         }
     }
 
-    suspend fun checkForUpdates(): Int = withContext(Dispatchers.IO) {
-        var updateAvailableCount = 0
+    suspend fun checkForUpdates(): List<SourcePluginConfig> = withContext(Dispatchers.IO) {
+        var configs = mutableListOf<SourcePluginConfig>()
         val updatesAvailableFor = hashSetOf<String>()
         for (availableClient in getAvailableClients()) {
             if (availableClient !is JSClient) {
@@ -950,13 +950,13 @@ class StatePlatform {
             }
 
             if (checkForUpdates(availableClient.config)) {
-                updateAvailableCount++
+                configs.add(availableClient.config);
                 updatesAvailableFor.add(availableClient.config.id)
             }
         }
 
         _updatesAvailableMap = updatesAvailableFor
-        return@withContext updateAvailableCount
+        return@withContext configs;
     }
 
     fun clearUpdateAvailable(c: SourcePluginConfig) {
diff --git a/app/src/main/java/com/futo/platformplayer/states/StateSaved.kt b/app/src/main/java/com/futo/platformplayer/states/StateSaved.kt
deleted file mode 100644
index 1bd4df34148cfe69837dc64382ee624e032d43dc..0000000000000000000000000000000000000000
--- a/app/src/main/java/com/futo/platformplayer/states/StateSaved.kt
+++ /dev/null
@@ -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()
-    }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionsTaskFetchAlgorithm.kt b/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionsTaskFetchAlgorithm.kt
index b4b90624963508dd0f79d0ec4386666e4c0371a7..a7b57b74b30c2362a3c309e1c2d393a2889b0ef0 100644
--- a/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionsTaskFetchAlgorithm.kt
+++ b/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionsTaskFetchAlgorithm.kt
@@ -55,7 +55,7 @@ abstract class SubscriptionsTaskFetchAlgorithm(
                 val clientCacheCount = clientTasks.value.size - clientTaskCount;
                 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) {
-                    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)");
                 }
             }
 
diff --git a/app/src/main/java/com/futo/platformplayer/views/ToastView.kt b/app/src/main/java/com/futo/platformplayer/views/ToastView.kt
new file mode 100644
index 0000000000000000000000000000000000000000..2600dff25c51f0faf502c142385b5d094beba969
--- /dev/null
+++ b/app/src/main/java/com/futo/platformplayer/views/ToastView.kt
@@ -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
+    );
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/background_toast.xml b/app/src/main/res/drawable/background_toast.xml
new file mode 100644
index 0000000000000000000000000000000000000000..85dbffb768f89f2359759a38a9b25154d68b6e2d
--- /dev/null
+++ b/app/src/main/res/drawable/background_toast.xml
@@ -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>
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index e7c51891c566d9217e21c22b1c46844d7a4dcbd2..815d9f73d5d77c6057ea4240e7c9f86a6fa76d80 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -70,4 +70,13 @@
         android:visibility="gone"
         android:elevation="15dp">
     </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>
\ No newline at end of file
diff --git a/app/src/main/res/layout/toast.xml b/app/src/main/res/layout/toast.xml
new file mode 100644
index 0000000000000000000000000000000000000000..9ecd8c0e3059f2df6b08246f3ab2c7e936068e09
--- /dev/null
+++ b/app/src/main/res/layout/toast.xml
@@ -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>
\ No newline at end of file