diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index a659758ae5e4a2503fe97008851eeeaaa967e474..ed7a2988f88d6162cbe3930f47dc2f886cb54e63 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -61,6 +61,14 @@
 
                 <data android:scheme="grayjay" />
             </intent-filter>
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW" />
+
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
+
+                <data android:scheme="fcast" />
+            </intent-filter>
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
 
@@ -210,5 +218,9 @@
             android:name=".activities.QRCaptureActivity"
             android:screenOrientation="portrait"
             android:theme="@style/Theme.FutoVideo.NoActionBar" />
+        <activity
+            android:name=".activities.FCastGuideActivity"
+            android:screenOrientation="portrait"
+            android:theme="@style/Theme.FutoVideo.NoActionBar" />
     </application>
 </manifest>
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/Settings.kt b/app/src/main/java/com/futo/platformplayer/Settings.kt
index 8daa4736094abea37f6331693d0b12889696abfa..db6bfd5298d71aeabcb40f1df78df89a921044de 100644
--- a/app/src/main/java/com/futo/platformplayer/Settings.kt
+++ b/app/src/main/java/com/futo/platformplayer/Settings.kt
@@ -111,7 +111,15 @@ class Settings : FragmentedStorageFileJson() {
         }
     }
 
-
+    @FormField(R.string.link_handling, FieldForm.BUTTON, R.string.allow_grayjay_to_handle_links, -1)
+    @FormFieldButton(R.drawable.ic_link)
+    fun manageLinks() {
+        try {
+            SettingsActivity.getActivity()?.let { UIDialogs.showUrlHandlingPrompt(it) }
+        } catch (e: Throwable) {
+            Logger.e(TAG, "Failed to show url handling prompt", e)
+        }
+    }
 
     @FormField(R.string.language, "group", -1, 0)
     var language = LanguageSettings();
@@ -377,6 +385,14 @@ class Settings : FragmentedStorageFileJson() {
 
         @FormField(R.string.background_switch_audio, FieldForm.TOGGLE, R.string.background_switch_audio_description, 10)
         var backgroundSwitchToAudio: Boolean = true;
+
+        @FormField(R.string.restart_after_audio_focus_loss, FieldForm.DROPDOWN, R.string.restart_playback_when_gaining_audio_focus_after_a_loss, 11)
+        @DropdownFieldOptionsId(R.array.restart_playback_after_loss)
+        var restartPlaybackAfterLoss: Int = 1;
+
+        @FormField(R.string.restart_after_connectivity_loss, FieldForm.DROPDOWN, R.string.restart_playback_when_gaining_connectivity_after_a_loss, 12)
+        @DropdownFieldOptionsId(R.array.restart_playback_after_loss)
+        var restartPlaybackAfterConnectivityLoss: Int = 1;
     }
 
     @FormField(R.string.comments, "group", R.string.comments_description, 6)
diff --git a/app/src/main/java/com/futo/platformplayer/UIDialogs.kt b/app/src/main/java/com/futo/platformplayer/UIDialogs.kt
index 39e3ad84fdaa5e2ee4b1e1447fba3f995993aac7..86d73e28021262ef4601d378176d5954ff1d4933 100644
--- a/app/src/main/java/com/futo/platformplayer/UIDialogs.kt
+++ b/app/src/main/java/com/futo/platformplayer/UIDialogs.kt
@@ -1,8 +1,11 @@
 package com.futo.platformplayer
 
+import android.app.Activity
 import android.app.AlertDialog
 import android.content.Context
+import android.content.Intent
 import android.graphics.Color
+import android.net.Uri
 import android.util.TypedValue
 import android.view.Gravity
 import android.view.LayoutInflater
@@ -15,7 +18,6 @@ import com.futo.platformplayer.casting.StateCasting
 import com.futo.platformplayer.dialogs.*
 import com.futo.platformplayer.engine.exceptions.PluginException
 import com.futo.platformplayer.logging.Logger
-import com.futo.platformplayer.states.StateAnnouncement
 import com.futo.platformplayer.states.StateApp
 import com.futo.platformplayer.states.StateBackup
 import com.futo.platformplayer.stores.v2.ManagedStore
@@ -91,6 +93,50 @@ class UIDialogs {
                 }.toTypedArray());
         }
 
+        fun showUrlHandlingPrompt(context: Context, onYes: (() -> Unit)? = null) {
+            val builder = AlertDialog.Builder(context)
+            val view = LayoutInflater.from(context).inflate(R.layout.dialog_url_handling, null)
+            builder.setView(view)
+
+            val dialog = builder.create()
+            registerDialogOpened(dialog)
+
+            view.findViewById<TextView>(R.id.button_no).apply {
+                this.setOnClickListener {
+                    dialog.dismiss()
+                }
+            }
+
+            view.findViewById<LinearLayout>(R.id.button_yes).apply {
+                this.setOnClickListener {
+                    if (BuildConfig.IS_PLAYSTORE_BUILD) {
+                        dialog.dismiss()
+                        showDialogOk(context, R.drawable.ic_error_pred, context.getString(R.string.play_store_version_does_not_support_default_url_handling)) {
+                            onYes?.invoke()
+                        }
+                    } else {
+                        try {
+                            val intent =
+                                Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
+                            val uri = Uri.fromParts("package", context.packageName, null)
+                            intent.data = uri
+                            context.startActivity(intent)
+                        } catch (e: Throwable) {
+                            toast(context, context.getString(R.string.failed_to_show_settings))
+                        }
+
+                        onYes?.invoke()
+                        dialog.dismiss()
+                    }
+                }
+            }
+
+            dialog.setOnDismissListener {
+                registerDialogClosed(dialog)
+            }
+
+            dialog.show()
+        }
 
         fun showAutomaticBackupDialog(context: Context, skipRestoreCheck: Boolean = false, onClosed: (()->Unit)? = null) {
             val dialogAction: ()->Unit = {
@@ -107,7 +153,8 @@ class UIDialogs {
                     }, UIDialogs.ActionStyle.DANGEROUS),
                     UIDialogs.Action(context.getString(R.string.restore), {
                         UIDialogs.showAutomaticRestoreDialog(context, StateApp.instance.scope);
-                    }, UIDialogs.ActionStyle.PRIMARY));
+                    }, UIDialogs.ActionStyle.PRIMARY)
+                );
             else {
                 dialogAction();
             }
@@ -291,11 +338,22 @@ class UIDialogs {
             } else {
                 val dialog = ConnectCastingDialog(context);
                 registerDialogOpened(dialog);
+                val c = context
+                if (c is Activity) {
+                    dialog.setOwnerActivity(c);
+                }
                 dialog.setOnDismissListener { registerDialogClosed(dialog) };
                 dialog.show();
             }
         }
 
+        fun showCastingTutorialDialog(context: Context) {
+            val dialog = CastingHelpDialog(context);
+            registerDialogOpened(dialog);
+            dialog.setOnDismissListener { registerDialogClosed(dialog) };
+            dialog.show();
+        }
+
         fun showCastingAddDialog(context: Context) {
             val dialog = CastingAddDialog(context);
             registerDialogOpened(dialog);
diff --git a/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt b/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt
index e06c525fdf9dfb803d0a82a858faf8bf0f45f414..6b401f1cbb28fe1640cde45af6fd52ee9ddecab8 100644
--- a/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt
+++ b/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt
@@ -12,20 +12,27 @@ import android.widget.TextView
 import com.futo.platformplayer.api.http.ManagedHttpClient
 import com.futo.platformplayer.api.media.models.ResultCapabilities
 import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
+import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantAudioUrlSource
+import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantSubtitleUrlSource
+import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantVideoUrlSource
 import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
+import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudioSource
+import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
 import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
 import com.futo.platformplayer.api.media.models.streams.sources.SubtitleRawSource
 import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
 import com.futo.platformplayer.api.media.models.video.IPlatformVideo
 import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
 import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
+import com.futo.platformplayer.casting.StateCasting
 import com.futo.platformplayer.downloads.VideoLocal
 import com.futo.platformplayer.helpers.VideoHelper
 import com.futo.platformplayer.logging.Logger
 import com.futo.platformplayer.models.Playlist
 import com.futo.platformplayer.models.Subscription
+import com.futo.platformplayer.parsers.HLS
 import com.futo.platformplayer.states.*
-import com.futo.platformplayer.views.Loader
+import com.futo.platformplayer.views.LoaderView
 import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuGroup
 import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
 import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
@@ -33,10 +40,12 @@ import com.futo.platformplayer.views.pills.RoundButton
 import com.futo.platformplayer.views.pills.RoundButtonGroup
 import com.futo.platformplayer.views.overlays.slideup.*
 import com.futo.platformplayer.views.video.FutoVideoPlayerBase
+import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
+import okhttp3.internal.notifyAll
 import java.lang.IllegalStateException
 
 class UISlideOverlays {
@@ -127,6 +136,101 @@ class UISlideOverlays {
             }
         }
 
+        fun showHlsPicker(video: IPlatformVideoDetails, source: Any, sourceUrl: String, container: ViewGroup): SlideUpMenuOverlay {
+            val items = arrayListOf<View>(LoaderView(container.context))
+            val slideUpMenuOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.download_video), null, true, items)
+
+            StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
+                val masterPlaylistResponse = ManagedHttpClient().get(sourceUrl)
+                check(masterPlaylistResponse.isOk) { "Failed to get master playlist: ${masterPlaylistResponse.code}" }
+
+                val masterPlaylistContent = masterPlaylistResponse.body?.string()
+                    ?: throw Exception("Master playlist content is empty")
+
+                val videoButtons = arrayListOf<SlideUpMenuItem>()
+                val audioButtons = arrayListOf<SlideUpMenuItem>()
+                //TODO: Implement subtitles
+                //val subtitleButtons = arrayListOf<SlideUpMenuItem>()
+
+                var selectedVideoVariant: HLSVariantVideoUrlSource? = null
+                var selectedAudioVariant: HLSVariantAudioUrlSource? = null
+                //TODO: Implement subtitles
+                //var selectedSubtitleVariant: HLSVariantSubtitleUrlSource? = null
+
+                val masterPlaylist: HLS.MasterPlaylist
+                try {
+                    masterPlaylist = HLS.parseMasterPlaylist(masterPlaylistContent, sourceUrl)
+
+                    masterPlaylist.getAudioSources().forEach { it ->
+                        audioButtons.add(SlideUpMenuItem(container.context, R.drawable.ic_music, it.name, listOf(it.language, it.codec).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "), it, {
+                            selectedAudioVariant = it
+                            slideUpMenuOverlay.selectOption(audioButtons, it)
+                            slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
+                        }, false))
+                    }
+
+                    /*masterPlaylist.getSubtitleSources().forEach { it ->
+                        subtitleButtons.add(SlideUpMenuItem(container.context, R.drawable.ic_music, it.name, listOf(it.format).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "), it, {
+                            selectedSubtitleVariant = it
+                            slideUpMenuOverlay.selectOption(subtitleButtons, it)
+                            slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
+                        }, false))
+                    }*/
+
+                    masterPlaylist.getVideoSources().forEach {
+                        videoButtons.add(SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "${it.width}x${it.height}", it, {
+                            selectedVideoVariant = it
+                            slideUpMenuOverlay.selectOption(videoButtons, it)
+                            slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
+                        }, false))
+                    }
+
+                    val newItems = arrayListOf<View>()
+                    if (videoButtons.isNotEmpty()) {
+                        newItems.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.video), videoButtons, videoButtons))
+                    }
+                    if (audioButtons.isNotEmpty()) {
+                        newItems.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.audio), audioButtons, audioButtons))
+                    }
+                    //TODO: Implement subtitles
+                    /*if (subtitleButtons.isNotEmpty()) {
+                        newItems.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.subtitles), subtitleButtons, subtitleButtons))
+                    }*/
+
+                    slideUpMenuOverlay.onOK.subscribe {
+                        //TODO: Fix SubtitleRawSource issue
+                        StateDownloads.instance.download(video, selectedVideoVariant, selectedAudioVariant, null);
+                        slideUpMenuOverlay.hide()
+                    }
+
+                    withContext(Dispatchers.Main) {
+                        slideUpMenuOverlay.setItems(newItems)
+                    }
+                } catch (e: Throwable) {
+                    if (masterPlaylistContent.lines().any { it.startsWith("#EXTINF:") }) {
+                        withContext(Dispatchers.Main) {
+                            if (source is IHLSManifestSource) {
+                                StateDownloads.instance.download(video, HLSVariantVideoUrlSource("variant", 0, 0, "application/vnd.apple.mpegurl", "", null, 0, false, sourceUrl), null, null)
+                                UIDialogs.toast(container.context, "Variant video HLS playlist download started")
+                                slideUpMenuOverlay.hide()
+                            } else if (source is IHLSManifestAudioSource) {
+                                StateDownloads.instance.download(video, null, HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, sourceUrl), null)
+                                UIDialogs.toast(container.context, "Variant audio HLS playlist download started")
+                                slideUpMenuOverlay.hide()
+                            } else {
+                                throw NotImplementedError()
+                            }
+                        }
+                    } else {
+                        throw e
+                    }
+                }
+            }
+
+            return slideUpMenuOverlay.apply { show() }
+
+        }
+
         fun showDownloadVideoOverlay(video: IPlatformVideoDetails, container: ViewGroup, contentResolver: ContentResolver? = null): SlideUpMenuOverlay? {
             val items = arrayListOf<View>();
             var menu: SlideUpMenuOverlay? = null;
@@ -166,30 +270,49 @@ class UISlideOverlays {
                 videoSources
                 .filter { it.isDownloadable() }
                 .map {
-                    SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "${it.width}x${it.height}", it, {
-                        selectedVideo = it as IVideoUrlSource;
-                        menu?.selectOption(videoSources, it);
-                        if(selectedAudio != null || !requiresAudio)
-                            menu?.setOk(container.context.getString(R.string.download));
-                    }, false)
+                    if (it is IVideoUrlSource) {
+                        SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "${it.width}x${it.height}", it, {
+                            selectedVideo = it
+                            menu?.selectOption(videoSources, it);
+                            if(selectedAudio != null || !requiresAudio)
+                                menu?.setOk(container.context.getString(R.string.download));
+                        }, false)
+                    } else if (it is IHLSManifestSource) {
+                        SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "HLS", it, {
+                            showHlsPicker(video, it, it.url, container)
+                        }, false)
+                    } else {
+                        throw Exception("Unhandled source type")
+                    }
                 }).flatten().toList()
             ));
 
-            if(Settings.instance.downloads.getDefaultVideoQualityPixels() > 0 && videoSources.size > 0)
-                selectedVideo = VideoHelper.selectBestVideoSource(videoSources.filter { it.isDownloadable() }.asIterable(),
+            if(Settings.instance.downloads.getDefaultVideoQualityPixels() > 0 && videoSources.size > 0) {
+                //TODO: Add HLS support here
+                selectedVideo = VideoHelper.selectBestVideoSource(
+                    videoSources.filter { it is IVideoUrlSource && it.isDownloadable() }.asIterable(),
                     Settings.instance.downloads.getDefaultVideoQualityPixels(),
-                    FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS) as IVideoUrlSource;
-
+                    FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS
+                ) as IVideoUrlSource;
+            }
 
             audioSources?.let { audioSources ->
                 items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.audio), audioSources, audioSources
                     .filter { VideoHelper.isDownloadable(it) }
                     .map {
-                        SlideUpMenuItem(container.context, R.drawable.ic_music, it.name, "${it.bitrate}", it, {
-                            selectedAudio = it as IAudioUrlSource;
-                            menu?.selectOption(audioSources, it);
-                            menu?.setOk(container.context.getString(R.string.download));
-                        }, false);
+                        if (it is IAudioUrlSource) {
+                            SlideUpMenuItem(container.context, R.drawable.ic_music, it.name, "${it.bitrate}", it, {
+                                selectedAudio = it
+                                menu?.selectOption(audioSources, it);
+                                menu?.setOk(container.context.getString(R.string.download));
+                            }, false);
+                        } else if (it is IHLSManifestAudioSource) {
+                            SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "HLS Audio", it, {
+                                showHlsPicker(video, it, it.url, container)
+                            }, false)
+                        } else {
+                            throw Exception("Unhandled source type")
+                        }
                     }));
                 val asources = audioSources;
                 val preferredAudioSource = VideoHelper.selectBestAudioSource(asources.asIterable(),
@@ -198,15 +321,15 @@ class UISlideOverlays {
                     if(Settings.instance.downloads.isHighBitrateDefault()) 99999999 else 1);
                 menu?.selectOption(asources, preferredAudioSource);
 
-
-                selectedAudio = VideoHelper.selectBestAudioSource(audioSources.filter { it.isDownloadable() }.asIterable(),
+                //TODO: Add HLS support here
+                selectedAudio = VideoHelper.selectBestAudioSource(audioSources.filter { it is IAudioUrlSource && it.isDownloadable() }.asIterable(),
                     FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS,
                     Settings.instance.playback.getPrimaryLanguage(container.context),
                     if(Settings.instance.downloads.isHighBitrateDefault()) 9999999 else 1) as IAudioUrlSource?;
             }
 
             //ContentResolver is required for subtitles..
-            if(contentResolver != null) {
+            if(contentResolver != null && subtitleSources.isNotEmpty()) {
                 items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.subtitles), subtitleSources, subtitleSources
                     .map {
                         SlideUpMenuItem(container.context, R.drawable.ic_edit, it.name, "", it, {
@@ -378,7 +501,7 @@ class UISlideOverlays {
             val dp70 = 70.dp(container.context.resources);
             val dp15 = 15.dp(container.context.resources);
             val overlay = SlideUpMenuOverlay(container.context, container, text, null, true, listOf(
-                Loader(container.context, true, dp70).apply {
+                LoaderView(container.context, true, dp70).apply {
                     this.setPadding(0, dp15, 0, dp15);
                 }
             ), true);
diff --git a/app/src/main/java/com/futo/platformplayer/activities/FCastGuideActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/FCastGuideActivity.kt
new file mode 100644
index 0000000000000000000000000000000000000000..691bbb77a314fda38786edf90e8671201f645625
--- /dev/null
+++ b/app/src/main/java/com/futo/platformplayer/activities/FCastGuideActivity.kt
@@ -0,0 +1,108 @@
+package com.futo.platformplayer.activities
+
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.text.Html
+import android.widget.ImageButton
+import android.widget.TextView
+import androidx.appcompat.app.AppCompatActivity
+import com.futo.platformplayer.R
+import com.futo.platformplayer.UIDialogs
+import com.futo.platformplayer.dialogs.CastingHelpDialog
+import com.futo.platformplayer.logging.Logger
+import com.futo.platformplayer.setNavigationBarColorAndIcons
+import com.futo.platformplayer.states.StateApp
+import com.futo.platformplayer.views.buttons.BigButton
+
+class FCastGuideActivity : AppCompatActivity() {
+    override fun attachBaseContext(newBase: Context?) {
+        super.attachBaseContext(StateApp.instance.getLocaleContext(newBase))
+    }
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_fcast_guide);
+        setNavigationBarColorAndIcons();
+
+        findViewById<TextView>(R.id.text_explanation).apply {
+            val guideText = """
+                <h3>1. Install FCast Receiver:</h3>
+                <p>- Open Play Store, FireStore, or FCast website on your TV/desktop.<br>
+                - Search for "FCast Receiver", install and open it.</p>
+                <br>
+                
+                <h3>2. Prepare the Grayjay App:</h3>
+                <p>- Ensure it's connected to the same network as the FCast Receiver.</p>
+                <br>
+                
+                <h3>3. Initiate Casting from Grayjay:</h3>
+                <p>- Click the cast button in Grayjay.</p>
+                <br>
+                
+                <h3>4. Connect to FCast Receiver:</h3>
+                <p>- Wait for your device to show in the list or add it manually with its IP address.</p>
+                <br>
+                
+                <h3>5. Confirm Connection:</h3>
+                <p>- Click "OK" to confirm your device selection.</p>
+                <br>
+                
+                <h3>6. Start Casting:</h3>
+                <p>- Press "start" next to the device you've added.</p>
+                <br>
+                
+                <h3>7. Play Your Video:</h3>
+                <p>- Start any video in Grayjay to cast.</p>
+                <br>
+                
+                <h3>Finding Your IP Address:</h3>
+                <p><b>On FCast Receiver (Android):</b> Displayed on the main screen.<br>
+                <b>On Windows:</b> Use 'ipconfig' in Command Prompt.<br>
+                <b>On Linux:</b> Use 'hostname -I' or 'ip addr' in Terminal.<br>
+                <b>On MacOS:</b> System Preferences > Network.</p>
+            """.trimIndent()
+
+            text = Html.fromHtml(guideText, Html.FROM_HTML_MODE_COMPACT)
+        }
+
+        findViewById<ImageButton>(R.id.button_back).setOnClickListener {
+            UIDialogs.showCastingTutorialDialog(this)
+            finish()
+        }
+
+        findViewById<BigButton>(R.id.button_close).onClick.subscribe {
+            UIDialogs.showCastingTutorialDialog(this)
+            finish()
+        }
+
+        findViewById<BigButton>(R.id.button_website).onClick.subscribe {
+            try {
+                val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://fcast.org/"))
+                startActivity(browserIntent);
+            } catch (e: Throwable) {
+                Logger.i(TAG, "Failed to open browser.", e)
+            }
+        }
+
+        findViewById<BigButton>(R.id.button_technical).onClick.subscribe {
+            try {
+                val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://gitlab.com/futo-org/fcast/-/wikis/Protocol-version-1"))
+                startActivity(browserIntent);
+            } catch (e: Throwable) {
+                Logger.i(TAG, "Failed to open browser.", e)
+            }
+        }
+    }
+
+    override fun onBackPressed() {
+        UIDialogs.showCastingTutorialDialog(this)
+        finish()
+    }
+
+    companion object {
+        private const val TAG = "FCastGuideActivity";
+    }
+}
\ No newline at end of file
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 c1d849ef950a91b2b8661f32f1baf730d2701ce0..84dbb104b68b409c82a0629df4428a068b7697b9 100644
--- a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt
+++ b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt
@@ -7,7 +7,6 @@ import android.content.pm.ActivityInfo
 import android.content.res.Configuration
 import android.net.Uri
 import android.os.Bundle
-import android.preference.PreferenceManager
 import android.util.Log
 import android.util.TypedValue
 import android.view.View
@@ -25,11 +24,9 @@ import androidx.fragment.app.FragmentContainerView
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.lifecycleScope
 import com.futo.platformplayer.*
-import com.futo.platformplayer.api.media.PlatformID
-import com.futo.platformplayer.api.media.models.channels.SerializedChannel
 import com.futo.platformplayer.casting.StateCasting
 import com.futo.platformplayer.constructs.Event1
-import com.futo.platformplayer.constructs.Event3
+import com.futo.platformplayer.dialogs.ConnectCastingDialog
 import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
 import com.futo.platformplayer.fragment.mainactivity.main.*
 import com.futo.platformplayer.fragment.mainactivity.topbar.AddTopBarFragment
@@ -45,6 +42,7 @@ import com.futo.platformplayer.stores.FragmentedStorage
 import com.futo.platformplayer.stores.SubscriptionStorage
 import com.futo.platformplayer.stores.v2.ManagedStore
 import com.google.gson.JsonParser
+import com.google.zxing.integration.android.IntentIntegrator
 import kotlinx.coroutines.*
 import kotlinx.serialization.decodeFromString
 import kotlinx.serialization.json.Json
@@ -90,6 +88,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
     lateinit var _fragMainPlaylistSearchResults: PlaylistSearchResultsFragment;
     lateinit var _fragMainSuggestions: SuggestionsFragment;
     lateinit var _fragMainSubscriptions: CreatorsFragment;
+    lateinit var _fragMainComments: CommentsFragment;
     lateinit var _fragMainSubscriptionsFeed: SubscriptionsFeedFragment;
     lateinit var _fragMainChannel: ChannelFragment;
     lateinit var _fragMainSources: SourcesFragment;
@@ -123,6 +122,24 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
     private var _isVisible = true;
     private var _wasStopped = false;
 
+    private val _urlQrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
+        val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data)
+        scanResult?.let {
+            val content = it.contents
+            if (content == null) {
+                UIDialogs.toast(this, getString(R.string.failed_to_scan_qr_code))
+                return@let
+            }
+
+            try {
+                handleUrlAll(content)
+            } catch (e: Throwable) {
+                Logger.i(TAG, "Failed to handle URL.", e)
+                UIDialogs.toast(this, "Failed to handle URL: ${e.message}")
+            }
+        }
+    }
+
     constructor() : super() {
         Thread.setDefaultUncaughtExceptionHandler { _, throwable ->
             val writer = StringWriter();
@@ -205,6 +222,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
         _fragMainCreatorSearchResults = CreatorSearchResultsFragment.newInstance();
         _fragMainPlaylistSearchResults = PlaylistSearchResultsFragment.newInstance();
         _fragMainSubscriptions = CreatorsFragment.newInstance();
+        _fragMainComments = CommentsFragment.newInstance();
         _fragMainChannel = ChannelFragment.newInstance();
         _fragMainSubscriptionsFeed = SubscriptionsFeedFragment.newInstance();
         _fragMainSources = SourcesFragment.newInstance();
@@ -282,6 +300,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
         //Set top bars
         _fragMainHome.topBar = _fragTopBarGeneral;
         _fragMainSubscriptions.topBar = _fragTopBarGeneral;
+        _fragMainComments.topBar = _fragTopBarGeneral;
         _fragMainSuggestions.topBar = _fragTopBarSearch;
         _fragMainVideoSearchResults.topBar = _fragTopBarSearch;
         _fragMainCreatorSearchResults.topBar = _fragTopBarSearch;
@@ -406,6 +425,23 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
             UIDialogs.toast(this, "No external file permissions\nExporting and auto backups will not work");
     }*/
 
+    fun showUrlQrCodeScanner() {
+        try {
+            val integrator = IntentIntegrator(this)
+            integrator.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
+            integrator.setPrompt(getString(R.string.scan_a_qr_code))
+            integrator.setOrientationLocked(true);
+            integrator.setCameraId(0)
+            integrator.setBeepEnabled(false)
+            integrator.setBarcodeImageEnabled(true)
+            integrator.captureActivity = QRCaptureActivity::class.java
+            _urlQrCodeResultLauncher.launch(integrator.createScanIntent())
+        } catch (e: Throwable) {
+            Logger.i(TAG, "Failed to handle show QR scanner.", e)
+            UIDialogs.toast(this, "Failed to show QR scanner: ${e.message}")
+        }
+    }
+
     override fun onResume() {
         super.onResume();
         Logger.v(TAG, "onResume")
@@ -493,76 +529,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
 
         try {
             if (targetData != null) {
-                when(intent.scheme) {
-                    "grayjay" -> {
-                        if(targetData.startsWith("grayjay://license/")) {
-                            if(StatePayment.instance.setPaymentLicenseUrl(targetData))
-                            {
-                                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)
-                                    closeSegment(fragCurrent);
-                            }
-                            else
-                                UIDialogs.toast(getString(R.string.invalid_license_format));
-
-                        }
-                        else if(targetData.startsWith("grayjay://plugin/")) {
-                            val intent = Intent(this, AddSourceActivity::class.java).apply {
-                                data = Uri.parse(targetData.substring("grayjay://plugin/".length));
-                            };
-                            startActivity(intent);
-                        }
-                        else if(targetData.startsWith("grayjay://video/")) {
-                            val videoUrl = targetData.substring("grayjay://video/".length);
-                            navigate(_fragVideoDetail, videoUrl);
-                        }
-                        else if(targetData.startsWith("grayjay://channel/")) {
-                            val channelUrl = targetData.substring("grayjay://channel/".length);
-                            navigate(_fragMainChannel, channelUrl);
-                        }
-                    }
-                    "content" -> {
-                        if(!handleContent(targetData, intent.type)) {
-                            UIDialogs.showSingleButtonDialog(
-                                this,
-                                R.drawable.ic_play,
-                                getString(R.string.unknown_content_format) + " [${targetData}]",
-                                "Ok",
-                                { });
-                        }
-                    }
-                    "file" -> {
-                        if(!handleFile(targetData)) {
-                            UIDialogs.showSingleButtonDialog(
-                                this,
-                                R.drawable.ic_play,
-                                getString(R.string.unknown_file_format) + " [${targetData}]",
-                                "Ok",
-                                { });
-                        }
-                    }
-                    "polycentric" -> {
-                        if(!handlePolycentric(targetData)) {
-                            UIDialogs.showSingleButtonDialog(
-                                this,
-                                R.drawable.ic_play,
-                                getString(R.string.unknown_polycentric_format) + " [${targetData}]",
-                                "Ok",
-                                { });
-                        }
-                    }
-                    else -> {
-                        if (!handleUrl(targetData)) {
-                            UIDialogs.showSingleButtonDialog(
-                                this,
-                                R.drawable.ic_play,
-                                getString(R.string.unknown_url_format) + " [${targetData}]",
-                                "Ok",
-                                { });
-                        }
-                    }
-                }
+                handleUrlAll(targetData)
             }
         }
         catch(ex: Throwable) {
@@ -570,6 +537,90 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
         }
     }
 
+    fun handleUrlAll(url: String) {
+        val uri = Uri.parse(url)
+        when (uri.scheme) {
+            "grayjay" -> {
+                if(url.startsWith("grayjay://license/")) {
+                    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));
+
+                        if(fragCurrent is BuyFragment)
+                            closeSegment(fragCurrent);
+                    }
+                    else
+                        UIDialogs.toast(getString(R.string.invalid_license_format));
+
+                }
+                else if(url.startsWith("grayjay://plugin/")) {
+                    val intent = Intent(this, AddSourceActivity::class.java).apply {
+                        data = Uri.parse(url.substring("grayjay://plugin/".length));
+                    };
+                    startActivity(intent);
+                }
+                else if(url.startsWith("grayjay://video/")) {
+                    val videoUrl = url.substring("grayjay://video/".length);
+                    navigate(_fragVideoDetail, videoUrl);
+                }
+                else if(url.startsWith("grayjay://channel/")) {
+                    val channelUrl = url.substring("grayjay://channel/".length);
+                    navigate(_fragMainChannel, channelUrl);
+                }
+            }
+            "content" -> {
+                if(!handleContent(url, intent.type)) {
+                    UIDialogs.showSingleButtonDialog(
+                        this,
+                        R.drawable.ic_play,
+                        getString(R.string.unknown_content_format) + " [${url}]",
+                        "Ok",
+                        { });
+                }
+            }
+            "file" -> {
+                if(!handleFile(url)) {
+                    UIDialogs.showSingleButtonDialog(
+                        this,
+                        R.drawable.ic_play,
+                        getString(R.string.unknown_file_format) + " [${url}]",
+                        "Ok",
+                        { });
+                }
+            }
+            "polycentric" -> {
+                if(!handlePolycentric(url)) {
+                    UIDialogs.showSingleButtonDialog(
+                        this,
+                        R.drawable.ic_play,
+                        getString(R.string.unknown_polycentric_format) + " [${url}]",
+                        "Ok",
+                        { });
+                }
+            }
+            "fcast" -> {
+                if(!handleFCast(url)) {
+                    UIDialogs.showSingleButtonDialog(
+                        this,
+                        R.drawable.ic_cast,
+                        "Unknown FCast format [${url}]",
+                        "Ok",
+                        { });
+                }
+            }
+            else -> {
+                if (!handleUrl(url)) {
+                    UIDialogs.showSingleButtonDialog(
+                        this,
+                        R.drawable.ic_play,
+                        getString(R.string.unknown_url_format) + " [${url}]",
+                        "Ok",
+                        { });
+                }
+            }
+        }
+    }
+
     fun handleUrl(url: String): Boolean {
         Logger.i(TAG, "handleUrl(url=$url)")
 
@@ -716,6 +767,20 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
         startActivity(Intent(this, PolycentricImportProfileActivity::class.java).apply { putExtra("url", url) })
         return true;
     }
+
+    fun handleFCast(url: String): Boolean {
+        Logger.i(TAG, "handleFCast");
+
+        try {
+            StateCasting.instance.handleUrl(this, url)
+            return true;
+        } catch (e: Throwable) {
+            Log.e(TAG, "Failed to parse FCast URL '${url}'.", e)
+        }
+
+        return false
+    }
+
     private fun readSharedContent(contentPath: String): ByteArray {
         return contentResolver.openInputStream(Uri.parse(contentPath))?.use {
             return it.readBytes();
@@ -916,6 +981,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
             GeneralTopBarFragment::class -> _fragTopBarGeneral as T;
             SearchTopBarFragment::class -> _fragTopBarSearch as T;
             CreatorsFragment::class -> _fragMainSubscriptions as T;
+            CommentsFragment::class -> _fragMainComments as T;
             SubscriptionsFeedFragment::class -> _fragMainSubscriptionsFeed as T;
             PlaylistSearchResultsFragment::class -> _fragMainPlaylistSearchResults as T;
             ChannelFragment::class -> _fragMainChannel as T;
diff --git a/app/src/main/java/com/futo/platformplayer/activities/SettingsActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/SettingsActivity.kt
index 3e5259a98c6472e62c673ed3b71074d634ca9457..8527e2d6de4f67832f1ff5baee7a5e06032c1a1f 100644
--- a/app/src/main/java/com/futo/platformplayer/activities/SettingsActivity.kt
+++ b/app/src/main/java/com/futo/platformplayer/activities/SettingsActivity.kt
@@ -15,7 +15,7 @@ import androidx.lifecycle.lifecycleScope
 import com.futo.platformplayer.*
 import com.futo.platformplayer.logging.Logger
 import com.futo.platformplayer.states.StateApp
-import com.futo.platformplayer.views.Loader
+import com.futo.platformplayer.views.LoaderView
 import com.futo.platformplayer.views.fields.FieldForm
 import com.futo.platformplayer.views.fields.ReadOnlyTextField
 import com.google.android.material.button.MaterialButton
@@ -23,7 +23,7 @@ import com.google.android.material.button.MaterialButton
 class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
     private lateinit var _form: FieldForm;
     private lateinit var _buttonBack: ImageButton;
-    private lateinit var _loader: Loader;
+    private lateinit var _loaderView: LoaderView;
 
     private lateinit var _devSets: LinearLayout;
     private lateinit var _buttonDev: MaterialButton;
@@ -43,7 +43,7 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
         _buttonBack = findViewById(R.id.button_back);
         _buttonDev = findViewById(R.id.button_dev);
         _devSets = findViewById(R.id.dev_settings);
-        _loader = findViewById(R.id.loader);
+        _loaderView = findViewById(R.id.loader);
 
         _form.onChanged.subscribe { field, value ->
             Logger.i("SettingsActivity", "Setting [${field.field?.name}] changed, saving");
@@ -70,9 +70,9 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
 
     fun reloadSettings() {
         _form.setSearchVisible(false);
-        _loader.start();
+        _loaderView.start();
         _form.fromObject(lifecycleScope, Settings.instance) {
-            _loader.stop();
+            _loaderView.stop();
             _form.setSearchVisible(true);
 
             var devCounter = 0;
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/comments/PolycentricPlatformComment.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/comments/PolycentricPlatformComment.kt
index 69c92f496e62fffa795695eba503e11bf23e7aa6..90a65e00094ec12acf9ebedfe7d19e8e6fa8d155 100644
--- a/app/src/main/java/com/futo/platformplayer/api/media/models/comments/PolycentricPlatformComment.kt
+++ b/app/src/main/java/com/futo/platformplayer/api/media/models/comments/PolycentricPlatformComment.kt
@@ -4,10 +4,7 @@ import com.futo.platformplayer.api.media.IPlatformClient
 import com.futo.platformplayer.api.media.models.PlatformAuthorLink
 import com.futo.platformplayer.api.media.models.ratings.IRating
 import com.futo.platformplayer.api.media.structures.IPager
-import com.futo.platformplayer.polycentric.PolycentricCache
-import com.futo.platformplayer.states.StatePolycentric
 import com.futo.polycentric.core.Pointer
-import com.futo.polycentric.core.SignedEvent
 import userpackage.Protocol.Reference
 import java.time.OffsetDateTime
 
@@ -20,16 +17,18 @@ class PolycentricPlatformComment : IPlatformComment {
 
     override val replyCount: Int?;
 
+    val eventPointer: Pointer;
     val reference: Reference;
 
-    constructor(contextUrl: String, author: PlatformAuthorLink, msg: String, rating: IRating, date: OffsetDateTime, reference: Reference, replyCount: Int? = null) {
+    constructor(contextUrl: String, author: PlatformAuthorLink, msg: String, rating: IRating, date: OffsetDateTime, eventPointer: Pointer, replyCount: Int? = null) {
         this.contextUrl = contextUrl;
         this.author = author;
         this.message = msg;
         this.rating = rating;
         this.date = date;
         this.replyCount = replyCount;
-        this.reference = reference;
+        this.eventPointer = eventPointer;
+        this.reference = eventPointer.toReference();
     }
 
     override fun getReplies(client: IPlatformClient): IPager<IPlatformComment> {
@@ -37,7 +36,7 @@ class PolycentricPlatformComment : IPlatformComment {
     }
 
     fun cloneWithUpdatedReplyCount(replyCount: Int?): PolycentricPlatformComment {
-        return PolycentricPlatformComment(contextUrl, author, message, rating, date, reference, replyCount);
+        return PolycentricPlatformComment(contextUrl, author, message, rating, date, eventPointer, replyCount);
     }
 
     companion object {
diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/streams/sources/HLSVariantUrlSource.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/streams/sources/HLSVariantUrlSource.kt
new file mode 100644
index 0000000000000000000000000000000000000000..36df5fb28ac25ab5892ff21cccc041ecf37d348f
--- /dev/null
+++ b/app/src/main/java/com/futo/platformplayer/api/media/models/streams/sources/HLSVariantUrlSource.kt
@@ -0,0 +1,51 @@
+package com.futo.platformplayer.api.media.models.streams.sources
+
+import android.net.Uri
+import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
+
+class HLSVariantVideoUrlSource(
+    override val name: String,
+    override val width: Int,
+    override val height: Int,
+    override val container: String,
+    override val codec: String,
+    override val bitrate: Int?,
+    override val duration: Long,
+    override val priority: Boolean,
+    val url: String
+) : IVideoUrlSource {
+    override fun getVideoUrl(): String {
+        return url
+    }
+}
+
+class HLSVariantAudioUrlSource(
+    override val name: String,
+    override val bitrate: Int,
+    override val container: String,
+    override val codec: String,
+    override val language: String,
+    override val duration: Long?,
+    override val priority: Boolean,
+    val url: String
+) : IAudioUrlSource {
+    override fun getAudioUrl(): String {
+        return url
+    }
+}
+
+class HLSVariantSubtitleUrlSource(
+    override val name: String,
+    override val url: String,
+    override val format: String,
+) : ISubtitleSource {
+    override val hasFetch: Boolean = false
+
+    override fun getSubtitles(): String? {
+        return null
+    }
+
+    override suspend fun getSubtitlesURI(): Uri? {
+        return Uri.parse(url)
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/casting/AirPlayCastingDevice.kt b/app/src/main/java/com/futo/platformplayer/casting/AirPlayCastingDevice.kt
index e8a8a57312d0af86301115713a75e4b4ece1f5a4..a6748cf2a0357d2de2fd649341d8a2c2efeb0d45 100644
--- a/app/src/main/java/com/futo/platformplayer/casting/AirPlayCastingDevice.kt
+++ b/app/src/main/java/com/futo/platformplayer/casting/AirPlayCastingDevice.kt
@@ -3,7 +3,6 @@ package com.futo.platformplayer.casting
 import android.os.Looper
 import com.futo.platformplayer.logging.Logger
 import com.futo.platformplayer.api.http.ManagedHttpClient
-import com.futo.platformplayer.casting.models.FastCastSetVolumeMessage
 import com.futo.platformplayer.getConnectedSocket
 import com.futo.platformplayer.models.CastingDeviceInfo
 import com.futo.platformplayer.toInetAddress
@@ -49,7 +48,7 @@ class AirPlayCastingDevice : CastingDevice {
             return;
         }
 
-        Logger.i(FastCastCastingDevice.TAG, "Start streaming (streamType: $streamType, contentType: $contentType, contentId: $contentId, resumePosition: $resumePosition, duration: $duration)");
+        Logger.i(FCastCastingDevice.TAG, "Start streaming (streamType: $streamType, contentType: $contentType, contentId: $contentId, resumePosition: $resumePosition, duration: $duration)");
 
         time = resumePosition;
         if (resumePosition > 0.0) {
diff --git a/app/src/main/java/com/futo/platformplayer/casting/CastingDevice.kt b/app/src/main/java/com/futo/platformplayer/casting/CastingDevice.kt
index 66a655be9c42b08b45bc3ac69651067c4bbee9f0..8beba2f2347df1800a9d69841e51613316603518 100644
--- a/app/src/main/java/com/futo/platformplayer/casting/CastingDevice.kt
+++ b/app/src/main/java/com/futo/platformplayer/casting/CastingDevice.kt
@@ -1,10 +1,15 @@
 package com.futo.platformplayer.casting
 
-import android.content.Context
-import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
 import com.futo.platformplayer.constructs.Event1
 import com.futo.platformplayer.getNowDiffMiliseconds
 import com.futo.platformplayer.models.CastingDeviceInfo
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.descriptors.PrimitiveKind
+import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
 import java.net.InetAddress
 import java.time.OffsetDateTime
 
@@ -14,10 +19,27 @@ enum class CastConnectionState {
     CONNECTED
 }
 
+@Serializable(with = CastProtocolType.CastProtocolTypeSerializer::class)
 enum class CastProtocolType {
     CHROMECAST,
     AIRPLAY,
-    FASTCAST
+    FCAST;
+
+    object CastProtocolTypeSerializer : KSerializer<CastProtocolType> {
+        override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("CastProtocolType", PrimitiveKind.STRING)
+
+        override fun serialize(encoder: Encoder, value: CastProtocolType) {
+            encoder.encodeString(value.name)
+        }
+
+        override fun deserialize(decoder: Decoder): CastProtocolType {
+            val name = decoder.decodeString()
+            return when (name) {
+                "FASTCAST" -> FCAST // Handle the renamed case
+                else -> CastProtocolType.valueOf(name)
+            }
+        }
+    }
 }
 
 abstract class CastingDevice {
diff --git a/app/src/main/java/com/futo/platformplayer/casting/ChomecastCastingDevice.kt b/app/src/main/java/com/futo/platformplayer/casting/ChomecastCastingDevice.kt
index 39b8c64022e8c08ab1d0451dce90df4bec909704..eb254b6d2e0ace0fc9ffaef7e5487623c22188e2 100644
--- a/app/src/main/java/com/futo/platformplayer/casting/ChomecastCastingDevice.kt
+++ b/app/src/main/java/com/futo/platformplayer/casting/ChomecastCastingDevice.kt
@@ -2,18 +2,16 @@ package com.futo.platformplayer.casting
 
 import android.os.Looper
 import android.util.Log
-import com.futo.platformplayer.casting.models.FastCastSetVolumeMessage
 import com.futo.platformplayer.logging.Logger
 import com.futo.platformplayer.getConnectedSocket
 import com.futo.platformplayer.models.CastingDeviceInfo
-import com.futo.platformplayer.protos.DeviceAuthMessageOuterClass
+import com.futo.platformplayer.protos.ChromeCast
 import com.futo.platformplayer.toHexString
 import com.futo.platformplayer.toInetAddress
 import kotlinx.coroutines.*
 import org.json.JSONObject
 import java.io.DataInputStream
 import java.io.DataOutputStream
-import java.io.IOException
 import java.net.InetAddress
 import java.security.cert.X509Certificate
 import javax.net.ssl.SSLContext
@@ -376,7 +374,7 @@ class ChromecastCastingDevice : CastingDevice {
                         //TODO: In the future perhaps this size-1 will cause issues, why is there a 0 on the end?
                         val messageBytes = buffer.sliceArray(IntRange(0, size - 1));
                         Log.d(TAG, "Received $size bytes: ${messageBytes.toHexString()}.");
-                        val message = DeviceAuthMessageOuterClass.CastMessage.parseFrom(messageBytes);
+                        val message = ChromeCast.CastMessage.parseFrom(messageBytes);
                         if (message.namespace != "urn:x-cast:com.google.cast.tp.heartbeat") {
                             Logger.i(TAG, "Received message: $message");
                         }
@@ -429,12 +427,12 @@ class ChromecastCastingDevice : CastingDevice {
 
     private fun sendChannelMessage(sourceId: String, destinationId: String, namespace: String, json: String) {
         try {
-            val castMessage = DeviceAuthMessageOuterClass.CastMessage.newBuilder()
-                .setProtocolVersion(DeviceAuthMessageOuterClass.CastMessage.ProtocolVersion.CASTV2_1_0)
+            val castMessage = ChromeCast.CastMessage.newBuilder()
+                .setProtocolVersion(ChromeCast.CastMessage.ProtocolVersion.CASTV2_1_0)
                 .setSourceId(sourceId)
                 .setDestinationId(destinationId)
                 .setNamespace(namespace)
-                .setPayloadType(DeviceAuthMessageOuterClass.CastMessage.PayloadType.STRING)
+                .setPayloadType(ChromeCast.CastMessage.PayloadType.STRING)
                 .setPayloadUtf8(json)
                 .build();
 
@@ -448,8 +446,8 @@ class ChromecastCastingDevice : CastingDevice {
         }
     }
 
-    private fun handleMessage(message: DeviceAuthMessageOuterClass.CastMessage) {
-        if (message.payloadType == DeviceAuthMessageOuterClass.CastMessage.PayloadType.STRING) {
+    private fun handleMessage(message: ChromeCast.CastMessage) {
+        if (message.payloadType == ChromeCast.CastMessage.PayloadType.STRING) {
             val jsonObject = JSONObject(message.payloadUtf8);
             val type = jsonObject.getString("type");
             if (type == "RECEIVER_STATUS") {
diff --git a/app/src/main/java/com/futo/platformplayer/casting/FastCastCastingDevice.kt b/app/src/main/java/com/futo/platformplayer/casting/FCastCastingDevice.kt
similarity index 95%
rename from app/src/main/java/com/futo/platformplayer/casting/FastCastCastingDevice.kt
rename to app/src/main/java/com/futo/platformplayer/casting/FCastCastingDevice.kt
index da4f8fbf881b901d88fd90b2f11af7098ba95b0d..e3524846d347821950dbed415bf02c15e94a0152 100644
--- a/app/src/main/java/com/futo/platformplayer/casting/FastCastCastingDevice.kt
+++ b/app/src/main/java/com/futo/platformplayer/casting/FCastCastingDevice.kt
@@ -30,10 +30,10 @@ enum class Opcode(val value: Byte) {
     SET_VOLUME(8)
 }
 
-class FastCastCastingDevice : CastingDevice {
+class FCastCastingDevice : CastingDevice {
     //See for more info: TODO
 
-    override val protocol: CastProtocolType get() = CastProtocolType.FASTCAST;
+    override val protocol: CastProtocolType get() = CastProtocolType.FCAST;
     override val isReady: Boolean get() = name != null && addresses != null && addresses?.isNotEmpty() == true && port != 0;
     override var usedRemoteAddress: InetAddress? = null;
     override var localAddress: InetAddress? = null;
@@ -72,7 +72,7 @@ class FastCastCastingDevice : CastingDevice {
         Logger.i(TAG, "Start streaming (streamType: $streamType, contentType: $contentType, contentId: $contentId, resumePosition: $resumePosition, duration: $duration)");
 
         time = resumePosition;
-        sendMessage(Opcode.PLAY, FastCastPlayMessage(
+        sendMessage(Opcode.PLAY, FCastPlayMessage(
             container = contentType,
             url = contentId,
             time = resumePosition.toInt()
@@ -87,7 +87,7 @@ class FastCastCastingDevice : CastingDevice {
         Logger.i(TAG, "Start streaming content (contentType: $contentType, resumePosition: $resumePosition, duration: $duration)");
 
         time = resumePosition;
-        sendMessage(Opcode.PLAY, FastCastPlayMessage(
+        sendMessage(Opcode.PLAY, FCastPlayMessage(
             container = contentType,
             content = content,
             time = resumePosition.toInt()
@@ -100,7 +100,7 @@ class FastCastCastingDevice : CastingDevice {
         }
 
         this.volume = volume
-        sendMessage(Opcode.SET_VOLUME, FastCastSetVolumeMessage(volume))
+        sendMessage(Opcode.SET_VOLUME, FCastSetVolumeMessage(volume))
     }
 
     override fun seekVideo(timeSeconds: Double) {
@@ -108,7 +108,7 @@ class FastCastCastingDevice : CastingDevice {
             return;
         }
 
-        sendMessage(Opcode.SEEK, FastCastSeekMessage(
+        sendMessage(Opcode.SEEK, FCastSeekMessage(
             time = timeSeconds.toInt()
         ));
     }
@@ -282,7 +282,7 @@ class FastCastCastingDevice : CastingDevice {
                     return;
                 }
 
-                val playbackUpdate = Json.decodeFromString<FastCastPlaybackUpdateMessage>(json);
+                val playbackUpdate = Json.decodeFromString<FCastPlaybackUpdateMessage>(json);
                 time = playbackUpdate.time.toDouble();
                 isPlaying = when (playbackUpdate.state) {
                     1 -> true
@@ -295,7 +295,7 @@ class FastCastCastingDevice : CastingDevice {
                     return;
                 }
 
-                val volumeUpdate = Json.decodeFromString<FastCastVolumeUpdateMessage>(json);
+                val volumeUpdate = Json.decodeFromString<FCastVolumeUpdateMessage>(json);
                 volume = volumeUpdate.volume;
             }
             else -> { }
@@ -398,7 +398,7 @@ class FastCastCastingDevice : CastingDevice {
     }
 
     override fun getDeviceInfo(): CastingDeviceInfo {
-        return CastingDeviceInfo(name!!, CastProtocolType.FASTCAST, addresses!!.filter { a -> a.hostAddress != null }.map { a -> a.hostAddress!!  }.toTypedArray(), port);
+        return CastingDeviceInfo(name!!, CastProtocolType.FCAST, addresses!!.filter { a -> a.hostAddress != null }.map { a -> a.hostAddress!!  }.toTypedArray(), port);
     }
 
     companion object {
diff --git a/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt b/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt
index b71094b2e8425d9a402b32c9e59292813b987ee5..f59b55ad28ba52303927f180695ad6f8ad7da0e4 100644
--- a/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt
+++ b/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt
@@ -2,8 +2,11 @@ package com.futo.platformplayer.casting
 
 import android.content.ContentResolver
 import android.content.Context
+import android.net.Uri
 import android.os.Looper
+import android.util.Base64
 import com.futo.platformplayer.*
+import com.futo.platformplayer.activities.MainActivity
 import com.futo.platformplayer.api.http.ManagedHttpClient
 import com.futo.platformplayer.api.http.server.ManagedHttpServer
 import com.futo.platformplayer.api.http.server.handlers.*
@@ -27,6 +30,9 @@ import javax.jmdns.ServiceListener
 import kotlin.collections.HashMap
 import com.futo.platformplayer.stores.CastingDeviceInfoStorage
 import com.futo.platformplayer.stores.FragmentedStorage
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.decodeFromString
+import kotlinx.serialization.json.Json
 import javax.jmdns.ServiceTypeListener
 
 class StateCasting {
@@ -147,6 +153,32 @@ class StateCasting {
         }
     }
 
+    fun handleUrl(context: Context, url: String) {
+        val uri = Uri.parse(url)
+        if (uri.scheme != "fcast") {
+            throw Exception("Expected scheme to be FCast")
+        }
+
+        val type = uri.host
+        if (type != "r") {
+            throw Exception("Expected type r")
+        }
+
+        val connectionInfo = uri.pathSegments[0]
+        val json = Base64.decode(connectionInfo, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP).toString(Charsets.UTF_8)
+        val networkConfig = Json.decodeFromString<FCastNetworkConfig>(json)
+        val tcpService = networkConfig.services.first { v -> v.type == 0 }
+
+        addRememberedDevice(CastingDeviceInfo(
+            name = networkConfig.name,
+            type = CastProtocolType.FCAST,
+            addresses = networkConfig.addresses.toTypedArray(),
+            port = tcpService.port
+        ))
+
+        UIDialogs.toast(context,"FCast device '${networkConfig.name}' added")
+    }
+
     fun onStop() {
         val ad = activeDevice ?: return;
         Logger.i(TAG, "Stopping active device because of onStop.");
@@ -345,7 +377,7 @@ class StateCasting {
             } else {
                 StateApp.instance.scope.launch(Dispatchers.IO) {
                     try {
-                        if (ad is FastCastCastingDevice) {
+                        if (ad is FCastCastingDevice) {
                             Logger.i(TAG, "Casting as DASH direct");
                             castDashDirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition);
                         } else if (ad is AirPlayCastingDevice) {
@@ -961,7 +993,7 @@ class StateCasting {
 
     private suspend fun castDashIndirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double) : List<String> {
         val ad = activeDevice ?: return listOf();
-        val proxyStreams = ad !is FastCastCastingDevice;
+        val proxyStreams = ad !is FCastCastingDevice;
 
         val url = "http://${ad.localAddress.toString().trim('/')}:${_castServer.port}";
         val id = UUID.randomUUID();
@@ -1042,8 +1074,8 @@ class StateCasting {
             CastProtocolType.AIRPLAY -> {
                 AirPlayCastingDevice(deviceInfo);
             }
-            CastProtocolType.FASTCAST -> {
-                FastCastCastingDevice(deviceInfo);
+            CastProtocolType.FCAST -> {
+                FCastCastingDevice(deviceInfo);
             }
             else -> throw Exception("${deviceInfo.type} is not a valid casting protocol")
         }
@@ -1090,8 +1122,8 @@ class StateCasting {
     }
 
     private fun addOrUpdateFastCastDevice(name: String, addresses: Array<InetAddress>, port: Int) {
-        return addOrUpdateCastDevice<FastCastCastingDevice>(name,
-            deviceFactory = { FastCastCastingDevice(name, addresses, port) },
+        return addOrUpdateCastDevice<FCastCastingDevice>(name,
+            deviceFactory = { FCastCastingDevice(name, addresses, port) },
             deviceUpdater = { d ->
                 if (d.isReady) {
                     return@addOrUpdateCastDevice false;
@@ -1167,6 +1199,19 @@ class StateCasting {
         }
     }
 
+    @Serializable
+    private data class FCastNetworkConfig(
+        val name: String,
+        val addresses: List<String>,
+        val services: List<FCastService>
+    )
+
+    @Serializable
+    private data class FCastService(
+        val port: Int,
+        val type: Int
+    )
+
     companion object {
         val instance: StateCasting = StateCasting();
 
diff --git a/app/src/main/java/com/futo/platformplayer/casting/models/FastCast.kt b/app/src/main/java/com/futo/platformplayer/casting/models/FCast.kt
similarity index 71%
rename from app/src/main/java/com/futo/platformplayer/casting/models/FastCast.kt
rename to app/src/main/java/com/futo/platformplayer/casting/models/FCast.kt
index 5b8e8272850f738224ca18805d672e5d639a46b3..64de18ba5d466fb17cbac1ae7ce60509f38d2eb6 100644
--- a/app/src/main/java/com/futo/platformplayer/casting/models/FastCast.kt
+++ b/app/src/main/java/com/futo/platformplayer/casting/models/FCast.kt
@@ -3,7 +3,7 @@ package com.futo.platformplayer.casting.models
 import kotlinx.serialization.Serializable
 
 @kotlinx.serialization.Serializable
-data class FastCastPlayMessage(
+data class FCastPlayMessage(
     val container: String,
     val url: String? = null,
     val content: String? = null,
@@ -11,23 +11,23 @@ data class FastCastPlayMessage(
 ) { }
 
 @kotlinx.serialization.Serializable
-data class FastCastSeekMessage(
+data class FCastSeekMessage(
     val time: Int
 ) { }
 
 @kotlinx.serialization.Serializable
-data class FastCastPlaybackUpdateMessage(
+data class FCastPlaybackUpdateMessage(
     val time: Int,
     val state: Int
 ) { }
 
 
 @Serializable
-data class FastCastVolumeUpdateMessage(
+data class FCastVolumeUpdateMessage(
     val volume: Double
 )
 
 @Serializable
-data class FastCastSetVolumeMessage(
+data class FCastSetVolumeMessage(
     val volume: Double
 )
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/dialogs/CastingAddDialog.kt b/app/src/main/java/com/futo/platformplayer/dialogs/CastingAddDialog.kt
index 9966e40ae2894d0b6b1f9eb5db5b0cc212c794cf..295e191a739539c20322aa68a3461d11b0b49af7 100644
--- a/app/src/main/java/com/futo/platformplayer/dialogs/CastingAddDialog.kt
+++ b/app/src/main/java/com/futo/platformplayer/dialogs/CastingAddDialog.kt
@@ -12,10 +12,7 @@ import com.futo.platformplayer.UIDialogs
 import com.futo.platformplayer.casting.CastProtocolType
 import com.futo.platformplayer.casting.StateCasting
 import com.futo.platformplayer.models.CastingDeviceInfo
-import com.futo.platformplayer.states.StateApp
 import com.futo.platformplayer.toInetAddress
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
 
 
 class CastingAddDialog(context: Context?) : AlertDialog(context) {
@@ -26,6 +23,7 @@ class CastingAddDialog(context: Context?) : AlertDialog(context) {
     private lateinit var _textError: TextView;
     private lateinit var _buttonCancel: Button;
     private lateinit var _buttonConfirm: LinearLayout;
+    private lateinit var _buttonTutorial: TextView;
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState);
@@ -38,6 +36,7 @@ class CastingAddDialog(context: Context?) : AlertDialog(context) {
         _textError = findViewById(R.id.text_error);
         _buttonCancel = findViewById(R.id.button_cancel);
         _buttonConfirm = findViewById(R.id.button_confirm);
+        _buttonTutorial = findViewById(R.id.button_tutorial)
 
         ArrayAdapter.createFromResource(context, R.array.casting_device_type_array, R.layout.spinner_item_simple).also { adapter ->
             adapter.setDropDownViewResource(R.layout.spinner_dropdownitem_simple);
@@ -62,7 +61,7 @@ class CastingAddDialog(context: Context?) : AlertDialog(context) {
 
         _buttonConfirm.setOnClickListener {
             val castProtocolType: CastProtocolType = when (_spinnerType.selectedItemPosition) {
-                0 -> CastProtocolType.FASTCAST
+                0 -> CastProtocolType.FCAST
                 1 -> CastProtocolType.CHROMECAST
                 2 -> CastProtocolType.AIRPLAY
                 else -> {
@@ -105,6 +104,11 @@ class CastingAddDialog(context: Context?) : AlertDialog(context) {
             StateCasting.instance.addRememberedDevice(castingDeviceInfo);
             performDismiss();
         };
+
+        _buttonTutorial.setOnClickListener {
+            UIDialogs.showCastingTutorialDialog(context)
+            dismiss()
+        }
     }
 
     override fun show() {
diff --git a/app/src/main/java/com/futo/platformplayer/dialogs/CastingHelpDialog.kt b/app/src/main/java/com/futo/platformplayer/dialogs/CastingHelpDialog.kt
new file mode 100644
index 0000000000000000000000000000000000000000..9f305b18fbe73d8f65a5cf21fc6b02affb96a594
--- /dev/null
+++ b/app/src/main/java/com/futo/platformplayer/dialogs/CastingHelpDialog.kt
@@ -0,0 +1,63 @@
+package com.futo.platformplayer.dialogs
+
+import android.app.AlertDialog
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.view.LayoutInflater
+import com.futo.platformplayer.R
+import com.futo.platformplayer.UIDialogs
+import com.futo.platformplayer.activities.FCastGuideActivity
+import com.futo.platformplayer.activities.PolycentricWhyActivity
+import com.futo.platformplayer.logging.Logger
+import com.futo.platformplayer.views.buttons.BigButton
+
+
+class CastingHelpDialog(context: Context?) : AlertDialog(context) {
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState);
+        setContentView(LayoutInflater.from(context).inflate(R.layout.dialog_casting_help, null));
+
+        findViewById<BigButton>(R.id.button_guide).onClick.subscribe {
+            context.startActivity(Intent(context, FCastGuideActivity::class.java))
+        }
+
+        findViewById<BigButton>(R.id.button_video).onClick.subscribe {
+            try {
+                //TODO: Replace the URL with the casting video URL
+                val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://fcast.org/"))
+                context.startActivity(browserIntent);
+            } catch (e: Throwable) {
+                Logger.i(TAG, "Failed to open browser.", e)
+            }
+        }
+
+        findViewById<BigButton>(R.id.button_website).onClick.subscribe {
+            try {
+                val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://fcast.org/"))
+                context.startActivity(browserIntent);
+            } catch (e: Throwable) {
+                Logger.i(TAG, "Failed to open browser.", e)
+            }
+        }
+
+        findViewById<BigButton>(R.id.button_technical).onClick.subscribe {
+            try {
+                val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://gitlab.com/futo-org/fcast/-/wikis/Protocol-version-1"))
+                context.startActivity(browserIntent);
+            } catch (e: Throwable) {
+                Logger.i(TAG, "Failed to open browser.", e)
+            }
+        }
+
+        findViewById<BigButton>(R.id.button_close).onClick.subscribe {
+            dismiss()
+            UIDialogs.showCastingAddDialog(context)
+        }
+    }
+
+    companion object {
+        private val TAG = "CastingTutorialDialog";
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/dialogs/CommentDialog.kt b/app/src/main/java/com/futo/platformplayer/dialogs/CommentDialog.kt
index 584c8465f75000479a15cfc9876371dfa966e6d4..cc9015ebc4642556f10a0699dc730397a49f1e40 100644
--- a/app/src/main/java/com/futo/platformplayer/dialogs/CommentDialog.kt
+++ b/app/src/main/java/com/futo/platformplayer/dialogs/CommentDialog.kt
@@ -118,7 +118,7 @@ class CommentDialog(context: Context?, val contextUrl: String, val ref: Protocol
                 msg = comment,
                 rating = RatingLikeDislikes(0, 0),
                 date = OffsetDateTime.now(),
-                reference = eventPointer.toReference()
+                eventPointer = eventPointer
             ));
 
             dismiss();
diff --git a/app/src/main/java/com/futo/platformplayer/dialogs/ConnectCastingDialog.kt b/app/src/main/java/com/futo/platformplayer/dialogs/ConnectCastingDialog.kt
index dca3809140369dd2bc05d4e55b030a44161cdb65..8f13545c2ee29ce615471a8707a87f7a037b29f4 100644
--- a/app/src/main/java/com/futo/platformplayer/dialogs/ConnectCastingDialog.kt
+++ b/app/src/main/java/com/futo/platformplayer/dialogs/ConnectCastingDialog.kt
@@ -1,24 +1,33 @@
 package com.futo.platformplayer.dialogs
 
+import android.app.Activity
 import android.app.AlertDialog
 import android.content.Context
+import android.content.Intent
 import android.graphics.drawable.Animatable
+import android.net.Uri
 import android.os.Bundle
 import android.view.LayoutInflater
 import android.view.View
 import android.widget.Button
 import android.widget.ImageView
 import android.widget.TextView
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts
 import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.RecyclerView
 import com.futo.platformplayer.logging.Logger
 import com.futo.platformplayer.R
 import com.futo.platformplayer.UIDialogs
+import com.futo.platformplayer.activities.AddSourceActivity
+import com.futo.platformplayer.activities.MainActivity
+import com.futo.platformplayer.activities.QRCaptureActivity
 import com.futo.platformplayer.casting.CastConnectionState
 import com.futo.platformplayer.casting.CastingDevice
 import com.futo.platformplayer.casting.StateCasting
 import com.futo.platformplayer.states.StateApp
 import com.futo.platformplayer.views.adapters.DeviceAdapter
+import com.google.zxing.integration.android.IntentIntegrator
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.GlobalScope
 import kotlinx.coroutines.launch
@@ -28,6 +37,7 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
     private lateinit var _imageLoader: ImageView;
     private lateinit var _buttonClose: Button;
     private lateinit var _buttonAdd: Button;
+    private lateinit var _buttonScanQR: Button;
     private lateinit var _textNoDevicesFound: TextView;
     private lateinit var _textNoDevicesRemembered: TextView;
     private lateinit var _recyclerDevices: RecyclerView;
@@ -44,6 +54,7 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
         _imageLoader = findViewById(R.id.image_loader);
         _buttonClose = findViewById(R.id.button_close);
         _buttonAdd = findViewById(R.id.button_add);
+        _buttonScanQR = findViewById(R.id.button_scan_qr);
         _recyclerDevices = findViewById(R.id.recycler_devices);
         _recyclerRememberedDevices = findViewById(R.id.recycler_remembered_devices);
         _textNoDevicesFound = findViewById(R.id.text_no_devices_found);
@@ -77,6 +88,17 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
             UIDialogs.showCastingAddDialog(context);
             dismiss();
         };
+
+        val c = ownerActivity
+        if (c is MainActivity) {
+            _buttonScanQR.visibility = View.VISIBLE
+            _buttonScanQR.setOnClickListener {
+                c.showUrlQrCodeScanner()
+                dismiss()
+            };
+        } else {
+            _buttonScanQR.visibility = View.GONE
+        }
     }
 
     override fun show() {
diff --git a/app/src/main/java/com/futo/platformplayer/dialogs/ConnectedCastingDialog.kt b/app/src/main/java/com/futo/platformplayer/dialogs/ConnectedCastingDialog.kt
index 612c7a8ca4b5759c528ab05deeac58e179c95184..619db900c157f57913d89899470a508fbbf4e6a9 100644
--- a/app/src/main/java/com/futo/platformplayer/dialogs/ConnectedCastingDialog.kt
+++ b/app/src/main/java/com/futo/platformplayer/dialogs/ConnectedCastingDialog.kt
@@ -16,9 +16,7 @@ import com.futo.platformplayer.casting.*
 import com.futo.platformplayer.states.StateApp
 import com.google.android.material.slider.Slider
 import com.google.android.material.slider.Slider.OnChangeListener
-import com.google.android.material.slider.Slider.OnSliderTouchListener
 import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.GlobalScope
 import kotlinx.coroutines.launch
 
 class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
@@ -105,7 +103,7 @@ class ConnectedCastingDialog(context: Context?) : AlertDialog(context) {
         } else if (d is AirPlayCastingDevice) {
             _imageDevice.setImageResource(R.drawable.ic_airplay);
             _textType.text = "AirPlay";
-        } else if (d is FastCastCastingDevice) {
+        } else if (d is FCastCastingDevice) {
             _imageDevice.setImageResource(R.drawable.ic_fc);
             _textType.text = "FastCast";
         }
diff --git a/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt b/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt
index 7f082407f6050d0a08bb877ad7e2bd460898092e..048e36d39cf284ed9c6e245e7cbf47075faa91d4 100644
--- a/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt
+++ b/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt
@@ -1,11 +1,17 @@
 package com.futo.platformplayer.downloads
 
+import android.content.Context
+import android.util.Log
+import com.arthenica.ffmpegkit.FFmpegKit
+import com.arthenica.ffmpegkit.ReturnCode
+import com.arthenica.ffmpegkit.StatisticsCallback
 import com.futo.platformplayer.Settings
 import com.futo.platformplayer.logging.Logger
 import com.futo.platformplayer.states.StateDownloads
 import com.futo.platformplayer.states.StatePlatform
 import com.futo.platformplayer.api.http.ManagedHttpClient
 import com.futo.platformplayer.api.media.PlatformID
+import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
 import com.futo.platformplayer.api.media.models.streams.sources.*
 import com.futo.platformplayer.api.media.models.streams.sources.other.IStreamMetaDataSource
 import com.futo.platformplayer.api.media.models.video.IPlatformVideo
@@ -18,22 +24,28 @@ import com.futo.platformplayer.hasAnySource
 import com.futo.platformplayer.helpers.FileHelper.Companion.sanitizeFileName
 import com.futo.platformplayer.helpers.VideoHelper
 import com.futo.platformplayer.isDownloadable
+import com.futo.platformplayer.parsers.HLS
 import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
-import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
 import com.futo.platformplayer.toHumanBitrate
 import com.futo.platformplayer.toHumanBytesSpeed
 import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.async
 import kotlinx.coroutines.awaitAll
 import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withContext
 import java.io.File
 import java.io.FileOutputStream
 import java.io.IOException
 import java.time.OffsetDateTime
+import java.util.UUID
+import java.util.concurrent.Executors
 import java.util.concurrent.ForkJoinPool
 import java.util.concurrent.ForkJoinTask
 import java.util.concurrent.ThreadLocalRandom
+import kotlin.coroutines.resumeWithException
 
 @kotlinx.serialization.Serializable
 class VideoDownload {
@@ -137,7 +149,7 @@ class VideoDownload {
         return items.joinToString(" • ");
     }
 
-    suspend fun prepare() {
+    suspend fun prepare(client: ManagedHttpClient) {
         Logger.i(TAG, "VideoDownload Prepare [${name}]");
         if(video == null && videoDetails == null)
             throw IllegalStateException("Missing information for download to complete");
@@ -157,24 +169,65 @@ class VideoDownload {
 
             videoDetails = SerializedPlatformVideoDetails.fromVideo(original, if (subtitleSource != null) listOf(subtitleSource!!) else listOf());
             if(videoSource == null && targetPixelCount != null) {
-                val vsource = VideoHelper.selectBestVideoSource(videoDetails!!.video, targetPixelCount!!.toInt(), arrayOf())
+                val videoSources = arrayListOf<IVideoSource>()
+                for (source in original.video.videoSources) {
+                    if (source is IHLSManifestSource) {
+                        try {
+                            val playlistResponse = client.get(source.url)
+                            if (playlistResponse.isOk) {
+                                val playlistContent = playlistResponse.body?.string()
+                                if (playlistContent != null) {
+                                    videoSources.addAll(HLS.parseAndGetVideoSources(source, playlistContent, source.url))
+                                }
+                            }
+                        } catch (e: Throwable) {
+                            Log.i(TAG, "Failed to get HLS video sources", e)
+                        }
+                    } else {
+                        videoSources.add(source)
+                    }
+                }
+
+                val vsource = VideoHelper.selectBestVideoSource(videoSources, targetPixelCount!!.toInt(), arrayOf())
                 //    ?: throw IllegalStateException("Could not find a valid video source for video");
                 if(vsource != null) {
                     if (vsource is IVideoUrlSource)
-                        videoSource = VideoUrlSource.fromUrlSource(vsource);
+                        videoSource = VideoUrlSource.fromUrlSource(vsource)
                     else
                         throw DownloadException("Video source is not supported for downloading (yet)", false);
                 }
             }
 
             if(audioSource == null && targetBitrate != null) {
-                val asource = VideoHelper.selectBestAudioSource(videoDetails!!.video, arrayOf(), null, targetPixelCount)
+                val audioSources = arrayListOf<IAudioSource>()
+                val video = original.video
+                if (video is VideoUnMuxedSourceDescriptor) {
+                    for (source in video.audioSources) {
+                        if (source is IHLSManifestSource) {
+                            try {
+                                val playlistResponse = client.get(source.url)
+                                if (playlistResponse.isOk) {
+                                    val playlistContent = playlistResponse.body?.string()
+                                    if (playlistContent != null) {
+                                        audioSources.addAll(HLS.parseAndGetAudioSources(source, playlistContent, source.url))
+                                    }
+                                }
+                            } catch (e: Throwable) {
+                                Log.i(TAG, "Failed to get HLS audio sources", e)
+                            }
+                        } else {
+                            audioSources.add(source)
+                        }
+                    }
+                }
+
+                val asource = VideoHelper.selectBestAudioSource(audioSources, arrayOf(), null, targetBitrate)
                     ?: if(videoSource != null ) null
                     else throw DownloadException("Could not find a valid video or audio source for download")
                 if(asource == null)
                     audioSource = null;
                 else if(asource is IAudioUrlSource)
-                    audioSource = AudioUrlSource.fromUrlSource(asource);
+                    audioSource = AudioUrlSource.fromUrlSource(asource)
                 else
                     throw DownloadException("Audio source is not supported for downloading (yet)", false);
             }
@@ -183,7 +236,8 @@ class VideoDownload {
                 throw DownloadException("No valid sources found for video/audio");
         }
     }
-    suspend fun download(client: ManagedHttpClient, onProgress: ((Double) -> Unit)? = null) = coroutineScope {
+
+    suspend fun download(context: Context, client: ManagedHttpClient, onProgress: ((Double) -> Unit)? = null) = coroutineScope {
         Logger.i(TAG, "VideoDownload Download [${name}]");
         if(videoDetails == null || (videoSource == null && audioSource == null))
             throw IllegalStateException("Missing information for download to complete");
@@ -199,7 +253,7 @@ class VideoDownload {
             videoFilePath = File(downloadDir, videoFileName!!).absolutePath;
         }
         if(audioSource != null) {
-            audioFileName = "${videoDetails!!.id.value!!} [${audioSource!!.bitrate}].${audioContainerToExtension(audioSource!!.container)}".sanitizeFileName();
+            audioFileName = "${videoDetails!!.id.value!!} [${audioSource!!.language}-${audioSource!!.bitrate}].${audioContainerToExtension(audioSource!!.container)}".sanitizeFileName();
             audioFilePath = File(downloadDir, audioFileName!!).absolutePath;
         }
         if(subtitleSource != null) {
@@ -217,7 +271,8 @@ class VideoDownload {
         if(videoSource != null) {
             sourcesToDownload.add(async {
                 Logger.i(TAG, "Started downloading video");
-                videoFileSize = downloadSource("Video", client, videoSource!!.getVideoUrl(), File(downloadDir, videoFileName!!)) { length, totalRead, speed ->
+
+                val progressCallback = { length: Long, totalRead: Long, speed: Long ->
                     synchronized(progressLock) {
                         lastVideoLength = length;
                         lastVideoRead = totalRead;
@@ -235,12 +290,18 @@ class VideoDownload {
                         }
                     }
                 }
+
+                videoFileSize = when (videoSource!!.container) {
+                    "application/vnd.apple.mpegurl" -> downloadHlsSource(context, "Video", client, videoSource!!.getVideoUrl(), File(downloadDir, videoFileName!!), progressCallback)
+                    else -> downloadFileSource("Video", client, videoSource!!.getVideoUrl(), File(downloadDir, videoFileName!!), progressCallback)
+                }
             });
         }
         if(audioSource != null) {
             sourcesToDownload.add(async {
                 Logger.i(TAG, "Started downloading audio");
-                audioFileSize = downloadSource("Audio", client, audioSource!!.getAudioUrl(), File(downloadDir, audioFileName!!)) { length, totalRead, speed ->
+
+                val progressCallback = { length: Long, totalRead: Long, speed: Long ->
                     synchronized(progressLock) {
                         lastAudioLength = length;
                         lastAudioRead = totalRead;
@@ -258,6 +319,11 @@ class VideoDownload {
                         }
                     }
                 }
+
+                audioFileSize = when (audioSource!!.container) {
+                    "application/vnd.apple.mpegurl" -> downloadHlsSource(context, "Audio", client, audioSource!!.getAudioUrl(), File(downloadDir, audioFileName!!), progressCallback)
+                    else -> downloadFileSource("Audio", client, audioSource!!.getAudioUrl(), File(downloadDir, audioFileName!!), progressCallback)
+                }
             });
         }
         if (subtitleSource != null) {
@@ -279,7 +345,105 @@ class VideoDownload {
             throw ex;
         }
     }
-    private fun downloadSource(name: String, client: ManagedHttpClient, videoUrl: String, targetFile: File, onProgress: (Long, Long, Long) -> Unit): Long {
+
+    private suspend fun downloadHlsSource(context: Context, name: String, client: ManagedHttpClient, hlsUrl: String, targetFile: File, onProgress: (Long, Long, Long) -> Unit): Long {
+        if(targetFile.exists())
+            targetFile.delete();
+
+        var downloadedTotalLength = 0L
+
+        val segmentFiles = arrayListOf<File>()
+        try {
+            val response = client.get(hlsUrl)
+            check(response.isOk) { "Failed to get variant playlist: ${response.code}" }
+
+            val vpContent = response.body?.string()
+                ?: throw Exception("Variant playlist content is empty")
+
+            val variantPlaylist = HLS.parseVariantPlaylist(vpContent, hlsUrl)
+            variantPlaylist.segments.forEachIndexed { index, segment ->
+                if (segment !is HLS.MediaSegment) {
+                    return@forEachIndexed
+                }
+
+                Logger.i(TAG, "Download '$name' segment $index Sequential");
+                val segmentFile = File(context.cacheDir, "segment-${UUID.randomUUID()}")
+                segmentFiles.add(segmentFile)
+
+                val segmentLength = downloadSource_Sequential(client, segmentFile.outputStream(), segment.uri) { segmentLength, totalRead, lastSpeed ->
+                    val averageSegmentLength = if (index == 0) segmentLength else downloadedTotalLength / index
+                    val expectedTotalLength = averageSegmentLength * (variantPlaylist.segments.size - 1) + segmentLength
+                    onProgress(expectedTotalLength, downloadedTotalLength + totalRead, lastSpeed)
+                }
+
+                downloadedTotalLength += segmentLength
+            }
+
+            Logger.i(TAG, "Combining segments into $targetFile");
+            combineSegments(context, segmentFiles, targetFile)
+
+            Logger.i(TAG, "${name} downloadSource Finished");
+        }
+        catch(ioex: IOException) {
+            if(targetFile.exists() ?: false)
+                targetFile.delete();
+            if(ioex.message?.contains("ENOSPC") ?: false)
+                throw Exception("Not enough space on device", ioex);
+            else
+                throw ioex;
+        }
+        catch(ex: Throwable) {
+            if(targetFile.exists() ?: false)
+                targetFile.delete();
+            throw ex;
+        }
+        finally {
+            for (segmentFile in segmentFiles) {
+                segmentFile.delete()
+            }
+        }
+        return downloadedTotalLength;
+    }
+
+    private suspend fun combineSegments(context: Context, segmentFiles: List<File>, targetFile: File) = withContext(Dispatchers.IO) {
+        suspendCancellableCoroutine { continuation ->
+            val fileList = File(context.cacheDir, "fileList-${UUID.randomUUID()}.txt")
+            fileList.writeText(segmentFiles.joinToString("\n") { "file '${it.absolutePath}'" })
+
+            val cmd = "-f concat -safe 0 -i \"${fileList.absolutePath}\" -c copy \"${targetFile.absolutePath}\""
+
+            val statisticsCallback = StatisticsCallback { statistics ->
+                //TODO: Show progress?
+            }
+
+            val executorService = Executors.newSingleThreadExecutor()
+            val session = FFmpegKit.executeAsync(cmd,
+                { session ->
+                    if (ReturnCode.isSuccess(session.returnCode)) {
+                        fileList.delete()
+                        continuation.resumeWith(Result.success(Unit))
+                    } else {
+                        val errorMessage = if (ReturnCode.isCancel(session.returnCode)) {
+                            "Command cancelled"
+                        } else {
+                            "Command failed with state '${session.state}' and return code ${session.returnCode}, stack trace ${session.failStackTrace}"
+                        }
+                        fileList.delete()
+                        continuation.resumeWithException(RuntimeException(errorMessage))
+                    }
+                },
+                { Logger.v(TAG, it.message) },
+                statisticsCallback,
+                executorService
+            )
+
+            continuation.invokeOnCancellation {
+                session.cancel()
+            }
+        }
+    }
+
+    private fun downloadFileSource(name: String, client: ManagedHttpClient, videoUrl: String, targetFile: File, onProgress: (Long, Long, Long) -> Unit): Long {
         if(targetFile.exists())
             targetFile.delete();
 
@@ -472,8 +636,10 @@ class VideoDownload {
             val expectedFile = File(videoFilePath!!);
             if(!expectedFile.exists())
                 throw IllegalStateException("Video file missing after download");
-            if(expectedFile.length() != videoFileSize)
-                throw IllegalStateException("Expected size [${videoFileSize}], but found ${expectedFile.length()}");
+            if (videoSource?.container != "application/vnd.apple.mpegurl") {
+                if (expectedFile.length() != videoFileSize)
+                    throw IllegalStateException("Expected size [${videoFileSize}], but found ${expectedFile.length()}");
+            }
         }
         if(audioSource != null) {
             if(audioFilePath == null)
@@ -481,8 +647,10 @@ class VideoDownload {
             val expectedFile = File(audioFilePath!!);
             if(!expectedFile.exists())
                 throw IllegalStateException("Audio file missing after download");
-            if(expectedFile.length() != audioFileSize)
-                throw IllegalStateException("Expected size [${audioFileSize}], but found ${expectedFile.length()}");
+            if (audioSource?.container != "application/vnd.apple.mpegurl") {
+                if (expectedFile.length() != audioFileSize)
+                    throw IllegalStateException("Expected size [${audioFileSize}], but found ${expectedFile.length()}");
+            }
         }
         if(subtitleSource != null) {
             if(subtitleFilePath == null)
@@ -560,7 +728,7 @@ class VideoDownload {
         const val GROUP_PLAYLIST = "Playlist";
 
         fun videoContainerToExtension(container: String): String? {
-            if (container.contains("video/mp4"))
+            if (container.contains("video/mp4") || container == "application/vnd.apple.mpegurl")
                 return "mp4";
             else if (container.contains("application/x-mpegURL"))
                 return "m3u8";
@@ -585,6 +753,8 @@ class VideoDownload {
                 return "mp3";
             else if (container.contains("audio/webm"))
                 return "webma";
+            else if (container == "application/vnd.apple.mpegurl")
+                return "mp4";
             else
                 return "audio";
         }
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt
index 6d7b899178ce7ddb3003b4d0790a59f633897fa0..f2e420b9e32e8725dda4683dfd9a610df44dead9 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt
@@ -351,6 +351,7 @@ class MenuBottomBarFragment : MainActivityFragment() {
             ButtonDefinition(4, R.drawable.ic_playlist, R.drawable.ic_playlist_filled, R.string.playlists, canToggle = false, { it.currentMain is PlaylistsFragment }, { it.navigate<PlaylistsFragment>() }),
             ButtonDefinition(5, R.drawable.ic_history, R.drawable.ic_history, R.string.history, canToggle = false, { it.currentMain is HistoryFragment }, { it.navigate<HistoryFragment>() }),
             ButtonDefinition(6, R.drawable.ic_download, R.drawable.ic_download, R.string.downloads, canToggle = false, { it.currentMain is DownloadsFragment }, { it.navigate<DownloadsFragment>() }),
+            ButtonDefinition(8, R.drawable.ic_chat, R.drawable.ic_chat_filled, R.string.comments, canToggle = true, { it.currentMain is CommentsFragment }, { it.navigate<CommentsFragment>() }),
             ButtonDefinition(7, R.drawable.ic_settings, R.drawable.ic_settings, R.string.settings, canToggle = false, { false }, {
                 val c = it.context ?: return@ButtonDefinition;
                 Logger.i(TAG, "settings preventPictureInPicture()");
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/BuyFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/BuyFragment.kt
index e0d13b28a3dabf99e8a0218d1a9d0c7669088daf..ca970cfb885c90bb94497bbe040ed937278d5712 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/BuyFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/BuyFragment.kt
@@ -35,6 +35,11 @@ class BuyFragment : MainFragment() {
         return view;
     }
 
+    override fun onDestroyMainView() {
+        super.onDestroyMainView()
+        _view = null
+    }
+
     class BuyView: LinearLayout {
         private val _fragment: BuyFragment;
 
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/CommentsFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/CommentsFragment.kt
new file mode 100644
index 0000000000000000000000000000000000000000..8e03e4e446aad256efc3c36c2193ffbbfaa0be41
--- /dev/null
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/CommentsFragment.kt
@@ -0,0 +1,316 @@
+package com.futo.platformplayer.fragment.mainactivity.main
+
+import android.content.Intent
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewPropertyAnimator
+import android.widget.AdapterView
+import android.widget.ArrayAdapter
+import android.widget.FrameLayout
+import android.widget.LinearLayout
+import android.widget.Spinner
+import android.widget.TextView
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.futo.platformplayer.R
+import com.futo.platformplayer.UIDialogs
+import com.futo.platformplayer.activities.PolycentricHomeActivity
+import com.futo.platformplayer.api.media.models.comments.IPlatformComment
+import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
+import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
+import com.futo.platformplayer.constructs.TaskHandler
+import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
+import com.futo.platformplayer.logging.Logger
+import com.futo.platformplayer.states.StateApp
+import com.futo.platformplayer.states.StatePlatform
+import com.futo.platformplayer.states.StatePolycentric
+import com.futo.platformplayer.views.adapters.CommentWithReferenceViewHolder
+import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
+import com.futo.platformplayer.views.overlays.RepliesOverlay
+import com.futo.polycentric.core.PublicKey
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import java.net.UnknownHostException
+import java.util.IdentityHashMap
+
+class CommentsFragment : MainFragment() {
+    override val isMainView : Boolean = true
+    override val isTab: Boolean = true
+    override val hasBottomBar: Boolean get() = true
+
+    private var _view: CommentsView? = null
+
+    override fun onShownWithView(parameter: Any?, isBack: Boolean) {
+        super.onShownWithView(parameter, isBack)
+        _view?.onShown()
+    }
+
+    override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+        val view = CommentsView(this, inflater)
+        _view = view
+        return view
+    }
+
+    override fun onDestroyMainView() {
+        super.onDestroyMainView()
+        _view = null
+    }
+
+    override fun onBackPressed(): Boolean {
+        return _view?.onBackPressed() ?: false
+    }
+
+    override fun onResume() {
+        super.onResume()
+        _view?.onShown()
+    }
+
+    companion object {
+        fun newInstance() = CommentsFragment().apply {}
+        private const val TAG = "CommentsFragment"
+    }
+
+    class CommentsView : FrameLayout {
+        private val _fragment: CommentsFragment
+        private val _recyclerComments: RecyclerView;
+        private val _adapterComments: InsertedViewAdapterWithLoader<CommentWithReferenceViewHolder>;
+        private val _textCommentCount: TextView
+        private val _comments: ArrayList<IPlatformComment> = arrayListOf();
+        private val _llmReplies: LinearLayoutManager;
+        private val _spinnerSortBy: Spinner;
+        private val _layoutNotLoggedIn: LinearLayout;
+        private val _buttonLogin: LinearLayout;
+        private var _loading = false;
+        private val _repliesOverlay: RepliesOverlay;
+        private var _repliesAnimator: ViewPropertyAnimator? = null;
+        private val _cache: IdentityHashMap<IPlatformComment, StatePolycentric.LikesDislikesReplies> = IdentityHashMap()
+
+        private val _taskLoadComments = if(!isInEditMode) TaskHandler<PublicKey, List<IPlatformComment>>(
+            StateApp.instance.scopeGetter, { StatePolycentric.instance.getSystemComments(context, it) })
+            .success { pager -> onCommentsLoaded(pager); }
+            .exception<UnknownHostException> {
+                UIDialogs.toast("Failed to load comments");
+                setLoading(false);
+            }
+            .exception<Throwable> {
+                Logger.e(TAG, "Failed to load comments.", it);
+                UIDialogs.toast(context, context.getString(R.string.failed_to_load_comments) + "\n" + (it.message ?: ""));
+                setLoading(false);
+            } else TaskHandler(IPlatformVideoDetails::class.java, StateApp.instance.scopeGetter);
+
+        constructor(fragment: CommentsFragment, inflater: LayoutInflater) : super(inflater.context) {
+            _fragment = fragment
+            inflater.inflate(R.layout.fragment_comments, this)
+
+            val commentHeader = findViewById<LinearLayout>(R.id.layout_header)
+            (commentHeader.parent as ViewGroup).removeView(commentHeader)
+            _textCommentCount = commentHeader.findViewById(R.id.text_comment_count)
+
+            _recyclerComments = findViewById(R.id.recycler_comments)
+            _adapterComments = InsertedViewAdapterWithLoader(context, arrayListOf(commentHeader), arrayListOf(),
+                childCountGetter = { _comments.size },
+                childViewHolderBinder = { viewHolder, position -> viewHolder.bind(_comments[position]); },
+                childViewHolderFactory = { viewGroup, _ ->
+                    val holder = CommentWithReferenceViewHolder(viewGroup, _cache);
+                    holder.onDelete.subscribe(::onDelete);
+                    holder.onRepliesClick.subscribe(::onRepliesClick);
+                    return@InsertedViewAdapterWithLoader holder;
+                }
+            );
+
+            _spinnerSortBy = commentHeader.findViewById(R.id.spinner_sortby);
+            _spinnerSortBy.adapter = ArrayAdapter(context, R.layout.spinner_item_simple, resources.getStringArray(R.array.comments_sortby_array)).also {
+                it.setDropDownViewResource(R.layout.spinner_dropdownitem_simple);
+            };
+            _spinnerSortBy.setSelection(0);
+            _spinnerSortBy.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
+                override fun onItemSelected(parent: AdapterView<*>, view: View?, pos: Int, id: Long) {
+                    if (_spinnerSortBy.selectedItemPosition == 0) {
+                        _comments.sortByDescending { it.date!! }
+                    } else if (_spinnerSortBy.selectedItemPosition == 1) {
+                        _comments.sortBy { it.date!! }
+                    }
+
+                    _adapterComments.notifyDataSetChanged()
+                }
+                override fun onNothingSelected(parent: AdapterView<*>?) = Unit
+            }
+
+            _llmReplies = LinearLayoutManager(context);
+            _recyclerComments.layoutManager = _llmReplies;
+            _recyclerComments.adapter = _adapterComments;
+            updateCommentCountString();
+
+            _layoutNotLoggedIn = findViewById(R.id.layout_not_logged_in)
+            _layoutNotLoggedIn.visibility = View.GONE
+
+            _buttonLogin = findViewById(R.id.button_login)
+            _buttonLogin.setOnClickListener {
+                context.startActivity(Intent(context, PolycentricHomeActivity::class.java));
+            }
+
+            _repliesOverlay = findViewById(R.id.replies_overlay);
+            _repliesOverlay.onClose.subscribe { setRepliesOverlayVisible(isVisible = false, animate = true); };
+        }
+
+        private fun onDelete(comment: IPlatformComment) {
+            UIDialogs.showConfirmationDialog(context, "Are you sure you want to delete this comment?", {
+                val processHandle = StatePolycentric.instance.processHandle ?: return@showConfirmationDialog
+                if (comment !is PolycentricPlatformComment) {
+                    return@showConfirmationDialog
+                }
+
+                val index = _comments.indexOf(comment)
+                if (index != -1) {
+                    _comments.removeAt(index)
+                    _adapterComments.notifyItemRemoved(_adapterComments.childToParentPosition(index))
+
+                    StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
+                        try {
+                            processHandle.delete(comment.eventPointer.process, comment.eventPointer.logicalClock)
+                        } catch (e: Throwable) {
+                            Logger.e(TAG, "Failed to delete event.", e);
+                            return@launch
+                        }
+
+                        try {
+                            Logger.i(TAG, "Started backfill");
+                            processHandle.fullyBackfillServersAnnounceExceptions();
+                            Logger.i(TAG, "Finished backfill");
+                        } catch (e: Throwable) {
+                            Logger.e(TAG, "Failed to fully backfill servers.", e);
+                        }
+                    }
+                }
+            })
+        }
+
+        fun onBackPressed(): Boolean {
+            if (_repliesOverlay.visibility == View.VISIBLE) {
+                setRepliesOverlayVisible(isVisible = false, animate = true);
+                return true
+            }
+
+            return false
+        }
+
+        private fun onRepliesClick(c: IPlatformComment) {
+            val replyCount = c.replyCount ?: 0;
+            var metadata = "";
+            if (replyCount > 0) {
+                metadata += "$replyCount " + context.getString(R.string.replies);
+            }
+
+            if (c is PolycentricPlatformComment) {
+                _repliesOverlay.load(false, metadata, c.contextUrl, c.reference, c,
+                    { StatePolycentric.instance.getCommentPager(c.contextUrl, c.reference) },
+                    { newComment ->
+                        synchronized(_cache) {
+                            _cache.remove(c)
+                        }
+
+                        val newCommentIndex = if (_spinnerSortBy.selectedItemPosition == 0) {
+                            _comments.indexOfFirst { it.date!! < newComment.date!! }.takeIf { it != -1 } ?: _comments.size
+                        } else {
+                            _comments.indexOfFirst { it.date!! > newComment.date!! }.takeIf { it != -1 } ?: _comments.size
+                        }
+
+                        _comments.add(newCommentIndex, newComment)
+                        _adapterComments.notifyItemInserted(_adapterComments.childToParentPosition(newCommentIndex))
+                    });
+            } else {
+                _repliesOverlay.load(true, metadata, null, null, c, { StatePlatform.instance.getSubComments(c) });
+            }
+
+            setRepliesOverlayVisible(isVisible = true, animate = true);
+        }
+
+        private fun setRepliesOverlayVisible(isVisible: Boolean, animate: Boolean) {
+            val desiredVisibility = if (isVisible) View.VISIBLE else View.GONE
+            if (_repliesOverlay.visibility == desiredVisibility) {
+                return;
+            }
+
+            _repliesAnimator?.cancel();
+
+            if (isVisible) {
+                _repliesOverlay.visibility = View.VISIBLE;
+
+                if (animate) {
+                    _repliesOverlay.translationY = _repliesOverlay.height.toFloat();
+
+                    _repliesAnimator = _repliesOverlay.animate()
+                        .setDuration(300)
+                        .translationY(0f)
+                        .withEndAction {
+                            _repliesAnimator = null;
+                        }.apply { start() };
+                }
+            } else {
+                if (animate) {
+                    _repliesOverlay.translationY = 0f;
+
+                    _repliesAnimator = _repliesOverlay.animate()
+                        .setDuration(300)
+                        .translationY(_repliesOverlay.height.toFloat())
+                        .withEndAction {
+                            _repliesOverlay.visibility = GONE;
+                            _repliesAnimator = null;
+                        }.apply { start(); }
+                } else {
+                    _repliesOverlay.visibility = View.GONE;
+                    _repliesOverlay.translationY = _repliesOverlay.height.toFloat();
+                }
+            }
+        }
+
+        private fun updateCommentCountString() {
+            _textCommentCount.text = context.getString(R.string.these_are_all_commentcount_comments_you_have_made_in_grayjay).replace("{commentCount}", _comments.size.toString())
+        }
+
+        private fun setLoading(loading: Boolean) {
+            if (_loading == loading) {
+                return;
+            }
+
+            _loading = loading;
+            _adapterComments.setLoading(loading);
+        }
+
+        private fun fetchComments() {
+            val system = StatePolycentric.instance.processHandle?.system ?: return
+            _comments.clear()
+            _adapterComments.notifyDataSetChanged()
+            setLoading(true)
+            _taskLoadComments.run(system)
+        }
+
+        private fun onCommentsLoaded(comments: List<IPlatformComment>) {
+            setLoading(false)
+            _comments.addAll(comments)
+
+            if (_spinnerSortBy.selectedItemPosition == 0) {
+                _comments.sortByDescending { it.date!! }
+            } else if (_spinnerSortBy.selectedItemPosition == 1) {
+                _comments.sortBy { it.date!! }
+            }
+
+            _adapterComments.notifyDataSetChanged()
+            updateCommentCountString()
+        }
+
+        fun onShown() {
+            val processHandle = StatePolycentric.instance.processHandle
+            if (processHandle != null) {
+                _layoutNotLoggedIn.visibility = View.GONE
+                _recyclerComments.visibility = View.VISIBLE
+                fetchComments()
+            } else {
+                _layoutNotLoggedIn.visibility = View.VISIBLE
+                _recyclerComments.visibility=  View.GONE
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/FeedView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/FeedView.kt
index 1dad57ef0e5c638995a03171a5a2bb3ce5d30da0..0e498476cbdc106eba2de34d02c59a01fa3d01fb 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/FeedView.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/FeedView.kt
@@ -13,7 +13,6 @@ import androidx.recyclerview.widget.RecyclerView.LayoutManager
 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
 import com.futo.platformplayer.*
 import com.futo.platformplayer.api.media.IPlatformClient
-import com.futo.platformplayer.api.media.models.contents.IPlatformContent
 import com.futo.platformplayer.api.media.platforms.js.models.JSPager
 import com.futo.platformplayer.api.media.structures.*
 import com.futo.platformplayer.constructs.Event1
@@ -21,7 +20,6 @@ import com.futo.platformplayer.constructs.TaskHandler
 import com.futo.platformplayer.engine.exceptions.PluginException
 import com.futo.platformplayer.logging.Logger
 import com.futo.platformplayer.views.FeedStyle
-import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
 import com.futo.platformplayer.views.others.ProgressBar
 import com.futo.platformplayer.views.others.TagsView
 import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
@@ -64,6 +62,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
     val fragment: TFragment;
 
     private val _scrollListener: RecyclerView.OnScrollListener;
+    private var _automaticNextPageCounter = 0;
 
     constructor(fragment: TFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<TViewHolder>, LinearLayoutManager, TPager, TResult, TConverted, InsertedViewHolder<TViewHolder>>? = null) : super(inflater.context) {
         this.fragment = fragment;
@@ -122,7 +121,6 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
 
         _toolbarContentView = findViewById(R.id.container_toolbar_content);
 
-        var filteredNextPageCounter = 0;
         _nextPageHandler = TaskHandler<TPager, List<TResult>>({fragment.lifecycleScope}, {
             if (it is IAsyncPager<*>)
                 it.nextPageAsync();
@@ -142,15 +140,8 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
             val filteredResults = filterResults(it);
             recyclerData.results.addAll(filteredResults);
             recyclerData.resultsUnfiltered.addAll(it);
-            if(filteredResults.isEmpty()) {
-                filteredNextPageCounter++
-                if(filteredNextPageCounter <= 4)
-                    loadNextPage()
-            }
-            else {
-                filteredNextPageCounter = 0;
-                recyclerData.adapter.notifyItemRangeInserted(recyclerData.adapter.childToParentPosition(posBefore), filteredResults.size);
-            }
+            recyclerData.adapter.notifyItemRangeInserted(recyclerData.adapter.childToParentPosition(posBefore), filteredResults.size);
+            ensureEnoughContentVisible(filteredResults)
         }.exception<Throwable> {
             Logger.w(TAG, "Failed to load next page.", it);
             UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_next_page), it, {
@@ -170,8 +161,10 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
 
                 val visibleItemCount = _recyclerResults.childCount;
                 val firstVisibleItem = recyclerData.layoutManager.findFirstVisibleItemPosition();
+                //Logger.i(TAG, "onScrolled loadNextPage visibleItemCount=$visibleItemCount firstVisibleItem=$visibleItemCount")
+
                 if (!_loading && firstVisibleItem + visibleItemCount + visibleThreshold >= recyclerData.results.size && firstVisibleItem > 0) {
-                    //Logger.i(TAG, "loadNextPage(): firstVisibleItem=$firstVisibleItem visibleItemCount=$visibleItemCount visibleThreshold=$visibleThreshold _results.size=${_results.size}")
+                    //Logger.i(TAG, "onScrolled loadNextPage(): firstVisibleItem=$firstVisibleItem visibleItemCount=$visibleItemCount visibleThreshold=$visibleThreshold recyclerData.results.size=${recyclerData.results.size}")
                     loadNextPage();
                 }
             }
@@ -180,6 +173,33 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
         _recyclerResults.addOnScrollListener(_scrollListener);
     }
 
+    private fun ensureEnoughContentVisible(filteredResults: List<TConverted>) {
+        val canScroll = if (recyclerData.results.isEmpty()) false else {
+            val layoutManager = recyclerData.layoutManager
+            val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
+
+            if (firstVisibleItemPosition != RecyclerView.NO_POSITION) {
+                val firstVisibleView = layoutManager.findViewByPosition(firstVisibleItemPosition)
+                val itemHeight = firstVisibleView?.height ?: 0
+                val occupiedSpace = recyclerData.results.size * itemHeight
+                val recyclerViewHeight = _recyclerResults.height
+                Logger.i(TAG, "ensureEnoughContentVisible loadNextPage occupiedSpace=$occupiedSpace recyclerViewHeight=$recyclerViewHeight")
+                occupiedSpace >= recyclerViewHeight
+            } else {
+                false
+            }
+
+        }
+        Logger.i(TAG, "ensureEnoughContentVisible loadNextPage canScroll=$canScroll _automaticNextPageCounter=$_automaticNextPageCounter")
+        if (!canScroll || filteredResults.isEmpty()) {
+            _automaticNextPageCounter++
+            if(_automaticNextPageCounter <= 4)
+                loadNextPage()
+        } else {
+            _automaticNextPageCounter = 0;
+        }
+    }
+
     protected fun setTextCentered(text: String?) {
         _textCentered.text = text;
     }
@@ -369,6 +389,7 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L
         recyclerData.resultsUnfiltered.addAll(toAdd);
         recyclerData.adapter.notifyDataSetChanged();
         recyclerData.loadedFeedStyle = feedStyle;
+        ensureEnoughContentVisible(filteredResults)
     }
 
     private fun detachPagerEvents() {
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PostDetailFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PostDetailFragment.kt
index b35cd91222ee29b249852a384adaf2f5288c535d..667b6ac9c29cdc1ce9616ff9c401761c53754c69 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PostDetailFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PostDetailFragment.kt
@@ -224,7 +224,7 @@ class PostDetailFragment : MainFragment {
                 updateCommentType(false);
             };
 
-            _commentsList.onClick.subscribe { c ->
+            _commentsList.onRepliesClick.subscribe { c ->
                 val replyCount = c.replyCount ?: 0;
                 var metadata = "";
                 if (replyCount > 0) {
@@ -233,7 +233,7 @@ class PostDetailFragment : MainFragment {
 
                 if (c is PolycentricPlatformComment) {
                     var parentComment: PolycentricPlatformComment = c;
-                    _repliesOverlay.load(_toggleCommentType.value, metadata, c.contextUrl, c.reference,
+                    _repliesOverlay.load(_toggleCommentType.value, metadata, c.contextUrl, c.reference, c,
                         { StatePolycentric.instance.getCommentPager(c.contextUrl, c.reference) },
                         {
                             val newComment = parentComment.cloneWithUpdatedReplyCount((parentComment.replyCount ?: 0) + 1);
@@ -241,7 +241,7 @@ class PostDetailFragment : MainFragment {
                             parentComment = newComment;
                         });
                 } else {
-                    _repliesOverlay.load(_toggleCommentType.value, metadata, null, null, { StatePlatform.instance.getSubComments(c) });
+                    _repliesOverlay.load(_toggleCommentType.value, metadata, null, null, c, { StatePlatform.instance.getSubComments(c) });
                 }
 
                 setRepliesOverlayVisible(isVisible = true, animate = true);
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 6a84494a38935299299a6fbc13d0a2e2130574a5..c65cffa2887a5fad39ef25c982e993083e47df54 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
@@ -37,7 +37,6 @@ import com.futo.platformplayer.api.media.LiveChatManager
 import com.futo.platformplayer.api.media.PlatformID
 import com.futo.platformplayer.api.media.exceptions.ContentNotAvailableYetException
 import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
-import com.futo.platformplayer.api.media.models.PlatformAuthorLink
 import com.futo.platformplayer.api.media.models.PlatformAuthorMembershipLink
 import com.futo.platformplayer.api.media.models.chapters.ChapterType
 import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
@@ -52,7 +51,6 @@ import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
 import com.futo.platformplayer.api.media.models.video.IPlatformVideo
 import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
 import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
-import com.futo.platformplayer.api.media.platforms.js.models.IJSContentDetails
 import com.futo.platformplayer.api.media.platforms.js.models.JSVideoDetails
 import com.futo.platformplayer.api.media.structures.IPager
 import com.futo.platformplayer.casting.CastConnectionState
@@ -60,7 +58,6 @@ import com.futo.platformplayer.casting.StateCasting
 import com.futo.platformplayer.constructs.Event0
 import com.futo.platformplayer.constructs.Event1
 import com.futo.platformplayer.constructs.TaskHandler
-import com.futo.platformplayer.dialogs.AutoUpdateDialog
 import com.futo.platformplayer.downloads.VideoLocal
 import com.futo.platformplayer.engine.exceptions.ScriptAgeException
 import com.futo.platformplayer.engine.exceptions.ScriptException
@@ -109,7 +106,6 @@ import java.time.OffsetDateTime
 import kotlin.collections.ArrayList
 import kotlin.math.abs
 import kotlin.math.roundToLong
-import kotlin.streams.toList
 
 
 class VideoDetailView : ConstraintLayout {
@@ -578,7 +574,7 @@ class VideoDetailView : ConstraintLayout {
 
         _container_content_current = _container_content_main;
 
-        _commentsList.onClick.subscribe { c ->
+        _commentsList.onRepliesClick.subscribe { c ->
             val replyCount = c.replyCount ?: 0;
             var metadata = "";
             if (replyCount > 0) {
@@ -587,7 +583,7 @@ class VideoDetailView : ConstraintLayout {
 
             if (c is PolycentricPlatformComment) {
                 var parentComment: PolycentricPlatformComment = c;
-                _container_content_replies.load(_toggleCommentType.value, metadata, c.contextUrl, c.reference,
+                _container_content_replies.load(_toggleCommentType.value, metadata, c.contextUrl, c.reference, c,
                     { StatePolycentric.instance.getCommentPager(c.contextUrl, c.reference) },
                     {
                         val newComment = parentComment.cloneWithUpdatedReplyCount((parentComment.replyCount ?: 0) + 1);
@@ -595,7 +591,7 @@ class VideoDetailView : ConstraintLayout {
                         parentComment = newComment;
                     });
             } else {
-                _container_content_replies.load(_toggleCommentType.value, metadata, null, null, { StatePlatform.instance.getSubComments(c) });
+                _container_content_replies.load(_toggleCommentType.value, metadata, null, null, c, { StatePlatform.instance.getSubComments(c) });
             }
             switchContentView(_container_content_replies);
         };
diff --git a/app/src/main/java/com/futo/platformplayer/helpers/VideoHelper.kt b/app/src/main/java/com/futo/platformplayer/helpers/VideoHelper.kt
index a2aa67ef53ab113911063b83fc4550ee57822478..e40f83cbc9105854365e80c68011555bb967fe58 100644
--- a/app/src/main/java/com/futo/platformplayer/helpers/VideoHelper.kt
+++ b/app/src/main/java/com/futo/platformplayer/helpers/VideoHelper.kt
@@ -3,8 +3,11 @@ package com.futo.platformplayer.helpers
 import android.net.Uri
 import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
 import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
+import com.futo.platformplayer.api.media.models.streams.sources.HLSManifestSource
 import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
 import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
+import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudioSource
+import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
 import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
 import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
 import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
@@ -20,11 +23,23 @@ import com.google.android.exoplayer2.upstream.ResolvingDataSource
 class VideoHelper {
     companion object {
 
-        fun isDownloadable(detail: IPlatformVideoDetails) =
-            (detail.video.videoSources.any { isDownloadable(it) }) ||
-                    (if (detail is VideoUnMuxedSourceDescriptor) detail.audioSources.any { isDownloadable(it) } else false);
-        fun isDownloadable(source: IVideoSource) = source is IVideoUrlSource;
-        fun isDownloadable(source: IAudioSource) = source is IAudioUrlSource;
+        fun isDownloadable(detail: IPlatformVideoDetails): Boolean {
+            if (detail.video.videoSources.any { isDownloadable(it) }) {
+                return true
+            }
+
+            val descriptor = detail.video
+            if (descriptor is VideoUnMuxedSourceDescriptor) {
+                if (descriptor.audioSources.any { isDownloadable(it) }) {
+                    return true
+                }
+            }
+
+            return false
+        }
+
+        fun isDownloadable(source: IVideoSource) = source is IVideoUrlSource || source is IHLSManifestSource;
+        fun isDownloadable(source: IAudioSource) = source is IAudioUrlSource || source is IHLSManifestAudioSource;
 
         fun selectBestVideoSource(desc: IVideoSourceDescriptor, desiredPixelCount : Int, prefContainers : Array<String>) : IVideoSource? = selectBestVideoSource(desc.videoSources.toList(), desiredPixelCount, prefContainers);
         fun selectBestVideoSource(sources: Iterable<IVideoSource>, desiredPixelCount : Int, prefContainers : Array<String>) : IVideoSource? {
diff --git a/app/src/main/java/com/futo/platformplayer/parsers/HLS.kt b/app/src/main/java/com/futo/platformplayer/parsers/HLS.kt
index e07b8a178c928914e1f3df9e55408c99fcd3ab24..57f42576397158613c8b396a65886ff6404ae2ae 100644
--- a/app/src/main/java/com/futo/platformplayer/parsers/HLS.kt
+++ b/app/src/main/java/com/futo/platformplayer/parsers/HLS.kt
@@ -1,8 +1,22 @@
 package com.futo.platformplayer.parsers
 
+import android.view.View
+import com.futo.platformplayer.R
+import com.futo.platformplayer.UIDialogs
 import com.futo.platformplayer.api.http.ManagedHttpClient
+import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantAudioUrlSource
+import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantSubtitleUrlSource
+import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantVideoUrlSource
+import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudioSource
+import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
+import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
+import com.futo.platformplayer.states.StateDownloads
 import com.futo.platformplayer.toYesNo
+import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuGroup
+import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
 import com.futo.platformplayer.yesNoToBoolean
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
 import java.net.URI
 import java.time.ZonedDateTime
 import java.time.format.DateTimeFormatter
@@ -85,6 +99,48 @@ class HLS {
             return VariantPlaylist(version, targetDuration, mediaSequence, discontinuitySequence, programDateTime, playlistType, streamInfo, segments)
         }
 
+        fun parseAndGetVideoSources(source: Any, content: String, url: String): List<HLSVariantVideoUrlSource> {
+            val masterPlaylist: MasterPlaylist
+            try {
+                masterPlaylist = parseMasterPlaylist(content, url)
+                return masterPlaylist.getVideoSources()
+            } catch (e: Throwable) {
+                if (content.lines().any { it.startsWith("#EXTINF:") }) {
+                    return if (source is IHLSManifestSource) {
+                        listOf(HLSVariantVideoUrlSource("variant", 0, 0, "application/vnd.apple.mpegurl", "", null, 0, false, url))
+                    } else if (source is IHLSManifestAudioSource) {
+                        listOf()
+                    } else {
+                        throw NotImplementedError()
+                    }
+                } else {
+                    throw e
+                }
+            }
+        }
+
+        fun parseAndGetAudioSources(source: Any, content: String, url: String): List<HLSVariantAudioUrlSource> {
+            val masterPlaylist: MasterPlaylist
+            try {
+                masterPlaylist = parseMasterPlaylist(content, url)
+                return masterPlaylist.getAudioSources()
+            } catch (e: Throwable) {
+                if (content.lines().any { it.startsWith("#EXTINF:") }) {
+                    return if (source is IHLSManifestSource) {
+                        listOf()
+                    } else if (source is IHLSManifestAudioSource) {
+                        listOf(HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, url))
+                    } else {
+                        throw NotImplementedError()
+                    }
+                } else {
+                    throw e
+                }
+            }
+        }
+
+        //TODO: getSubtitleSources
+
         private fun resolveUrl(baseUrl: String, url: String): String {
             val baseUri = URI(baseUrl)
             val urlUri = URI(url)
@@ -269,6 +325,49 @@ class HLS {
 
             return builder.toString()
         }
+
+        fun getVideoSources(): List<HLSVariantVideoUrlSource> {
+            return variantPlaylistsRefs.map {
+                var width: Int? = null
+                var height: Int? = null
+                val resolutionTokens = it.streamInfo.resolution?.split('x')
+                if (resolutionTokens?.isNotEmpty() == true) {
+                    width = resolutionTokens[0].toIntOrNull()
+                    height = resolutionTokens[1].toIntOrNull()
+                }
+
+                val suffix = listOf(it.streamInfo.video, it.streamInfo.codecs).mapNotNull { x -> x?.ifEmpty { null } }.joinToString(", ")
+                HLSVariantVideoUrlSource(suffix, width ?: 0, height ?: 0, "application/vnd.apple.mpegurl", it.streamInfo.codecs ?: "", it.streamInfo.bandwidth, 0, false, it.url)
+            }
+        }
+
+        fun getAudioSources(): List<HLSVariantAudioUrlSource> {
+            return mediaRenditions.mapNotNull {
+                if (it.uri == null) {
+                    return@mapNotNull null
+                }
+
+                val suffix = listOf(it.language, it.groupID).mapNotNull { x -> x?.ifEmpty { null } }.joinToString(", ")
+                return@mapNotNull when (it.type) {
+                    "AUDIO" -> HLSVariantAudioUrlSource(it.name?.ifEmpty { "Audio (${suffix})" } ?: "Audio (${suffix})", 0, "application/vnd.apple.mpegurl", "", it.language ?: "", null, false, it.uri)
+                    else -> null
+                }
+            }
+        }
+
+        fun getSubtitleSources(): List<HLSVariantSubtitleUrlSource> {
+            return mediaRenditions.mapNotNull {
+                if (it.uri == null) {
+                    return@mapNotNull null
+                }
+
+                val suffix = listOf(it.language, it.groupID).mapNotNull { x -> x?.ifEmpty { null } }.joinToString(", ")
+                return@mapNotNull when (it.type) {
+                    "SUBTITLE" -> HLSVariantSubtitleUrlSource(it.name?.ifEmpty { "Subtitle (${suffix})" } ?: "Subtitle (${suffix})", it.uri, "application/vnd.apple.mpegurl")
+                    else -> null
+                }
+            }
+        }
     }
 
     data class VariantPlaylistReference(val url: String, val streamInfo: StreamInfo) {
diff --git a/app/src/main/java/com/futo/platformplayer/polycentric/PolycentricCache.kt b/app/src/main/java/com/futo/platformplayer/polycentric/PolycentricCache.kt
index dc5ce5bb8615f5a3d67ce11dd327048d80197fc1..2a4bb4b4a575304c3144afe1bb4c7b416db63b37 100644
--- a/app/src/main/java/com/futo/platformplayer/polycentric/PolycentricCache.kt
+++ b/app/src/main/java/com/futo/platformplayer/polycentric/PolycentricCache.kt
@@ -7,7 +7,6 @@ import com.futo.platformplayer.constructs.BatchedTaskHandler
 import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
 import com.futo.platformplayer.getNowDiffSeconds
 import com.futo.platformplayer.logging.Logger
-import com.futo.platformplayer.resolveChannelUrl
 import com.futo.platformplayer.resolveChannelUrls
 import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
 import com.futo.platformplayer.stores.CachedPolycentricProfileStorage
@@ -19,17 +18,21 @@ import java.nio.ByteBuffer
 import java.time.OffsetDateTime
 
 class PolycentricCache {
-    data class CachedOwnedClaims(val ownedClaims: List<OwnedClaim>?, val creationTime: OffsetDateTime = OffsetDateTime.now());
+    data class CachedOwnedClaims(val ownedClaims: List<OwnedClaim>?, val creationTime: OffsetDateTime = OffsetDateTime.now()) {
+        val expired get() = creationTime.getNowDiffSeconds() > CACHE_EXPIRATION_SECONDS
+    }
     @Serializable
-    data class CachedPolycentricProfile(val profile: PolycentricProfile?, @Serializable(with = OffsetDateTimeSerializer::class) val creationTime: OffsetDateTime = OffsetDateTime.now());
+    data class CachedPolycentricProfile(val profile: PolycentricProfile?, @Serializable(with = OffsetDateTimeSerializer::class) val creationTime: OffsetDateTime = OffsetDateTime.now()) {
+        val expired get() = creationTime.getNowDiffSeconds() > CACHE_EXPIRATION_SECONDS
+    }
 
-    private val _cacheExpirationSeconds = 60 * 60 * 3;
     private val _cache = hashMapOf<PlatformID, CachedOwnedClaims>()
     private val _profileCache = hashMapOf<PublicKey, CachedPolycentricProfile>()
     private val _profileUrlCache = FragmentedStorage.get<CachedPolycentricProfileStorage>("profileUrlCache")
     private val _scope = CoroutineScope(Dispatchers.IO);
 
-    private val _taskGetProfile = BatchedTaskHandler<PublicKey, CachedPolycentricProfile>(_scope, { system ->
+    private val _taskGetProfile = BatchedTaskHandler<PublicKey, CachedPolycentricProfile>(_scope,
+        { system ->
             val signedProfileEvents = ApiMethods.getQueryLatest(
                 SERVER,
                 system.toProto(),
@@ -150,7 +153,7 @@ class PolycentricCache {
                 return null
             }
 
-            if (!ignoreExpired && cached.creationTime.getNowDiffSeconds() > _cacheExpirationSeconds) {
+            if (!ignoreExpired && cached.expired) {
                 return  null;
             }
 
@@ -188,7 +191,7 @@ class PolycentricCache {
     fun getCachedProfile(url: String, ignoreExpired: Boolean = false): CachedPolycentricProfile? {
         synchronized (_profileCache) {
             val cached = _profileUrlCache.get(url) ?: return null;
-            if (!ignoreExpired && cached.creationTime.getNowDiffSeconds() > _cacheExpirationSeconds) {
+            if (!ignoreExpired && cached.expired) {
                 return  null;
             }
 
@@ -199,7 +202,7 @@ class PolycentricCache {
     fun getCachedProfile(system: PublicKey, ignoreExpired: Boolean = false): CachedPolycentricProfile? {
         synchronized(_profileCache) {
             val cached = _profileCache[system] ?: return null;
-            if (!ignoreExpired && cached.creationTime.getNowDiffSeconds() > _cacheExpirationSeconds) {
+            if (!ignoreExpired && cached.expired) {
                 return null;
             }
 
@@ -281,6 +284,7 @@ class PolycentricCache {
         private const val TAG = "PolycentricCache"
         const val SERVER = "https://srv1-stg.polycentric.io"
         private var _instance: PolycentricCache? = null;
+        private val CACHE_EXPIRATION_SECONDS = 60 * 60 * 3;
 
         @JvmStatic
         val instance: PolycentricCache
diff --git a/app/src/main/java/com/futo/platformplayer/services/DownloadService.kt b/app/src/main/java/com/futo/platformplayer/services/DownloadService.kt
index a58a9b29171ceb71b519a09851cef5bb99318d8f..cf6e0ba21d6fed3b1be536ac59dcda1985830e08 100644
--- a/app/src/main/java/com/futo/platformplayer/services/DownloadService.kt
+++ b/app/src/main/java/com/futo/platformplayer/services/DownloadService.kt
@@ -162,6 +162,8 @@ class DownloadService : Service() {
         Logger.i(TAG, "doDownloading - Ending Downloads");
         stopService(this);
     }
+
+
     private suspend fun doDownload(download: VideoDownload) {
         if(!Settings.instance.downloads.shouldDownload())
             throw IllegalStateException("Downloading disabled on current network");
@@ -183,14 +185,14 @@ class DownloadService : Service() {
 
         Logger.i(TAG, "Preparing [${download.name}] started");
         if(download.state == VideoDownload.State.PREPARING)
-            download.prepare();
+            download.prepare(_client);
         download.changeState(VideoDownload.State.DOWNLOADING);
         notifyDownload(download);
 
         var lastNotifyTime: Long = 0L;
         Logger.i(TAG, "Downloading [${download.name}] started");
         //TODO: Use plugin client?
-        download.download(_client) { progress ->
+        download.download(applicationContext, _client) { progress ->
             download.progress = progress;
 
             val currentTime = System.currentTimeMillis();
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 7f076304ff4e1ad54e98af921bb283b26536e4ab..a5b08b81794cfa77bf45fb06d5ba69851675b907 100644
--- a/app/src/main/java/com/futo/platformplayer/services/MediaPlaybackService.kt
+++ b/app/src/main/java/com/futo/platformplayer/services/MediaPlaybackService.kt
@@ -23,6 +23,7 @@ import com.bumptech.glide.request.target.CustomTarget
 import com.bumptech.glide.request.transition.Transition
 import com.futo.platformplayer.logging.Logger
 import com.futo.platformplayer.R
+import com.futo.platformplayer.Settings
 import com.futo.platformplayer.states.StatePlatform
 import com.futo.platformplayer.states.StatePlayer
 import com.futo.platformplayer.activities.MainActivity
@@ -49,6 +50,7 @@ class MediaPlaybackService : Service() {
     private var _mediaSession: MediaSessionCompat? = null;
     private var _hasFocus: Boolean = false;
     private var _focusRequest: AudioFocusRequest? = null;
+    private var _audioFocusLossTime_ms: Long? = null
 
     override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
         Logger.v(TAG, "onStartCommand");
@@ -335,16 +337,32 @@ class MediaPlaybackService : Service() {
                         //Do not start playing on gaining audo focus
                         //MediaControlReceiver.onPlayReceived.emit();
                         _hasFocus = true;
-                        Log.i(TAG, "Audio focus gained");
+                        Log.i(TAG, "Audio focus gained (restartPlaybackAfterLoss = ${Settings.instance.playback.restartPlaybackAfterLoss}, _audioFocusLossTime_ms = $_audioFocusLossTime_ms)");
+
+                        if (Settings.instance.playback.restartPlaybackAfterLoss == 1) {
+                            val lossTime_ms = _audioFocusLossTime_ms
+                            if (lossTime_ms != null && System.currentTimeMillis() - lossTime_ms < 1000 * 30) {
+                                MediaControlReceiver.onPlayReceived.emit()
+                            }
+                        } else if (Settings.instance.playback.restartPlaybackAfterLoss == 2) {
+                            val lossTime_ms = _audioFocusLossTime_ms
+                            if (lossTime_ms != null && System.currentTimeMillis() - lossTime_ms < 1000 * 10) {
+                                MediaControlReceiver.onPlayReceived.emit()
+                            }
+                        } else if (Settings.instance.playback.restartPlaybackAfterLoss == 3) {
+                            MediaControlReceiver.onPlayReceived.emit()
+                        }
                     }
                     AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
                         MediaControlReceiver.onPauseReceived.emit();
+                        _audioFocusLossTime_ms = System.currentTimeMillis()
                         Log.i(TAG, "Audio focus transient loss");
                     }
                     AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
                         Log.i(TAG, "Audio focus transient loss, can duck");
                     }
                     AudioManager.AUDIOFOCUS_LOSS -> {
+                        _audioFocusLossTime_ms = System.currentTimeMillis()
                         _hasFocus = false;
                         MediaControlReceiver.onPauseReceived.emit();
                         Log.i(TAG, "Audio focus lost");
diff --git a/app/src/main/java/com/futo/platformplayer/states/StateAnnouncement.kt b/app/src/main/java/com/futo/platformplayer/states/StateAnnouncement.kt
index 76d06783b618d2be386cfd59abd46c989d5cd938..9bced80b10d28613a727aea7ed69be922fca9f1c 100644
--- a/app/src/main/java/com/futo/platformplayer/states/StateAnnouncement.kt
+++ b/app/src/main/java/com/futo/platformplayer/states/StateAnnouncement.kt
@@ -1,6 +1,7 @@
 package com.futo.platformplayer.states
 
 import android.content.Context
+import com.futo.platformplayer.UIDialogs
 import com.futo.platformplayer.api.http.ManagedHttpClient
 import com.futo.platformplayer.constructs.Event0
 import com.futo.platformplayer.constructs.Event1
@@ -256,9 +257,6 @@ class StateAnnouncement {
 
     }
 
-
-
-
     fun registerDidYouKnow() {
         val random = Random();
         val message: String? = when (random.nextInt(4 * 18 + 1)) {
@@ -294,6 +292,23 @@ class StateAnnouncement {
         }
     }
 
+    fun registerDefaultHandlerAnnouncement() {
+        registerAnnouncement(
+            "default-url-handler",
+            "Allow Grayjay to open URLs",
+            "Click here to allow Grayjay to open URLs",
+            AnnouncementType.SESSION_RECURRING,
+            null,
+            null,
+            "Allow"
+        ) {
+            UIDialogs.showUrlHandlingPrompt(StateApp.instance.context) {
+                instance.neverAnnouncement("default-url-handler")
+                instance.onAnnouncementChanged.emit()
+            }
+        }
+    }
+
     companion object {
         private var _instance: StateAnnouncement? = null;
         val instance: StateAnnouncement
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 c30a4311e83b2bdaa9628570f482396cf58820f9..bfdc4836a386417edad4880d514ad27f7c68c578 100644
--- a/app/src/main/java/com/futo/platformplayer/states/StateApp.kt
+++ b/app/src/main/java/com/futo/platformplayer/states/StateApp.kt
@@ -543,6 +543,7 @@ class StateApp {
             );
         }
 
+        StateAnnouncement.instance.registerDefaultHandlerAnnouncement();
         StateAnnouncement.instance.registerDidYouKnow();
         Logger.i(TAG, "MainApp Started: Finished");
     }
diff --git a/app/src/main/java/com/futo/platformplayer/states/StatePolycentric.kt b/app/src/main/java/com/futo/platformplayer/states/StatePolycentric.kt
index f294a6f93d7cca4b6834ca1776503e1ba1ff2cd3..73468d3f98c8e8f9bcdf1ad645f30ca79a0027a4 100644
--- a/app/src/main/java/com/futo/platformplayer/states/StatePolycentric.kt
+++ b/app/src/main/java/com/futo/platformplayer/states/StatePolycentric.kt
@@ -11,17 +11,12 @@ import com.futo.platformplayer.api.media.models.PlatformAuthorLink
 import com.futo.platformplayer.api.media.models.comments.IPlatformComment
 import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
 import com.futo.platformplayer.api.media.models.contents.IPlatformContent
-import com.futo.platformplayer.api.media.models.contents.PlatformContentPlaceholder
 import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes
 import com.futo.platformplayer.api.media.structures.DedupContentPager
 import com.futo.platformplayer.api.media.structures.EmptyPager
 import com.futo.platformplayer.api.media.structures.IAsyncPager
 import com.futo.platformplayer.api.media.structures.IPager
 import com.futo.platformplayer.api.media.structures.MultiChronoContentPager
-import com.futo.platformplayer.api.media.structures.PlaceholderPager
-import com.futo.platformplayer.api.media.structures.RefreshChronoContentPager
-import com.futo.platformplayer.api.media.structures.RefreshDedupContentPager
-import com.futo.platformplayer.api.media.structures.RefreshDistributionContentPager
 import com.futo.platformplayer.awaitFirstDeferred
 import com.futo.platformplayer.dp
 import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
@@ -218,6 +213,67 @@ class StatePolycentric {
         }
     }
 
+    fun getSystemComments(context: Context, system: PublicKey): List<IPlatformComment> {
+        val dp_25 = 25.dp(context.resources)
+        val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(system))
+        val author = system.systemToURLInfoSystemLinkUrl(systemState.servers.asIterable())
+        val posts = arrayListOf<PolycentricPlatformComment>()
+        Store.instance.enumerateSignedEvents(system, ContentType.POST) { se ->
+            val ev = se.event
+            val post = Protocol.Post.parseFrom(ev.content)
+
+            posts.add(PolycentricPlatformComment(
+                contextUrl = author,
+                author = PlatformAuthorLink(
+                    id = PlatformID("polycentric", author, null, ClaimType.POLYCENTRIC.value.toInt()),
+                    name = systemState.username,
+                    url = author,
+                    thumbnail = systemState.avatar?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(system.toProto(), img.process, listOf(PolycentricCache.SERVER)) },
+                    subscribers = null
+                ),
+                msg = if (post.content.count() > PolycentricPlatformComment.MAX_COMMENT_SIZE) post.content.substring(0, PolycentricPlatformComment.MAX_COMMENT_SIZE) else post.content,
+                rating = RatingLikeDislikes(0, 0),
+                date = if (ev.unixMilliseconds != null) Instant.ofEpochMilli(ev.unixMilliseconds!!).atOffset(ZoneOffset.UTC) else OffsetDateTime.MIN,
+                replyCount = 0,
+                eventPointer = se.toPointer()
+            ))
+        }
+
+        return posts
+    }
+
+    data class LikesDislikesReplies(
+        var likes: Long,
+        var dislikes: Long,
+        var replyCount: Long
+    )
+
+    suspend fun getLikesDislikesReplies(reference: Protocol.Reference): LikesDislikesReplies {
+        val response = ApiMethods.getQueryReferences(PolycentricCache.SERVER, reference, null,
+            null,
+            listOf(
+                Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder()
+                    .setFromType(ContentType.OPINION.value)
+                    .setValue(ByteString.copyFrom(Opinion.like.data))
+                    .build(),
+                Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder()
+                    .setFromType(ContentType.OPINION.value)
+                    .setValue(ByteString.copyFrom(Opinion.dislike.data))
+                    .build()
+            ),
+            listOf(
+                Protocol.QueryReferencesRequestCountReferences.newBuilder()
+                    .setFromType(ContentType.POST.value)
+                    .build()
+            )
+        );
+
+        val likes = response.countsList[0];
+        val dislikes = response.countsList[1];
+        val replyCount = response.countsList[2];
+        return LikesDislikesReplies(likes, dislikes, replyCount)
+    }
+
     suspend fun getCommentPager(contextUrl: String, reference: Protocol.Reference): IPager<IPlatformComment> {
         val response = ApiMethods.getQueryReferences(PolycentricCache.SERVER, reference, null,
             Protocol.QueryReferencesRequestEvents.newBuilder()
@@ -284,7 +340,7 @@ class StatePolycentric {
         };
     }
 
-    private suspend fun mapQueryReferences(contextUrl: String, response: Protocol.QueryReferencesResponse): List<IPlatformComment> {
+    private suspend fun mapQueryReferences(contextUrl: String, response: Protocol.QueryReferencesResponse): List<PolycentricPlatformComment> {
         return response.itemsList.mapNotNull {
             val sev = SignedEvent.fromProto(it.event);
             val ev = sev.event;
@@ -294,7 +350,6 @@ class StatePolycentric {
 
             try {
                 val post = Protocol.Post.parseFrom(ev.content);
-                val id = ev.system.toProto().key.toByteArray().toBase64();
                 val likes = it.countsList[0];
                 val dislikes = it.countsList[1];
                 val replies = it.countsList[2];
@@ -338,7 +393,7 @@ class StatePolycentric {
                     rating = RatingLikeDislikes(likes, dislikes),
                     date = if (unixMilliseconds != null) Instant.ofEpochMilli(unixMilliseconds).atOffset(ZoneOffset.UTC) else OffsetDateTime.MIN,
                     replyCount = replies.toInt(),
-                    reference = sev.toPointer().toReference()
+                    eventPointer = sev.toPointer()
                 );
             } catch (e: Throwable) {
                 return@mapNotNull null;
diff --git a/app/src/main/java/com/futo/platformplayer/views/Loader.kt b/app/src/main/java/com/futo/platformplayer/views/LoaderView.kt
similarity index 83%
rename from app/src/main/java/com/futo/platformplayer/views/Loader.kt
rename to app/src/main/java/com/futo/platformplayer/views/LoaderView.kt
index 8e4a64d3622003f58543f8b1c1c23afab54231f3..2e0610e36d5482d3de7b90ce7a30b62e97ccd74a 100644
--- a/app/src/main/java/com/futo/platformplayer/views/Loader.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/LoaderView.kt
@@ -1,6 +1,7 @@
 package com.futo.platformplayer.views
 
 import android.content.Context
+import android.graphics.Color
 import android.graphics.drawable.Animatable
 import android.util.AttributeSet
 import android.view.LayoutInflater
@@ -11,9 +12,10 @@ import android.widget.LinearLayout
 import androidx.core.view.updateLayoutParams
 import com.futo.platformplayer.R
 
-class Loader : LinearLayout {
+class LoaderView : LinearLayout {
     private val _imageLoader: ImageView;
     private val _automatic: Boolean;
+    private var _isWhite: Boolean;
     private val _animatable: Animatable;
 
     constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
@@ -24,18 +26,25 @@ class Loader : LinearLayout {
         if (attrs != null) {
             val attrArr = context.obtainStyledAttributes(attrs, R.styleable.LoaderView, 0, 0);
             _automatic = attrArr.getBoolean(R.styleable.LoaderView_automatic, false);
+            _isWhite = attrArr.getBoolean(R.styleable.LoaderView_isWhite, false);
             attrArr.recycle();
         } else {
             _automatic = false;
+            _isWhite = false;
         }
 
         visibility = View.GONE;
+
+        if (_isWhite) {
+            _imageLoader.setColorFilter(Color.WHITE)
+        }
     }
-    constructor(context: Context, automatic: Boolean, height: Int = -1) : super(context) {
+    constructor(context: Context, automatic: Boolean, height: Int = -1, isWhite: Boolean = false) : super(context) {
         inflate(context, R.layout.view_loader, this);
         _imageLoader = findViewById(R.id.image_loader);
         _animatable = _imageLoader.drawable as Animatable;
         _automatic = automatic;
+        _isWhite = isWhite;
 
         if(height > 0) {
             layoutParams = ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, height);
diff --git a/app/src/main/java/com/futo/platformplayer/views/MonetizationView.kt b/app/src/main/java/com/futo/platformplayer/views/MonetizationView.kt
index 4a6f467787c2a850bfd2c726ae585faf77fb5278..d9e1011c3642ebd55f500c559a320c9bd13d6dfe 100644
--- a/app/src/main/java/com/futo/platformplayer/views/MonetizationView.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/MonetizationView.kt
@@ -41,7 +41,7 @@ class MonetizationView : LinearLayout {
 
     private val _textMerchandise: TextView;
     private val _recyclerMerchandise: RecyclerView;
-    private val _loaderMerchandise: Loader;
+    private val _loaderViewMerchandise: LoaderView;
     private val _layoutMerchandise: FrameLayout;
     private var _merchandiseAdapterView: AnyAdapterView<StoreItem, StoreItemViewHolder>? = null;
 
@@ -81,7 +81,7 @@ class MonetizationView : LinearLayout {
 
         _textMerchandise = findViewById(R.id.text_merchandise);
         _recyclerMerchandise = findViewById(R.id.recycler_merchandise);
-        _loaderMerchandise = findViewById(R.id.loader_merchandise);
+        _loaderViewMerchandise = findViewById(R.id.loader_merchandise);
         _layoutMerchandise = findViewById(R.id.layout_merchandise);
 
         _root = findViewById(R.id.root);
@@ -108,7 +108,7 @@ class MonetizationView : LinearLayout {
     }
 
     private fun setMerchandise(items: List<StoreItem>?) {
-        _loaderMerchandise.stop();
+        _loaderViewMerchandise.stop();
 
         if (items == null) {
             _textMerchandise.visibility = View.GONE;
@@ -147,7 +147,7 @@ class MonetizationView : LinearLayout {
                     val uri = Uri.parse(storeData);
                     if (uri.isAbsolute) {
                         _taskLoadMerchandise.run(storeData);
-                        _loaderMerchandise.start();
+                        _loaderViewMerchandise.start();
                     } else {
                         Logger.i(TAG, "Merchandise not loaded, not URL nor JSON")
                     }
diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/CommentViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/CommentViewHolder.kt
index 7966020713794f838fb6af8bde45dd9fe489e448..89c3c1bb1039828ae953c5dc8c630bde13e1b0c0 100644
--- a/app/src/main/java/com/futo/platformplayer/views/adapters/CommentViewHolder.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/adapters/CommentViewHolder.kt
@@ -3,6 +3,7 @@ package com.futo.platformplayer.views.adapters
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
+import android.widget.FrameLayout
 import android.widget.ImageView
 import android.widget.LinearLayout
 import android.widget.TextView
@@ -37,8 +38,10 @@ class CommentViewHolder : ViewHolder {
     private val _layoutRating: LinearLayout;
     private val _pillRatingLikesDislikes: PillRatingLikesDislikes;
     private val _layoutComment: ConstraintLayout;
+    private val _buttonDelete: FrameLayout;
 
-    var onClick = Event1<IPlatformComment>();
+    var onRepliesClick = Event1<IPlatformComment>();
+    var onDelete = Event1<IPlatformComment>();
     var comment: IPlatformComment? = null
         private set;
 
@@ -55,6 +58,7 @@ class CommentViewHolder : ViewHolder {
         _buttonReplies = itemView.findViewById(R.id.button_replies);
         _layoutRating = itemView.findViewById(R.id.layout_rating);
         _pillRatingLikesDislikes = itemView.findViewById(R.id.rating);
+        _buttonDelete = itemView.findViewById(R.id.button_delete);
 
         _pillRatingLikesDislikes.onLikeDislikeUpdated.subscribe { args ->
             val c = comment
@@ -87,7 +91,12 @@ class CommentViewHolder : ViewHolder {
 
         _buttonReplies.onClick.subscribe {
             val c = comment ?: return@subscribe;
-            onClick.emit(c);
+            onRepliesClick.emit(c);
+        }
+
+        _buttonDelete.setOnClickListener {
+            val c = comment ?: return@setOnClickListener;
+            onDelete.emit(c);
         }
 
         _textBody.setPlatformPlayerLinkMovementMethod(viewGroup.context);
@@ -167,6 +176,13 @@ class CommentViewHolder : ViewHolder {
             _buttonReplies.visibility = View.GONE;
         }
 
+        val processHandle = StatePolycentric.instance.processHandle
+        if (processHandle != null && comment is PolycentricPlatformComment && processHandle.system == comment.eventPointer.system) {
+            _buttonDelete.visibility = View.VISIBLE
+        } else {
+            _buttonDelete.visibility = View.GONE
+        }
+
         this.comment = comment;
     }
 
diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/CommentWithReferenceViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/CommentWithReferenceViewHolder.kt
new file mode 100644
index 0000000000000000000000000000000000000000..f0581abd1fb785814439b3926cbcaa7ff8637d03
--- /dev/null
+++ b/app/src/main/java/com/futo/platformplayer/views/adapters/CommentWithReferenceViewHolder.kt
@@ -0,0 +1,195 @@
+package com.futo.platformplayer.views.adapters
+
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import android.widget.TextView
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
+import com.futo.platformplayer.*
+import com.futo.platformplayer.api.media.models.comments.IPlatformComment
+import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
+import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes
+import com.futo.platformplayer.constructs.Event1
+import com.futo.platformplayer.constructs.TaskHandler
+import com.futo.platformplayer.logging.Logger
+import com.futo.platformplayer.states.StateApp
+import com.futo.platformplayer.states.StatePolycentric
+import com.futo.platformplayer.views.others.CreatorThumbnail
+import com.futo.platformplayer.views.pills.PillButton
+import com.futo.platformplayer.views.pills.PillRatingLikesDislikes
+import com.futo.polycentric.core.Opinion
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import userpackage.Protocol
+import java.util.IdentityHashMap
+
+class CommentWithReferenceViewHolder : ViewHolder {
+    private val _creatorThumbnail: CreatorThumbnail;
+    private val _textAuthor: TextView;
+    private val _textMetadata: TextView;
+    private val _textBody: TextView;
+    private val _buttonReplies: PillButton;
+    private val _pillRatingLikesDislikes: PillRatingLikesDislikes;
+    private val _layoutComment: ConstraintLayout;
+    private val _buttonDelete: FrameLayout;
+    private val _cache: IdentityHashMap<IPlatformComment, StatePolycentric.LikesDislikesReplies>;
+    private var _likesDislikesReplies: StatePolycentric.LikesDislikesReplies? = null;
+
+    private val _taskGetLiveComment = TaskHandler(StateApp.instance.scopeGetter, ::getLikesDislikesReplies)
+        .success {
+            _likesDislikesReplies = it
+            updateLikesDislikesReplies()
+        }
+        .exception<Throwable> {
+            Logger.w(TAG, "Failed to get live comment.", it);
+            //TODO: Show error
+            hideLikesDislikesReplies()
+        }
+
+    var onRepliesClick = Event1<IPlatformComment>();
+    var onDelete = Event1<IPlatformComment>();
+    var comment: IPlatformComment? = null
+        private set;
+
+    constructor(viewGroup: ViewGroup, cache: IdentityHashMap<IPlatformComment, StatePolycentric.LikesDislikesReplies>) : super(LayoutInflater.from(viewGroup.context).inflate(R.layout.list_comment_with_reference, viewGroup, false)) {
+        _layoutComment = itemView.findViewById(R.id.layout_comment);
+        _creatorThumbnail = itemView.findViewById(R.id.image_thumbnail);
+        _textAuthor = itemView.findViewById(R.id.text_author);
+        _textMetadata = itemView.findViewById(R.id.text_metadata);
+        _textBody = itemView.findViewById(R.id.text_body);
+        _buttonReplies = itemView.findViewById(R.id.button_replies);
+        _pillRatingLikesDislikes = itemView.findViewById(R.id.rating);
+        _buttonDelete = itemView.findViewById(R.id.button_delete)
+        _cache = cache
+
+        _pillRatingLikesDislikes.onLikeDislikeUpdated.subscribe { args ->
+            val c = comment
+            if (c !is PolycentricPlatformComment) {
+                throw Exception("Not implemented for non polycentric comments")
+            }
+
+            if (args.hasLiked) {
+                args.processHandle.opinion(c.reference, Opinion.like);
+            } else if (args.hasDisliked) {
+                args.processHandle.opinion(c.reference, Opinion.dislike);
+            } else {
+                args.processHandle.opinion(c.reference, Opinion.neutral);
+            }
+
+            _layoutComment.alpha = if (args.dislikes > 2 && args.dislikes.toFloat() / (args.likes + args.dislikes).toFloat() >= 0.7f) 0.5f else 1.0f;
+
+            StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
+                try {
+                    Logger.i(TAG, "Started backfill");
+                    args.processHandle.fullyBackfillServersAnnounceExceptions();
+                    Logger.i(TAG, "Finished backfill");
+                } catch (e: Throwable) {
+                    Logger.e(TAG, "Failed to backfill servers.", e)
+                }
+            }
+
+            StatePolycentric.instance.updateLikeMap(c.reference, args.hasLiked, args.hasDisliked)
+        };
+
+        _buttonReplies.onClick.subscribe {
+            val c = comment ?: return@subscribe;
+            onRepliesClick.emit(c);
+        }
+
+        _buttonDelete.setOnClickListener {
+            val c = comment ?: return@setOnClickListener;
+            onDelete.emit(c);
+        }
+
+        _textBody.setPlatformPlayerLinkMovementMethod(viewGroup.context);
+    }
+
+    private suspend fun getLikesDislikesReplies(c: PolycentricPlatformComment): StatePolycentric.LikesDislikesReplies {
+        val likesDislikesReplies = StatePolycentric.instance.getLikesDislikesReplies(c.reference)
+        synchronized(_cache) {
+            _cache[c] = likesDislikesReplies
+        }
+        return likesDislikesReplies
+    }
+
+    fun bind(comment: IPlatformComment) {
+        Log.i(TAG, "bind")
+
+        _likesDislikesReplies = null;
+        _taskGetLiveComment.cancel()
+
+        _creatorThumbnail.setThumbnail(comment.author.thumbnail, false);
+        _creatorThumbnail.setHarborAvailable(comment is PolycentricPlatformComment,false);
+        _textAuthor.text = comment.author.name;
+
+        val date = comment.date;
+        if (date != null) {
+            _textMetadata.visibility = View.VISIBLE;
+            _textMetadata.text = " • ${date.toHumanNowDiffString()} ago";
+        } else {
+            _textMetadata.visibility = View.GONE;
+        }
+
+        val rating = comment.rating;
+        if (rating is RatingLikeDislikes) {
+            _layoutComment.alpha = if (rating.dislikes > 2 && rating.dislikes.toFloat() / (rating.likes + rating.dislikes).toFloat() >= 0.7f) 0.5f else 1.0f;
+        } else {
+            _layoutComment.alpha = 1.0f;
+        }
+
+        _textBody.text = comment.message.fixHtmlLinks();
+
+        this.comment = comment;
+        updateLikesDislikesReplies();
+    }
+
+    private fun updateLikesDislikesReplies() {
+        Log.i(TAG, "updateLikesDislikesReplies")
+
+        val c = comment ?: return
+        if (c is PolycentricPlatformComment) {
+            if (_likesDislikesReplies == null) {
+                Log.i(TAG, "updateLikesDislikesReplies retrieving from cache")
+
+                synchronized(_cache) {
+                    _likesDislikesReplies = _cache[c]
+                }
+            }
+
+            val likesDislikesReplies = _likesDislikesReplies
+            if (likesDislikesReplies != null) {
+                Log.i(TAG, "updateLikesDislikesReplies set")
+
+                val hasLiked = StatePolycentric.instance.hasLiked(c.reference);
+                val hasDisliked = StatePolycentric.instance.hasDisliked(c.reference);
+                _pillRatingLikesDislikes.setRating(RatingLikeDislikes(likesDislikesReplies.likes, likesDislikesReplies.dislikes), hasLiked, hasDisliked);
+
+                _buttonReplies.setLoading(false)
+
+                val replies = likesDislikesReplies.replyCount ?: 0;
+                _buttonReplies.visibility = View.VISIBLE;
+                _buttonReplies.text.text = "$replies " + itemView.context.getString(R.string.replies);
+            } else {
+                Log.i(TAG, "updateLikesDislikesReplies to load")
+
+                _pillRatingLikesDislikes.setLoading(true)
+                _buttonReplies.setLoading(true)
+                _taskGetLiveComment.run(c)
+            }
+        } else {
+            hideLikesDislikesReplies()
+        }
+    }
+
+    private fun hideLikesDislikesReplies() {
+        _pillRatingLikesDislikes.visibility = View.GONE
+        _buttonReplies.visibility = View.GONE
+    }
+
+    companion object {
+        private const val TAG = "CommentWithReferenceViewHolder";
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/DeviceViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/DeviceViewHolder.kt
index 6eddcc9892de5d20fd0a3619100a2e1e9c25ef35..8d4a6f0a257b26725b0b1efb10d3d2107d1fd09b 100644
--- a/app/src/main/java/com/futo/platformplayer/views/adapters/DeviceViewHolder.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/adapters/DeviceViewHolder.kt
@@ -74,9 +74,9 @@ class DeviceViewHolder : ViewHolder {
         } else if (d is AirPlayCastingDevice) {
             _imageDevice.setImageResource(R.drawable.ic_airplay);
             _textType.text = "AirPlay";
-        } else if (d is FastCastCastingDevice) {
+        } else if (d is FCastCastingDevice) {
             _imageDevice.setImageResource(R.drawable.ic_fc);
-            _textType.text = "FastCast";
+            _textType.text = "FCast";
         }
 
         _textName.text = d.name;
diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/SubscriptionAdapter.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/SubscriptionAdapter.kt
index 75e57c47839649cb90b6294308b9b53bd39132ab..f7a63d535c246885cfc6580ea8e331e9cf35b9dc 100644
--- a/app/src/main/java/com/futo/platformplayer/views/adapters/SubscriptionAdapter.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/adapters/SubscriptionAdapter.kt
@@ -17,8 +17,8 @@ class SubscriptionAdapter : RecyclerView.Adapter<SubscriptionViewHolder> {
     var onSettings = Event1<Subscription>();
     var sortBy: Int = 3
         set(value) {
-            field = value;
-            updateDataset();
+            field = value
+            updateDataset()
         }
 
     constructor(inflater: LayoutInflater, confirmationMessage: String) : super() {
diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/SubscriptionViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/SubscriptionViewHolder.kt
index 80f107824348f7291aa6652a63f2ce342a357e46..62918eed2d32aad31f2dcaafccb027f1bcef9e13 100644
--- a/app/src/main/java/com/futo/platformplayer/views/adapters/SubscriptionViewHolder.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/adapters/SubscriptionViewHolder.kt
@@ -84,6 +84,9 @@ class SubscriptionViewHolder : ViewHolder {
         val cachedProfile = PolycentricCache.instance.getCachedProfile(sub.channel.url, true);
         if (cachedProfile != null) {
             onProfileLoaded(sub, cachedProfile, false);
+            if (cachedProfile.expired) {
+                _taskLoadProfile.run(sub.channel.id);
+            }
         } else {
             _creatorThumbnail.setThumbnail(sub.channel.thumbnail, false);
             _taskLoadProfile.run(sub.channel.id);
diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/feedtypes/PreviewNestedVideoView.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/feedtypes/PreviewNestedVideoView.kt
index 6644d7ebd311536dba28a00725ece9b13e9aecee..d66bb466aa240af47cbf9efb77694fcde9927f6e 100644
--- a/app/src/main/java/com/futo/platformplayer/views/adapters/feedtypes/PreviewNestedVideoView.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/adapters/feedtypes/PreviewNestedVideoView.kt
@@ -19,7 +19,7 @@ import com.futo.platformplayer.states.StateApp
 import com.futo.platformplayer.states.StatePlatform
 import com.futo.platformplayer.video.PlayerManager
 import com.futo.platformplayer.views.FeedStyle
-import com.futo.platformplayer.views.Loader
+import com.futo.platformplayer.views.LoaderView
 import com.futo.platformplayer.views.platform.PlatformIndicator
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
@@ -28,7 +28,7 @@ class PreviewNestedVideoView : PreviewVideoView {
 
     protected val _platformIndicatorNested: PlatformIndicator;
     protected val _containerLoader: LinearLayout;
-    protected val _loader: Loader;
+    protected val _loaderView: LoaderView;
     protected val _containerUnavailable: LinearLayout;
     protected val _textNestedUrl: TextView;
 
@@ -42,7 +42,7 @@ class PreviewNestedVideoView : PreviewVideoView {
     constructor(context: Context, feedStyle: FeedStyle, exoPlayer: PlayerManager? = null): super(context, feedStyle, exoPlayer) {
         _platformIndicatorNested = findViewById(R.id.thumbnail_platform_nested);
         _containerLoader = findViewById(R.id.container_loader);
-        _loader = findViewById(R.id.loader);
+        _loaderView = findViewById(R.id.loader);
         _containerUnavailable = findViewById(R.id.container_unavailable);
         _textNestedUrl = findViewById(R.id.text_nested_url);
 
@@ -116,7 +116,7 @@ class PreviewNestedVideoView : PreviewVideoView {
             if(!_contentSupported) {
                 _containerUnavailable.visibility = View.VISIBLE;
                 _containerLoader.visibility = View.GONE;
-                _loader.stop();
+                _loaderView.stop();
             }
             else {
                 if(_feedStyle == FeedStyle.THUMBNAIL)
@@ -132,14 +132,14 @@ class PreviewNestedVideoView : PreviewVideoView {
             _contentSupported = false;
             _containerUnavailable.visibility = View.VISIBLE;
             _containerLoader.visibility = View.GONE;
-            _loader.stop();
+            _loaderView.stop();
         }
     }
 
     private fun loadNested(content: IPlatformNestedContent, onCompleted: ((IPlatformContentDetails)->Unit)? = null) {
         Logger.i(TAG, "Loading nested content [${content.contentUrl}]");
         _containerLoader.visibility = View.VISIBLE;
-        _loader.start();
+        _loaderView.start();
         StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
             val def = StatePlatform.instance.getContentDetails(content.contentUrl);
             def.invokeOnCompletion {
@@ -150,13 +150,13 @@ class PreviewNestedVideoView : PreviewVideoView {
                         if(_content == content) {
                             _containerUnavailable.visibility = View.VISIBLE;
                             _containerLoader.visibility = View.GONE;
-                            _loader.stop();
+                            _loaderView.stop();
                         }
                         //TODO: Handle exception
                     }
                     else if(_content == content) {
                         _containerLoader.visibility = View.GONE;
-                        _loader.stop();
+                        _loaderView.stop();
                         val nestedContent = def.getCompleted();
                         _contentNested = nestedContent;
                         if(nestedContent is IPlatformVideoDetails) {
diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/feedtypes/PreviewVideoView.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/feedtypes/PreviewVideoView.kt
index 8bc3770eb61246e1f5eca0c0b464a465a27ab05d..6ddf89c460d5c4d41af2c78017c0e4fe185a8998 100644
--- a/app/src/main/java/com/futo/platformplayer/views/adapters/feedtypes/PreviewVideoView.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/adapters/feedtypes/PreviewVideoView.kt
@@ -178,6 +178,9 @@ open class PreviewVideoView : LinearLayout {
         val cachedProfile = PolycentricCache.instance.getCachedProfile(content.author.url, true);
         if (cachedProfile != null) {
             onProfileLoaded(cachedProfile, false);
+            if (cachedProfile.expired) {
+                _taskLoadProfile.run(content.author.id);
+            }
         } else {
             _imageNeopassChannel?.visibility = View.GONE;
             _creatorThumbnail?.setThumbnail(content.author.thumbnail, false);
diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/CreatorViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/CreatorViewHolder.kt
index 487ac8e7c9b5ecd73b29915229f3d25533fe48a1..9a59debb13a62b4529db9bd1f88cd99f599e9c2c 100644
--- a/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/CreatorViewHolder.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/CreatorViewHolder.kt
@@ -68,6 +68,9 @@ class CreatorViewHolder(private val _viewGroup: ViewGroup, private val _tiny: Bo
         val cachedProfile = PolycentricCache.instance.getCachedProfile(authorLink.url, true);
         if (cachedProfile != null) {
             onProfileLoaded(cachedProfile, false);
+            if (cachedProfile.expired) {
+                _taskLoadProfile.run(authorLink.id);
+            }
         } else {
             _creatorThumbnail.setThumbnail(authorLink.thumbnail, false);
             _taskLoadProfile.run(authorLink.id);
diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/SubscriptionBarViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/SubscriptionBarViewHolder.kt
index 1b2d9c37429b22606ba19f10da1cb6922cb88d9e..fe2cb7e0b88321dd288c69fc042b80e6c44dff76 100644
--- a/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/SubscriptionBarViewHolder.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/SubscriptionBarViewHolder.kt
@@ -54,6 +54,9 @@ class SubscriptionBarViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.
         val cachedProfile = PolycentricCache.instance.getCachedProfile(subscription.channel.url, true);
         if (cachedProfile != null) {
             onProfileLoaded(cachedProfile, false);
+            if (cachedProfile.expired) {
+                _taskLoadProfile.run(subscription.channel.id);
+            }
         } else {
             _creatorThumbnail.setThumbnail(subscription.channel.thumbnail, false);
             _taskLoadProfile.run(subscription.channel.id);
diff --git a/app/src/main/java/com/futo/platformplayer/views/behavior/GestureControlView.kt b/app/src/main/java/com/futo/platformplayer/views/behavior/GestureControlView.kt
index c4990dce292d1684efbb9033e3ee12b4a9c1d474..322636429532e01e529e99aba18c3b7a2e355228 100644
--- a/app/src/main/java/com/futo/platformplayer/views/behavior/GestureControlView.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/behavior/GestureControlView.kt
@@ -6,7 +6,6 @@ import android.animation.ObjectAnimator
 import android.content.Context
 import android.graphics.drawable.Animatable
 import android.util.AttributeSet
-import android.util.Log
 import android.view.GestureDetector
 import android.view.LayoutInflater
 import android.view.MotionEvent
@@ -63,11 +62,15 @@ class GestureControlView : LinearLayout {
     private var _fullScreenFactorUp = 1.0f;
     private var _fullScreenFactorDown = 1.0f;
 
+    private val _gestureController: GestureDetectorCompat;
+
     val onSeek = Event1<Long>();
     val onBrightnessAdjusted = Event1<Float>();
     val onSoundAdjusted = Event1<Float>();
     val onToggleFullscreen = Event0();
 
+    var fullScreenGestureEnabled = true
+
     constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
         LayoutInflater.from(context).inflate(R.layout.view_gesture_controls, this, true);
 
@@ -82,13 +85,8 @@ class GestureControlView : LinearLayout {
         _layoutControlsBrightness = findViewById(R.id.layout_controls_brightness);
         _progressBrightness = findViewById(R.id.progress_brightness);
         _layoutControlsFullscreen = findViewById(R.id.layout_controls_fullscreen);
-    }
 
-    fun setupTouchArea(view: View, layoutControls: ViewGroup? = null, background: View? = null) {
-        _layoutControls = layoutControls;
-        _background = background;
-
-        val gestureController = GestureDetectorCompat(context, object : GestureDetector.OnGestureListener {
+        _gestureController = GestureDetectorCompat(context, object : GestureDetector.OnGestureListener {
             override fun onDown(p0: MotionEvent): Boolean { return false; }
             override fun onShowPress(p0: MotionEvent) = Unit;
             override fun onSingleTapUp(p0: MotionEvent): Boolean { return false; }
@@ -112,15 +110,14 @@ class GestureControlView : LinearLayout {
                     _fullScreenFactorDown = (_fullScreenFactorDown + adjustAmount).coerceAtLeast(0.0f).coerceAtMost(1.0f);
                     _layoutControlsFullscreen.alpha = _fullScreenFactorDown;
                 } else {
-                    val rx = p0.x / width;
-                    val ry = p0.y / height;
-                    Logger.v(TAG, "rx = $rx, ry = $ry, _isFullScreen = $_isFullScreen")
+                    val rx = (p0.x + p1.x) / (2 * width);
+                    val ry = (p0.y + p1.y) / (2 * height);
                     if (ry > 0.1 && ry < 0.9) {
-                        if (_isFullScreen && rx < 0.4) {
+                        if (_isFullScreen && rx < 0.2) {
                             startAdjustingBrightness();
-                        } else if (_isFullScreen && rx > 0.6) {
+                        } else if (_isFullScreen && rx > 0.8) {
                             startAdjustingSound();
-                        } else if (rx >= 0.4 && rx <= 0.6) {
+                        } else if (fullScreenGestureEnabled && rx in 0.3..0.7) {
                             if (_isFullScreen) {
                                 startAdjustingFullscreenDown();
                             } else {
@@ -136,7 +133,7 @@ class GestureControlView : LinearLayout {
             override fun onFling(p0: MotionEvent, p1: MotionEvent, p2: Float, p3: Float): Boolean { return false; }
         });
 
-        gestureController.setOnDoubleTapListener(object : GestureDetector.OnDoubleTapListener {
+        _gestureController.setOnDoubleTapListener(object : GestureDetector.OnDoubleTapListener {
             override fun onSingleTapConfirmed(ev: MotionEvent): Boolean {
                 if (_skipping) {
                     return false;
@@ -166,52 +163,58 @@ class GestureControlView : LinearLayout {
             }
         });
 
-        val touchListener = object : OnTouchListener {
-            override fun onTouch(v: View?, ev: MotionEvent): Boolean {
-                cancelHideJob();
+        isClickable = true
+    }
 
-                if (_skipping) {
-                    if (ev.action == MotionEvent.ACTION_UP) {
-                        startExitFastForward();
-                        stopAutoFastForward();
-                    } else if (ev.action == MotionEvent.ACTION_DOWN) {
-                        _jobExitFastForward?.cancel();
-                        _jobExitFastForward = null;
-
-                        startAutoFastForward();
-                        fastForwardTick();
-                    }
-                }
+    fun setupTouchArea(layoutControls: ViewGroup? = null, background: View? = null) {
+        _layoutControls = layoutControls;
+        _background = background;
+    }
 
-                if (_adjustingSound && ev.action == MotionEvent.ACTION_UP) {
-                    stopAdjustingSound();
-                }
+    override fun onTouchEvent(event: MotionEvent?): Boolean {
+        val ev = event ?: return super.onTouchEvent(event);
 
-                if (_adjustingBrightness && ev.action == MotionEvent.ACTION_UP) {
-                    stopAdjustingBrightness();
-                }
+        cancelHideJob();
 
-                if (_adjustingFullscreenUp && ev.action == MotionEvent.ACTION_UP) {
-                    if (_fullScreenFactorUp > 0.5) {
-                        onToggleFullscreen.emit();
-                    }
-                    stopAdjustingFullscreenUp();
-                }
+        if (_skipping) {
+            if (ev.action == MotionEvent.ACTION_UP) {
+                startExitFastForward();
+                stopAutoFastForward();
+            } else if (ev.action == MotionEvent.ACTION_DOWN) {
+                _jobExitFastForward?.cancel();
+                _jobExitFastForward = null;
 
-                if (_adjustingFullscreenDown && ev.action == MotionEvent.ACTION_UP) {
-                    if (_fullScreenFactorDown > 0.5) {
-                        onToggleFullscreen.emit();
-                    }
-                    stopAdjustingFullscreenDown();
-                }
+                startAutoFastForward();
+                fastForwardTick();
+            }
+        }
+
+        if (_adjustingSound && ev.action == MotionEvent.ACTION_UP) {
+            stopAdjustingSound();
+        }
+
+        if (_adjustingBrightness && ev.action == MotionEvent.ACTION_UP) {
+            stopAdjustingBrightness();
+        }
 
-                startHideJobIfNecessary();
-                return gestureController.onTouchEvent(ev);
+        if (_adjustingFullscreenUp && ev.action == MotionEvent.ACTION_UP) {
+            if (_fullScreenFactorUp > 0.5) {
+                onToggleFullscreen.emit();
             }
-        };
+            stopAdjustingFullscreenUp();
+        }
+
+        if (_adjustingFullscreenDown && ev.action == MotionEvent.ACTION_UP) {
+            if (_fullScreenFactorDown > 0.5) {
+                onToggleFullscreen.emit();
+            }
+            stopAdjustingFullscreenDown();
+        }
+
+        startHideJobIfNecessary();
 
-        view.setOnTouchListener(touchListener);
-        view.isClickable = true;
+        _gestureController.onTouchEvent(ev)
+        return true;
     }
 
     fun cancelHideJob() {
diff --git a/app/src/main/java/com/futo/platformplayer/views/casting/CastView.kt b/app/src/main/java/com/futo/platformplayer/views/casting/CastView.kt
index 0c0be08b5cd02a42f30b7ad58c2e30e6802afe50..5045f59d35ac3ec2f4f40c092d5366e2296c7198 100644
--- a/app/src/main/java/com/futo/platformplayer/views/casting/CastView.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/casting/CastView.kt
@@ -58,7 +58,8 @@ class CastView : ConstraintLayout {
         _timeBar = findViewById(R.id.time_progress);
         _background = findViewById(R.id.layout_background);
         _gestureControlView = findViewById(R.id.gesture_control);
-        _gestureControlView.setupTouchArea(_background);
+        _gestureControlView.fullScreenGestureEnabled = false
+        _gestureControlView.setupTouchArea();
         _gestureControlView.onSeek.subscribe {
             val d = StateCasting.instance.activeDevice ?: return@subscribe;
             StateCasting.instance.videoSeekTo(d.expectedCurrentTime + it / 1000);
diff --git a/app/src/main/java/com/futo/platformplayer/views/overlays/RepliesOverlay.kt b/app/src/main/java/com/futo/platformplayer/views/overlays/RepliesOverlay.kt
index 44cb602080ecdbd965236d1953db012c9ce3cc42..77a0ba883f734218a13673ce04e69cd598131d9d 100644
--- a/app/src/main/java/com/futo/platformplayer/views/overlays/RepliesOverlay.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/overlays/RepliesOverlay.kt
@@ -4,15 +4,21 @@ import android.content.Context
 import android.util.AttributeSet
 import android.view.View
 import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.constraintlayout.widget.ConstraintLayout
 import com.futo.platformplayer.UIDialogs
 import com.futo.platformplayer.R
 import com.futo.platformplayer.api.media.models.comments.IPlatformComment
 import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment
 import com.futo.platformplayer.api.media.structures.IPager
 import com.futo.platformplayer.constructs.Event0
+import com.futo.platformplayer.fixHtmlLinks
 import com.futo.platformplayer.states.StatePlatform
 import com.futo.platformplayer.states.StatePolycentric
+import com.futo.platformplayer.toHumanNowDiffString
+import com.futo.platformplayer.views.behavior.NonScrollingTextView
 import com.futo.platformplayer.views.comments.AddCommentView
+import com.futo.platformplayer.views.others.CreatorThumbnail
 import com.futo.platformplayer.views.segments.CommentsList
 import userpackage.Protocol
 
@@ -22,6 +28,11 @@ class RepliesOverlay : LinearLayout {
     private val _topbar: OverlayTopbar;
     private val _commentsList: CommentsList;
     private val _addCommentView: AddCommentView;
+    private val _textBody: NonScrollingTextView;
+    private val _textAuthor: TextView;
+    private val _textMetadata: TextView;
+    private val _creatorThumbnail: CreatorThumbnail;
+    private val _layoutParentComment: ConstraintLayout;
     private var _readonly = false;
     private var _onCommentAdded: ((comment: IPlatformComment) -> Unit)? = null;
 
@@ -30,6 +41,11 @@ class RepliesOverlay : LinearLayout {
         _topbar = findViewById(R.id.topbar);
         _commentsList = findViewById(R.id.comments_list);
         _addCommentView = findViewById(R.id.add_comment_view);
+        _textBody = findViewById(R.id.text_body)
+        _textMetadata = findViewById(R.id.text_metadata)
+        _textAuthor = findViewById(R.id.text_author)
+        _creatorThumbnail = findViewById(R.id.image_thumbnail)
+        _layoutParentComment = findViewById(R.id.layout_parent_comment)
 
         _addCommentView.onCommentAdded.subscribe {
             _commentsList.addComment(it);
@@ -42,7 +58,7 @@ class RepliesOverlay : LinearLayout {
             }
         }
 
-        _commentsList.onClick.subscribe { c ->
+        _commentsList.onRepliesClick.subscribe { c ->
             val replyCount = c.replyCount;
             var metadata = "";
             if (replyCount != null && replyCount > 0) {
@@ -50,9 +66,9 @@ class RepliesOverlay : LinearLayout {
             }
 
             if (c is PolycentricPlatformComment) {
-                load(false, metadata, c.contextUrl, c.reference, { StatePolycentric.instance.getCommentPager(c.contextUrl, c.reference) });
+                load(false, metadata, c.contextUrl, c.reference, c, { StatePolycentric.instance.getCommentPager(c.contextUrl, c.reference) });
             } else {
-                load(true, metadata, null, null, { StatePlatform.instance.getSubComments(c) });
+                load(true, metadata, null, null, c, { StatePlatform.instance.getSubComments(c) });
             }
         };
 
@@ -60,7 +76,7 @@ class RepliesOverlay : LinearLayout {
         _topbar.setInfo(context.getString(R.string.Replies), "");
     }
 
-    fun load(readonly: Boolean, metadata: String, contextUrl: String?, ref: Protocol.Reference?, loader: suspend () -> IPager<IPlatformComment>, onCommentAdded: ((comment: IPlatformComment) -> Unit)? = null) {
+    fun load(readonly: Boolean, metadata: String, contextUrl: String?, ref: Protocol.Reference?, parentComment: IPlatformComment? = null, loader: suspend () -> IPager<IPlatformComment>, onCommentAdded: ((comment: IPlatformComment) -> Unit)? = null) {
         _readonly = readonly;
         if (readonly) {
             _addCommentView.visibility = View.GONE;
@@ -69,6 +85,26 @@ class RepliesOverlay : LinearLayout {
             _addCommentView.setContext(contextUrl, ref);
         }
 
+        if (parentComment == null) {
+            _layoutParentComment.visibility = View.GONE
+        } else {
+            _layoutParentComment.visibility = View.VISIBLE
+
+            _textBody.text = parentComment.message.fixHtmlLinks()
+            _textAuthor.text = parentComment.author.name
+
+            val date = parentComment.date
+            if (date != null) {
+                _textMetadata.visibility = View.VISIBLE
+                _textMetadata.text = " • ${date.toHumanNowDiffString()} ago"
+            } else {
+                _textMetadata.visibility = View.GONE
+            }
+
+            _creatorThumbnail.setThumbnail(parentComment.author.thumbnail, false);
+            _creatorThumbnail.setHarborAvailable(parentComment is PolycentricPlatformComment,false);
+        }
+
         _topbar.setInfo(context.getString(R.string.Replies), metadata);
         _commentsList.load(readonly, loader);
         _onCommentAdded = onCommentAdded;
diff --git a/app/src/main/java/com/futo/platformplayer/views/overlays/slideup/SlideUpMenuOverlay.kt b/app/src/main/java/com/futo/platformplayer/views/overlays/slideup/SlideUpMenuOverlay.kt
index cc8e30f19d4351e47380f1fff400256a237291b6..2c34dc5e4b7c1cfb2b42079cfcbd344a0b24f16c 100644
--- a/app/src/main/java/com/futo/platformplayer/views/overlays/slideup/SlideUpMenuOverlay.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/overlays/slideup/SlideUpMenuOverlay.kt
@@ -73,8 +73,9 @@ class SlideUpMenuOverlay : RelativeLayout {
                 item.setParentClickListener { hide() };
             else if(item is SlideUpMenuItem)
                 item.setParentClickListener { hide() };
-
         }
+
+        _groupItems = items;
     }
 
     private fun init(animated: Boolean, okText: String?){
diff --git a/app/src/main/java/com/futo/platformplayer/views/pills/PillButton.kt b/app/src/main/java/com/futo/platformplayer/views/pills/PillButton.kt
index 014a24b4cddc107c607d7a377f292d52fd979067..2a84c372254d4947663fefe88a1054c25b47fdd9 100644
--- a/app/src/main/java/com/futo/platformplayer/views/pills/PillButton.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/pills/PillButton.kt
@@ -9,16 +9,20 @@ import android.widget.LinearLayout
 import android.widget.TextView
 import com.futo.platformplayer.R
 import com.futo.platformplayer.constructs.Event0
+import com.futo.platformplayer.views.LoaderView
 
 class PillButton : LinearLayout {
     val icon: ImageView;
     val text: TextView;
+    val loaderView: LoaderView;
     val onClick = Event0();
+    private var _isLoading = false;
 
     constructor(context : Context, attrs : AttributeSet) : super(context, attrs) {
         LayoutInflater.from(context).inflate(R.layout.pill_button, this, true);
         icon = findViewById(R.id.pill_icon);
         text = findViewById(R.id.pill_text);
+        loaderView = findViewById(R.id.loader)
 
         val attrArr = context.obtainStyledAttributes(attrs, R.styleable.PillButton, 0, 0);
         val attrIconRef = attrArr.getResourceId(R.styleable.PillButton_pillIcon, -1);
@@ -31,7 +35,29 @@ class PillButton : LinearLayout {
         text.text = attrText;
 
         findViewById<LinearLayout>(R.id.root).setOnClickListener {
+            if (_isLoading) {
+                return@setOnClickListener
+            }
+
             onClick.emit();
         };
     }
+
+    fun setLoading(loading: Boolean) {
+        if (loading == _isLoading) {
+            return
+        }
+
+        if (loading) {
+            text.visibility = View.GONE
+            loaderView.visibility = View.VISIBLE
+            loaderView.start()
+        } else {
+            loaderView.stop()
+            text.visibility = View.VISIBLE
+            loaderView.visibility = View.GONE
+        }
+
+        _isLoading = loading
+    }
 }
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/views/pills/PillRatingLikesDislikes.kt b/app/src/main/java/com/futo/platformplayer/views/pills/PillRatingLikesDislikes.kt
index f56feced3986524118635dc64ff3647faff5764d..8854f60609e6aa7903b447cf3749fc2046569ffe 100644
--- a/app/src/main/java/com/futo/platformplayer/views/pills/PillRatingLikesDislikes.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/pills/PillRatingLikesDislikes.kt
@@ -16,6 +16,7 @@ import com.futo.platformplayer.constructs.Event1
 import com.futo.platformplayer.constructs.Event3
 import com.futo.platformplayer.states.StatePolycentric
 import com.futo.platformplayer.toHumanNumber
+import com.futo.platformplayer.views.LoaderView
 import com.futo.polycentric.core.ProcessHandle
 
 data class OnLikeDislikeUpdatedArgs(
@@ -29,9 +30,12 @@ data class OnLikeDislikeUpdatedArgs(
 class PillRatingLikesDislikes : LinearLayout {
     private val _textLikes: TextView;
     private val _textDislikes: TextView;
+    private val _loaderViewLikes: LoaderView;
+    private val _loaderViewDislikes: LoaderView;
     private val _seperator: View;
     private val _iconLikes: ImageView;
     private val _iconDislikes: ImageView;
+    private var _isLoading: Boolean = false;
 
     private var _likes = 0L;
     private var _hasLiked = false;
@@ -47,14 +51,42 @@ class PillRatingLikesDislikes : LinearLayout {
         _seperator = findViewById(R.id.pill_seperator);
         _iconDislikes = findViewById(R.id.pill_dislike_icon);
         _iconLikes = findViewById(R.id.pill_like_icon);
+        _loaderViewLikes = findViewById(R.id.loader_likes)
+        _loaderViewDislikes = findViewById(R.id.loader_dislikes)
 
-        _iconLikes.setOnClickListener { StatePolycentric.instance.requireLogin(context, context.getString(R.string.please_login_to_like)) { like(it) }; };
-        _textLikes.setOnClickListener { StatePolycentric.instance.requireLogin(context, context.getString(R.string.please_login_to_like)) { like(it) }; };
-        _iconDislikes.setOnClickListener { StatePolycentric.instance.requireLogin(context, context.getString(R.string.please_login_to_dislike)) { dislike(it) }; };
-        _textDislikes.setOnClickListener { StatePolycentric.instance.requireLogin(context, context.getString(R.string.please_login_to_dislike)) { dislike(it) }; };
+        _iconLikes.setOnClickListener { if (!_isLoading) StatePolycentric.instance.requireLogin(context, context.getString(R.string.please_login_to_like)) { like(it) }; };
+        _textLikes.setOnClickListener { if (!_isLoading) StatePolycentric.instance.requireLogin(context, context.getString(R.string.please_login_to_like)) { like(it) }; };
+        _iconDislikes.setOnClickListener { if (!_isLoading) StatePolycentric.instance.requireLogin(context, context.getString(R.string.please_login_to_dislike)) { dislike(it) }; };
+        _textDislikes.setOnClickListener { if (!_isLoading) StatePolycentric.instance.requireLogin(context, context.getString(R.string.please_login_to_dislike)) { dislike(it) }; };
+    }
+
+    fun setLoading(loading: Boolean) {
+        if (_isLoading == loading) {
+            return
+        }
+
+        if (loading) {
+            _textLikes.visibility = View.GONE
+            _loaderViewLikes.visibility = View.VISIBLE
+            _textDislikes.visibility = View.GONE
+            _loaderViewDislikes.visibility = View.VISIBLE
+            _loaderViewLikes.start()
+            _loaderViewDislikes.start()
+        } else {
+            _loaderViewLikes.stop()
+            _loaderViewDislikes.stop()
+            _textLikes.visibility = View.VISIBLE
+            _loaderViewLikes.visibility = View.GONE
+            _textDislikes.visibility = View.VISIBLE
+            _loaderViewDislikes.visibility = View.GONE
+        }
+
+        _isLoading = loading
     }
 
     fun setRating(rating: IRating, hasLiked: Boolean = false, hasDisliked: Boolean = false) {
+        setLoading(false)
+
         when (rating) {
             is RatingLikeDislikes -> {
                 setRating(rating, hasLiked, hasDisliked);
@@ -127,6 +159,8 @@ class PillRatingLikesDislikes : LinearLayout {
     }
 
     fun setRating(rating: RatingLikeDislikes, hasLiked: Boolean = false, hasDisliked: Boolean = false) {
+        setLoading(false)
+
         _textLikes.text = rating.likes.toHumanNumber();
         _textDislikes.text = rating.dislikes.toHumanNumber();
         _textLikes.visibility = View.VISIBLE;
@@ -140,6 +174,8 @@ class PillRatingLikesDislikes : LinearLayout {
         updateColors();
     }
     fun setRating(rating: RatingLikes, hasLiked: Boolean = false) {
+        setLoading(false)
+
         _textLikes.text = rating.likes.toHumanNumber();
         _textLikes.visibility = View.VISIBLE;
         _textDislikes.visibility = View.GONE;
diff --git a/app/src/main/java/com/futo/platformplayer/views/segments/CommentsList.kt b/app/src/main/java/com/futo/platformplayer/views/segments/CommentsList.kt
index 0677aa99c6fe3526c6f8b3419e1e0fba3f75a1b3..e02205b7e4dcbfd5381c4d1f63f07d8c830b4ad4 100644
--- a/app/src/main/java/com/futo/platformplayer/views/segments/CommentsList.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/segments/CommentsList.kt
@@ -1,10 +1,14 @@
 package com.futo.platformplayer.views.segments
 
 import android.content.Context
+import android.graphics.Color
 import android.util.AttributeSet
+import android.view.Gravity
+import android.view.KeyCharacterMap.UnavailableException
 import android.view.LayoutInflater
 import android.view.View
 import android.widget.FrameLayout
+import android.widget.TextView
 import androidx.constraintlayout.widget.ConstraintLayout
 import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.RecyclerView
@@ -19,22 +23,33 @@ import com.futo.platformplayer.api.media.structures.IAsyncPager
 import com.futo.platformplayer.api.media.structures.IPager
 import com.futo.platformplayer.constructs.Event1
 import com.futo.platformplayer.constructs.TaskHandler
-import com.futo.platformplayer.fragment.mainactivity.main.ChannelFragment
+import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
+import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
+import com.futo.platformplayer.states.StatePolycentric
 import com.futo.platformplayer.views.adapters.CommentViewHolder
 import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
 import java.net.UnknownHostException
 
 class CommentsList : ConstraintLayout {
     private val _llmReplies: LinearLayoutManager;
+    private val _textMessage: TextView;
     private val _taskLoadComments = if(!isInEditMode) TaskHandler<suspend () -> IPager<IPlatformComment>, IPager<IPlatformComment>>(StateApp.instance.scopeGetter, { it(); })
         .success { pager -> onCommentsLoaded(pager); }
         .exception<UnknownHostException> {
-            UIDialogs.toast("Failed to load comments");
+            setMessage("UnknownHostException: " + it.message);
+            Logger.e(TAG, "Failed to load comments.", it);
+            setLoading(false);
+        }
+        .exception<ScriptUnavailableException> {
+            setMessage(it.message);
+            Logger.e(TAG, "Failed to load comments.", it);
             setLoading(false);
         }
         .exception<Throwable> {
+            setMessage("Throwable: " + it.message);
             Logger.e(TAG, "Failed to load comments.", it);
-            UIDialogs.toast(context, context.getString(R.string.failed_to_load_comments) + "\n" + (it.message ?: ""));
             //UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_comments) + (it.message ?: ""), it, ::fetchComments);
             setLoading(false);
         } else TaskHandler(IPlatformVideoDetails::class.java, StateApp.instance.scopeGetter);
@@ -69,23 +84,35 @@ class CommentsList : ConstraintLayout {
     private val _prependedView: FrameLayout;
     private var _readonly: Boolean = false;
 
-    var onClick = Event1<IPlatformComment>();
+    var onRepliesClick = Event1<IPlatformComment>();
     var onCommentsLoaded = Event1<Int>();
 
+
+
     constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
         LayoutInflater.from(context).inflate(R.layout.view_comments_list, this, true);
 
         _recyclerComments = findViewById(R.id.recycler_comments);
+        _textMessage = TextView(context).apply {
+            layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT).apply {
+                setMargins(0, 30, 0, 0)
+            }
+            textSize = 12.0f
+            setTextColor(Color.WHITE)
+            typeface = resources.getFont(R.font.inter_regular)
+            gravity = Gravity.CENTER_HORIZONTAL
+        }
 
         _prependedView = FrameLayout(context);
         _prependedView.layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT);
 
-        _adapterComments = InsertedViewAdapterWithLoader(context, arrayListOf(_prependedView), arrayListOf(),
+        _adapterComments = InsertedViewAdapterWithLoader(context, arrayListOf(_prependedView, _textMessage), arrayListOf(),
             childCountGetter = { _comments.size },
             childViewHolderBinder = { viewHolder, position -> viewHolder.bind(_comments[position], _readonly); },
             childViewHolderFactory = { viewGroup, _ ->
                 val holder = CommentViewHolder(viewGroup);
-                holder.onClick.subscribe { c -> onClick.emit(c) };
+                holder.onRepliesClick.subscribe { c -> onRepliesClick.emit(c) };
+                holder.onDelete.subscribe(::onDelete);
                 return@InsertedViewAdapterWithLoader holder;
             }
         );
@@ -96,6 +123,16 @@ class CommentsList : ConstraintLayout {
         _recyclerComments.addOnScrollListener(_scrollListener);
     }
 
+    private fun setMessage(message: String?) {
+        Logger.i(TAG, "setMessage " + message)
+        if (message != null) {
+            _textMessage.visibility = View.VISIBLE
+            _textMessage.text = message
+        } else {
+            _textMessage.visibility = View.GONE
+        }
+    }
+
     fun addComment(comment: IPlatformComment) {
         _comments.add(0, comment);
         _adapterComments.notifyItemRangeInserted(_adapterComments.childToParentPosition(0), 1);
@@ -106,6 +143,38 @@ class CommentsList : ConstraintLayout {
         _prependedView.addView(view);
     }
 
+    private fun onDelete(comment: IPlatformComment) {
+        UIDialogs.showConfirmationDialog(context, "Are you sure you want to delete this comment?", {
+            val processHandle = StatePolycentric.instance.processHandle ?: return@showConfirmationDialog
+            if (comment !is PolycentricPlatformComment) {
+                return@showConfirmationDialog
+            }
+
+            val index = _comments.indexOf(comment)
+            if (index != -1) {
+                _comments.removeAt(index)
+                _adapterComments.notifyItemRemoved(_adapterComments.childToParentPosition(index))
+
+                StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
+                    try {
+                        processHandle.delete(comment.eventPointer.process, comment.eventPointer.logicalClock)
+                    } catch (e: Throwable) {
+                        Logger.e(TAG, "Failed to delete event.", e);
+                        return@launch;
+                    }
+
+                    try {
+                        Logger.i(TAG, "Started backfill");
+                        processHandle.fullyBackfillServersAnnounceExceptions();
+                        Logger.i(TAG, "Finished backfill");
+                    } catch (e: Throwable) {
+                        Logger.e(TAG, "Failed to fully backfill servers.", e);
+                    }
+                }
+            }
+        })
+    }
+
     private fun onScrolled() {
         val visibleItemCount = _recyclerComments.childCount;
         val firstVisibleItem = _llmReplies.findFirstVisibleItemPosition();
@@ -147,6 +216,7 @@ class CommentsList : ConstraintLayout {
 
     fun load(readonly: Boolean, loader: suspend () -> IPager<IPlatformComment>) {
         cancel();
+        setMessage(null);
 
         _readonly = readonly;
         setLoading(true);
@@ -177,6 +247,7 @@ class CommentsList : ConstraintLayout {
         _comments.clear();
         _commentsPager = null;
         _adapterComments.notifyDataSetChanged();
+        setMessage(null);
     }
 
     fun cancel() {
diff --git a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt
index 01eb189c23dec86350ec5433e4c12a3f4531578c..3b1c94304eca27b211a37aa8bcbf5c7d023edc37 100644
--- a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt
@@ -156,7 +156,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
         _layoutControls = findViewById(R.id.layout_controls);
         gestureControl = findViewById(R.id.gesture_control);
 
-        _videoView?.videoSurfaceView?.let { gestureControl.setupTouchArea(it, _layoutControls, background); };
+        gestureControl.setupTouchArea(_layoutControls, background);
         gestureControl.onSeek.subscribe { seekFromCurrent(it); };
         gestureControl.onSoundAdjusted.subscribe { setVolume(it) };
         gestureControl.onToggleFullscreen.subscribe { setFullScreen(!isFullScreen) };
diff --git a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt
index 3f873b8c81044399e895e20d795745b2a99fdf00..ae1a109ba558fe2484a3f2a211036658407089d1 100644
--- a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt
+++ b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt
@@ -1,10 +1,10 @@
 package com.futo.platformplayer.views.video
 
 import android.content.Context
-import android.media.session.PlaybackState
 import android.net.Uri
 import android.util.AttributeSet
 import android.widget.RelativeLayout
+import com.futo.platformplayer.Settings
 import com.futo.platformplayer.api.media.models.chapters.IChapter
 import com.futo.platformplayer.logging.Logger
 import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor
@@ -16,6 +16,7 @@ import com.futo.platformplayer.api.media.platforms.js.models.sources.JSAudioUrlR
 import com.futo.platformplayer.api.media.platforms.js.models.sources.JSHLSManifestAudioSource
 import com.futo.platformplayer.api.media.platforms.js.models.sources.JSVideoUrlRangeSource
 import com.futo.platformplayer.constructs.Event1
+import com.futo.platformplayer.receivers.MediaControlReceiver
 import com.futo.platformplayer.states.StateApp
 import com.futo.platformplayer.video.PlayerManager
 import com.google.android.exoplayer2.*
@@ -54,6 +55,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
     private var _lastSubtitleMediaSource: MediaSource? = null;
     private var _shouldPlaybackRestartOnConnectivity: Boolean = false;
     private val _referenceObject = Object();
+    private var _connectivityLossTime_ms: Long? = null
 
     private var _chapters: List<IChapter>? = null;
 
@@ -152,7 +154,24 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
 
             val pos = position;
             val dur = duration;
+            var shouldRestartPlayback = false
             if (_shouldPlaybackRestartOnConnectivity && abs(pos - dur) > 2000) {
+                if (Settings.instance.playback.restartPlaybackAfterConnectivityLoss == 1) {
+                    val lossTime_ms = _connectivityLossTime_ms
+                    if (lossTime_ms != null && System.currentTimeMillis() - lossTime_ms < 1000 * 30) {
+                        shouldRestartPlayback = true
+                    }
+                } else if (Settings.instance.playback.restartPlaybackAfterConnectivityLoss == 2) {
+                    val lossTime_ms = _connectivityLossTime_ms
+                    if (lossTime_ms != null && System.currentTimeMillis() - lossTime_ms < 1000 * 10) {
+                        shouldRestartPlayback = true
+                    }
+                } else if (Settings.instance.playback.restartPlaybackAfterConnectivityLoss == 3) {
+                    shouldRestartPlayback = true
+                }
+            }
+
+            if (shouldRestartPlayback) {
                 Logger.i(TAG, "Playback ended due to connection loss, resuming playback since connection is restored.");
                 exoPlayer?.player?.playWhenReady = true;
                 exoPlayer?.player?.prepare();
@@ -509,16 +528,17 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
             PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS -> {
                 onDatasourceError.emit(error);
             }
-            PlaybackException.ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED,
-            PlaybackException.ERROR_CODE_IO_FILE_NOT_FOUND,
-            PlaybackException.ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE,
+            //PlaybackException.ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED,
+            //PlaybackException.ERROR_CODE_IO_FILE_NOT_FOUND,
+            //PlaybackException.ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE,
             PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED,
             PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT,
-            PlaybackException.ERROR_CODE_IO_NO_PERMISSION,
-            PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE,
+            //PlaybackException.ERROR_CODE_IO_NO_PERMISSION,
+            //PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE,
             PlaybackException.ERROR_CODE_IO_UNSPECIFIED -> {
                 Logger.i(TAG, "IO error, set _shouldPlaybackRestartOnConnectivity=true");
                 _shouldPlaybackRestartOnConnectivity = true;
+                _connectivityLossTime_ms = System.currentTimeMillis()
             }
         }
     }
@@ -536,8 +556,6 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
             Logger.i(TAG, "_shouldPlaybackRestartOnConnectivity=false");
             _shouldPlaybackRestartOnConnectivity = false;
         }
-
-
     }
 
     companion object {
diff --git a/app/src/main/proto/com/futo/platformplayer/protos/ChromeCast.proto b/app/src/main/proto/com/futo/platformplayer/protos/ChromeCast.proto
new file mode 100644
index 0000000000000000000000000000000000000000..395c488960e3554a95d4d6af578486cd3df57013
--- /dev/null
+++ b/app/src/main/proto/com/futo/platformplayer/protos/ChromeCast.proto
@@ -0,0 +1,18 @@
+syntax = "proto2";
+option optimize_for = LITE_RUNTIME;
+package com.futo.platformplayer.protos;
+
+message CastMessage {
+  enum ProtocolVersion { CASTV2_1_0 = 0; }
+  required ProtocolVersion protocol_version = 1;
+  required string source_id = 2;
+  required string destination_id = 3;
+  required string namespace = 4;
+  enum PayloadType {
+    STRING = 0;
+    BINARY = 1;
+  }
+  required PayloadType payload_type = 5;
+  optional string payload_utf8 = 6;
+  optional bytes payload_binary = 7;
+}
\ No newline at end of file
diff --git a/app/src/main/proto/com/futo/platformplayer/protos/DeviceAuthMessage.proto b/app/src/main/proto/com/futo/platformplayer/protos/DeviceAuthMessage.proto
deleted file mode 100644
index f6b090d952b32c79cc43af5ac4a72826c51f1942..0000000000000000000000000000000000000000
--- a/app/src/main/proto/com/futo/platformplayer/protos/DeviceAuthMessage.proto
+++ /dev/null
@@ -1,82 +0,0 @@
-syntax = "proto2";
-option optimize_for = LITE_RUNTIME;
-package com.futo.platformplayer.protos;
-
-message CastMessage {
-  // Always pass a version of the protocol for future compatibility
-  // requirements.
-  enum ProtocolVersion { CASTV2_1_0 = 0; }
-  required ProtocolVersion protocol_version = 1;
-  // source and destination ids identify the origin and destination of the
-  // message.  They are used to route messages between endpoints that share a
-  // device-to-device channel.
-  //
-  // For messages between applications:
-  //   - The sender application id is a unique identifier generated on behalf of
-  //     the sender application.
-  //   - The receiver id is always the the session id for the application.
-  //
-  // For messages to or from the sender or receiver platform, the special ids
-  // 'sender-0' and 'receiver-0' can be used.
-  //
-  // For messages intended for all endpoints using a given channel, the
-  // wildcard destination_id '*' can be used.
-  required string source_id = 2;
-  required string destination_id = 3;
-  // This is the core multiplexing key.  All messages are sent on a namespace
-  // and endpoints sharing a channel listen on one or more namespaces.  The
-  // namespace defines the protocol and semantics of the message.
-  required string namespace = 4;
-  // Encoding and payload info follows.
-  // What type of data do we have in this message.
-  enum PayloadType {
-    STRING = 0;
-    BINARY = 1;
-  }
-  required PayloadType payload_type = 5;
-  // Depending on payload_type, exactly one of the following optional fields
-  // will always be set.
-  optional string payload_utf8 = 6;
-  optional bytes payload_binary = 7;
-}
-enum SignatureAlgorithm {
-  UNSPECIFIED = 0;
-  RSASSA_PKCS1v15 = 1;
-  RSASSA_PSS = 2;
-}
-enum HashAlgorithm {
-  SHA1 = 0;
-  SHA256 = 1;
-}
-// Messages for authentication protocol between a sender and a receiver.
-message AuthChallenge {
-  optional SignatureAlgorithm signature_algorithm = 1
-      [default = RSASSA_PKCS1v15];
-  optional bytes sender_nonce = 2;
-  optional HashAlgorithm hash_algorithm = 3 [default = SHA1];
-}
-message AuthResponse {
-  required bytes signature = 1;
-  required bytes client_auth_certificate = 2;
-  repeated bytes intermediate_certificate = 3;
-  optional SignatureAlgorithm signature_algorithm = 4
-      [default = RSASSA_PKCS1v15];
-  optional bytes sender_nonce = 5;
-  optional HashAlgorithm hash_algorithm = 6 [default = SHA1];
-  optional bytes crl = 7;
-}
-message AuthError {
-  enum ErrorType {
-    INTERNAL_ERROR = 0;
-    NO_TLS = 1;  // The underlying connection is not TLS
-    SIGNATURE_ALGORITHM_UNAVAILABLE = 2;
-  }
-  required ErrorType error_type = 1;
-}
-message DeviceAuthMessage {
-  // Request fields
-  optional AuthChallenge challenge = 1;
-  // Response fields
-  optional AuthResponse response = 2;
-  optional AuthError error = 3;
-}
diff --git a/app/src/main/res/drawable/background_comment.xml b/app/src/main/res/drawable/background_comment.xml
new file mode 100644
index 0000000000000000000000000000000000000000..152c90b938c25107283e2cae76f6b5c50d54ad36
--- /dev/null
+++ b/app/src/main/res/drawable/background_comment.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="#1A1A1A" />
+    <corners android:radius="4dp" />
+    <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/drawable/background_pill_pred.xml b/app/src/main/res/drawable/background_pill_pred.xml
new file mode 100644
index 0000000000000000000000000000000000000000..85ae3542760be8401709f193b277d3f00f14e8af
--- /dev/null
+++ b/app/src/main/res/drawable/background_pill_pred.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <solid android:color="#A03D3D" />
+    <corners android:radius="500dp" />
+    <size android:height="20dp" />
+    <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/drawable/ic_chat_filled.xml b/app/src/main/res/drawable/ic_chat_filled.xml
new file mode 100644
index 0000000000000000000000000000000000000000..dda8bf17fa7e656c56800822d86b6979f67014bb
--- /dev/null
+++ b/app/src/main/res/drawable/ic_chat_filled.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="960"
+    android:viewportHeight="960">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M289.23,649.23q-13.08,0 -21.92,-8.85 -8.85,-8.85 -8.85,-21.92v-40h470l25.38,25.38L753.85,240h40q13.08,0 21.92,8.85 8.85,8.85 8.85,21.92v501.54L701.54,649.23L289.23,649.23ZM135.38,621.54v-470.77q0,-13.08 8.85,-21.92Q153.08,120 166.15,120h476.92q13.08,0 21.92,8.85 8.85,8.85 8.85,21.92v316.92q0,13.08 -8.85,21.92 -8.85,8.85 -21.92,8.85L258.46,498.46L135.38,621.54Z"/>
+</vector>
diff --git a/app/src/main/res/drawable/ic_fcast.xml b/app/src/main/res/drawable/ic_fcast.xml
new file mode 100644
index 0000000000000000000000000000000000000000..22ac06f5eb045adfaa7efe2613a91948e73bf2ee
--- /dev/null
+++ b/app/src/main/res/drawable/ic_fcast.xml
@@ -0,0 +1,12 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="97dp"
+    android:height="97dp"
+    android:viewportWidth="97"
+    android:viewportHeight="97">
+  <path
+      android:pathData="M20,0L77,0A20,20 0,0 1,97 20L97,77A20,20 0,0 1,77 97L20,97A20,20 0,0 1,0 77L0,20A20,20 0,0 1,20 0z"
+      android:fillColor="#D9D9D9"/>
+  <path
+      android:pathData="M17.03,67V30.636H42.598V38.591H26.902V44.841H41.036V52.796H26.902V67H17.03ZM80.178,44.273H70.164C70.093,43.444 69.904,42.693 69.596,42.018C69.3,41.343 68.886,40.763 68.353,40.278C67.832,39.78 67.199,39.402 66.453,39.141C65.707,38.869 64.861,38.733 63.914,38.733C62.257,38.733 60.854,39.135 59.706,39.94C58.57,40.745 57.706,41.899 57.114,43.403C56.534,44.906 56.244,46.711 56.244,48.818C56.244,51.044 56.54,52.908 57.132,54.411C57.735,55.903 58.605,57.027 59.742,57.785C60.878,58.53 62.245,58.903 63.843,58.903C64.755,58.903 65.571,58.791 66.293,58.566C67.016,58.329 67.643,57.992 68.175,57.554C68.708,57.116 69.14,56.589 69.472,55.974C69.815,55.346 70.046,54.642 70.164,53.861L80.178,53.932C80.06,55.471 79.628,57.039 78.882,58.637C78.136,60.223 77.077,61.691 75.704,63.041C74.343,64.378 72.656,65.455 70.644,66.272C68.631,67.089 66.293,67.497 63.63,67.497C60.292,67.497 57.297,66.781 54.646,65.349C52.006,63.916 49.917,61.809 48.378,59.028C46.851,56.246 46.087,52.843 46.087,48.818C46.087,44.77 46.869,41.361 48.431,38.591C49.994,35.809 52.101,33.708 54.752,32.288C57.404,30.855 60.363,30.139 63.63,30.139C65.926,30.139 68.039,30.453 69.969,31.08C71.898,31.708 73.591,32.625 75.047,33.832C76.503,35.028 77.675,36.502 78.563,38.254C79.45,40.005 79.989,42.012 80.178,44.273Z"
+      android:fillColor="#000000"/>
+</vector>
diff --git a/app/src/main/res/layout/activity_fcast_guide.xml b/app/src/main/res/layout/activity_fcast_guide.xml
new file mode 100644
index 0000000000000000000000000000000000000000..4d6a2b89627f682da4b48e4866290b5c7bb35591
--- /dev/null
+++ b/app/src/main/res/layout/activity_fcast_guide.xml
@@ -0,0 +1,121 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:background="@color/black">
+
+    <ImageButton
+        android:id="@+id/button_back"
+        android:layout_width="50dp"
+        android:layout_height="50dp"
+        android:padding="10dp"
+        android:scaleType="fitCenter"
+        app:srcCompat="@drawable/ic_back_thin_white_16dp"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintLeft_toLeftOf="parent" />
+
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintRight_toRightOf="parent"
+        android:gravity="center_vertical"
+        android:layout_marginTop="4dp">
+
+        <ImageView
+            android:layout_width="32dp"
+            android:layout_height="32dp"
+            app:srcCompat="@drawable/ic_fcast" />
+
+        <TextView
+            android:id="@+id/text_polycentric"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/fcast"
+            android:fontFamily="@font/inter_light"
+            android:includeFontPadding="false"
+            android:textSize="32dp"
+            android:layout_marginLeft="12dp"/>
+    </LinearLayout>
+
+    <LinearLayout
+        android:id="@+id/text_description"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:animateLayoutChanges="true"
+        android:orientation="vertical"
+        android:background="@drawable/background_videodetail_description"
+        android:layout_marginLeft="14dp"
+        android:layout_marginRight="14dp"
+        android:layout_marginTop="14dp"
+        android:layout_marginBottom="14dp"
+        android:paddingTop="3dp"
+        android:paddingBottom="5dp"
+        android:paddingLeft="12dp"
+        android:paddingRight="12dp"
+        app:layout_constraintTop_toBottomOf="@id/button_back"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintRight_toRightOf="parent"
+        app:layout_constraintBottom_toTopOf="@id/layout_buttons">
+
+        <ScrollView
+            android:layout_width="match_parent"
+            android:layout_height="match_parent">
+
+            <com.futo.platformplayer.views.behavior.NonScrollingTextView
+                android:id="@+id/text_explanation"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:paddingTop="5dp"
+                android:paddingBottom="5dp"
+                android:textColor="@color/white"
+                android:fontFamily="@font/inter_light"
+                android:background="@color/transparent"
+                android:textSize="14sp" />
+        </ScrollView>
+    </LinearLayout>
+
+    <LinearLayout
+        android:id="@+id/layout_buttons"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        android:layout_marginStart="20dp"
+        android:layout_marginEnd="20dp"
+        android:layout_marginTop="20dp"
+        android:layout_marginBottom="20dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintRight_toRightOf="parent">
+
+        <com.futo.platformplayer.views.buttons.BigButton
+            android:id="@+id/button_website"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            app:buttonText="@string/fcast_website"
+            app:buttonSubText="@string/open_the_fcast_website"
+            app:buttonIcon="@drawable/ic_link" />
+
+        <com.futo.platformplayer.views.buttons.BigButton
+            android:id="@+id/button_technical"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            app:buttonText="@string/fcast_technical_documentation"
+            app:buttonSubText="@string/view_the_fcast_technical_documentation"
+            app:buttonIcon="@drawable/ic_wrench"
+            android:layout_marginTop="8dp" />
+
+        <com.futo.platformplayer.views.buttons.BigButton
+            android:id="@+id/button_close"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            app:buttonText="@string/close"
+            app:buttonSubText="@string/go_back_to_casting_add_dialog"
+            app:buttonIcon="@drawable/ic_close"
+            android:layout_marginTop="8dp" />
+    </LinearLayout>
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml
index 747ad391947b8d2b91319d89a5e3b593c65d6033..ac34014a5695173e9e9eee482ab89546b4b4615c 100644
--- a/app/src/main/res/layout/activity_settings.xml
+++ b/app/src/main/res/layout/activity_settings.xml
@@ -52,7 +52,7 @@
             android:orientation="vertical"
             android:layout_width="match_parent"
             android:layout_height="wrap_content">
-            <com.futo.platformplayer.views.Loader
+            <com.futo.platformplayer.views.LoaderView
                 android:id="@+id/loader"
                 android:layout_marginBottom="15dp"
                 android:layout_marginTop="15dp"
diff --git a/app/src/main/res/layout/dialog_casting_add.xml b/app/src/main/res/layout/dialog_casting_add.xml
index b94b44dbe912561a94346cb2c6f08b7d1abe6b63..a24ac94804aa99ff4d8aab7b4638849323a0f5c2 100644
--- a/app/src/main/res/layout/dialog_casting_add.xml
+++ b/app/src/main/res/layout/dialog_casting_add.xml
@@ -8,13 +8,31 @@
     android:background="@color/gray_1d"
     android:padding="20dp">
 
-    <TextView
-        android:layout_width="wrap_content"
+    <LinearLayout
+        android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:text="@string/add_casting_device"
-        android:textSize="14dp"
-        android:textColor="@color/white"
-        android:fontFamily="@font/inter_regular" />
+        android:orientation="horizontal">
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/add_casting_device"
+            android:textSize="14dp"
+            android:textColor="@color/white"
+            android:fontFamily="@font/inter_regular" />
+
+        <TextView
+            android:id="@+id/button_tutorial"
+            android:layout_width="0dp"
+            android:layout_weight="1"
+            android:layout_height="wrap_content"
+            android:text="Help"
+            android:textSize="14dp"
+            android:padding="8dp"
+            android:gravity="end|center_vertical"
+            android:textColor="@color/primary"
+            android:fontFamily="@font/inter_regular" />
+    </LinearLayout>
 
     <Spinner
         android:id="@+id/spinner_type"
diff --git a/app/src/main/res/layout/dialog_casting_connect.xml b/app/src/main/res/layout/dialog_casting_connect.xml
index ecadd61f51f4072ab59098536c88991dfaf0a383..5cb43c350d64b93f12fe467540dffb593bbffb17 100644
--- a/app/src/main/res/layout/dialog_casting_connect.xml
+++ b/app/src/main/res/layout/dialog_casting_connect.xml
@@ -89,18 +89,32 @@
             android:textColor="@color/white"
             android:fontFamily="@font/inter_regular" />
 
-        <Space android:layout_width="0dp"
-            android:layout_height="match_parent"
-            android:layout_weight="1" />
+        <Button
+            android:id="@+id/button_scan_qr"
+            android:layout_width="0dp"
+            android:layout_weight="1.7"
+            android:layout_height="wrap_content"
+            android:text="@string/scan_qr"
+            android:textSize="14dp"
+            android:textAlignment="center"
+            android:layout_marginEnd="2dp"
+            android:ellipsize="end"
+            android:maxLines="1"
+            android:fontFamily="@font/inter_regular"
+            android:textColor="@color/colorPrimary"
+            android:background="@color/transparent" />
 
         <Button
             android:id="@+id/button_add"
-            android:layout_width="wrap_content"
+            android:layout_width="0dp"
+            android:layout_weight="1"
             android:layout_height="wrap_content"
             android:text="@string/add"
             android:textSize="14dp"
             android:textAlignment="textEnd"
             android:layout_marginEnd="2dp"
+            android:ellipsize="end"
+            android:maxLines="1"
             android:fontFamily="@font/inter_regular"
             android:textColor="@color/colorPrimary"
             android:background="@color/transparent" />
diff --git a/app/src/main/res/layout/dialog_casting_help.xml b/app/src/main/res/layout/dialog_casting_help.xml
new file mode 100644
index 0000000000000000000000000000000000000000..f62b6c62f26f1d9eae84ee4cc8554cbd5a5a1b10
--- /dev/null
+++ b/app/src/main/res/layout/dialog_casting_help.xml
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:background="@color/gray_1d"
+    android:padding="12dp">
+
+    <LinearLayout
+        android:id="@+id/layout_buttons"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical">
+
+        <com.futo.platformplayer.views.buttons.BigButton
+            android:id="@+id/button_video"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            app:buttonText="@string/video"
+            app:buttonSubText="@string/view_a_video_about_how_to_cast"
+            app:buttonIcon="@drawable/ic_smart_display"
+            android:layout_marginTop="8dp"
+            app:buttonBackground="@drawable/background_big_button_black"
+            android:alpha="0.4" />
+
+        <com.futo.platformplayer.views.buttons.BigButton
+            android:id="@+id/button_guide"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            app:buttonText="FCast Guide"
+            app:buttonSubText="@string/how_to_use_fcast_guide"
+            app:buttonIcon="@drawable/ic_code"
+            android:layout_marginTop="8dp"
+            app:buttonBackground="@drawable/background_big_button_black" />
+
+        <com.futo.platformplayer.views.buttons.BigButton
+            android:id="@+id/button_website"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            app:buttonText="@string/fcast_website"
+            app:buttonSubText="@string/open_the_fcast_website"
+            app:buttonIcon="@drawable/ic_link"
+            android:layout_marginTop="8dp"
+            app:buttonBackground="@drawable/background_big_button_black" />
+
+        <com.futo.platformplayer.views.buttons.BigButton
+            android:id="@+id/button_technical"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            app:buttonText="@string/fcast_technical_documentation"
+            app:buttonSubText="@string/view_the_fcast_technical_documentation"
+            app:buttonIcon="@drawable/ic_wrench"
+            android:layout_marginTop="8dp"
+            app:buttonBackground="@drawable/background_big_button_black" />
+
+        <com.futo.platformplayer.views.buttons.BigButton
+            android:id="@+id/button_close"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            app:buttonText="@string/close"
+            app:buttonSubText="@string/go_back_to_casting_add_dialog"
+            app:buttonIcon="@drawable/ic_close"
+            android:layout_marginTop="8dp"
+            app:buttonBackground="@drawable/background_big_button_black"/>
+    </LinearLayout>
+</FrameLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_url_handling.xml b/app/src/main/res/layout/dialog_url_handling.xml
new file mode 100644
index 0000000000000000000000000000000000000000..fa860162606257da797c6e26769a6d413e246320
--- /dev/null
+++ b/app/src/main/res/layout/dialog_url_handling.xml
@@ -0,0 +1,86 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:gravity="center"
+    android:background="@color/gray_1d">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        android:gravity="center">
+
+        <TextView
+            android:id="@+id/dialog_text"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/allow_grayjay_to_handle_specific_urls"
+            android:textSize="14dp"
+            android:textColor="@color/white"
+            android:fontFamily="@font/inter_regular"
+            android:textAlignment="center"
+            android:layout_marginTop="25dp"
+            android:layout_marginStart="30dp"
+            android:layout_marginEnd="30dp" />
+        <TextView
+            android:id="@+id/dialog_text_details"
+            android:layout_width="match_parent"
+            android:textColor="#AAAAAA"
+            android:fontFamily="@font/inter_regular"
+            android:text="@string/allow_grayjay_to_handle_specific_urls_please_set_it_as_default_in_the_app_settings"
+            android:layout_marginStart="30dp"
+            android:layout_marginEnd="30dp"
+            android:layout_marginTop="12dp"
+            android:textSize="12dp"
+            android:layout_height="wrap_content" />
+        <LinearLayout
+            android:id="@+id/dialog_buttons"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal"
+            android:gravity="center_vertical|end"
+            android:layout_marginTop="14dp"
+            android:layout_marginBottom="28dp">
+
+            <TextView
+                android:id="@+id/button_no"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/no"
+                android:textSize="14dp"
+                android:textColor="@color/primary"
+                android:fontFamily="@font/inter_regular"
+                android:paddingTop="10dp"
+                android:paddingBottom="10dp"
+                android:paddingStart="32dp"
+                android:paddingEnd="32dp"
+                android:layout_marginEnd="16dp"/>
+
+            <LinearLayout
+                android:id="@+id/button_yes"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:background="@drawable/background_button_primary"
+                android:layout_marginEnd="28dp"
+                android:clickable="true">
+
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="@string/yes"
+                    android:textSize="14dp"
+                    android:textColor="@color/white"
+                    android:fontFamily="@font/inter_regular"
+                    android:paddingTop="10dp"
+                    android:paddingBottom="10dp"
+                    android:paddingStart="28dp"
+                    android:paddingEnd="28dp"/>
+            </LinearLayout>
+        </LinearLayout>
+    </LinearLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_comments.xml b/app/src/main/res/layout/fragment_comments.xml
new file mode 100644
index 0000000000000000000000000000000000000000..cbaccaebcda10f5a20b39d6dec4622b8c9d311fc
--- /dev/null
+++ b/app/src/main/res/layout/fragment_comments.xml
@@ -0,0 +1,111 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <LinearLayout
+        android:id="@+id/layout_header"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        android:layout_marginTop="12dp"
+        android:layout_marginStart="16dp"
+        android:layout_marginEnd="16dp"
+        android:layout_marginBottom="12dp">
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textSize="24dp"
+            android:text="@string/comments"
+            android:fontFamily="@font/inter_extra_light"
+            android:textColor="@color/white" />
+
+        <TextView
+            android:id="@+id/text_comment_count"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textSize="12dp"
+            android:text="@string/these_are_all_commentcount_comments_you_have_made_in_grayjay"
+            android:fontFamily="@font/inter_regular"
+            android:textColor="#808080" />
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:gravity="center_vertical"
+            android:paddingTop="8dp"
+            android:paddingBottom="8dp">
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:textSize="14dp"
+                android:textColor="@color/gray_ac"
+                android:fontFamily="@font/inter_light"
+                android:text="@string/sort_by" />
+
+            <Spinner
+                android:id="@+id/spinner_sortby"
+                android:layout_width="0dp"
+                android:layout_weight="1"
+                android:layout_height="wrap_content"
+                android:paddingStart="20dp"
+                android:paddingEnd="12dp" />
+        </LinearLayout>
+    </LinearLayout>
+
+    <androidx.recyclerview.widget.RecyclerView
+        android:id="@+id/recycler_comments"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+
+    <com.futo.platformplayer.views.overlays.RepliesOverlay
+        android:id="@+id/replies_overlay"
+        android:visibility="gone"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+
+    <LinearLayout android:id="@+id/layout_not_logged_in"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:gravity="center"
+        android:orientation="vertical"
+        android:background="@color/black">
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="Login to view your comments"
+            android:textSize="14dp"
+            android:textColor="@color/white"
+            android:fontFamily="@font/inter_regular"
+            android:paddingTop="10dp"
+            android:paddingBottom="10dp"
+            android:paddingStart="28dp"
+            android:paddingEnd="28dp"
+            android:layout_marginBottom="20dp"/>
+
+        <LinearLayout
+            android:id="@+id/button_login"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:background="@drawable/background_button_primary"
+            android:clickable="true">
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="Login"
+                android:textSize="14dp"
+                android:textColor="@color/white"
+                android:fontFamily="@font/inter_regular"
+                android:paddingTop="10dp"
+                android:paddingBottom="10dp"
+                android:paddingStart="28dp"
+                android:paddingEnd="28dp"/>
+        </LinearLayout>
+    </LinearLayout>
+
+</FrameLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/list_comment.xml b/app/src/main/res/layout/list_comment.xml
index 8491b028e730b1af7ec637a853f157a8c39909c8..3aa9fb8e480309fc3340c590b66b9f9c9af3c3e2 100644
--- a/app/src/main/res/layout/list_comment.xml
+++ b/app/src/main/res/layout/list_comment.xml
@@ -70,7 +70,7 @@
 
     <LinearLayout
         android:orientation="horizontal"
-        android:layout_width="wrap_content"
+        android:layout_width="match_parent"
         android:layout_height="wrap_content"
         app:layout_constraintLeft_toRightOf="@id/image_thumbnail"
         app:layout_constraintTop_toBottomOf="@id/text_body"
@@ -136,6 +136,30 @@
             app:pillIcon="@drawable/ic_forum"
             app:pillText="55 Replies"
             android:layout_marginStart="15dp" />
+
+        <Space android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_weight="1" />
+
+        <FrameLayout
+            android:id="@+id/button_delete"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:background="@drawable/background_pill_pred"
+            android:paddingTop="8dp"
+            android:paddingBottom="8dp"
+            android:paddingStart="16dp"
+            android:paddingEnd="16dp">
+            <TextView
+                android:id="@+id/pill_text"
+                android:layout_width="wrap_content"
+                android:layout_height="match_parent"
+                android:textColor="@color/white"
+                android:textSize="13dp"
+                android:gravity="center_vertical"
+                android:fontFamily="@font/inter_light"
+                android:text="@string/delete" />
+        </FrameLayout>
     </LinearLayout>
 
 
diff --git a/app/src/main/res/layout/list_comment_with_reference.xml b/app/src/main/res/layout/list_comment_with_reference.xml
new file mode 100644
index 0000000000000000000000000000000000000000..2e8282552223110e34d79c60fdb806b6492cd984
--- /dev/null
+++ b/app/src/main/res/layout/list_comment_with_reference.xml
@@ -0,0 +1,124 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/layout_comment"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_marginTop="5dp"
+    android:layout_marginBottom="5dp"
+    android:layout_marginStart="14dp"
+    android:layout_marginEnd="14dp"
+    android:orientation="vertical"
+    android:background="@drawable/background_comment"
+    android:padding="16dp">
+
+    <com.futo.platformplayer.views.others.CreatorThumbnail
+        android:id="@+id/image_thumbnail"
+        android:layout_width="25dp"
+        android:layout_height="25dp"
+        android:contentDescription="@string/channel_image"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        tools:src="@drawable/placeholder_channel_thumbnail" />
+
+    <TextView
+        android:id="@+id/text_author"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="10dp"
+        android:ellipsize="end"
+        android:gravity="center_vertical"
+        android:maxLines="1"
+        android:fontFamily="@font/inter_regular"
+        android:textColor="@color/white"
+        android:textSize="14sp"
+        app:layout_constraintLeft_toRightOf="@id/image_thumbnail"
+        app:layout_constraintTop_toTopOf="@id/image_thumbnail"
+        tools:text="ShortCircuit" />
+
+    <TextView
+        android:id="@+id/text_metadata"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:ellipsize="end"
+        android:gravity="center_vertical"
+        android:maxLines="1"
+        android:fontFamily="@font/inter_regular"
+        android:textColor="@color/gray_ac"
+        android:textSize="14sp"
+        app:layout_constraintBottom_toBottomOf="@id/text_author"
+        app:layout_constraintLeft_toRightOf="@id/text_author"
+        app:layout_constraintRight_toRightOf="parent"
+        app:layout_constraintTop_toTopOf="@id/text_author"
+        tools:text=" • 3 years ago" />
+
+    <com.futo.platformplayer.views.behavior.NonScrollingTextView
+        android:id="@+id/text_body"
+        android:layout_width="0dp"
+        android:layout_height="match_parent"
+        android:layout_marginTop="5dp"
+        android:layout_marginStart="10dp"
+        android:background="@color/transparent"
+        android:fontFamily="@font/inter_regular"
+        android:isScrollContainer="false"
+        android:textColor="#CCCCCC"
+        android:textSize="13sp"
+        android:maxLines="100"
+        app:layout_constraintTop_toBottomOf="@id/text_metadata"
+        app:layout_constraintLeft_toRightOf="@id/image_thumbnail"
+        app:layout_constraintRight_toRightOf="parent"
+        tools:text="@string/lorem_ipsum" />
+
+    <LinearLayout
+        android:orientation="horizontal"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        app:layout_constraintLeft_toRightOf="@id/image_thumbnail"
+        app:layout_constraintTop_toBottomOf="@id/text_body"
+        android:layout_marginTop="8dp"
+        android:gravity="center_vertical">
+
+        <com.futo.platformplayer.views.pills.PillRatingLikesDislikes
+            android:id="@+id/rating"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            app:layout_constraintLeft_toLeftOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            android:layout_marginStart="10dp" />
+
+        <com.futo.platformplayer.views.pills.PillButton
+            android:id="@+id/button_replies"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            app:pillIcon="@drawable/ic_forum"
+            app:pillText="55 Replies"
+            android:layout_marginStart="15dp" />
+
+        <Space android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_weight="1" />
+
+        <FrameLayout
+            android:id="@+id/button_delete"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:background="@drawable/background_pill_pred"
+            android:paddingTop="8dp"
+            android:paddingBottom="8dp"
+            android:paddingStart="16dp"
+            android:paddingEnd="16dp">
+            <TextView
+                android:id="@+id/pill_text"
+                android:layout_width="wrap_content"
+                android:layout_height="match_parent"
+                android:textColor="@color/white"
+                android:textSize="13dp"
+                android:gravity="center_vertical"
+                android:fontFamily="@font/inter_light"
+                android:text="@string/delete" />
+        </FrameLayout>
+    </LinearLayout>
+
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/list_video_thumbnail_nested.xml b/app/src/main/res/layout/list_video_thumbnail_nested.xml
index a54e6276f6af27ebe157f99736c9e6c969d8a36c..cd2afb51a968c6dfb105c50f26831980ae2c68e1 100644
--- a/app/src/main/res/layout/list_video_thumbnail_nested.xml
+++ b/app/src/main/res/layout/list_video_thumbnail_nested.xml
@@ -127,7 +127,7 @@
                 android:visibility="gone"
                 android:gravity="center"
                 android:orientation="vertical">
-                <com.futo.platformplayer.views.Loader
+                <com.futo.platformplayer.views.LoaderView
                     android:id="@+id/loader"
                     android:layout_width="50dp"
                     android:layout_height="50dp" />
diff --git a/app/src/main/res/layout/overlay_replies.xml b/app/src/main/res/layout/overlay_replies.xml
index 9ce3174ba412a56a1a91e868f524dd4095774160..c683b38a3a0cd2e502ecbab3c9b0b7a8e1620178 100644
--- a/app/src/main/res/layout/overlay_replies.xml
+++ b/app/src/main/res/layout/overlay_replies.xml
@@ -2,6 +2,7 @@
 <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
+    xmlns:tools="http://schemas.android.com/tools"
     android:background="@color/black"
     xmlns:app="http://schemas.android.com/apk/res-auto">
 
@@ -15,6 +16,78 @@
         app:layout_constraintLeft_toLeftOf="parent"
         app:layout_constraintRight_toRightOf="parent" />
 
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/layout_parent_comment"
+        android:layout_height="wrap_content"
+        android:layout_width="match_parent"
+        app:layout_constraintTop_toBottomOf="@id/topbar"
+        app:layout_constraintLeft_toLeftOf="parent"
+        android:layout_marginStart="12dp"
+        android:layout_marginEnd="12dp"
+        android:layout_marginTop="6dp"
+        android:padding="12dp"
+        android:background="@drawable/background_16_round_4dp">
+
+        <com.futo.platformplayer.views.others.CreatorThumbnail
+            android:id="@+id/image_thumbnail"
+            android:layout_width="25dp"
+            android:layout_height="25dp"
+            android:contentDescription="@string/channel_image"
+            app:layout_constraintLeft_toLeftOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            tools:src="@drawable/placeholder_channel_thumbnail" />
+
+        <TextView
+            android:id="@+id/text_author"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="10dp"
+            android:ellipsize="end"
+            android:gravity="center_vertical"
+            android:maxLines="1"
+            android:fontFamily="@font/inter_regular"
+            android:textColor="@color/white"
+            android:textSize="14sp"
+            app:layout_constraintLeft_toRightOf="@id/image_thumbnail"
+            app:layout_constraintTop_toTopOf="@id/image_thumbnail"
+            android:text="ShortCircuit" />
+
+        <TextView
+            android:id="@+id/text_metadata"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:ellipsize="end"
+            android:gravity="center_vertical"
+            android:maxLines="1"
+            android:fontFamily="@font/inter_regular"
+            android:textColor="@color/gray_ac"
+            android:textSize="14sp"
+            app:layout_constraintBottom_toBottomOf="@id/text_author"
+            app:layout_constraintLeft_toRightOf="@id/text_author"
+            app:layout_constraintRight_toRightOf="parent"
+            app:layout_constraintTop_toTopOf="@id/text_author"
+            android:text=" • 3 years ago" />
+
+        <com.futo.platformplayer.views.behavior.NonScrollingTextView
+            android:id="@+id/text_body"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="5dp"
+            android:layout_marginStart="10dp"
+            android:background="@color/transparent"
+            android:fontFamily="@font/inter_regular"
+            android:isScrollContainer="false"
+            android:textColor="#CCCCCC"
+            android:textSize="13sp"
+            android:maxLines="3"
+            android:ellipsize="end"
+            app:layout_constraintTop_toBottomOf="@id/text_metadata"
+            app:layout_constraintLeft_toRightOf="@id/image_thumbnail"
+            app:layout_constraintRight_toRightOf="parent"
+            android:text="@string/lorem_ipsum" />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
     <com.futo.platformplayer.views.comments.AddCommentView
         android:id="@+id/add_comment_view"
         android:layout_width="match_parent"
@@ -22,8 +95,7 @@
         android:layout_marginTop="12dp"
         android:layout_marginStart="12dp"
         android:layout_marginEnd="12dp"
-        android:paddingBottom="12dp"
-        app:layout_constraintTop_toBottomOf="@id/topbar"
+        app:layout_constraintTop_toBottomOf="@id/layout_parent_comment"
         app:layout_constraintLeft_toLeftOf="parent"
         app:layout_constraintRight_toRightOf="parent" />
 
@@ -32,6 +104,7 @@
         android:layout_width="match_parent"
         android:layout_height="0dp"
         app:layout_constraintTop_toBottomOf="@id/add_comment_view"
-        app:layout_constraintBottom_toBottomOf="parent" />
+        app:layout_constraintBottom_toBottomOf="parent"
+        android:layout_marginTop="12dp" />
 
 </androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/pill_button.xml b/app/src/main/res/layout/pill_button.xml
index 759e090f1beba6192601f2d762c849d21bc14e93..96457b81856f924ea260cc87dca95e5a1910ce1d 100644
--- a/app/src/main/res/layout/pill_button.xml
+++ b/app/src/main/res/layout/pill_button.xml
@@ -9,12 +9,13 @@
     android:paddingStart="7dp"
     android:paddingEnd="12dp"
     android:background="@drawable/background_pill"
-    android:id="@+id/root">
+    android:id="@+id/root"
+    android:gravity="center_vertical">
 
     <ImageView
         android:id="@+id/pill_icon"
-        android:layout_width="20dp"
-        android:layout_height="20dp"
+        android:layout_width="18dp"
+        android:layout_height="18dp"
         android:layout_marginRight="5dp"
         android:layout_marginLeft="5dp"
         android:layout_marginTop="0dp"
@@ -31,4 +32,10 @@
         android:fontFamily="@font/inter_light"
         tools:text="500K" />
 
+    <com.futo.platformplayer.views.LoaderView
+        android:id="@+id/loader"
+        android:layout_width="14dp"
+        android:layout_height="14dp"
+        app:isWhite="true" />
+
 </LinearLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/rating_likesdislikes.xml b/app/src/main/res/layout/rating_likesdislikes.xml
index c98194e77a70026caae2428ce60307e6b570a381..a4c6599c0aef7f336b08f35a72e0b10293ce8f82 100644
--- a/app/src/main/res/layout/rating_likesdislikes.xml
+++ b/app/src/main/res/layout/rating_likesdislikes.xml
@@ -8,7 +8,8 @@
     android:paddingBottom="7dp"
     android:paddingLeft="7dp"
     android:paddingRight="12dp"
-    android:background="@drawable/background_pill">
+    android:background="@drawable/background_pill"
+    android:gravity="center_vertical">
     <ImageView
         android:id="@+id/pill_like_icon"
         android:layout_width="30dp"
@@ -22,6 +23,11 @@
         android:textSize="13dp"
         android:gravity="center_vertical"
         tools:text="500K" />
+    <com.futo.platformplayer.views.LoaderView
+        android:id="@+id/loader_likes"
+        android:layout_width="14dp"
+        android:layout_height="14dp"
+        app:isWhite="true" />
 
     <View
         android:id="@+id/pill_seperator"
@@ -44,5 +50,10 @@
         android:gravity="center_vertical"
         android:textSize="13dp"
         tools:text="500K" />
+    <com.futo.platformplayer.views.LoaderView
+        android:id="@+id/loader_dislikes"
+        android:layout_width="14dp"
+        android:layout_height="14dp"
+        app:isWhite="true" />
 
 </LinearLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/view_cast.xml b/app/src/main/res/layout/view_cast.xml
index 18958a083fbcd638f12a12fe9613c07ca7bd2ecc..9344fa9ef71ea55ecd3e54fa8eb8232df7f11eb2 100644
--- a/app/src/main/res/layout/view_cast.xml
+++ b/app/src/main/res/layout/view_cast.xml
@@ -23,6 +23,11 @@
         android:background="#cc000000"
         android:layout_marginBottom="6dp" />
 
+    <com.futo.platformplayer.views.behavior.GestureControlView
+        android:id="@+id/gesture_control"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+
     <ImageButton
         android:id="@+id/button_minimize"
         android:layout_width="50dp"
@@ -129,11 +134,6 @@
         app:layout_constraintTop_toTopOf="@id/text_position"
         app:layout_constraintBottom_toBottomOf="@id/text_position"/>
 
-    <com.futo.platformplayer.views.behavior.GestureControlView
-        android:id="@+id/gesture_control"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent" />
-
     <com.google.android.exoplayer2.ui.DefaultTimeBar
         android:id="@+id/time_progress"
         android:layout_width="match_parent"
diff --git a/app/src/main/res/layout/view_comments_list.xml b/app/src/main/res/layout/view_comments_list.xml
index f285cae02b40d5f2d67895d0d8f3378d5809e2a1..48318415377fe1bc2a02be04ce62743df9087801 100644
--- a/app/src/main/res/layout/view_comments_list.xml
+++ b/app/src/main/res/layout/view_comments_list.xml
@@ -1,13 +1,11 @@
 <?xml version="1.0" encoding="utf-8"?>
-<androidx.constraintlayout.widget.ConstraintLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
+<FrameLayout 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">
+    android:layout_height="match_parent">
 
     <androidx.recyclerview.widget.RecyclerView
         android:id="@+id/recycler_comments"
         android:layout_width="match_parent"
         android:layout_height="match_parent" />
     
-</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
+</FrameLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/view_monetization.xml b/app/src/main/res/layout/view_monetization.xml
index aa11779eb9d1906b63c867d59b1e63022a416786..d89ee66c11c3d9cc295bbdf87a63ed39e5787a24 100644
--- a/app/src/main/res/layout/view_monetization.xml
+++ b/app/src/main/res/layout/view_monetization.xml
@@ -120,7 +120,7 @@
             android:orientation="horizontal"
             android:layout_gravity="center" />
 
-        <com.futo.platformplayer.views.Loader
+        <com.futo.platformplayer.views.LoaderView
             android:id="@+id/loader_merchandise"
             android:layout_width="64dp"
             android:layout_height="64dp"
diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml
index e40c1b4fff348c17d744c7071dddf477efedf3c5..585cdf8969ffad2a66154b67577e5dcac74cfa09 100644
--- a/app/src/main/res/values-ar/strings.xml
+++ b/app/src/main/res/values-ar/strings.xml
@@ -722,4 +722,8 @@
         <item>معلومات</item>
         <item>تفصيلي</item>
     </string-array>
+    <string-array name="comments_sortby_array">
+        <item>Newest</item>
+        <item>Oldest</item>
+    </string-array>
 </resources>
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 45ba4cec706f642ac5fa42f081540437e6754c1b..72d223fbc261fe64e7a81cc8609e90ac85b38cfc 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -722,4 +722,8 @@
         <item>Information</item>
         <item>Ausführlich</item>
     </string-array>
+    <string-array name="comments_sortby_array">
+        <item>Newest</item>
+        <item>Oldest</item>
+    </string-array>
 </resources>
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 2e3a05cbaaf0465087149af2a539bdb551f3ba85..b73d92bf4bb529b67ed22be35018831f5a662306 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -738,4 +738,8 @@
         <item>Información</item>
         <item>Detallado</item>
     </string-array>
+    <string-array name="comments_sortby_array">
+        <item>Newest</item>
+        <item>Oldest</item>
+    </string-array>
 </resources>
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index 5949281b727830d21a3ab2c0ffbe16da9a9a9d30..2691d5d0ed44631569613fd7bac88fb7df8c6cd1 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -722,4 +722,8 @@
         <item>Information</item>
         <item>Verbeux</item>
     </string-array>
+    <string-array name="comments_sortby_array">
+        <item>Newest</item>
+        <item>Oldest</item>
+    </string-array>
 </resources>
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index cb450e4b7c9add38512d15f1f004566d4da3c5bf..c58233cea98712e68393fb6493c20c7fbbd25d79 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -722,4 +722,8 @@
         <item>情報</item>
         <item>詳細</item>
     </string-array>
+    <string-array name="comments_sortby_array">
+        <item>Newest</item>
+        <item>Oldest</item>
+    </string-array>
 </resources>
diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml
index 10fa75608aac94e5270356da095f53578c320ea1..2124a56cfd6380f9f5b512895a036b603c5de225 100644
--- a/app/src/main/res/values-ko/strings.xml
+++ b/app/src/main/res/values-ko/strings.xml
@@ -722,4 +722,8 @@
         <item>ì •ë³´</item>
         <item>상세</item>
     </string-array>
+    <string-array name="comments_sortby_array">
+        <item>Newest</item>
+        <item>Oldest</item>
+    </string-array>
 </resources>
diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml
index 26f4f3b2d97e0daf946c4a6e6cb1ca945f8f31ed..793ff525ef1384ccb6657de99c6373d36120d36e 100644
--- a/app/src/main/res/values-pt/strings.xml
+++ b/app/src/main/res/values-pt/strings.xml
@@ -722,4 +722,8 @@
         <item>Informação</item>
         <item>Detalhado</item>
     </string-array>
+    <string-array name="comments_sortby_array">
+        <item>Newest</item>
+        <item>Oldest</item>
+    </string-array>
 </resources>
\ No newline at end of file
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 9a5b94406f554442de30a6ed8d76fcded757fac3..ebc3654b29bb0f48f93b311e2ecb75f4c4e59b71 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -722,4 +722,8 @@
         <item>Информация</item>
         <item>Подробно</item>
     </string-array>
+    <string-array name="comments_sortby_array">
+        <item>Newest</item>
+        <item>Oldest</item>
+    </string-array>
 </resources>
diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml
index 701aee7f83b01b92c2284d26ee4b3d4e443437b5..7a0d2f77b93bb6f9ce6a023e9c00f32bda0a3e04 100644
--- a/app/src/main/res/values-zh/strings.xml
+++ b/app/src/main/res/values-zh/strings.xml
@@ -722,4 +722,8 @@
         <item>信息</item>
         <item>详细</item>
     </string-array>
+    <string-array name="comments_sortby_array">
+        <item>Newest</item>
+        <item>Oldest</item>
+    </string-array>
 </resources>
diff --git a/app/src/main/res/values/loader_attrs.xml b/app/src/main/res/values/loader_attrs.xml
index 73aa80fd3310fa9e3d7c1ea6aa299553bda7658f..0c4919985e3ed90cb144bb6788944a2386665dae 100644
--- a/app/src/main/res/values/loader_attrs.xml
+++ b/app/src/main/res/values/loader_attrs.xml
@@ -2,5 +2,6 @@
 <resources>
     <declare-styleable name="LoaderView">
         <attr name="automatic" format="boolean" />
+        <attr name="isWhite" format="boolean" />
     </declare-styleable>
 </resources>
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 8552e9c4760d9e80f29b7ea2f51318db577645bf..72462219674bd59add616acd81ea2fe0cfc8e6bc 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -292,6 +292,8 @@
     <string name="clear_external_downloads_directory">Clear external Downloads directory</string>
     <string name="change_external_general_directory">Change external General directory</string>
     <string name="change_tabs_visible_on_the_home_screen">Change tabs visible on the home screen</string>
+    <string name="link_handling">Link Handling</string>
+    <string name="allow_grayjay_to_handle_links">Allow Grayjay to handle links</string>
     <string name="change_the_external_directory_for_general_files">Change the external directory for general files</string>
     <string name="clear_the_external_storage_for_download_files">Clear the external storage for download files</string>
     <string name="change_the_external_storage_for_download_files">Change the external storage for download files</string>
@@ -377,6 +379,10 @@
     <string name="restore_a_previous_automatic_backup">Restore a previous automatic backup</string>
     <string name="resume_after_preview">Resume After Preview</string>
     <string name="review_the_current_and_past_changelogs">Review the current and past changelogs</string>
+    <string name="restart_after_audio_focus_loss">Restart after audio focus loss</string>
+    <string name="restart_playback_when_gaining_audio_focus_after_a_loss">Restart playback when gaining audio focus after a loss</string>
+    <string name="restart_after_connectivity_loss">Restart after connectivity loss</string>
+    <string name="restart_playback_when_gaining_connectivity_after_a_loss">Restart playback when gaining connectivity after a loss</string>
     <string name="chapter_update_fps_title">Chapter Update FPS</string>
     <string name="chapter_update_fps_description">Change accuracy of chapter updating, higher might cost more performance</string>
     <string name="set_automatic_backup">Set Automatic Backup</string>
@@ -678,6 +684,22 @@
     <string name="plus_tax">" + Tax"</string>
     <string name="new_playlist">New playlist</string>
     <string name="add_to_new_playlist">Add to new playlist</string>
+    <string name="url_handling">URL Handling</string>
+    <string name="allow_grayjay_to_handle_specific_urls">Allow Grayjay to handle specific URLs?</string>
+    <string name="allow_grayjay_to_handle_specific_urls_please_set_it_as_default_in_the_app_settings">When you click \'Yes\', the Grayjay app settings will open.\n\nThere, navigate to:\n1. "Open by default" or "Set as default" section.\nYou might find this option directly or under \'Advanced\' settings, depending on your device.\n\n2. Choose \'Open supported links\' for Grayjay.\n\n(some devices have this listed under \'Default Apps\' in the main settings followed by selecting Grayjay for relevant categories)</string>
+    <string name="failed_to_show_settings">Failed to show settings</string>
+    <string name="play_store_version_does_not_support_default_url_handling">Play store version does not support default URL handling.</string>
+    <string name="these_are_all_commentcount_comments_you_have_made_in_grayjay">These are all {commentCount} comments you have made in Grayjay.</string>
+    <string name="tutorial">Tutorial</string>
+    <string name="go_back_to_casting_add_dialog">Go back to casting add dialog</string>
+    <string name="view_a_video_about_how_to_cast">View a video about how to cast</string>
+    <string name="view_the_fcast_technical_documentation">View the FCast technical documentation</string>
+    <string name="guide">Guide</string>
+    <string name="how_to_use_fcast_guide">How to use FCast guide</string>
+    <string name="fcast">FCast</string>
+    <string name="open_the_fcast_website">Open the FCast website</string>
+    <string name="fcast_website">FCast Website</string>
+    <string name="fcast_technical_documentation">FCast Technical Documentation</string>
     <string-array name="home_screen_array">
         <item>Recommendations</item>
         <item>Subscriptions</item>
@@ -765,6 +787,10 @@
         <item>Disabled</item>
         <item>Enabled</item>
     </string-array>
+    <string-array name="comments_sortby_array">
+        <item>Newest</item>
+        <item>Oldest</item>
+    </string-array>
     <string-array name="subscriptions_sortby_array">
         <item>Name Ascending</item>
         <item>Name Descending</item>
@@ -825,7 +851,7 @@
         <item>Russian</item>
     </string-array>
     <string-array name="casting_device_type_array" translatable="false">
-        <item>FastCast</item>
+        <item>FCast</item>
         <item>ChromeCast</item>
         <item>AirPlay</item>
     </string-array>
@@ -836,4 +862,10 @@
         <item>Information</item>
         <item>Verbose</item>
     </string-array>
+    <string-array name="restart_playback_after_loss">
+        <item>Never</item>
+        <item>Within 10 seconds of loss</item>
+        <item>Within 30 seconds of loss</item>
+        <item>Always</item>
+    </string-array>
 </resources>
\ No newline at end of file
diff --git a/app/src/stable/assets/sources/odysee b/app/src/stable/assets/sources/odysee
index 6ea204605d4a27867702d7b024237506904d53c7..a05feced804a5b664c75568162a7b3fa5562d8b3 160000
--- a/app/src/stable/assets/sources/odysee
+++ b/app/src/stable/assets/sources/odysee
@@ -1 +1 @@
-Subproject commit 6ea204605d4a27867702d7b024237506904d53c7
+Subproject commit a05feced804a5b664c75568162a7b3fa5562d8b3
diff --git a/app/src/stable/assets/sources/rumble b/app/src/stable/assets/sources/rumble
index 60a7ee2ddf71b936d9c289a3343020cc20edfe56..b0e35a9b6631fb3279fb38619ecc3eba812e5ed6 160000
--- a/app/src/stable/assets/sources/rumble
+++ b/app/src/stable/assets/sources/rumble
@@ -1 +1 @@
-Subproject commit 60a7ee2ddf71b936d9c289a3343020cc20edfe56
+Subproject commit b0e35a9b6631fb3279fb38619ecc3eba812e5ed6
diff --git a/app/src/unstable/AndroidManifest.xml b/app/src/unstable/AndroidManifest.xml
index 74db3f15ff5e9cc395aecadc414ea8828a0fead2..0f4a00b848d2235d699826f8a6e4f9d07003855c 100644
--- a/app/src/unstable/AndroidManifest.xml
+++ b/app/src/unstable/AndroidManifest.xml
@@ -24,6 +24,12 @@
                 <data android:host="www.youtube.com"  />
                 <data android:host="m.youtube.com" />
                 <data android:host="rumble.com" />
+                <data android:host="kick.com" />
+                <data android:host="nebula.tv" />
+                <data android:host="odysee.com" />
+                <data android:host="patreon.com" />
+                <data android:host="soundcloud.com" />
+                <data android:host="twitch.tv" />
                 <data android:pathPrefix="/" />
             </intent-filter>
             <intent-filter android:autoVerify="true">
@@ -33,11 +39,18 @@
 
                 <data android:mimeType="text/plain" />
 
+                <data android:host="youtu.be" />
+                <data android:host="www.you.be" />
                 <data android:host="youtube.com"  />
+                <data android:host="www.youtube.com"  />
                 <data android:host="m.youtube.com" />
-                <data android:host="you.be" />
-                <data android:host="www.you.be" />
                 <data android:host="rumble.com" />
+                <data android:host="kick.com" />
+                <data android:host="nebula.tv" />
+                <data android:host="odysee.com" />
+                <data android:host="patreon.com" />
+                <data android:host="soundcloud.com" />
+                <data android:host="twitch.tv" />
             </intent-filter>
         </activity>
     </application>
diff --git a/app/src/unstable/assets/sources/odysee b/app/src/unstable/assets/sources/odysee
index 6ea204605d4a27867702d7b024237506904d53c7..a05feced804a5b664c75568162a7b3fa5562d8b3 160000
--- a/app/src/unstable/assets/sources/odysee
+++ b/app/src/unstable/assets/sources/odysee
@@ -1 +1 @@
-Subproject commit 6ea204605d4a27867702d7b024237506904d53c7
+Subproject commit a05feced804a5b664c75568162a7b3fa5562d8b3
diff --git a/app/src/unstable/assets/sources/rumble b/app/src/unstable/assets/sources/rumble
index 60a7ee2ddf71b936d9c289a3343020cc20edfe56..b0e35a9b6631fb3279fb38619ecc3eba812e5ed6 160000
--- a/app/src/unstable/assets/sources/rumble
+++ b/app/src/unstable/assets/sources/rumble
@@ -1 +1 @@
-Subproject commit 60a7ee2ddf71b936d9c289a3343020cc20edfe56
+Subproject commit b0e35a9b6631fb3279fb38619ecc3eba812e5ed6
diff --git a/dep/polycentricandroid b/dep/polycentricandroid
index 839e4c4a4f5ed6cb6f68047f88b26c5831e6e703..faaa7a6d8efb3f92fc239e7d77ec2f9a46c3a958 160000
--- a/dep/polycentricandroid
+++ b/dep/polycentricandroid
@@ -1 +1 @@
-Subproject commit 839e4c4a4f5ed6cb6f68047f88b26c5831e6e703
+Subproject commit faaa7a6d8efb3f92fc239e7d77ec2f9a46c3a958
diff --git a/docs/Example Plugin.md b/docs/Example Plugin.md
new file mode 100644
index 0000000000000000000000000000000000000000..a4831f05d1ec3a1d55fd1105fbb0aa81536a1dcd
--- /dev/null
+++ b/docs/Example Plugin.md	
@@ -0,0 +1,244 @@
+# Example plugin
+
+Note that this is just a starting point, plugins can also implement optional features such as login, importing playlists/subscriptions, etc. For full examples please see in-house developed plugins (click [here](https://gitlab.futo.org/videostreaming/plugins)).
+
+```js
+source.enable = function (conf) {
+    /**
+     * @param conf: SourceV8PluginConfig (the SomeConfig.js)
+     */
+}
+
+source.getHome = function(continuationToken) {
+    /**
+     * @param continuationToken: any?
+     * @returns: VideoPager
+     */
+    const videos = []; // The results (PlatformVideo)
+    const hasMore = false; // Are there more pages?
+    const context = { continuationToken: continuationToken }; // Relevant data for the next page
+    return new SomeHomeVideoPager(videos, hasMore, context);
+}
+
+source.searchSuggestions = function(query) {
+    /**
+     * @param query: string
+     * @returns: string[]
+     */
+
+    const suggestions = []; //The suggestions for a specific search query
+    return suggestions;
+}
+
+source.getSearchCapabilities = function() {
+    //This is an example of how to return search capabilities like available sorts, filters and which feed types are available (see source.js for more details) 
+	return {
+		types: [Type.Feed.Mixed],
+		sorts: [Type.Order.Chronological, "^release_time"],
+		filters: [
+			{
+				id: "date",
+				name: "Date",
+				isMultiSelect: false,
+				filters: [
+					{ id: Type.Date.Today, name: "Last 24 hours", value: "today" },
+					{ id: Type.Date.LastWeek, name: "Last week", value: "thisweek" },
+					{ id: Type.Date.LastMonth, name: "Last month", value: "thismonth" },
+					{ id: Type.Date.LastYear, name: "Last year", value: "thisyear" }
+				]
+			},
+		]
+	};
+}
+
+source.search = function (query, type, order, filters, continuationToken) {
+    /**
+     * @param query: string
+     * @param type: string
+     * @param order: string
+     * @param filters: Map<string, Array<string>>
+     * @param continuationToken: any?
+     * @returns: VideoPager
+     */
+    const videos = []; // The results (PlatformVideo)
+    const hasMore = false; // Are there more pages?
+    const context = { query: query, type: type, order: order, filters: filters, continuationToken: continuationToken }; // Relevant data for the next page
+    return new SomeSearchVideoPager(videos, hasMore, context);
+}
+
+source.getSearchChannelContentsCapabilities = function () {
+    //This is an example of how to return search capabilities on a channel like available sorts, filters and which feed types are available (see source.js for more details)
+	return {
+		types: [Type.Feed.Mixed],
+		sorts: [Type.Order.Chronological],
+		filters: []
+	};
+}
+
+source.searchChannelContents = function (url, query, type, order, filters, continuationToken) {
+    /**
+     * @param url: string
+     * @param query: string
+     * @param type: string
+     * @param order: string
+     * @param filters: Map<string, Array<string>>
+     * @param continuationToken: any?
+     * @returns: VideoPager
+     */
+
+    const videos = []; // The results (PlatformVideo)
+    const hasMore = false; // Are there more pages?
+    const context = { channelUrl: channelUrl, query: query, type: type, order: order, filters: filters, continuationToken: continuationToken }; // Relevant data for the next page
+    return new SomeSearchChannelVideoPager(videos, hasMore, context);
+}
+
+source.searchChannels = function (query, continuationToken) {
+    /**
+     * @param query: string
+     * @param continuationToken: any?
+     * @returns: ChannelPager
+     */
+
+    const channels = []; // The results (PlatformChannel)
+    const hasMore = false; // Are there more pages?
+    const context = { query: query, continuationToken: continuationToken }; // Relevant data for the next page
+    return new SomeChannelPager(channels, hasMore, context);
+}
+
+source.isChannelUrl = function(url) {
+    /**
+     * @param url: string
+     * @returns: boolean
+     */
+
+	return REGEX_CHANNEL_URL.test(url);
+}
+
+source.getChannel = function(url) {
+	return new PlatformChannel({
+		//... see source.js for more details
+	});
+}
+
+source.getChannelContents = function(url, type, order, filters, continuationToken) {
+    /**
+     * @param url: string
+     * @param type: string
+     * @param order: string
+     * @param filters: Map<string, Array<string>>
+     * @param continuationToken: any?
+     * @returns: VideoPager
+     */
+
+    const videos = []; // The results (PlatformVideo)
+    const hasMore = false; // Are there more pages?
+    const context = { url: url, query: query, type: type, order: order, filters: filters, continuationToken: continuationToken }; // Relevant data for the next page
+    return new SomeChannelVideoPager(videos, hasMore, context);
+}
+
+source.isContentDetailsUrl = function(url) {
+    /**
+     * @param url: string
+     * @returns: boolean
+     */
+
+	return REGEX_DETAILS_URL.test(url);
+}
+
+source.getContentDetails = function(url) {
+    /**
+     * @param url: string
+     * @returns: PlatformVideoDetails
+     */
+
+	return new PlatformVideoDetails({
+		//... see source.js for more details
+	});
+}
+
+source.getComments = function (url, continuationToken) {
+    /**
+     * @param url: string
+     * @param continuationToken: any?
+     * @returns: CommentPager
+     */
+
+    const comments = []; // The results (Comment)
+    const hasMore = false; // Are there more pages?
+    const context = { url: url, continuationToken: continuationToken }; // Relevant data for the next page
+    return new SomeCommentPager(comments, hasMore, context);
+
+}
+source.getSubComments = function (comment) {
+    /**
+     * @param comment: Comment
+     * @returns: SomeCommentPager
+     */
+
+	if (typeof comment === 'string') {
+		comment = JSON.parse(comment);
+	}
+
+	return getCommentsPager(comment.context.claimId, comment.context.claimId, 1, false, comment.context.commentId);
+}
+
+class SomeCommentPager extends CommentPager {
+    constructor(results, hasMore, context) {
+        super(results, hasMore, context);
+    }
+
+    nextPage() {
+        return source.getComments(this.context.url, this.context.continuationToken);
+    }
+}
+
+class SomeHomeVideoPager extends VideoPager {
+	constructor(results, hasMore, context) {
+		super(results, hasMore, context);
+	}
+	
+	nextPage() {
+		return source.getHome(this.context.continuationToken);
+	}
+}
+
+class SomeSearchVideoPager extends VideoPager {
+	constructor(results, hasMore, context) {
+		super(results, hasMore, context);
+	}
+	
+	nextPage() {
+		return source.search(this.context.query, this.context.type, this.context.order, this.context.filters, this.context.continuationToken);
+	}
+}
+
+class SomeSearchChannelVideoPager extends VideoPager {
+	constructor(results, hasMore, context) {
+		super(results, hasMore, context);
+	}
+	
+	nextPage() {
+		return source.searchChannelContents(this.context.channelUrl, this.context.query, this.context.type, this.context.order, this.context.filters, this.context.continuationToken);
+	}
+}
+
+class SomeChannelPager extends ChannelPager {
+	constructor(results, hasMore, context) {
+		super(results, hasMore, context);
+	}
+	
+	nextPage() {
+		return source.searchChannelContents(this.context.query, this.context.continuationToken);
+	}
+}
+
+class SomeChannelVideoPager extends VideoPager {
+	constructor(results, hasMore, context) {
+		super(results, hasMore, context);
+	}
+	
+	nextPage() {
+		return source.getChannelContents(this.context.url, this.context.type, this.context.order, this.context.filters, this.context.continuationToken);
+	}
+}
+```
\ No newline at end of file
diff --git a/docs/Script Signing.md b/docs/Script Signing.md
new file mode 100644
index 0000000000000000000000000000000000000000..5e593e356ee9713c6b5a36910327649e08045286
--- /dev/null
+++ b/docs/Script Signing.md	
@@ -0,0 +1,31 @@
+# Script signing
+
+The `scriptSignature` and `scriptPublicKey` should be set whenever you deploy your script (NOT REQUIRED DURING DEVELOPMENT). The purpose of these fields is to verify that a plugin update was made by the same individual that developed the original plugin. This prevents somebody from hijacking your plugin without having access to your public private keypair. When this value is not present, you can still use this plugin, however the user will be informed that these values are missing and that this is a security risk. Here is an example script showing you how to generate these values. See below for more details.
+
+You can use this script to generate the `scriptSignature` and `scriptPublicKey` fields above:
+
+`sign-script.sh`
+```sh
+#!/bin/sh
+#Example usage:
+#cat script.js | sign-script.sh
+#sh sign-script.sh script.js
+
+#Set your key paths here
+PRIVATE_KEY_PATH=~/.ssh/id_rsa
+PUBLIC_KEY_PATH=~/.ssh/id_rsa.pub
+
+PUBLIC_KEY_PKCS8=$(ssh-keygen -f "$PUBLIC_KEY_PATH" -e -m pkcs8 | tail -n +2 | head -n -1 | tr -d '\n')
+echo "This is your public key: '$PUBLIC_KEY_PKCS8'"
+
+if [ $# -eq 0 ]; then
+  # No parameter provided, read from stdin
+  DATA=$(cat)
+else
+  # Parameter provided, read from file
+  DATA=$(cat "$1")
+fi
+
+SIGNATURE=$(echo -n "$DATA" | openssl dgst -sha512 -sign ~/.ssh/id_rsa | base64 -w 0)
+echo "This is your signature: '$SIGNATURE'"
+```
\ No newline at end of file
diff --git a/plugin-development.md b/plugin-development.md
index 8330a53ff9b2e1c63606535edabf8b611a2f0975..6f49269e4e997383a1d23ab9f9f68ebbc88e4c64 100644
--- a/plugin-development.md
+++ b/plugin-development.md
@@ -3,363 +3,142 @@
 ## Table of Contents
 
 - [Introduction](#introduction)
-- [Grayjay App Overview](#grayjay-app-overview)
-- [Plugin Development Overview](#plugin-development-overview)
-- [Setting up the Development Environment](#setting-up-the-development-environment)
-- [Using the Developer Interface](#using-the-developer-interface)
+- [Quick Start](#quick-start)
+- [Configuration file](#configuration-file)
+- [Packages](#packages)
+- [Authentication](#authentication)
+- [Content Types](#content-types)
+- [Example plugin](#example-plugin)
+- [Pagination](#pagination)
+- [Script signing](#script-signing)
 - [Plugin Deployment](#plugin-deployment)
 - [Common Issues and Troubleshooting](#common-issues-and-troubleshooting)
-- [Additional Resources](#additional-resources)
 - [Support and Contact](#support-and-contact)
 
-
 ## Introduction
 
-Welcome to the Grayjay App plugin development documentation. This guide will provide an overview of Grayjay's plugin system and guide you through the steps necessary to create, test, debug, and deploy plugins.
+Welcome to the Grayjay App plugin development documentation. Plugins are additional components that you can create to extend the functionality of the Grayjay app, for example a YouTube or Odysee plugin. This guide will provide an overview of Grayjay's plugin system and guide you through the steps necessary to create, test, debug, and deploy plugins.
+
+## Quick Start
+
+### Download GrayJay:
+
+- Download the GrayJay app for Android [here](https://grayjay.app/).
+
+### Enable GrayJay Developer Mode:
 
-## Grayjay App Overview
+- Enable developer mode in the GrayJay app (not Android settings app) by tapping the “More” tab, tapping “Settings”, scrolling all the way to the bottom, and tapping the “Version Code” multiple times.
 
-Grayjay is a unique media application that aims to revolutionize the relationship between content creators and their audiences. By shifting the focus from platforms to creators, Grayjay democratizes the content delivery process, empowering creators to retain full ownership of their content and directly monetize their work.
+### Run the GrayJay DevServer:
 
-For users, Grayjay offers a more privacy-focused and personalized content viewing experience. Rather than being manipulated by opaque algorithms, users can decide what they want to watch, thus enhancing their engagement and enjoyment of the content. 
+- At the bottom of the Settings page in the GrayJay app, Click the purple “Developer Settings” button. Then click the “Start Server” button to start the DevServer.
 
-Our ultimate goal is to create the best media app, merging content and features that users love with a strong emphasis on user and creator empowerment and privacy.
+  <img src="https://gitlab.futo.org/videostreaming/grayjay/uploads/07fc4919b0a8446c4cdf5335565c0611/image.png" width="200">
 
-By developing Grayjay, we strive to make a stride toward a more open, interconnected, and equitable media ecosystem. This ecosystem fosters a thriving community of creators who are supported by their audiences, all facilitated through a platform that respects and prioritizes privacy and ownership.
+### Open the GrayJay DevServer on your computer:
 
-## Plugin Development Overview
+- Open the Android settings app and search for “IP address”. The IP address should look like `192.168.X.X`.
+- Open `http://<phone-ip>:11337/dev` in your web browser.
+    
+  <img src="https://gitlab.futo.org/videostreaming/grayjay/uploads/72885c3bc51b8efe9462ee68d47e3b51/image.png" width="600">
 
-Plugins are additional components that you can create to extend the functionality of the Grayjay app.
+### Create and host your plugin:
 
-## Setting up the Developer Environment
+- Clone the [Odysee plugin](https://gitlab.futo.org/videostreaming/plugins/odysee) as an example
+- `cd` into the project folder and serve with `npx serve` (if you have [Node.js](https://nodejs.org/en/)) or any other HTTP Server you desire.
+- `npx serve` should give you a Network url (not the localhost one) that looks like `http://192.168.X.X:3000`. Your config file URL will be something like `http://192.168.X.X:3000/OdyseeConfig.json`.
+    
+  <img src="https://gitlab.futo.org/videostreaming/grayjay/uploads/cc266da0a0b85c5770abca22c0b03b3b/image.png" width="600">
 
-Before you start developing plugins, it is necessary to set up a suitable developer environment. Here's how to do it:
+### Test your plugin:
 
-1. Create a plugin, the minimal starting point is the following.
+- When the DevServer is open in your browser, enter the config file URL and click “Load Plugin”. This will NOT inject the plugin into the app, for that you need to click "Inject Plugin" on the Integration tab.
+    
+  <img src="https://gitlab.futo.org/videostreaming/grayjay/uploads/386a562f30a60cfcbb8a8a1345a788e5/image.png" width="600">
+    
+- On the Testing tab, you can individually test the methods in your plugin. To reload once you make changes on the plugin, click the top-right refresh button. *Note: While testing, the custom domParser package is overwritten with the browser's implementation, so it may behave differently than once it is loaded into the app.*
+    
+  <img src="https://gitlab.futo.org/videostreaming/grayjay/uploads/08830eb8cc56cc55ba445dd49db86235/image.png" width="600">
+    
+- On the Integration tab you can test your plugin end-to-end in the GrayJay app and monitor device logs. You can click "Inject Plugin" in order to inject the plugin into the app. Your plugin should show up on the Sources tab in the GrayJay app. If you make changes and want to reload the plugin, click "Inject Plugin" again.
+    
+  <img src="https://gitlab.futo.org/videostreaming/grayjay/uploads/74813fbf37dcfc63055595061e41c48b/image.png" width="600">
 
-`SomeConfig.js`
-```json
+## Configuration file
+
+Create a configuration file for your plugin.
+
+`SomeConfig.json`
+```js
 {
 	"name": "Some name",
 	"description": "A description for your plugin",
 	"author": "Your author name",
 	"authorUrl": "https://yoursite.com",
 	
+    // The `sourceUrl` field should contain the URL where your plugin will be publically accessible in the future. This allows the app to scan this location to see if there are any updates available.
 	"sourceUrl": "https://yoursite.com/SomeConfig.json",
 	"repositoryUrl": "https://github.com/someuser/someproject",
 	"scriptUrl": "./SomeScript.js",
 	"version": 1,
 	
 	"iconUrl": "./someimage.png",
+
+    // The `id` field should be a uniquely generated UUID like from [https://www.uuidgenerator.net/](https://www.uuidgenerator.net/). This will be used to distinguish your plugin from others.
 	"id": "309b2e83-7ede-4af8-8ee9-822bc4647a24",
 	
-	"scriptSignature": "<ommitted>",
-	"scriptPublicKey": "<ommitted>",
+    // See the "Script Signing" section for details
+	"scriptSignature": "<omitted>",
+	"scriptPublicKey": "<omitted>",
+
+    // See the "Packages" section for details, currently allowed values are: ["Http", "DOMParser", "Utilities"]
 	"packages": ["Http"],
 	
 	"allowEval": false,
+
+    // The `allowUrls` field is allowed to be `everywhere`, this means that the plugin is allowed to access all URLs. However, this will popup a warning for the user that this is the case. Therefore, it is recommended to narrow the scope of the accessible URLs only to the URLs that you actually need. Other requests will be blocked. During development it can be convenient to use `everywhere`. Possible values are `odysee.com`, `api.odysee.com`, etc.
 	"allowUrls": [
 		"everywhere"
 	]
 }
 ```
 
-The `sourceUrl` field should contain the URL where your plugin will be publically accessible in the future. This allows the app to scan this location to see if there are any updates available.
-
-The `id` field should be a uniquely generated UUID like from [https://www.uuidgenerator.net/](https://www.uuidgenerator.net/). This will be used to distinguish your plugin from others.
-
-The `allowUrls` field is allowed to be `everywhere`, this means that the plugin is allowed to access all URLs. However, this will popup a warning for the user that this is the case. Therefore, it is recommended to narrow the scope of the accessible URLs only to the URLs that you actually need. Other requests will be blocked. During development it can be convenient to use `everywhere`. Possible values are `odysee.com`, `api.odysee.com`, etc.
-
-The `scriptSignature` and `scriptPublicKey` should be set whenever you deploy your script (NOT REQUIRED DURING DEVELOPMENT). The purpose of these fields is to verify that a plugin update was made by the same individual that developed the original plugin. This prevents somebody from hijacking your plugin without having access to your public private keypair. When this value is not present, you can still use this plugin, however the user will be informed that these values are missing and that this is a security risk. Here is an example script showing you how to generate these values.
-
-`sign-script.sh`
-```sh
-#!/bin/sh
-#Example usage:
-#cat script.js | sign-script.sh
-#sh sign-script.sh script.js
-
-#Set your key paths here
-PRIVATE_KEY_PATH=~/.ssh/id_rsa
-PUBLIC_KEY_PATH=~/.ssh/id_rsa.pub
-
-PUBLIC_KEY_PKCS8=$(ssh-keygen -f "$PUBLIC_KEY_PATH" -e -m pkcs8 | tail -n +2 | head -n -1 | tr -d '\n')
-echo "This is your public key: '$PUBLIC_KEY_PKCS8'"
-
-if [ $# -eq 0 ]; then
-  # No parameter provided, read from stdin
-  DATA=$(cat)
-else
-  # Parameter provided, read from file
-  DATA=$(cat "$1")
-fi
-
-SIGNATURE=$(echo -n "$DATA" | openssl dgst -sha512 -sign ~/.ssh/id_rsa | base64 -w 0)
-echo "This is your signature: '$SIGNATURE'"
-
-```
+## Packages
 
 The `packages` field allows you to specify which packages you want to use, current available packages are:
-- `Http`: for performing HTTP requests (see [docs](TODO))
-- `DOMParser`: for parsing a DOM (see [docs](TODO))
-- `Utilities`: for various utility functions like generating random UUIDs or converting to Base64 (see [docs](TODO))
+- `Http`: for performing HTTP requests (see [docs](https://gitlab.futo.org/videostreaming/grayjay/-/blob/master/docs/packages/packageHttp.md))
+- `DOMParser`: for parsing a DOM (no docs yet, see [source code](https://gitlab.futo.org/videostreaming/grayjay/-/blob/master/app/src/main/java/com/futo/platformplayer/engine/packages/PackageDOMParser.kt))
+- `Utilities`: for various utility functions like generating UUIDs or converting to Base64 (no docs yet, see [source code](https://gitlab.futo.org/videostreaming/grayjay/-/blob/master/app/src/main/java/com/futo/platformplayer/engine/packages/PackageUtilities.kt))
 
-Note that this is just a starting point, plugins can also implement optional features such as login, importing playlists/subscriptions, etc. For full examples please see in-house developed plugins (click [here](TODO)).
+## Authentication
 
-`SomeScript.js`
-```js
-source.enable = function (conf) {
-    /**
-     * @param conf: SourceV8PluginConfig (the SomeConfig.js)
-     */
-}
+Authentication is sometimes required by plugins to access user data and premium content, for example on YouTube or Patreon.
 
-source.getHome = function(continuationToken) {
-    /**
-     * @param continuationToken: any?
-     * @returns: VideoPager
-     */
-    const videos = []; // The results (PlatformVideo)
-    const hasMore = false; // Are there more pages?
-    const context = { continuationToken: continuationToken }; // Relevant data for the next page
-    return new SomeHomeVideoPager(videos, hasMore, context);
-}
-
-source.searchSuggestions = function(query) {
-    /**
-     * @param query: string
-     * @returns: string[]
-     */
-
-    const suggestions = []; //The suggestions for a specific search query
-    return suggestions;
-}
+See [Authentication.md](https://gitlab.futo.org/videostreaming/grayjay/-/blob/master/docs/Authentication.md)
 
-source.getSearchCapabilities = function() {
-    //This is an example of how to return search capabilities like available sorts, filters and which feed types are available (see source.js for more details) 
-	return {
-		types: [Type.Feed.Mixed],
-		sorts: [Type.Order.Chronological, "^release_time"],
-		filters: [
-			{
-				id: "date",
-				name: "Date",
-				isMultiSelect: false,
-				filters: [
-					{ id: Type.Date.Today, name: "Last 24 hours", value: "today" },
-					{ id: Type.Date.LastWeek, name: "Last week", value: "thisweek" },
-					{ id: Type.Date.LastMonth, name: "Last month", value: "thismonth" },
-					{ id: Type.Date.LastYear, name: "Last year", value: "thisyear" }
-				]
-			},
-		]
-	};
-}
+## Content Types
 
-source.search = function (query, type, order, filters, continuationToken) {
-    /**
-     * @param query: string
-     * @param type: string
-     * @param order: string
-     * @param filters: Map<string, Array<string>>
-     * @param continuationToken: any?
-     * @returns: VideoPager
-     */
-    const videos = []; // The results (PlatformVideo)
-    const hasMore = false; // Are there more pages?
-    const context = { query: query, type: type, order: order, filters: filters, continuationToken: continuationToken }; // Relevant data for the next page
-    return new SomeSearchVideoPager(videos, hasMore, context);
-}
+Docs for data structures like PlatformVideo your plugin uses to communicate with the GrayJay app.
 
-source.getSearchChannelContentsCapabilities = function () {
-    //This is an example of how to return search capabilities on a channel like available sorts, filters and which feed types are available (see source.js for more details)
-	return {
-		types: [Type.Feed.Mixed],
-		sorts: [Type.Order.Chronological],
-		filters: []
-	};
-}
+See [Content Types.md](https://gitlab.futo.org/videostreaming/grayjay/-/blob/master/docs/Content%20Types.md)
 
-source.searchChannelContents = function (url, query, type, order, filters, continuationToken) {
-    /**
-     * @param url: string
-     * @param query: string
-     * @param type: string
-     * @param order: string
-     * @param filters: Map<string, Array<string>>
-     * @param continuationToken: any?
-     * @returns: VideoPager
-     */
-
-    const videos = []; // The results (PlatformVideo)
-    const hasMore = false; // Are there more pages?
-    const context = { channelUrl: channelUrl, query: query, type: type, order: order, filters: filters, continuationToken: continuationToken }; // Relevant data for the next page
-    return new SomeSearchChannelVideoPager(videos, hasMore, context);
-}
+## Example plugin
 
-source.searchChannels = function (query, continuationToken) {
-    /**
-     * @param query: string
-     * @param continuationToken: any?
-     * @returns: ChannelPager
-     */
-
-    const channels = []; // The results (PlatformChannel)
-    const hasMore = false; // Are there more pages?
-    const context = { query: query, continuationToken: continuationToken }; // Relevant data for the next page
-    return new SomeChannelPager(channels, hasMore, context);
-}
+See the example plugin to better understand the plugin API e.g. `getHome` and `search`.
 
-source.isChannelUrl = function(url) {
-    /**
-     * @param url: string
-     * @returns: boolean
-     */
+See [Example Plugin.md](https://gitlab.futo.org/videostreaming/grayjay/-/blob/master/docs/Example%20Plugin.md)
 
-	return REGEX_CHANNEL_URL.test(url);
-}
+## Pagination
 
-source.getChannel = function(url) {
-	return new PlatformChannel({
-		//... see source.js for more details
-	});
-}
+Plugins use "Pagers" to send paginated data to the GrayJay app.
 
-source.getChannelContents = function(url, type, order, filters, continuationToken) {
-    /**
-     * @param url: string
-     * @param type: string
-     * @param order: string
-     * @param filters: Map<string, Array<string>>
-     * @param continuationToken: any?
-     * @returns: VideoPager
-     */
-
-    const videos = []; // The results (PlatformVideo)
-    const hasMore = false; // Are there more pages?
-    const context = { url: url, query: query, type: type, order: order, filters: filters, continuationToken: continuationToken }; // Relevant data for the next page
-    return new SomeChannelVideoPager(videos, hasMore, context);
-}
+See [Pagers.md](https://gitlab.futo.org/videostreaming/grayjay/-/blob/master/docs/Pagers.md)
 
-source.isContentDetailsUrl = function(url) {
-    /**
-     * @param url: string
-     * @returns: boolean
-     */
+## Script signing
 
-	return REGEX_DETAILS_URL.test(url);
-}
+When you deploy your plugin, you'll need to add code signing for security.
 
-source.getContentDetails = function(url) {
-    /**
-     * @param url: string
-     * @returns: PlatformVideoDetails
-     */
-
-	return new PlatformVideoDetails({
-		//... see source.js for more details
-	});
-}
-
-source.getComments = function (url, continuationToken) {
-    /**
-     * @param url: string
-     * @param continuationToken: any?
-     * @returns: CommentPager
-     */
-
-    const comments = []; // The results (Comment)
-    const hasMore = false; // Are there more pages?
-    const context = { url: url, continuationToken: continuationToken }; // Relevant data for the next page
-    return new SomeCommentPager(comments, hasMore, context);
-
-}
-source.getSubComments = function (comment) {
-    /**
-     * @param comment: Comment
-     * @returns: SomeCommentPager
-     */
-
-	if (typeof comment === 'string') {
-		comment = JSON.parse(comment);
-	}
-
-	return getCommentsPager(comment.context.claimId, comment.context.claimId, 1, false, comment.context.commentId);
-}
-
-class SomeCommentPager extends CommentPager {
-    constructor(results, hasMore, context) {
-        super(results, hasMore, context);
-    }
-
-    nextPage() {
-        return source.getComments(this.context.url, this.context.continuationToken);
-    }
-}
-
-class SomeHomeVideoPager extends VideoPager {
-	constructor(results, hasMore, context) {
-		super(results, hasMore, context);
-	}
-	
-	nextPage() {
-		return source.getHome(this.context.continuationToken);
-	}
-}
-
-class SomeSearchVideoPager extends VideoPager {
-	constructor(results, hasMore, context) {
-		super(results, hasMore, context);
-	}
-	
-	nextPage() {
-		return source.search(this.context.query, this.context.type, this.context.order, this.context.filters, this.context.continuationToken);
-	}
-}
-
-class SomeSearchChannelVideoPager extends VideoPager {
-	constructor(results, hasMore, context) {
-		super(results, hasMore, context);
-	}
-	
-	nextPage() {
-		return source.searchChannelContents(this.context.channelUrl, this.context.query, this.context.type, this.context.order, this.context.filters, this.context.continuationToken);
-	}
-}
-
-class SomeChannelPager extends ChannelPager {
-	constructor(results, hasMore, context) {
-		super(results, hasMore, context);
-	}
-	
-	nextPage() {
-		return source.searchChannelContents(this.context.query, this.context.continuationToken);
-	}
-}
-
-class SomeChannelVideoPager extends VideoPager {
-	constructor(results, hasMore, context) {
-		super(results, hasMore, context);
-	}
-	
-	nextPage() {
-		return source.getChannelContents(this.context.url, this.context.type, this.context.order, this.context.filters, this.context.continuationToken);
-	}
-}
-```
-
-2. Configure a web server to host the plugin. This can be something as simple as a NGINX server where you just place the files in the wwwroot or a simple dotnet/npm program that hosts the file for you. The important part is that the webserver and the phone are on the same network and the phone can access the files hosted by the development machine. An example of what this would look like is [here](https://plugins.grayjay.app/Odysee/OdyseeConfig.json). Alternatively, you could simply point to a Github/Gitlab raw file if you do not want to host it yourself. Note that the URL is not required to be publically accessible during development and HTTPS is NOT required.
-3. Enable developer mode on the mobile application by going to settings, clicking on the version code multiple times. Once enabled, click on developer settings and then in the developer settings enable the webserver.
-4. You are now able to access the developer interface on the phone via `http://<phone-ip>:11337/dev`.
-
-## Using the Developer Interface
-
-Once in the web portal you will see several tabs and a form allowing you to load a plugin.
-
-1. Lets load your plugin. Take the URL that your plugin config is available at (like http://192.168.1.196:5000/Some/SomeConfig.json) and enter it in the `Plugin Config Json Url` field. Once entered, click load plugin.
-*The package override domParser will override the domParser with the browser implementation. This is useful when you quickly want to iterate on plugins that parse the DOM, but it is less accurate to what the plugin will behave like once in-app.*
-2. Once the plugin is loaded, you can click on the `Testing` tab and call individual methods. This allows you to quickly iterate, test methods and make sure they are returning the proper values. To reload once you make changes on the plugin, click the top-right refresh button.
-3. After you are sure everything is working properly, click the `Integration` tab in order to perform integration testing on your plugin. You can click the `Inject Plugin` button in order to inject the plugin into the app. On the sources page in your app you should see your source and you are able to test it and make sure everything works. If you make changes and want to reload the plugin, click the `Inject Plugin` button again.
+See [Script Signing.md](https://gitlab.futo.org/videostreaming/grayjay/-/blob/master/docs/Script%Signing.md)
 
 ## Plugin Deployment
 
@@ -403,12 +182,6 @@ Ensure the QR code correctly points to the plugin config URL. The URL must be pu
 
 Make sure the signature is correctly generated and added. Also, ensure the version number in the config matches the new version number.
 
-## Additional Resources
-
-Here are some additional resources that might help you with your plugin development:
-
-Please 
-
 ## Support and Contact
 
 If you have any issues or need further assistance, feel free to reach out to us at: