소스 검색

#120: Support v1.12.1
Code cleanup.
Update manifest and version.
Use blank sprite for collections when no results are found.
Remove custom keyboard. Use BSML keyboard.
Remove favorites filter button anymore.
Fix progress bar location.
SongCore 3.x support.
Support new BeatSaberUI elements.
Clang 7.0.
Add new required reference.

Halsafar 4 년 전
부모
커밋
174fab59c4

+ 55 - 19
SongBrowserPlugin/DataAccess/SongBrowserModel.cs

@@ -1,12 +1,12 @@
 using SongBrowser.DataAccess;
 using SongCore.Utilities;
 using System;
+using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.IO;
 using System.Linq;
 using System.Text.RegularExpressions;
-using TMPro;
 using UnityEngine;
 using Logger = SongBrowser.Logging.Logger;
 
@@ -25,15 +25,15 @@ namespace SongBrowser
         private SongBrowserSettings _settings;
 
         // song list management
-        private double _customSongDirLastWriteTime = 0;        
-        private Dictionary<String, double> _cachedLastWriteTimes;
+        private double _customSongDirLastWriteTime = 0;
+        private readonly Dictionary<String, double> _cachedLastWriteTimes;
         private Dictionary<string, int> _levelIdToPlayCount;
 
         public BeatmapCharacteristicSO CurrentBeatmapCharacteristicSO;
 
         public static Func<IAnnotatedBeatmapLevelCollection, List<IPreviewBeatmapLevel>> CustomFilterHandler;
         public static Func<List<IPreviewBeatmapLevel>, List<IPreviewBeatmapLevel>> CustomSortHandler;
-        public static Action<Dictionary<string, CustomPreviewBeatmapLevel>> didFinishProcessingSongs;
+        public static Action<ConcurrentDictionary<string, CustomPreviewBeatmapLevel>> didFinishProcessingSongs;
 
         public bool SortWasMissingData { get; private set; } = false;
 
@@ -159,7 +159,7 @@ namespace SongBrowser
         private double GetSongUserDate(CustomPreviewBeatmapLevel level)
         {
             var coverPath = Path.Combine(level.customLevelPath, level.standardLevelInfoSaveData.coverImageFilename);
-            var lastTime = EPOCH;
+            DateTime lastTime;
             if (File.Exists(coverPath))
             {
                 var lastWriteTime = File.GetLastWriteTimeUtc(coverPath);
@@ -210,7 +210,7 @@ namespace SongBrowser
         /// <summary>
         /// Sort the song list based on the settings.
         /// </summary>
-        public void ProcessSongList(IAnnotatedBeatmapLevelCollection selectedBeatmapCollection, LevelCollectionViewController levelCollectionViewController, LevelSelectionNavigationController navController)
+        public void ProcessSongList(IAnnotatedBeatmapLevelCollection selectedBeatmapCollection, LevelSelectionNavigationController navController)
         {
             Logger.Trace("ProcessSongList()");
 
@@ -224,8 +224,8 @@ namespace SongBrowser
                 Logger.Debug("Cannot process songs yet, no level collection selected...");
                 return;
             }
-            
-            Logger.Debug("Using songs from level collection: {0}", selectedBeatmapCollection.collectionName);
+
+            Logger.Debug($"Using songs from level collection: {selectedBeatmapCollection.collectionName} [num={selectedBeatmapCollection.beatmapLevelCollection.beatmapLevels.Length}");
             unsortedSongs = selectedBeatmapCollection.beatmapLevelCollection.beatmapLevels.ToList();
 
             // filter
@@ -261,7 +261,7 @@ namespace SongBrowser
             Logger.Info("Filtering songs took {0}ms", stopwatch.ElapsedMilliseconds);
 
             // sort
-            Logger.Debug("Starting to sort songs...");
+            Logger.Debug($"Starting to sort songs by {_settings.sortMode}");
             stopwatch = Stopwatch.StartNew();
 
             SortWasMissingData = false;
@@ -321,17 +321,50 @@ namespace SongBrowser
             // Still hacking in a custom level pack
             // Asterisk the pack name so it is identifable as filtered.
             var packName = selectedBeatmapCollection.collectionName;
+            if (packName == null)
+            {
+                packName = "";
+            }
+
             if (!packName.EndsWith("*") && _settings.filterMode != SongFilterMode.None)
             {
                 packName += "*";
             }
-            BeatmapLevelPack levelPack = new BeatmapLevelPack(SongBrowserModel.FilteredSongsCollectionName, packName, selectedBeatmapCollection.collectionName, selectedBeatmapCollection.coverImage, new BeatmapLevelCollection(sortedSongs.ToArray()));
 
-            GameObject _noDataGO = levelCollectionViewController.GetPrivateField<GameObject>("_noDataInfoGO");
-            bool _showPlayerStatsInDetailView = navController.GetPrivateField<bool>("_showPlayerStatsInDetailView");
-            bool _showPracticeButtonInDetailView = navController.GetPrivateField<bool>("_showPracticeButtonInDetailView");
+            // Some level categories have a null cover image, supply something, it won't show it anyway
+            var coverImage = selectedBeatmapCollection.coverImage;
+            if (coverImage == null)
+            {
+                coverImage = BeatSaberMarkupLanguage.Utilities.ImageResources.BlankSprite;
+            }
 
-            navController.SetData(levelPack, true, _showPlayerStatsInDetailView, _showPracticeButtonInDetailView, _noDataGO);
+            Logger.Debug("Creating filtered level pack...");
+            BeatmapLevelPack levelPack = new BeatmapLevelPack(SongBrowserModel.FilteredSongsCollectionName, packName, selectedBeatmapCollection.collectionName, coverImage, new BeatmapLevelCollection(sortedSongs.ToArray()));
+
+            /*
+             public virtual void SetData(
+                IAnnotatedBeatmapLevelCollection annotatedBeatmapLevelCollection, 
+                bool showPackHeader, bool showPlayerStats, bool showPracticeButton, 
+                string actionButtonText, 
+                GameObject noDataInfoPrefab, BeatmapDifficultyMask allowedBeatmapDifficultyMask, BeatmapCharacteristicSO[] notAllowedCharacteristics);
+            */
+            Logger.Debug("Acquiring necessary fields to call SetData(pack)...");
+            LevelCollectionNavigationController lcnvc = navController.GetPrivateField<LevelCollectionNavigationController>("_levelCollectionNavigationController");
+            var _showPlayerStatsInDetailView = navController.GetPrivateField<bool>("_showPlayerStatsInDetailView");
+            var _hidePracticeButton = navController.GetPrivateField<bool>("_hidePracticeButton");
+            var _actionButtonText = navController.GetPrivateField<string>("_actionButtonText");
+            var _allowedBeatmapDifficultyMask = navController.GetPrivateField<BeatmapDifficultyMask>("_allowedBeatmapDifficultyMask");
+            var _notAllowedCharacteristics = navController.GetPrivateField<BeatmapCharacteristicSO[]>("_notAllowedCharacteristics");
+
+            Logger.Debug("Calling lcnvc.SetData...");
+            lcnvc.SetData(levelPack,
+                true,
+                _showPlayerStatsInDetailView,
+                !_hidePracticeButton,
+                _actionButtonText,
+                null,
+                _allowedBeatmapDifficultyMask,
+                _notAllowedCharacteristics);
 
             //_sortedSongs.ForEach(x => Logger.Debug(x.levelID));
         }
@@ -400,7 +433,7 @@ namespace SongBrowser
                 double maxPP = 0.0;
                 if (SongDataCore.Plugin.Songs.Data.Songs.ContainsKey(hash))
                 {
-                     maxPP = SongDataCore.Plugin.Songs.Data.Songs[hash].diffs.Max(y => y.pp);
+                    maxPP = SongDataCore.Plugin.Songs.Data.Songs[hash].diffs.Max(y => y.pp);
                 }
 
                 if (maxPP > 0f)
@@ -519,7 +552,7 @@ namespace SongBrowser
                     var stars = 0.0;
                     if (SongDataCore.Plugin.Songs.Data.Songs.ContainsKey(hash))
                     {
-                        var diffs = SongDataCore.Plugin.Songs.Data.Songs[hash].diffs;   
+                        var diffs = SongDataCore.Plugin.Songs.Data.Songs[hash].diffs;
                         stars = diffs.Max(y => y.star);
                     }
 
@@ -589,7 +622,8 @@ namespace SongBrowser
             }
 
             return levelIds
-                .OrderByDescending(x => {
+                .OrderByDescending(x =>
+                {
                     var hash = SongBrowserModel.GetSongHash(x.levelID);
                     if (SongDataCore.Plugin.Songs.Data.Songs.ContainsKey(hash))
                     {
@@ -651,7 +685,8 @@ namespace SongBrowser
             }
 
             return levelIds
-                .OrderByDescending(x => {
+                .OrderByDescending(x =>
+                {
                     var hash = SongBrowserModel.GetSongHash(x.levelID);
                     if (SongDataCore.Plugin.Songs.Data.Songs.ContainsKey(hash))
                     {
@@ -682,7 +717,8 @@ namespace SongBrowser
             }
 
             return levelIds
-                .OrderByDescending(x => {
+                .OrderByDescending(x =>
+                {
                     var hash = SongBrowserModel.GetSongHash(x.levelID);
                     if (SongDataCore.Plugin.Songs.Data.Songs.ContainsKey(hash))
                     {

+ 2 - 1
SongBrowserPlugin/DataAccess/SongBrowserSettings.cs

@@ -16,7 +16,7 @@ namespace SongBrowser.DataAccess
         Default,
         Author,
         Original,
-        Newest,        
+        Newest,
         YourPlayCount,
         Difficulty,
         Random,
@@ -86,6 +86,7 @@ namespace SongBrowser.DataAccess
         public String currentLevelId = default(String);
         public String currentDirectory = default(String);
         public String currentLevelCollectionName = default(String);
+        public String currentLevelCategoryName = default(String);
 
         public bool randomInstantQueue = false;
         public bool deleteNumberedSongFolder = true;

+ 40 - 75
SongBrowserPlugin/Internals/BeatSaberExtensions.cs

@@ -1,6 +1,4 @@
-using BS_Utils.Utilities;
-using System.Linq;
-using TMPro;
+using System.Linq;
 using UnityEngine;
 using UnityEngine.Events;
 using UnityEngine.UI;
@@ -13,107 +11,74 @@ namespace SongBrowser.Internals
         #region Button Extensions
         public static void SetButtonText(this Button _button, string _text)
         {
-            Polyglot.LocalizedTextMeshProUGUI localizer = _button.GetComponentInChildren<Polyglot.LocalizedTextMeshProUGUI>();
-            if (localizer != null)
-                GameObject.Destroy(localizer);
-            TextMeshProUGUI tmpUgui = _button.GetComponentInChildren<TextMeshProUGUI>();
-            if (tmpUgui != null)
-                tmpUgui.text = _text;
+            HMUI.CurvedTextMeshPro textMesh = _button.GetComponentInChildren<HMUI.CurvedTextMeshPro>();
+            if (textMesh != null)
+            {
+                textMesh.SetText(_text);
+            }
         }
 
         public static void SetButtonTextSize(this Button _button, float _fontSize)
         {
-            if (_button.GetComponentInChildren<TextMeshProUGUI>() != null)
+            var txtMesh = _button.GetComponentInChildren<HMUI.CurvedTextMeshPro>();
+            if (txtMesh != null)
             {
-                _button.GetComponentInChildren<TextMeshProUGUI>().fontSize = _fontSize;
+                txtMesh.fontSize = _fontSize;
             }
         }
 
         public static void ToggleWordWrapping(this Button _button, bool enableWordWrapping)
         {
-            if (_button.GetComponentInChildren<TextMeshProUGUI>() != null)
+            var txtMesh = _button.GetComponentInChildren<HMUI.CurvedTextMeshPro>();
+            if (txtMesh != null)
             {
-                _button.GetComponentInChildren<TextMeshProUGUI>().enableWordWrapping = enableWordWrapping;
+                txtMesh.enableWordWrapping = enableWordWrapping;
             }
         }
 
-        public static void SetButtonIcon(this Button _button, Sprite _icon)
-        {
-            if (_button.GetComponentsInChildren<Image>().Count() > 1)
-                _button.GetComponentsInChildren<Image>().First(x => x.name == "Icon").sprite = _icon;
-        }
-
-        public static void SetButtonBackground(this Button _button, Sprite _background)
-        {
-            if (_button.GetComponentsInChildren<Image>().Count() > 0)
-                _button.GetComponentsInChildren<Image>()[0].sprite = _background;
-        }
-        #endregion
-
-        #region ViewController Extensions
-
-        public static Button CreateUIButton(this HMUI.ViewController parent, string buttonTemplate)
+        public static void SetButtonBackgroundActive(this Button parent, bool active)
         {
-            Button btn = BeatSaberUI.CreateUIButton(parent.rectTransform, buttonTemplate);
-            return btn;
+            HMUI.ImageView img = parent.GetComponentsInChildren<HMUI.ImageView>().Last(x => x.name == "BG");
+            if (img != null)
+            {
+                img.gameObject.SetActive(active);
+            }
         }
 
-        public static Button CreateUIButton(this HMUI.ViewController parent, string buttonTemplate, Vector2 anchoredPosition, Vector2 sizeDelta, UnityAction onClick = null, string buttonText = "BUTTON", Sprite icon = null)
+        public static void SetButtonUnderlineColor(this Button parent, Color color)
         {
-            Button btn = BeatSaberUI.CreateUIButton(parent.rectTransform, buttonTemplate, anchoredPosition, sizeDelta, onClick, buttonText, icon);
-            return btn;
+            HMUI.ImageView img = parent.GetComponentsInChildren<HMUI.ImageView>().FirstOrDefault(x => x.name == "Underline");
+            if (img != null)
+            {
+                img.color = color;
+            }
         }
 
-        public static Button CreateUIButton(this HMUI.ViewController parent, string buttonTemplate, Vector2 anchoredPosition, UnityAction onClick = null, string buttonText = "BUTTON", Sprite icon = null)
+        public static void SetButtonBorder(this Button button, Color color)
         {
-            Button btn = BeatSaberUI.CreateUIButton(parent.rectTransform, buttonTemplate, anchoredPosition, onClick, buttonText, icon);
-            return btn;
+            HMUI.ImageView img = button.GetComponentsInChildren<HMUI.ImageView>().FirstOrDefault(x => x.name == "Border");
+            if (img != null)
+            {
+                img.color0 = color;
+                img.color1 = color;
+                img.color = color;
+                img.fillMethod = Image.FillMethod.Horizontal;
+                img.SetAllDirty();
+            }
         }
+        #endregion
 
-        public static Button CreateUIButton(this HMUI.ViewController parent, string buttonTemplate, UnityAction onClick = null, string buttonText = "BUTTON", Sprite icon = null)
+        #region ViewController Extensions
+        public static Button CreateUIButton(this HMUI.ViewController parent, string name, string buttonTemplate, Vector2 anchoredPosition, Vector2 sizeDelta, UnityAction onClick = null, string buttonText = "BUTTON")
         {
-            Button btn = BeatSaberUI.CreateUIButton(parent.rectTransform, buttonTemplate, onClick, buttonText, icon);
+            Button btn = BeatSaberUI.CreateUIButton(name, parent.rectTransform, buttonTemplate, anchoredPosition, sizeDelta, onClick, buttonText);
             return btn;
         }
-
-        public static Button CreateBackButton(this HMUI.ViewController parent)
+        public static Button CreateIconButton(this HMUI.ViewController parent, string name, string buttonTemplate, Vector2 anchoredPosition, Vector2 sizeDelta, UnityAction onClick, Sprite icon)
         {
-            Button btn = BeatSaberUI.CreateBackButton(parent.rectTransform);
+            Button btn = BeatSaberUI.CreateIconButton(name, parent.rectTransform, buttonTemplate, anchoredPosition, sizeDelta, onClick, icon);
             return btn;
         }
-
-        /*public static GameObject CreateLoadingSpinner(this HMUI.ViewController parent)
-        {
-            GameObject loadingSpinner = BeatSaberUI.CreateLoadingSpinner(parent.rectTransform);
-            return loadingSpinner;
-        }*/
-
-        public static TextMeshProUGUI CreateText(this HMUI.ViewController parent, string text, Vector2 anchoredPosition, Vector2 sizeDelta)
-        {
-            TextMeshProUGUI textMesh = BeatSaberUI.CreateText(parent.rectTransform, text, anchoredPosition, sizeDelta);
-            return textMesh;
-        }
-
-        public static TextMeshProUGUI CreateText(this HMUI.ViewController parent, string text, Vector2 anchoredPosition)
-        {
-            TextMeshProUGUI textMesh = BeatSaberUI.CreateText(parent.rectTransform, text, anchoredPosition);
-            return textMesh;
-        }
-
-        public static void SetText(this LevelListTableCell cell, string text)
-        {
-            cell.GetPrivateField<TextMeshProUGUI>("_songNameText").text = text;
-        }
-
-        public static void SetSubText(this LevelListTableCell cell, string subtext)
-        {
-            cell.GetPrivateField<TextMeshProUGUI>("_authorText").text = subtext;
-        }
-
-        public static void SetIcon(this LevelListTableCell cell, Sprite icon)
-        {
-            cell.GetPrivateField<UnityEngine.UI.Image>("_coverImage").sprite = icon;
-        }
         #endregion
     }
 }

+ 96 - 145
SongBrowserPlugin/Internals/BeatSaberUI.cs

@@ -1,54 +1,62 @@
-using BS_Utils.Utilities;
-using HMUI;
+using HMUI;
+using IPA.Utilities;
 using System;
 using System.Linq;
 using TMPro;
 using UnityEngine;
 using UnityEngine.Events;
 using UnityEngine.UI;
+using VRUIControls;
 using Image = UnityEngine.UI.Image;
 using Logger = SongBrowser.Logging.Logger;
 
-
 namespace SongBrowser.Internals
 {
     public static class BeatSaberUI
     {
-        private static Button _backButtonInstance;
+        private static PhysicsRaycasterWithCache _physicsRaycaster;
+        public static PhysicsRaycasterWithCache PhysicsRaycasterWithCache
+        {
+            get
+            {
+                if (_physicsRaycaster == null)
+                    _physicsRaycaster = Resources.FindObjectsOfTypeAll<MainMenuViewController>().First().GetComponent<VRGraphicRaycaster>().GetField<PhysicsRaycasterWithCache, VRGraphicRaycaster>("_physicsRaycaster");
+                return _physicsRaycaster;
+            }
+        }
 
         /// <summary>
         /// Creates a ViewController of type T, and marks it to not be destroyed.
         /// </summary>
         /// <typeparam name="T">The variation of ViewController you want to create.</typeparam>
         /// <returns>The newly created ViewController of type T.</returns>
-        public static T CreateViewController<T>(string name="CustomViewController") where T : ViewController
+        public static T CreateViewController<T>(string name) where T : ViewController
         {
-            T vc = new GameObject(name).AddComponent<T>();
-            UnityEngine.GameObject.DontDestroyOnLoad(vc.gameObject);
+            T vc = new GameObject(typeof(T).Name, typeof(VRGraphicRaycaster), typeof(CanvasGroup), typeof(T)).GetComponent<T>();
+            vc.GetComponent<VRGraphicRaycaster>().SetField("_physicsRaycaster", PhysicsRaycasterWithCache);
 
             vc.rectTransform.anchorMin = new Vector2(0f, 0f);
             vc.rectTransform.anchorMax = new Vector2(1f, 1f);
             vc.rectTransform.sizeDelta = new Vector2(0f, 0f);
             vc.rectTransform.anchoredPosition = new Vector2(0f, 0f);
-
+            vc.gameObject.SetActive(false);
+            vc.name = name;
             return vc;
         }
 
-        /// <summary>
-        /// Clone a Unity Button into a Button we control.
-        /// </summary>
-        /// <param name="parent"></param>
-        /// <param name="buttonTemplate"></param>
-        /// <param name="buttonInstance"></param>
-        /// <returns></returns>
-        static public Button CreateUIButton(RectTransform parent, Button buttonTemplate)
+        public static T CreateCurvedViewController<T>(string name, float curveRadius) where T : ViewController
         {
-            Button btn = UnityEngine.Object.Instantiate(buttonTemplate, parent, false);
-            UnityEngine.Object.DestroyImmediate(btn.GetComponent<SignalOnUIButtonClick>());
-            btn.onClick = new Button.ButtonClickedEvent();
-            btn.name = "CustomUIButton";
+            T vc = new GameObject(typeof(T).Name, typeof(VRGraphicRaycaster), typeof(CurvedCanvasSettings), typeof(CanvasGroup), typeof(T)).GetComponent<T>();
+            vc.GetComponent<VRGraphicRaycaster>().SetField("_physicsRaycaster", PhysicsRaycasterWithCache);
 
-            return btn;
+            vc.GetComponent<CurvedCanvasSettings>().SetRadius(curveRadius);
+
+            vc.rectTransform.anchorMin = new Vector2(0f, 0f);
+            vc.rectTransform.anchorMax = new Vector2(1f, 1f);
+            vc.rectTransform.sizeDelta = new Vector2(0f, 0f);
+            vc.rectTransform.anchoredPosition = new Vector2(0f, 0f);
+            vc.gameObject.SetActive(false);
+            return vc;
         }
 
         /// <summary>
@@ -58,50 +66,46 @@ namespace SongBrowser.Internals
         /// <param name="buttonTemplate"></param>
         /// <param name="iconSprite"></param>
         /// <returns></returns>
-        public static Button CreateIconButton(RectTransform parent, Button buttonTemplate, Sprite iconSprite)
+        public static Button CreateIconButton(String name, RectTransform parent, String buttonTemplate, Vector2 anchoredPosition, Vector2 sizeDelta, UnityAction onClick, Sprite icon)
         {
-            Button newButton = BeatSaberUI.CreateUIButton(parent, buttonTemplate);
-            newButton.interactable = true;
-
-            RectTransform textRect = newButton.GetComponentsInChildren<RectTransform>(true).FirstOrDefault(c => c.name == "Text");
-            if (textRect != null)
+            Logger.Debug("CreateIconButton({0}, {1}, {2}, {3}, {4}", name, parent, buttonTemplate, anchoredPosition, sizeDelta);
+            Button btn = UnityEngine.Object.Instantiate(Resources.FindObjectsOfTypeAll<Button>().Last(x => (x.name == buttonTemplate)), parent, false);
+            btn.name = name;
+            btn.interactable = true;
+
+            UnityEngine.Object.Destroy(btn.GetComponent<HoverHint>());
+            GameObject.Destroy(btn.GetComponent<LocalizedHoverHint>());
+            btn.gameObject.AddComponent<BeatSaberMarkupLanguage.Components.ExternalComponents>().components.Add(btn.GetComponentsInChildren<LayoutGroup>().First(x => x.name == "Content"));
+
+            Transform contentTransform = btn.transform.Find("Content");
+            GameObject.Destroy(contentTransform.Find("Text").gameObject);
+            Image iconImage = new GameObject("Icon").AddComponent<ImageView>();
+            iconImage.material = BeatSaberMarkupLanguage.Utilities.ImageResources.NoGlowMat;
+            iconImage.rectTransform.SetParent(contentTransform, false);
+            iconImage.rectTransform.sizeDelta = new Vector2(10f, 10f);
+            iconImage.sprite = icon;
+            iconImage.preserveAspect = true;
+            if (iconImage != null)
             {
-                UnityEngine.Object.Destroy(textRect.gameObject);
+                BeatSaberMarkupLanguage.Components.ButtonIconImage btnIcon = btn.gameObject.AddComponent<BeatSaberMarkupLanguage.Components.ButtonIconImage>();
+                btnIcon.image = iconImage;
             }
 
-            newButton.SetButtonIcon(iconSprite);
-            newButton.onClick.RemoveAllListeners();
+            GameObject.Destroy(btn.transform.Find("Content").GetComponent<LayoutElement>());
+            btn.GetComponentsInChildren<RectTransform>().First(x => x.name == "Underline").gameObject.SetActive(false);
 
-            return newButton;
-        }
-
-        /// <summary>
-        /// Creates a copy of a template button and returns it.
-        /// </summary>
-        /// <param name="parent">The transform to parent the button to.</param>
-        /// <param name="buttonTemplate">The name of the button to make a copy of. Example: "QuitButton", "PlayButton", etc.</param>
-        /// <param name="anchoredPosition">The position the button should be anchored to.</param>
-        /// <param name="sizeDelta">The size of the buttons RectTransform.</param>
-        /// <param name="onClick">Callback for when the button is pressed.</param>
-        /// <param name="buttonText">The text that should be shown on the button.</param>
-        /// <param name="icon">The icon that should be shown on the button.</param>
-        /// <returns>The newly created button.</returns>
-        public static Button CreateUIButton(RectTransform parent, string buttonTemplate, Vector2 anchoredPosition, Vector2 sizeDelta, UnityAction onClick = null, string buttonText = "BUTTON", Sprite icon = null)
-        {
-            Button btn = UnityEngine.Object.Instantiate(Resources.FindObjectsOfTypeAll<Button>().Last(x => (x.name == buttonTemplate)), parent, false);
-            btn.onClick = new Button.ButtonClickedEvent();
-            if (onClick != null)
-                btn.onClick.AddListener(onClick);
-            btn.name = "CustomUIButton";
+            ContentSizeFitter buttonSizeFitter = btn.gameObject.AddComponent<ContentSizeFitter>();
+            buttonSizeFitter.verticalFit = ContentSizeFitter.FitMode.Unconstrained;
+            buttonSizeFitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained;
 
             (btn.transform as RectTransform).anchorMin = new Vector2(0.5f, 0.5f);
             (btn.transform as RectTransform).anchorMax = new Vector2(0.5f, 0.5f);
             (btn.transform as RectTransform).anchoredPosition = anchoredPosition;
             (btn.transform as RectTransform).sizeDelta = sizeDelta;
 
-            btn.SetButtonText(buttonText);
-            if (icon != null)
-                btn.SetButtonIcon(icon);
+            btn.onClick.RemoveAllListeners();
+            if (onClick != null)
+                btn.onClick.AddListener(onClick);
 
             return btn;
         }
@@ -112,80 +116,57 @@ namespace SongBrowser.Internals
         /// <param name="parent">The transform to parent the button to.</param>
         /// <param name="buttonTemplate">The name of the button to make a copy of. Example: "QuitButton", "PlayButton", etc.</param>
         /// <param name="anchoredPosition">The position the button should be anchored to.</param>
+        /// <param name="sizeDelta">The size of the buttons RectTransform.</param>
         /// <param name="onClick">Callback for when the button is pressed.</param>
         /// <param name="buttonText">The text that should be shown on the button.</param>
         /// <param name="icon">The icon that should be shown on the button.</param>
         /// <returns>The newly created button.</returns>
-        public static Button CreateUIButton(RectTransform parent, string buttonTemplate, Vector2 anchoredPosition, UnityAction onClick = null, string buttonText = "BUTTON", Sprite icon = null)
+        public static Button CreateUIButton(String name, RectTransform parent, string buttonTemplate, Vector2 anchoredPosition, Vector2 sizeDelta, UnityAction onClick = null, string buttonText = "BUTTON")
         {
+            Logger.Debug("CreateUIButton({0}, {1}, {2}, {3}, {4}", name, parent, buttonTemplate, anchoredPosition, sizeDelta);
             Button btn = UnityEngine.Object.Instantiate(Resources.FindObjectsOfTypeAll<Button>().Last(x => (x.name == buttonTemplate)), parent, false);
-            btn.onClick = new Button.ButtonClickedEvent();
-            if (onClick != null)
-                btn.onClick.AddListener(onClick);
-            btn.name = "CustomUIButton";
+            btn.gameObject.SetActive(true);
+            btn.name = name;
+            btn.interactable = true;
 
-            (btn.transform as RectTransform).anchorMin = new Vector2(0.5f, 0.5f);
-            (btn.transform as RectTransform).anchorMax = new Vector2(0.5f, 0.5f);
-            (btn.transform as RectTransform).anchoredPosition = anchoredPosition;
+            Polyglot.LocalizedTextMeshProUGUI localizer = btn.GetComponentInChildren<Polyglot.LocalizedTextMeshProUGUI>();
+            if (localizer != null)
+            {
+                GameObject.Destroy(localizer);
+            }
+            BeatSaberMarkupLanguage.Components.ExternalComponents externalComponents = btn.gameObject.AddComponent<BeatSaberMarkupLanguage.Components.ExternalComponents>();
+            TextMeshProUGUI textMesh = btn.GetComponentInChildren<TextMeshProUGUI>();
+            textMesh.richText = true;
+            externalComponents.components.Add(textMesh);
 
-            btn.SetButtonText(buttonText);
-            if (icon != null)
-                btn.SetButtonIcon(icon);
+            var contentTransform = btn.transform.Find("Content").GetComponent<LayoutElement>();
+            if (contentTransform != null)
+            {
+                GameObject.Destroy(contentTransform);
+            }
 
-            return btn;
-        }
+            ContentSizeFitter buttonSizeFitter = btn.gameObject.AddComponent<ContentSizeFitter>();
+            buttonSizeFitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
+            buttonSizeFitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained;
 
+            LayoutGroup stackLayoutGroup = btn.GetComponentInChildren<LayoutGroup>();
+            if (stackLayoutGroup != null)
+            {
+                externalComponents.components.Add(stackLayoutGroup);
+            }
 
-        /// <summary>
-        /// Creates a copy of a template button and returns it.
-        /// </summary>
-        /// <param name="parent">The transform to parent the button to.</param>
-        /// <param name="buttonTemplate">The name of the button to make a copy of. Example: "QuitButton", "PlayButton", etc.</param>
-        /// <param name="onClick">Callback for when the button is pressed.</param>
-        /// <param name="buttonText">The text that should be shown on the button.</param>
-        /// <param name="icon">The icon that should be shown on the button.</param>
-        /// <returns>The newly created button.</returns>
-        public static Button CreateUIButton(RectTransform parent, string buttonTemplate, UnityAction onClick = null, string buttonText = "BUTTON", Sprite icon = null)
-        {
-            Button btn = UnityEngine.Object.Instantiate(Resources.FindObjectsOfTypeAll<Button>().Last(x => (x.name == buttonTemplate)), parent, false);
-            btn.onClick = new Button.ButtonClickedEvent();
+            btn.onClick.RemoveAllListeners();
             if (onClick != null)
+            {
                 btn.onClick.AddListener(onClick);
-            btn.name = "CustomUIButton";
+            }
 
             (btn.transform as RectTransform).anchorMin = new Vector2(0.5f, 0.5f);
             (btn.transform as RectTransform).anchorMax = new Vector2(0.5f, 0.5f);
-            btn.SetButtonText(buttonText);
-            if (icon != null)
-                btn.SetButtonIcon(icon);
-            return btn;
-        }
-
-        /// <summary>
-        /// Creates a copy of a back button.
-        /// </summary>
-        /// <param name="parent">The transform to parent the new button to.</param>
-        /// <param name="onClick">Callback for when the button is pressed.</param>
-        /// <returns>The newly created back button.</returns>
-        public static Button CreateBackButton(RectTransform parent, UnityAction onClick = null)
-        {
-            if (_backButtonInstance == null)
-            {
-                try
-                {
-                    _backButtonInstance = Resources.FindObjectsOfTypeAll<Button>().First(x => (x.name == "BackArrowButton"));
-                }
-                catch
-                {
-                    return null;
-                }
-            }
+            (btn.transform as RectTransform).anchoredPosition = anchoredPosition;
+            (btn.transform as RectTransform).sizeDelta = sizeDelta;
 
-            Button btn = UnityEngine.GameObject.Instantiate(_backButtonInstance, parent, false);
-            btn.onClick = new Button.ButtonClickedEvent();
-            if (onClick != null)
-                btn.onClick.AddListener(onClick);
-            btn.name = "CustomUIButton";
+            btn.SetButtonText(buttonText);
 
             return btn;
         }
@@ -239,12 +220,10 @@ namespace SongBrowser.Internals
         /// <param name="text"></param>
         public static void SetHoverHint(RectTransform button, string name, string text)
         {
-            HoverHintController hoverHintController = Resources.FindObjectsOfTypeAll<HoverHintController>().First();
-            DestroyHoverHint(button);
-            var newHoverHint = button.gameObject.AddComponent<HoverHint>();
-            newHoverHint.SetPrivateField("_hoverHintController", hoverHintController);
-            newHoverHint.text = text;
-            newHoverHint.name = name;
+            HoverHint hover = button.gameObject.AddComponent<HoverHint>();
+            hover.text = text;
+            hover.name = name;
+            hover.SetField("_hoverHintController", Resources.FindObjectsOfTypeAll<HoverHintController>().First());
         }
 
         /// <summary>
@@ -275,34 +254,6 @@ namespace SongBrowser.Internals
         }
 
         /// <summary>
-        /// Adjust button border.
-        /// </summary>
-        /// <param name="button"></param>
-        /// <param name="color"></param>
-        static public void SetButtonBorder(Button button, Color color)
-        {
-            Image img = button.GetComponentsInChildren<Image>().FirstOrDefault(x => x.name == "Stroke");
-            if (img != null)
-            {
-                img.color = color;
-            }
-        }
-
-        /// <summary>
-        /// Adjust button border.
-        /// </summary>
-        /// <param name="button"></param>
-        /// <param name="color"></param>
-        static public void SetButtonBorderActive(Button button, bool active)
-        {
-            Image img = button.GetComponentsInChildren<Image>().FirstOrDefault(x => x.name == "Stroke");
-            if (img != null)
-            {
-                img.gameObject.SetActive(active);
-            }
-        }
-
-        /// <summary>
         /// Find and adjust a stat panel item text fields.
         /// </summary>
         /// <param name="rect"></param>

+ 0 - 16
SongBrowserPlugin/Internals/ReflectionUtils.cs

@@ -1,16 +0,0 @@
-using System;
-using System.Reflection;
-
-
-namespace SongBrowser.Internals
-{
-    public static class ReflectionUtils
-    {
-        public static object GetField(this object obj, string fieldName)
-        {
-            return (obj is Type ? (Type)obj : obj.GetType())
-                .GetField(fieldName, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)
-                .GetValue(obj);
-        }
-    }
-}

+ 5 - 6
SongBrowserPlugin/Plugin.cs

@@ -1,16 +1,15 @@
-using UnityEngine.SceneManagement;
+using BS_Utils.Utilities;
+using IPA;
 using SongBrowser.UI;
-using Logger = SongBrowser.Logging.Logger;
 using System;
-using IPA;
-using BS_Utils.Utilities;
+using Logger = SongBrowser.Logging.Logger;
 
 namespace SongBrowser
 {
     [Plugin(RuntimeOptions.SingleStartInit)]
     public class Plugin
     {
-        public const string VERSION_NUMBER = "6.0.7";
+        public const string VERSION_NUMBER = "6.1.0";
         public static Plugin Instance;
         public static IPA.Logging.Logger Log;
 
@@ -33,7 +32,7 @@ namespace SongBrowser
 
         [OnExit]
         public void OnApplicationQuit()
-        {            
+        {
         }
 
         private void OnMenuSceneLoadedFresh(ScenesTransitionSetupDataSO data)

+ 2 - 2
SongBrowserPlugin/Properties/AssemblyInfo.cs

@@ -31,5 +31,5 @@ using System.Runtime.InteropServices;
 // You can specify all the values or you can default the Build and Revision Numbers 
 // by using the '*' as shown below:
 // [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("6.0.7")]
-[assembly: AssemblyFileVersion("6.0.7")]
+[assembly: AssemblyVersion("6.1.0")]
+[assembly: AssemblyFileVersion("6.1.0")]

+ 17 - 3
SongBrowserPlugin/SongBrowser.csproj

@@ -25,6 +25,7 @@
     <ErrorReport>prompt</ErrorReport>
     <WarningLevel>4</WarningLevel>
     <Prefer32Bit>false</Prefer32Bit>
+    <LangVersion>7.0</LangVersion>
   </PropertyGroup>
   <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
     <PlatformTarget>AnyCPU</PlatformTarget>
@@ -35,8 +36,16 @@
     <ErrorReport>prompt</ErrorReport>
     <WarningLevel>4</WarningLevel>
     <Prefer32Bit>false</Prefer32Bit>
+    <LangVersion>7.0</LangVersion>
   </PropertyGroup>
   <ItemGroup>
+    <Reference Include="BeatmapCore">
+      <HintPath>D:\Games\Steam\SteamApps\common\Beat Saber\Beat Saber_Data\Managed\BeatmapCore.dll</HintPath>
+    </Reference>
+    <Reference Include="BSML, Version=1.3.5.0, Culture=neutral, processorArchitecture=MSIL">
+      <SpecificVersion>False</SpecificVersion>
+      <HintPath>D:\Games\Steam\SteamApps\common\Beat Saber\Plugins\BSML.dll</HintPath>
+    </Reference>
     <Reference Include="BS_Utils">
       <HintPath>D:\Games\Steam\SteamApps\common\Beat Saber\Plugins\BS_Utils.dll</HintPath>
     </Reference>
@@ -97,6 +106,10 @@
     <Reference Include="UnityEngine.Networking">
       <HintPath>C:\Program Files (x86)\Steam\steamapps\common\Beat Saber\Beat Saber_Data\Managed\UnityEngine.Networking.dll</HintPath>
     </Reference>
+    <Reference Include="UnityEngine.PhysicsModule, Version=0.0.0.0, Culture=neutral, processorArchitecture=MSIL">
+      <SpecificVersion>False</SpecificVersion>
+      <HintPath>D:\Games\Steam\SteamApps\common\Beat Saber\Beat Saber_Data\Managed\UnityEngine.PhysicsModule.dll</HintPath>
+    </Reference>
     <Reference Include="UnityEngine.TextRenderingModule, Version=0.0.0.0, Culture=neutral, processorArchitecture=MSIL">
       <SpecificVersion>False</SpecificVersion>
       <HintPath>C:\Program Files (x86)\Steam\steamapps\common\Beat Saber\Beat Saber_Data\Managed\UnityEngine.TextRenderingModule.dll</HintPath>
@@ -121,11 +134,14 @@
       <SpecificVersion>False</SpecificVersion>
       <HintPath>C:\Program Files (x86)\Steam\steamapps\common\Beat Saber\Beat Saber_Data\Managed\UnityEngine.UnityWebRequestWWWModule.dll</HintPath>
     </Reference>
+    <Reference Include="VRUI, Version=0.0.0.0, Culture=neutral, processorArchitecture=MSIL">
+      <SpecificVersion>False</SpecificVersion>
+      <HintPath>D:\Games\Steam\SteamApps\common\Beat Saber\Beat Saber_Data\Managed\VRUI.dll</HintPath>
+    </Reference>
   </ItemGroup>
   <ItemGroup>
     <Compile Include="Internals\BeatSaberUI.cs" />
     <Compile Include="Internals\BeatSaberExtensions.cs" />
-    <Compile Include="Internals\ReflectionUtils.cs" />
     <Compile Include="UI\Browser\BeatSaberUIController.cs" />
     <Compile Include="DataAccess\Playlist.cs" />
     <Compile Include="Internals\SimpleJSON.cs" />
@@ -137,8 +153,6 @@
     <Compile Include="DataAccess\SongBrowserModel.cs" />
     <Compile Include="DataAccess\SongBrowserSettings.cs" />
     <Compile Include="UI\Base64Sprites.cs" />
-    <Compile Include="UI\Keyboard\CustomUIKeyboard.cs" />
-    <Compile Include="UI\Keyboard\SearchKeyboardViewController.cs" />
     <Compile Include="UI\Browser\SongBrowserUI.cs" />
     <Compile Include="UI\Browser\SongFilterButton.cs" />
     <Compile Include="UI\Browser\SongSortButton.cs" />

+ 7 - 8
SongBrowserPlugin/SongBrowserApplication.cs

@@ -1,8 +1,7 @@
-using SongBrowser.DataAccess;
-using SongBrowser.UI;
+using SongBrowser.UI;
 using System;
 using System.Collections;
-using System.Collections.Generic;
+using System.Collections.Concurrent;
 using System.Linq;
 using UnityEngine;
 using UnityEngine.UI;
@@ -32,7 +31,7 @@ namespace SongBrowser
         /// Load the main song browser app.
         /// </summary>
         internal static void OnLoad()
-        {            
+        {
             if (Instance != null)
             {
                 return;
@@ -40,7 +39,7 @@ namespace SongBrowser
 
             new GameObject("Beat Saber SongBrowser Plugin").AddComponent<SongBrowserApplication>();
 
-            SongBrowserApplication.MainProgressBar = SongBrowser.UI.ProgressBar.Create();            
+            SongBrowserApplication.MainProgressBar = SongBrowser.UI.ProgressBar.Create();
 
             Console.WriteLine("SongBrowser Plugin Loaded()");
         }
@@ -100,7 +99,7 @@ namespace SongBrowser
         /// </summary>
         /// <param name="loader"></param>
         /// <param name="levels"></param>
-        private void OnSongLoaderLoadedSongs(SongCore.Loader loader, Dictionary<string, CustomPreviewBeatmapLevel> levels)
+        private void OnSongLoaderLoadedSongs(SongCore.Loader loader, ConcurrentDictionary<string, CustomPreviewBeatmapLevel> levels)
         {
             Logger.Trace("OnSongLoaderLoadedSongs-SongBrowserApplication()");
             try
@@ -139,8 +138,8 @@ namespace SongBrowser
         {
             // Append our own event to appropriate events so we can refresh the song list before the user sees it.
             MainFlowCoordinator mainFlow = Resources.FindObjectsOfTypeAll<MainFlowCoordinator>().First();
-            Button soloFreePlayButton = Resources.FindObjectsOfTypeAll<Button>().First(x => x.name == "SoloFreePlayButton");
-            Button partyFreePlayButton = Resources.FindObjectsOfTypeAll<Button>().First(x => x.name == "PartyFreePlayButton");
+            Button soloFreePlayButton = Resources.FindObjectsOfTypeAll<Button>().First(x => x.name == "SoloButton");
+            Button partyFreePlayButton = Resources.FindObjectsOfTypeAll<Button>().First(x => x.name == "PartyButton");
             Button campaignButton = Resources.FindObjectsOfTypeAll<Button>().First(x => x.name == "CampaignButton");
 
             soloFreePlayButton.onClick.AddListener(HandleSoloModeSelection);

+ 1 - 1
SongBrowserPlugin/UI/Base64Sprites.cs

@@ -37,7 +37,7 @@ namespace SongBrowser.UI
         {
             // prune base64 encoded image header
             Regex r = new Regex(@"data:image.*base64,");
-            base64 = r.Replace(base64, "");            
+            base64 = r.Replace(base64, "");
 
             Sprite s = null;
             try

+ 51 - 16
SongBrowserPlugin/UI/Browser/BeatSaberUIController.cs

@@ -1,6 +1,5 @@
 using BS_Utils.Utilities;
 using HMUI;
-using IPA.Utilities;
 using System;
 using System.Collections.Generic;
 using System.Linq;
@@ -17,6 +16,7 @@ namespace SongBrowser.DataAccess
         public LevelSelectionNavigationController LevelSelectionNavigationController;
 
         public LevelFilteringNavigationController LevelFilteringNavigationController;
+        public LevelCollectionNavigationController LevelCollectionNavigationController;
 
         public LevelCollectionViewController LevelCollectionViewController;
         public LevelCollectionTableView LevelCollectionTableView;
@@ -61,16 +61,20 @@ namespace SongBrowser.DataAccess
             LevelSelectionFlowCoordinator = flowCoordinator;
 
             // gather flow coordinator elements
-            LevelSelectionNavigationController = LevelSelectionFlowCoordinator.GetPrivateField<LevelSelectionNavigationController>("_levelSelectionNavigationController");
+            LevelSelectionNavigationController = LevelSelectionFlowCoordinator.GetPrivateField<LevelSelectionNavigationController>("levelSelectionNavigationController");
             Logger.Debug("Acquired LevelSelectionNavigationController [{0}]", LevelSelectionNavigationController.GetInstanceID());
 
-            LevelFilteringNavigationController = Resources.FindObjectsOfTypeAll<LevelFilteringNavigationController>().First();
+            //LevelFilteringNavigationController = Resources.FindObjectsOfTypeAll<LevelFilteringNavigationController>().First();
+            LevelFilteringNavigationController = LevelSelectionNavigationController.GetPrivateField<LevelFilteringNavigationController>("_levelFilteringNavigationController");
             Logger.Debug("Acquired LevelFilteringNavigationController [{0}]", LevelFilteringNavigationController.GetInstanceID());
 
-            LevelCollectionViewController = LevelSelectionNavigationController.GetPrivateField<LevelCollectionViewController>("_levelCollectionViewController");
+            LevelCollectionNavigationController = LevelSelectionNavigationController.GetPrivateField<LevelCollectionNavigationController>("_levelCollectionNavigationController");
+            Logger.Debug("Acquired LevelCollectionNavigationController [{0}]", LevelCollectionNavigationController.GetInstanceID());
+
+            LevelCollectionViewController = LevelCollectionNavigationController.GetPrivateField<LevelCollectionViewController>("_levelCollectionViewController");
             Logger.Debug("Acquired LevelPackLevelsViewController [{0}]", LevelCollectionViewController.GetInstanceID());
 
-            LevelDetailViewController = LevelSelectionNavigationController.GetPrivateField<StandardLevelDetailViewController>("_levelDetailViewController");
+            LevelDetailViewController = LevelCollectionNavigationController.GetPrivateField<StandardLevelDetailViewController>("_levelDetailViewController");
             Logger.Debug("Acquired StandardLevelDetailViewController [{0}]", LevelDetailViewController.GetInstanceID());
 
             LevelCollectionTableView = this.LevelCollectionViewController.GetPrivateField<LevelCollectionTableView>("_levelCollectionTableView");
@@ -95,15 +99,22 @@ namespace SongBrowser.DataAccess
             TableViewPageDownButton = tableView.GetPrivateField<Button>("_pageDownButton");
             Logger.Debug("Acquired Page Up and Down buttons...");
 
-            PlayContainer = StandardLevelDetailView.GetComponentsInChildren<RectTransform>().First(x => x.name == "PlayContainer");
-            PlayButtons = PlayContainer.GetComponentsInChildren<RectTransform>().First(x => x.name == "PlayButtons");
+            PlayButtons = Resources.FindObjectsOfTypeAll<RectTransform>().First(x => x.name == "ActionButtons");
+            Logger.Debug("Acquired ActionButtons [{0}]", PlayButtons);
+
+            PlayContainer = PlayButtons.parent as RectTransform;
+            Logger.Debug("Acquired ActionButtons parent [{0}]", PlayContainer);
 
             PlayButton = Resources.FindObjectsOfTypeAll<Button>().First(x => x.name == "PlayButton");
+            Logger.Debug("Acquired PlayButton [{0}]", PlayButton);
             PracticeButton = PlayButtons.GetComponentsInChildren<Button>().First(x => x.name == "PracticeButton");
+            Logger.Debug("Acquired PracticeButton [{0}]", PracticeButton);
 
             SimpleDialogPromptViewControllerPrefab = Resources.FindObjectsOfTypeAll<SimpleDialogPromptViewController>().First();
+            Logger.Debug("Acquired SimpleDialogPromptViewControllerPrefab [{0}]", SimpleDialogPromptViewControllerPrefab);
 
             BeatmapLevelsModel = Resources.FindObjectsOfTypeAll<BeatmapLevelsModel>().First();
+            Logger.Debug("Acquired BeatmapLevelsModel [{0}]", BeatmapLevelsModel);
         }
 
         /// <summary>
@@ -112,12 +123,12 @@ namespace SongBrowser.DataAccess
         /// <returns></returns>
         private IBeatmapLevelPack GetCurrentSelectedLevelPack()
         {
-            if (LevelSelectionNavigationController == null)
+            if (LevelCollectionNavigationController == null)
             {
                 return null;
             }
 
-            var pack = LevelSelectionNavigationController.GetPrivateField<IBeatmapLevelPack>("_levelPack");
+            var pack = LevelCollectionNavigationController.GetPrivateField<IBeatmapLevelPack>("_levelPack");
             return pack;
         }
 
@@ -199,7 +210,7 @@ namespace SongBrowser.DataAccess
                 Logger.Debug("Current selected level collection is null for some reason...");
                 return null;
             }
-            
+
             return levelCollection.beatmapLevelCollection.beatmapLevels;
         }
 
@@ -207,14 +218,31 @@ namespace SongBrowser.DataAccess
         /// Select a level collection.
         /// </summary>
         /// <param name="levelCollectionName"></param>
-        public void SelectLevelCollection(String levelCollectionName)
+        public void SelectLevelCollection(String levelCategoryName, String levelCollectionName)
         {
             Logger.Trace("SelectLevelCollection({0})", levelCollectionName);
 
             try
             {
-                IAnnotatedBeatmapLevelCollection collection = GetLevelCollectionByName(levelCollectionName);
+                if (String.IsNullOrEmpty(levelCategoryName))
+                {
+                    // hack for now, just assume custom levels if a user has an old settings file, corrects itself first time they change level packs.
+                    levelCategoryName = SelectLevelCategoryViewController.LevelCategory.CustomSongs.ToString();
+                }
 
+                SelectLevelCategoryViewController.LevelCategory category;
+                try
+                {
+                    category = (SelectLevelCategoryViewController.LevelCategory)Enum.Parse(typeof(SelectLevelCategoryViewController.LevelCategory), levelCategoryName, true);
+                }
+                catch (Exception)
+                {
+                    // invalid input
+                    return;
+                }
+
+
+                IAnnotatedBeatmapLevelCollection collection = GetLevelCollectionByName(levelCollectionName);
                 if (collection == null)
                 {
                     Logger.Debug("Could not locate requested level collection...");
@@ -223,9 +251,16 @@ namespace SongBrowser.DataAccess
 
                 Logger.Info("Selecting level collection: {0}", collection.collectionName);
 
-                LevelFilteringNavigationController.SelectBeatmapLevelPackOrPlayList(collection as IBeatmapLevelPack, collection as IPlaylist);
-                LevelFilteringNavigationController.TabBarDidSwitch();
-               
+                var selectLeveCategoryViewController = LevelFilteringNavigationController.GetComponentInChildren<SelectLevelCategoryViewController>();
+                var iconSegementController = selectLeveCategoryViewController.GetComponentInChildren<IconSegmentedControl>();
+
+                int selectCellNumber = (from x in selectLeveCategoryViewController.GetPrivateField<SelectLevelCategoryViewController.LevelCategoryInfo[]>("_levelCategoryInfos")
+                                        select x.levelCategory).ToList().IndexOf(category);
+
+                iconSegementController.SelectCellWithNumber(selectCellNumber);
+                LevelFilteringNavigationController.SelectAnnotatedBeatmapLevelCollection(collection as IBeatmapLevelPack);
+                LevelFilteringNavigationController.UpdateSecondChildControllerContent(category);
+
                 Logger.Debug("Done selecting level collection!");
             }
             catch (Exception e)
@@ -305,7 +340,7 @@ namespace SongBrowser.DataAccess
                 LevelCollectionTableView.HandleDidSelectRowEvent(tableView, selectedIndex);
             }
             tableView.ScrollToCellWithIdx(selectedIndex, TableViewScroller.ScrollPositionType.Beginning, true);
-            tableView.SelectCellWithIdx(selectedIndex);            
+            tableView.SelectCellWithIdx(selectedIndex);
         }
 
         /// <summary>

+ 186 - 216
SongBrowserPlugin/UI/Browser/SongBrowserUI.cs

@@ -1,16 +1,16 @@
-using UnityEngine;
-using System.Linq;
-using System;
-using System.Collections.Generic;
-using UnityEngine.UI;
+using BeatSaberMarkupLanguage.Components;
 using HMUI;
 using SongBrowser.DataAccess;
-using TMPro;
-using Logger = SongBrowser.Logging.Logger;
-using System.Collections;
-using SongCore.Utilities;
 using SongBrowser.Internals;
+using SongCore.Utilities;
 using SongDataCore.BeatStar;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using UnityEngine;
+using UnityEngine.UI;
+using Logger = SongBrowser.Logging.Logger;
 
 namespace SongBrowser.UI
 {
@@ -22,6 +22,11 @@ namespace SongBrowser.UI
         FilterBy
     }
 
+    public class SongBrowserViewController : ViewController
+    {
+        // Named instance
+    }
+
     /// <summary>
     /// Hijack the flow coordinator.  Have access to all StandardLevel easily.
     /// </summary>
@@ -32,11 +37,15 @@ namespace SongBrowser.UI
 
         private const float SEGMENT_PERCENT = 0.1f;
         private const int LIST_ITEMS_VISIBLE_AT_ONCE = 6;
+        private const float CLEAR_BUTTON_Y = -32.5f;
+        private const float BUTTON_ROW_Y = -32.5f;
 
         // BeatSaber Internal UI structures
         DataAccess.BeatSaberUIController _beatUi;
 
         // New UI Elements
+        private SongBrowserViewController _viewController;
+
         private List<SongSortButton> _sortButtonGroup;
         private List<SongFilterButton> _filterButtonGroup;
 
@@ -49,13 +58,11 @@ namespace SongBrowser.UI
         private Button _clearSortFilterButton;
 
         private SimpleDialogPromptViewController _deleteDialog;
-        private Button _deleteButton;        
+        private Button _deleteButton;
 
         private Button _pageUpFastButton;
         private Button _pageDownFastButton;
 
-        private SearchKeyboardViewController _searchViewController;
-
         private RectTransform _ppStatButton;
         private RectTransform _starStatButton;
         private RectTransform _njsStatButton;
@@ -77,7 +84,7 @@ namespace SongBrowser.UI
         }
 
         private bool _uiCreated = false;
-        
+
         private UIState _currentUiState = UIState.Disabled;
 
         private bool _asyncUpdating = false;
@@ -90,7 +97,7 @@ namespace SongBrowser.UI
             Logger.Trace("CreateUI()");
 
             // Determine the flow controller to use
-            FlowCoordinator flowCoordinator = null;
+            FlowCoordinator flowCoordinator;
             if (mode == MainMenuViewController.MenuButton.SoloFreePlay)
             {
                 Logger.Debug("Entering SOLO mode...");
@@ -103,8 +110,7 @@ namespace SongBrowser.UI
             }
             else
             {
-                Logger.Debug("Entering SOLO CAMPAIGN mode...");
-                flowCoordinator = Resources.FindObjectsOfTypeAll<CampaignFlowCoordinator>().First();
+                Logger.Info("Entering Unsupported mode...");                
                 return;
             }
 
@@ -120,7 +126,20 @@ namespace SongBrowser.UI
             }
 
             try
-            {                
+            {
+                // Create a view controller to store all SongBrowser elements
+                if (_viewController)
+                {
+                    UnityEngine.GameObject.Destroy(_viewController);
+                }
+                _viewController = BeatSaberUI.CreateCurvedViewController<SongBrowserViewController>("SongBrowserViewController", 125.0f);
+                _viewController.rectTransform.SetParent(_beatUi.LevelCollectionNavigationController.rectTransform, false);
+                _viewController.rectTransform.anchorMin = new Vector2(0f, 0f);
+                _viewController.rectTransform.anchorMax = new Vector2(1f, 1f);
+                _viewController.rectTransform.anchoredPosition = new Vector2(0, 0);
+                _viewController.rectTransform.sizeDelta = new Vector2(125, 25);
+                _viewController.gameObject.SetActive(true);
+
                 // delete dialog
                 this._deleteDialog = UnityEngine.Object.Instantiate<SimpleDialogPromptViewController>(_beatUi.SimpleDialogPromptViewControllerPrefab);
                 this._deleteDialog.name = "DeleteDialogPromptViewController";
@@ -157,32 +176,41 @@ namespace SongBrowser.UI
         {
             Logger.Debug("Creating outer UI...");
 
-            float clearButtonX = -32.5f;
-            float clearButtonY = 34.5f;
-            float buttonY = 37f;
+            float clearButtonX = -72.5f;
+            float clearButtonY = CLEAR_BUTTON_Y;
+            float buttonY = BUTTON_ROW_Y;
             float buttonHeight = 5.0f;
-            float sortByButtonX = -22.5f + buttonHeight;
+            float sortByButtonX = -62.5f + buttonHeight;
             float outerButtonFontSize = 3.0f;
             float displayButtonFontSize = 2.5f;
             float outerButtonWidth = 24.0f;
-            float randomButtonWidth = 8.0f;
+            float randomButtonWidth = 10.0f;
 
             // clear button
-            _clearSortFilterButton = CreateClearButton(clearButtonX, clearButtonY, buttonHeight, () =>
-            {                
-                if (_currentUiState == UIState.FilterBy || _currentUiState == UIState.SortBy)
+            _clearSortFilterButton = _viewController.CreateIconButton(
+                "ClearSortAndFilterButton",
+                "PracticeButton",
+                new Vector2(clearButtonX, clearButtonY),
+                new Vector2(randomButtonWidth, randomButtonWidth),
+                () =>
                 {
-                    RefreshOuterUIState(UIState.Main);
-                }
-                else
-                {
-                    OnClearButtonClickEvent();
-                }
-            });
+                    if (_currentUiState == UIState.FilterBy || _currentUiState == UIState.SortBy)
+                    {
+                        RefreshOuterUIState(UIState.Main);
+                    }
+                    else
+                    {
+                        OnClearButtonClickEvent();
+                    }
+                },
+                Base64Sprites.XIcon);
+            _clearSortFilterButton.SetButtonBackgroundActive(false);
 
             // create SortBy button and its display
             float curX = sortByButtonX;
-            _sortByButton = _beatUi.LevelCollectionViewController.CreateUIButton("ApplyButton", new Vector2(curX, buttonY), new Vector2(outerButtonWidth, buttonHeight), () =>
+
+            Logger.Debug("Creating Sort By...");
+            _sortByButton = _viewController.CreateUIButton("sortBy", "PracticeButton", new Vector2(curX, buttonY), new Vector2(outerButtonWidth, buttonHeight), () =>
             {
                 RefreshOuterUIState(UIState.SortBy);
             }, "Sort By");
@@ -191,16 +219,20 @@ namespace SongBrowser.UI
 
             curX += outerButtonWidth;
 
-            _sortByDisplay = _beatUi.LevelCollectionViewController.CreateUIButton("ApplyButton", new Vector2(curX, buttonY), new Vector2(outerButtonWidth, buttonHeight), () =>
+            Logger.Debug("Creating Sort By Display...");
+            _sortByDisplay = _viewController.CreateUIButton("sortByValue", "PracticeButton", new Vector2(curX, buttonY), new Vector2(outerButtonWidth, buttonHeight), () =>
             {
                 OnSortButtonClickEvent(_model.Settings.sortMode);
             }, "");
             _sortByDisplay.SetButtonTextSize(displayButtonFontSize);
             _sortByDisplay.ToggleWordWrapping(false);
+            _sortByDisplay.SetButtonBackgroundActive(false);
+
             curX += outerButtonWidth;
 
             // create FilterBy button and its display
-            _filterByButton = _beatUi.LevelCollectionViewController.CreateUIButton("ApplyButton", new Vector2(curX, buttonY), new Vector2(outerButtonWidth, buttonHeight), () =>
+            Logger.Debug("Creating Filter By...");
+            _filterByButton = _viewController.CreateUIButton("filterBy", "PracticeButton", new Vector2(curX, buttonY), new Vector2(outerButtonWidth, buttonHeight), () =>
             {
                 RefreshOuterUIState(UIState.FilterBy);
             }, "Filter By");
@@ -209,7 +241,8 @@ namespace SongBrowser.UI
 
             curX += outerButtonWidth;
 
-            _filterByDisplay = _beatUi.LevelCollectionViewController.CreateUIButton("ApplyButton", new Vector2(curX, buttonY), new Vector2(outerButtonWidth, buttonHeight), () =>
+            Logger.Debug("Creating Filter By Display...");
+            _filterByDisplay = _viewController.CreateUIButton("filterValue", "PracticeButton", new Vector2(curX, buttonY), new Vector2(outerButtonWidth, buttonHeight), () =>
             {
                 _model.Settings.filterMode = SongFilterMode.None;
                 CancelFilter();
@@ -218,38 +251,15 @@ namespace SongBrowser.UI
             }, "");
             _filterByDisplay.SetButtonTextSize(displayButtonFontSize);
             _filterByDisplay.ToggleWordWrapping(false);
+            _filterByDisplay.SetButtonBackgroundActive(false);
 
             // random button
-            _randomButton = _beatUi.LevelCollectionViewController.CreateUIButton("HowToPlayButton", new Vector2(curX + (outerButtonWidth / 2.0f) + (randomButtonWidth / 2.0f), clearButtonY), new Vector2(randomButtonWidth, buttonHeight), () =>
+            Logger.Debug("Creating Random Button...");
+            _randomButton = _viewController.CreateIconButton("randomButton", "PracticeButton", new Vector2(curX + (outerButtonWidth / 2.0f) + (randomButtonWidth / 2.0f), clearButtonY), new Vector2(randomButtonWidth, randomButtonWidth), () =>
             {
                 OnSortButtonClickEvent(SongSortMode.Random);
-            }, "",
-            Base64Sprites.RandomIcon);
-            _randomButton.GetComponentsInChildren<HorizontalLayoutGroup>().First(btn => btn.name == "Content").padding = new RectOffset(0, 0, 0, 0);
-            var textRect = _randomButton.GetComponentsInChildren<RectTransform>(true).FirstOrDefault(c => c.name == "Text");
-            if (textRect != null)
-            {
-                UnityEngine.Object.Destroy(textRect.gameObject);
-            }
-            BeatSaberUI.SetButtonBorderActive(_randomButton, false);
-        }
-
-        /// <summary>
-        /// Create the back button
-        /// </summary>
-        /// <returns></returns>
-        private Button CreateClearButton(float x, float y, float h, UnityEngine.Events.UnityAction callback)
-        {
-            Button b = _beatUi.LevelCollectionViewController.CreateUIButton("HowToPlayButton", new Vector2(x, y), new Vector2(h, h), callback, "", Base64Sprites.XIcon);
-            b.GetComponentsInChildren<HorizontalLayoutGroup>().First(btn => btn.name == "Content").padding = new RectOffset(1, 1, 0, 0);
-            RectTransform textRect = b.GetComponentsInChildren<RectTransform>(true).FirstOrDefault(c => c.name == "Text");
-            if (textRect != null)
-            {
-                UnityEngine.Object.Destroy(textRect.gameObject);
-            }
-            BeatSaberUI.SetButtonBorderActive(b, false);
-
-            return b;
+            }, Base64Sprites.RandomIcon);
+            _randomButton.SetButtonBackgroundActive(false);
         }
 
         /// <summary>
@@ -259,16 +269,16 @@ namespace SongBrowser.UI
         {
             Logger.Debug("Create sort buttons...");
 
-            float sortButtonFontSize = 2.15f;
-            float sortButtonX = -23.0f;
+            float sortButtonFontSize = 2.0f;
+            float sortButtonX = -63.0f;
             float sortButtonWidth = 12.0f;
             float buttonSpacing = 0.25f;
-            float buttonY = 37f;
+            float buttonY = BUTTON_ROW_Y;
             float buttonHeight = 5.0f;
 
             string[] sortButtonNames = new string[]
             {
-                    "Title", "Author", "Newest", "YourPlays", "PP", "Stars", "UpVotes", "Rating", "Heat"
+                    "Title", "Author", "Newest", "#Plays", "PP", "Stars", "UpVotes", "Rating", "Heat"
             };
 
             SongSortMode[] sortModes = new SongSortMode[]
@@ -282,7 +292,7 @@ namespace SongBrowser.UI
                 float curButtonX = sortButtonX + (sortButtonWidth * i) + (buttonSpacing * i);
                 SongSortButton sortButton = new SongSortButton();
                 sortButton.SortMode = sortModes[i];
-                sortButton.Button = _beatUi.LevelCollectionViewController.CreateUIButton("ApplyButton",
+                sortButton.Button = _viewController.CreateUIButton(String.Format("Sort{0}Button", sortButton.SortMode), "PracticeButton",
                     new Vector2(curButtonX, buttonY), new Vector2(sortButtonWidth, buttonHeight),
                     () =>
                     {
@@ -291,9 +301,7 @@ namespace SongBrowser.UI
                     },
                     sortButtonNames[i]);
                 sortButton.Button.SetButtonTextSize(sortButtonFontSize);
-                sortButton.Button.GetComponentsInChildren<HorizontalLayoutGroup>().First(btn => btn.name == "Content").padding = new RectOffset(4, 4, 2, 2);                
                 sortButton.Button.ToggleWordWrapping(false);
-                sortButton.Button.name = "Sort" + sortModes[i].ToString() + "Button";
 
                 _sortButtonGroup.Add(sortButton);
             }
@@ -307,20 +315,20 @@ namespace SongBrowser.UI
             Logger.Debug("Creating filter buttons...");
 
             float filterButtonFontSize = 2.25f;
-            float filterButtonX = -23.0f;
+            float filterButtonX = -63.0f;
             float filterButtonWidth = 12.25f;
             float buttonSpacing = 0.5f;
-            float buttonY = 37f;
+            float buttonY = BUTTON_ROW_Y;
             float buttonHeight = 5.0f;
 
             string[] filterButtonNames = new string[]
             {
-                    "Favorites", "Search", "Ranked", "Unranked"
+                    "Search", "Ranked", "Unranked"
             };
 
             SongFilterMode[] filterModes = new SongFilterMode[]
             {
-                    SongFilterMode.Favorites, SongFilterMode.Search, SongFilterMode.Ranked, SongFilterMode.Unranked
+                    SongFilterMode.Search, SongFilterMode.Ranked, SongFilterMode.Unranked
             };
 
             _filterButtonGroup = new List<SongFilterButton>();
@@ -329,7 +337,7 @@ namespace SongBrowser.UI
                 float curButtonX = filterButtonX + (filterButtonWidth * i) + (buttonSpacing * i);
                 SongFilterButton filterButton = new SongFilterButton();
                 filterButton.FilterMode = filterModes[i];
-                filterButton.Button = _beatUi.LevelCollectionViewController.CreateUIButton("ApplyButton",
+                filterButton.Button = _viewController.CreateUIButton(String.Format("Filter{0}Button", filterButton.FilterMode), "PracticeButton",
                     new Vector2(curButtonX, buttonY), new Vector2(filterButtonWidth, buttonHeight),
                     () =>
                     {
@@ -338,9 +346,7 @@ namespace SongBrowser.UI
                     },
                     filterButtonNames[i]);
                 filterButton.Button.SetButtonTextSize(filterButtonFontSize);
-                filterButton.Button.GetComponentsInChildren<HorizontalLayoutGroup>().First(btn => btn.name == "Content").padding = new RectOffset(4, 4, 2, 2);
                 filterButton.Button.ToggleWordWrapping(false);
-                filterButton.Button.name = "Filter" + filterButtonNames[i] + "Button";
 
                 _filterButtonGroup.Add(filterButton);
             }
@@ -352,30 +358,26 @@ namespace SongBrowser.UI
         private void CreateFastPageButtons()
         {
             Logger.Debug("Creating fast scroll button...");
-
-            _pageUpFastButton = Instantiate(_beatUi.TableViewPageUpButton, _beatUi.LevelCollectionTableViewTransform, false);
-            (_pageUpFastButton.transform as RectTransform).anchorMin = new Vector2(0.5f, 1f);
-            (_pageUpFastButton.transform as RectTransform).anchorMax = new Vector2(0.5f, 1f);
-            (_pageUpFastButton.transform as RectTransform).anchoredPosition = new Vector2(-26f, 1f);
-            (_pageUpFastButton.transform as RectTransform).sizeDelta = new Vector2(8f, 6f);
-            _pageUpFastButton.GetComponentsInChildren<RectTransform>().First(x => x.name == "BG").sizeDelta = new Vector2(8f, 6f);
-            _pageUpFastButton.GetComponentsInChildren<UnityEngine.UI.Image>().First(x => x.name == "Arrow").sprite = Base64Sprites.DoubleArrow;
-            _pageUpFastButton.onClick.AddListener(delegate ()
-            {
-                this.JumpSongList(-1, SEGMENT_PERCENT);
-            });
-
-            _pageDownFastButton = Instantiate(_beatUi.TableViewPageDownButton, _beatUi.LevelCollectionTableViewTransform, false);
-            (_pageDownFastButton.transform as RectTransform).anchorMin = new Vector2(0.5f, 0f);
-            (_pageDownFastButton.transform as RectTransform).anchorMax = new Vector2(0.5f, 0f);
-            (_pageDownFastButton.transform as RectTransform).anchoredPosition = new Vector2(-26f, -1f);
-            (_pageDownFastButton.transform as RectTransform).sizeDelta = new Vector2(8f, 6f);
-            _pageDownFastButton.GetComponentsInChildren<RectTransform>().First(x => x.name == "BG").sizeDelta = new Vector2(8f, 6f);
-            _pageDownFastButton.GetComponentsInChildren<UnityEngine.UI.Image>().First(x => x.name == "Arrow").sprite = Base64Sprites.DoubleArrow;
-            _pageDownFastButton.onClick.AddListener(delegate ()
-            {
-                this.JumpSongList(1, SEGMENT_PERCENT);
-            });
+            _pageUpFastButton = BeatSaberUI.CreateIconButton("PageUpFast",
+                _beatUi.LevelCollectionTableViewTransform, "PracticeButton",
+                new Vector2(42f, 24f),
+                new Vector2(10f, 10f),
+                delegate ()
+                {
+                    this.JumpSongList(-1, SEGMENT_PERCENT);
+                }, Base64Sprites.DoubleArrow);
+            _pageUpFastButton.SetButtonBackgroundActive(false);
+            (_pageUpFastButton.transform as RectTransform).Rotate(new Vector3(0, 0, 180));
+
+            _pageDownFastButton = BeatSaberUI.CreateIconButton("PageDownFast",
+                _beatUi.LevelCollectionTableViewTransform, "PracticeButton",
+                new Vector2(42f, -24f),
+                new Vector2(10f, 10f),
+                delegate ()
+                {
+                    this.JumpSongList(1, SEGMENT_PERCENT);
+                }, Base64Sprites.DoubleArrow);
+            _pageDownFastButton.SetButtonBackgroundActive(false);
         }
 
         /// <summary>
@@ -384,12 +386,12 @@ namespace SongBrowser.UI
         private void CreateDeleteButton()
         {
             // Create delete button
-            Logger.Debug("Creating delete button...");
+            /*Logger.Debug("Creating delete button...");
             _deleteButton = BeatSaberUI.CreateIconButton(_beatUi.PlayButtons, _beatUi.PracticeButton, Base64Sprites.DeleteIcon);
             _deleteButton.onClick.AddListener(delegate () {
                 HandleDeleteSelectedLevel();
             });
-            BeatSaberUI.DestroyHoverHint(_deleteButton.transform as RectTransform);
+            BeatSaberUI.DestroyHoverHint(_deleteButton.transform as RectTransform);*/
         }
 
         /// <summary>
@@ -397,50 +399,32 @@ namespace SongBrowser.UI
         /// </summary>
         private void ModifySongStatsPanel()
         {
-            // modify details view
+            // modify stat panel, inject extra row of stats
             Logger.Debug("Resizing Stats Panel...");
 
             var statsPanel = _beatUi.StandardLevelDetailView.GetPrivateField<LevelParamsPanel>("_levelParamsPanel");
-            var statTransforms = statsPanel.GetComponentsInChildren<RectTransform>();
-            var valueTexts = statsPanel.GetComponentsInChildren<TextMeshProUGUI>().Where(x => x.name == "ValueText").ToList();
-            RectTransform panelRect = (statsPanel.transform as RectTransform);
-            panelRect.sizeDelta = new Vector2(panelRect.sizeDelta.x * 1.2f, panelRect.sizeDelta.y * 1.2f);
-
-            for (int i = 0; i < statTransforms.Length; i++)
-            {
-                var r = statTransforms[i];
-                if (r.name == "Separator")
-                {
-                    continue;
-                }
-                r.sizeDelta = new Vector2(r.sizeDelta.x * 0.9f, r.sizeDelta.y * 0.9f);
-            }
-
-            for (int i = 0; i < valueTexts.Count; i++)
-            {
-                var text = valueTexts[i];
-                text.fontSize -= 0.75f;
-            }
+            (statsPanel.transform as RectTransform).Translate(0, 0.05f, 0);
 
-            // inject our components
-            _ppStatButton = UnityEngine.Object.Instantiate(statTransforms[1], statsPanel.transform, false);
+            _ppStatButton = UnityEngine.Object.Instantiate(statsPanel.GetComponentsInChildren<RectTransform>().First(x => x.name == "NPS"), statsPanel.transform, false);
+            _ppStatButton.name = "PPStatLabel";
+            (_ppStatButton.transform as RectTransform).Translate(0, -0.1f, 0);
             BeatSaberUI.SetStatButtonIcon(_ppStatButton, Base64Sprites.GraphIcon);
             BeatSaberUI.DestroyHoverHint(_ppStatButton);
-            //BeatSaberUI.SetHoverHint(_ppStatButton, "songBrowser_ppValue", "PP Value");
+            BeatSaberUI.SetHoverHint(_ppStatButton, "songBrowser_ppValue", "PP Value");
 
-            _starStatButton = UnityEngine.Object.Instantiate(statTransforms[1], statsPanel.transform, false);
+            _starStatButton = UnityEngine.Object.Instantiate(statsPanel.GetComponentsInChildren<RectTransform>().First(x => x.name == "NotesCount"), statsPanel.transform, false);
+            _starStatButton.name = "StarStatLabel";
+            (_starStatButton.transform as RectTransform).Translate(0, -0.1f, 0);
             BeatSaberUI.SetStatButtonIcon(_starStatButton, Base64Sprites.StarFullIcon);
             BeatSaberUI.DestroyHoverHint(_starStatButton);
-            //BeatSaberUI.SetHoverHint(_starStatButton, "songBrowser_starValue", "Star Difficulty Rating");
+            BeatSaberUI.SetHoverHint(_starStatButton, "songBrowser_starValue", "Star Difficulty Rating");
 
-            _njsStatButton = UnityEngine.Object.Instantiate(statTransforms[1], statsPanel.transform, false);
+            _njsStatButton = UnityEngine.Object.Instantiate(statsPanel.GetComponentsInChildren<RectTransform>().First(x => x.name == "ObstaclesCount"), statsPanel.transform, false);
+            _njsStatButton.name = "NoteJumpSpeedLabel";
+            (_njsStatButton.transform as RectTransform).Translate(0, -0.1f, 0);
             BeatSaberUI.SetStatButtonIcon(_njsStatButton, Base64Sprites.SpeedIcon);
             BeatSaberUI.DestroyHoverHint(_njsStatButton);
-            //BeatSaberUI.SetHoverHint(_njsStatButton, "songBrowser_njsValue", "Note Jump Speed");
-
-            // shrink title
-            var titleText = _beatUi.LevelDetailViewController.GetComponentsInChildren<TextMeshProUGUI>(true).First(x => x.name == "SongNameText");            
-            titleText.fontSize = 5.0f;
+            BeatSaberUI.SetHoverHint(_njsStatButton, "songBrowser_njsValue", "Note Jump Speed");
         }
 
         /// <summary>
@@ -448,20 +432,9 @@ namespace SongBrowser.UI
         /// </summary>
         public void ResizeSongUI()
         {
-            // Reposition the table view a bit
-            _beatUi.LevelCollectionTableViewTransform.anchoredPosition = new Vector2(0f, -2.5f);
-
-            // Move the page up/down buttons a bit
-            TableView tableView = ReflectionUtil.GetPrivateField<TableView>(_beatUi.LevelCollectionTableView, "_tableView");
-            RectTransform pageUpButton = _beatUi.TableViewPageUpButton.transform as RectTransform;
-            RectTransform pageDownButton = _beatUi.TableViewPageDownButton.transform as RectTransform;
-            pageUpButton.anchoredPosition = new Vector2(pageUpButton.anchoredPosition.x, pageUpButton.anchoredPosition.y - 1f);
-            pageDownButton.anchoredPosition = new Vector2(pageDownButton.anchoredPosition.x, pageDownButton.anchoredPosition.y + 1f);
-
             // shrink play button container
-            RectTransform playContainerRect = _beatUi.StandardLevelDetailView.GetComponentsInChildren<RectTransform>().First(x => x.name == "PlayContainer");
-            RectTransform playButtonsRect = playContainerRect.GetComponentsInChildren<RectTransform>().First(x => x.name == "PlayButtons");
-            playButtonsRect.localScale = new Vector3(0.825f, 0.825f, 0.825f);
+            //RectTransform playButtonsRect = Resources.FindObjectsOfTypeAll<RectTransform>().First(x => x.name == "ActionButtons");
+            //playButtonsRect.localScale = new Vector3(0.825f, 0.825f, 0.825f);
         }
 
         /// <summary>
@@ -476,8 +449,8 @@ namespace SongBrowser.UI
             _beatUi.LevelCollectionViewController.didSelectLevelEvent -= OnDidSelectLevelEvent;
             _beatUi.LevelCollectionViewController.didSelectLevelEvent += OnDidSelectLevelEvent;
 
-            _beatUi.LevelDetailViewController.didPresentContentEvent -= OnDidPresentContentEvent;
-            _beatUi.LevelDetailViewController.didPresentContentEvent += OnDidPresentContentEvent;
+            _beatUi.LevelDetailViewController.didChangeContentEvent -= OnDidPresentContentEvent;
+            _beatUi.LevelDetailViewController.didChangeContentEvent += OnDidPresentContentEvent;
 
             _beatUi.LevelDetailViewController.didChangeDifficultyBeatmapEvent -= OnDidChangeDifficultyEvent;
             _beatUi.LevelDetailViewController.didChangeDifficultyBeatmapEvent += OnDidChangeDifficultyEvent;
@@ -551,7 +524,7 @@ namespace SongBrowser.UI
         /// <summary>
         /// Helper to reduce code duplication...
         /// </summary>
-        public void RefreshSongUI(bool scrollToLevel=true)
+        public void RefreshSongUI(bool scrollToLevel = true)
         {
             if (!_uiCreated)
             {
@@ -578,7 +551,7 @@ namespace SongBrowser.UI
                 return;
             }
 
-            this._model.ProcessSongList(_lastLevelCollection, _beatUi.LevelCollectionViewController, _beatUi.LevelSelectionNavigationController);
+            this._model.ProcessSongList(_lastLevelCollection, _beatUi.LevelSelectionNavigationController);
         }
 
         /// <summary>
@@ -605,6 +578,8 @@ namespace SongBrowser.UI
         {
             Logger.Trace("handleDidSelectAnnotatedBeatmapLevelCollection()");
             _lastLevelCollection = annotatedBeatmapLevelCollection;
+            Model.Settings.currentLevelCategoryName = _beatUi.LevelFilteringNavigationController.selectedLevelCategory.ToString();
+            Model.Settings.Save();
             Logger.Debug("Selected Level Collection={0}", _lastLevelCollection);
         }
 
@@ -616,7 +591,7 @@ namespace SongBrowser.UI
         /// <param name="arg2"></param>
         /// <param name="arg3"></param>
         /// <param name="arg4"></param>
-        private void _levelFilteringNavController_didSelectAnnotatedBeatmapLevelCollectionEvent(LevelFilteringNavigationController arg1, IAnnotatedBeatmapLevelCollection arg2, 
+        private void _levelFilteringNavController_didSelectAnnotatedBeatmapLevelCollectionEvent(LevelFilteringNavigationController arg1, IAnnotatedBeatmapLevelCollection arg2,
             GameObject arg3, BeatmapCharacteristicSO arg4)
         {
             Logger.Trace("_levelFilteringNavController_didSelectAnnotatedBeatmapLevelCollectionEvent(levelCollection={0})", arg2);
@@ -676,6 +651,7 @@ namespace SongBrowser.UI
                 {
                     Logger.Debug("Recording levelCollection: {0}", levelCollection.collectionName);
                     _lastLevelCollection = levelCollection;
+                    Model.Settings.currentLevelCategoryName = _beatUi.LevelFilteringNavigationController.selectedLevelCategory.ToString();
                 }
 
                 // reset level selection
@@ -767,7 +743,7 @@ namespace SongBrowser.UI
             Logger.Debug($"FilterButton {mode} clicked.");
 
             var curCollection = _beatUi.GetCurrentSelectedAnnotatedBeatmapLevelCollection();
-            if (_lastLevelCollection == null || 
+            if (_lastLevelCollection == null ||
                 (curCollection != null &&
                 curCollection.collectionName != SongBrowserModel.FilteredSongsCollectionName &&
                 curCollection.collectionName != SongBrowserModel.PlaylistSongsCollectionName))
@@ -777,7 +753,7 @@ namespace SongBrowser.UI
 
             if (mode == SongFilterMode.Favorites)
             {
-                _beatUi.SelectLevelCollection(SongBrowserSettings.CUSTOM_SONGS_LEVEL_COLLECTION_NAME);
+                _beatUi.SelectLevelCollection(SelectLevelCategoryViewController.LevelCategory.Favorites.ToString(), SongBrowserSettings.CUSTOM_SONGS_LEVEL_COLLECTION_NAME);
             }
             else
             {
@@ -891,7 +867,10 @@ namespace SongBrowser.UI
                 return;
             }
 
-            _deleteButton.interactable = (view.selectedDifficultyBeatmap.level.levelID.Length >= 32);
+            if (_deleteButton != null)
+            {
+                _deleteButton.interactable = (view.selectedDifficultyBeatmap.level.levelID.Length >= 32);
+            }
 
             RefreshScoreSaberData(view.selectedDifficultyBeatmap.level);
             RefreshNoteJumpSpeed(beatmap.noteJumpMovementSpeed);
@@ -906,12 +885,21 @@ namespace SongBrowser.UI
         {
             Logger.Trace("OnDidPresentContentEvent()");
 
+            // v1.12.2 - TODO - is this safe to prevent us from trying to lookup empty/dead content?
+            if (type != StandardLevelDetailViewController.ContentType.OwnedAndReady)
+            {
+                return;
+            }
+
             if (view.selectedDifficultyBeatmap == null)
             {
                 return;
             }
 
-            _deleteButton.interactable = (_beatUi.LevelDetailViewController.selectedDifficultyBeatmap.level.levelID.Length >= 32);
+            if (_deleteButton != null)
+            {
+                _deleteButton.interactable = (_beatUi.LevelDetailViewController.selectedDifficultyBeatmap.level.levelID.Length >= 32);
+            }
 
             RefreshScoreSaberData(view.selectedDifficultyBeatmap.level);
             RefreshNoteJumpSpeed(view.selectedDifficultyBeatmap.noteJumpMovementSpeed);
@@ -925,7 +913,10 @@ namespace SongBrowser.UI
         {
             Logger.Trace("HandleDidSelectLevelRow({0})", level);
 
-            _deleteButton.interactable = (level.levelID.Length >= 32);
+            if (_deleteButton != null)
+            {
+                _deleteButton.interactable = (level.levelID.Length >= 32);
+            }
 
             RefreshQuickScrollButtons();
         }
@@ -982,35 +973,20 @@ namespace SongBrowser.UI
                     }
                 });
             _beatUi.LevelSelectionFlowCoordinator.InvokePrivateMethod("PresentViewController", new object[] { _deleteDialog, null, false });
-        }        
+        }
 
         /// <summary>
         /// Display the search keyboard
         /// </summary>
         void ShowSearchKeyboard()
         {
-            if (_searchViewController == null)
-            {
-                _searchViewController = BeatSaberUI.CreateViewController<SearchKeyboardViewController>("SearchKeyboardViewController");
-                _searchViewController.searchButtonPressed += SearchViewControllerSearchButtonPressed;
-                _searchViewController.backButtonPressed += SearchViewControllerbackButtonPressed;
-            }
-
-            Logger.Debug("Presenting search keyboard");
-            _beatUi.LevelSelectionFlowCoordinator.InvokePrivateMethod("PresentViewController", new object[] { _searchViewController, null, false });
-        }
-
-        /// <summary>
-        /// Handle back button event from search keyboard.
-        /// </summary>
-        private void SearchViewControllerbackButtonPressed()
-        {
-            _beatUi.LevelSelectionFlowCoordinator.InvokePrivateMethod("DismissViewController", new object[] { _searchViewController, null, false });
-
-            this._model.Settings.filterMode = SongFilterMode.None;
-            this._model.Settings.Save();
-
-            RefreshSongUI();
+            var modalKbTag = new BeatSaberMarkupLanguage.Tags.ModalKeyboardTag();
+            var modalKbView = modalKbTag.CreateObject(_beatUi.LevelSelectionNavigationController.rectTransform);
+            modalKbView.gameObject.SetActive(true);
+            var modalKb = modalKbView.GetComponent<ModalKeyboard>();
+            modalKb.gameObject.SetActive(true);
+            modalKb.keyboard.EnterPressed += SearchViewControllerSearchButtonPressed;
+            modalKb.modalView.Show(true, true);
         }
 
         /// <summary>
@@ -1019,8 +995,6 @@ namespace SongBrowser.UI
         /// <param name="searchFor"></param>
         private void SearchViewControllerSearchButtonPressed(string searchFor)
         {
-            _beatUi.LevelSelectionFlowCoordinator.InvokePrivateMethod("DismissViewController", new object[] { _searchViewController, null, false });
-
             Logger.Debug("Searching for \"{0}\"...", searchFor);
 
             _model.Settings.filterMode = SongFilterMode.Search;
@@ -1054,7 +1028,6 @@ namespace SongBrowser.UI
                 segmentSize = LIST_ITEMS_VISIBLE_AT_ONCE;
             }
 
-            TableView tableView = ReflectionUtil.GetPrivateField<TableView>(_beatUi.LevelCollectionTableView, "_tableView");
             int currentRow = _beatUi.LevelCollectionTableView.GetPrivateField<int>("_selectedRow");
             int jumpDirection = Math.Sign(numJumps);
             int newRow = currentRow + (jumpDirection * segmentSize);
@@ -1066,12 +1039,12 @@ namespace SongBrowser.UI
             {
                 newRow = totalSize - 1;
             }
-            
+
             Logger.Debug("jumpDirection: {0}, newRow: {1}", jumpDirection, newRow);
             _beatUi.SelectAndScrollToLevel(levels[newRow].levelID);
             RefreshQuickScrollButtons();
         }
-        
+
         /// <summary>
         /// Update GUI elements that show score saber data.
         /// </summary>
@@ -1119,7 +1092,7 @@ namespace SongBrowser.UI
             {
                 BeatSaberUI.SetStatButtonText(_ppStatButton, "NA");
                 BeatSaberUI.SetStatButtonText(_starStatButton, "NA");
-            }                
+            }
 
             Logger.Debug("Done refreshing score saber stats.");
         }
@@ -1137,7 +1110,7 @@ namespace SongBrowser.UI
         /// Update interactive state of the quick scroll buttons.
         /// </summary>
         private void RefreshQuickScrollButtons()
-        {         
+        {
             if (!_uiCreated)
             {
                 return;
@@ -1192,16 +1165,16 @@ namespace SongBrowser.UI
                 return;
             }
 
-            _ppStatButton.gameObject.SetActive(visible);
-            _starStatButton.gameObject.SetActive(visible);
-            _njsStatButton.gameObject.SetActive(visible);
+            _ppStatButton?.gameObject.SetActive(visible);
+            _starStatButton?.gameObject.SetActive(visible);
+            _njsStatButton?.gameObject.SetActive(visible);
 
             RefreshOuterUIState(visible == true ? UIState.Main : UIState.Disabled);
 
-            _deleteButton.gameObject.SetActive(visible);
+            _deleteButton?.gameObject.SetActive(visible);
 
-            _pageUpFastButton.gameObject.SetActive(visible);
-            _pageDownFastButton.gameObject.SetActive(visible);
+            _pageUpFastButton?.gameObject.SetActive(visible);
+            _pageDownFastButton?.gameObject.SetActive(visible);
         }
 
         /// <summary>
@@ -1234,12 +1207,12 @@ namespace SongBrowser.UI
             _sortButtonGroup.ForEach(x => x.Button.gameObject.SetActive(sortButtons));
             _filterButtonGroup.ForEach(x => x.Button.gameObject.SetActive(filterButtons));
 
-            _sortByButton.gameObject.SetActive(outerButtons);
-            _sortByDisplay.gameObject.SetActive(outerButtons);
-            _filterByButton.gameObject.SetActive(outerButtons);
-            _filterByDisplay.gameObject.SetActive(outerButtons);
-            _clearSortFilterButton.gameObject.SetActive(clearButton);
-            _randomButton.gameObject.SetActive(outerButtons);
+            _sortByButton?.gameObject.SetActive(outerButtons);
+            _sortByDisplay?.gameObject.SetActive(outerButtons);
+            _filterByButton?.gameObject.SetActive(outerButtons);
+            _filterByDisplay?.gameObject.SetActive(outerButtons);
+            _clearSortFilterButton?.gameObject.SetActive(clearButton);
+            _randomButton?.gameObject.SetActive(outerButtons);
 
             RefreshCurrentSelectionDisplay();
             _currentUiState = state;
@@ -1250,7 +1223,7 @@ namespace SongBrowser.UI
         /// </summary>
         private void RefreshCurrentSelectionDisplay()
         {
-            string sortByDisplay = null;
+            string sortByDisplay;
             if (_model.Settings.sortMode == SongSortMode.Default)
             {
                 sortByDisplay = "Title";
@@ -1277,57 +1250,55 @@ namespace SongBrowser.UI
                 return;
             }
 
-            // So far all we need to refresh is the sort buttons.
             foreach (SongSortButton sortButton in _sortButtonGroup)
             {
                 if (sortButton.SortMode.NeedsScoreSaberData() && !SongDataCore.Plugin.Songs.IsDataAvailable())
                 {
-                    BeatSaberUI.SetButtonBorder(sortButton.Button, Color.gray);
+                    sortButton.Button.SetButtonUnderlineColor(Color.gray);
                 }
                 else
                 {
-                    BeatSaberUI.SetButtonBorder(sortButton.Button, Color.white);
+                    sortButton.Button.SetButtonUnderlineColor(Color.white);
                 }
 
                 if (sortButton.SortMode == _model.Settings.sortMode)
                 {
                     if (this._model.Settings.invertSortResults)
                     {
-                        BeatSaberUI.SetButtonBorder(sortButton.Button, Color.red);
+                        sortButton.Button.SetButtonUnderlineColor(Color.red);
                     }
                     else
                     {
-                        BeatSaberUI.SetButtonBorder(sortButton.Button, Color.green);
+                        sortButton.Button.SetButtonUnderlineColor(Color.green);
                     }
                 }
             }
 
-            // refresh filter buttons
             foreach (SongFilterButton filterButton in _filterButtonGroup)
             {
-                BeatSaberUI.SetButtonBorder(filterButton.Button, Color.white);
+                filterButton.Button.SetButtonUnderlineColor(Color.white);
                 if (filterButton.FilterMode == _model.Settings.filterMode)
                 {
-                    BeatSaberUI.SetButtonBorder(filterButton.Button, Color.green);
+                    filterButton.Button.SetButtonUnderlineColor(Color.green);
                 }
             }
 
             if (this._model.Settings.invertSortResults)
             {
-                BeatSaberUI.SetButtonBorder(_sortByDisplay, Color.red);
+                _sortByDisplay.SetButtonUnderlineColor(Color.red);
             }
             else
             {
-                BeatSaberUI.SetButtonBorder(_sortByDisplay, Color.green);
+                _sortByDisplay.SetButtonUnderlineColor(Color.green);
             }
 
             if (this._model.Settings.filterMode != SongFilterMode.None)
             {
-                BeatSaberUI.SetButtonBorder(_filterByDisplay, Color.green);
+                _filterByDisplay.SetButtonUnderlineColor(Color.green);
             }
             else
             {
-                BeatSaberUI.SetButtonBorder(_filterByDisplay, Color.white);
+                _filterByDisplay.SetButtonUnderlineColor(Color.white);
             }
         }
 
@@ -1395,7 +1366,7 @@ namespace SongBrowser.UI
                     {
                         Hide();
                     }
-                    _beatUi.SelectLevelCollection(_model.Settings.currentLevelCollectionName);
+                    _beatUi.SelectLevelCollection(_model.Settings.currentLevelCategoryName, _model.Settings.currentLevelCollectionName);
                     _beatUi.LevelFilteringNavigationController.didSelectAnnotatedBeatmapLevelCollectionEvent += _levelFilteringNavController_didSelectAnnotatedBeatmapLevelCollectionEvent;
                 }
 
@@ -1415,4 +1386,3 @@ namespace SongBrowser.UI
         }
     }
 }
- 

+ 0 - 182
SongBrowserPlugin/UI/Keyboard/CustomUIKeyboard.cs

@@ -1,182 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using UnityEngine;
-using UnityEngine.UI;
-using Logger = SongBrowser.Logging.Logger;
-
-namespace SongBrowser.UI
-{
-    // https://github.com/andruzzzhka/BeatSaverDownloader/blob/master/BeatSaverDownloader/PluginUI/UIElements/CustomUIKeyboard.cs
-    class CustomUIKeyboard : MonoBehaviour
-    {
-        public event Action<char> textKeyWasPressedEvent;
-        public event Action deleteButtonWasPressedEvent;
-        public event Action cancelButtonWasPressedEvent;
-        public event Action okButtonWasPressedEvent;
-
-        public bool HideCancelButton { get { return hideCancelButton; } set { hideCancelButton = value; _cancelButton.gameObject.SetActive(!value); } }
-        public bool OkButtonInteractivity { get { return okButtonInteractivity; } set { okButtonInteractivity = value; _okButton.interactable = value; } }
-
-        private bool okButtonInteractivity;
-        private bool hideCancelButton;
-
-        TextMeshProButton _keyButtonPrefab;
-        Button _cancelButton;
-        Button _okButton;
-
-
-        public void Awake()
-        {
-            _keyButtonPrefab = Resources.FindObjectsOfTypeAll<TextMeshProButton>().First(x => x.name == "KeyboardButton");
-
-            Logger.Log("Found keyboard button!");
-
-            string[] array = new string[]
-            {
-                "q",
-                "w",
-                "e",
-                "r",
-                "t",
-                "y",
-                "u",
-                "i",
-                "o",
-                "p",
-                "a",
-                "s",
-                "d",
-                "f",
-                "g",
-                "h",
-                "j",
-                "k",
-                "l",
-                "z",
-                "x",
-                "c",
-                "v",
-                "b",
-                "n",
-                "m",
-                "<-",
-                "space",
-                "OK",
-                "Cancel"
-            };
-
-            for (int i = 0; i < array.Length; i++)
-            {
-                RectTransform parent = transform.GetChild(i) as RectTransform;
-                //TextMeshProButton textMeshProButton = Instantiate(_keyButtonPrefab, parent);
-                TextMeshProButton textMeshProButton = parent.GetComponentInChildren<TextMeshProButton>();
-                textMeshProButton.text.text = array[i];
-                RectTransform rectTransform = textMeshProButton.transform as RectTransform;
-                rectTransform.localPosition = Vector2.zero;
-                rectTransform.localScale = Vector3.one;
-                rectTransform.anchoredPosition = Vector2.zero;
-                rectTransform.anchorMin = Vector2.zero;
-                rectTransform.anchorMax = Vector3.one;
-                rectTransform.offsetMin = Vector2.zero;
-                rectTransform.offsetMax = Vector2.zero;
-                Navigation navigation = textMeshProButton.button.navigation;
-                navigation.mode = Navigation.Mode.None;
-                textMeshProButton.button.navigation = navigation;
-                textMeshProButton.button.onClick.RemoveAllListeners();
-                if (i < array.Length - 4)
-                {
-                    string key = array[i];
-                    textMeshProButton.button.onClick.AddListener(delegate ()
-                    {
-                        textKeyWasPressedEvent?.Invoke(key[0]);
-                    });
-                }
-                else if (i == array.Length - 4)
-                {
-                    textMeshProButton.button.onClick.AddListener(delegate ()
-                    {
-                        deleteButtonWasPressedEvent?.Invoke();
-                    });
-                }
-                else if (i == array.Length - 1)
-                {
-                    (textMeshProButton.transform as RectTransform).sizeDelta = new Vector2(7f, 1.5f);
-                    _cancelButton = textMeshProButton.button;
-                    _cancelButton.gameObject.SetActive(!HideCancelButton);
-                    textMeshProButton.button.onClick.AddListener(delegate ()
-                    {
-                        cancelButtonWasPressedEvent?.Invoke();
-                    });
-                }
-                else if (i == array.Length - 2)
-                {
-                    _okButton = textMeshProButton.button;
-                    _okButton.interactable = OkButtonInteractivity;
-                    textMeshProButton.button.onClick.AddListener(delegate ()
-                    {
-                        okButtonWasPressedEvent?.Invoke();
-                    });
-                }
-                else
-                {
-                    textMeshProButton.button.onClick.AddListener(delegate ()
-                    {
-                        textKeyWasPressedEvent?.Invoke(' ');
-                    });
-                }
-            }
-
-            name = "CustomUIKeyboard";
-
-            (transform as RectTransform).anchoredPosition -= new Vector2(0f, 0f);
-
-            for (int i = 1; i <= 10; i++)
-            {
-                TextMeshProButton textButton = Instantiate(_keyButtonPrefab);
-                textButton.text.text = (i % 10).ToString();
-
-                string key = (i % 10).ToString();
-                textButton.button.onClick.AddListener(delegate ()
-                {
-                    textKeyWasPressedEvent?.Invoke(key[0]);
-                });
-
-                RectTransform buttonRect = textButton.GetComponent<RectTransform>();
-                RectTransform component2 = transform.GetChild(i - 1).gameObject.GetComponent<RectTransform>();
-
-                RectTransform buttonHolder = Instantiate(component2, component2.parent, false);
-                Destroy(buttonHolder.GetComponentInChildren<Button>().gameObject);
-
-                buttonHolder.anchoredPosition -= new Vector2(0f, -10.5f);
-
-                buttonRect.SetParent(buttonHolder, false);
-
-                buttonRect.localPosition = Vector2.zero;
-                buttonRect.localScale = Vector3.one;
-                buttonRect.anchoredPosition = Vector2.zero;
-                buttonRect.anchorMin = Vector2.zero;
-                buttonRect.anchorMax = Vector3.one;
-                buttonRect.offsetMin = Vector2.zero;
-                buttonRect.offsetMax = Vector2.zero;
-            }
-
-        }
-
-        public void KeyPressed(char key)
-        {
-            textKeyWasPressedEvent.Invoke(key);
-        }
-
-        public void DeleteButtonWasPressed()
-        {
-            deleteButtonWasPressedEvent.Invoke();
-        }
-
-        public void OkButtonWasPressed()
-        {
-            okButtonWasPressedEvent.Invoke();
-        }
-    }
-}

+ 0 - 77
SongBrowserPlugin/UI/Keyboard/SearchKeyboardViewController.cs

@@ -1,77 +0,0 @@
-using SongBrowser.Internals;
-using System;
-using System.Linq;
-using TMPro;
-using UnityEngine;
-
-
-namespace SongBrowser.UI
-{
-    // https://github.com/andruzzzhka/BeatSaverDownloader/blob/master/BeatSaverDownloader/PluginUI/ViewControllers/SearchKeyboardViewController.cs
-    class SearchKeyboardViewController : HMUI.ViewController
-    {
-        GameObject _searchKeyboardGO;
-
-        CustomUIKeyboard _searchKeyboard;
-
-        TextMeshProUGUI _inputText;
-        public string _inputString = "";
-
-        public event Action<string> searchButtonPressed;
-        public event Action backButtonPressed;
-
-        protected override void DidActivate(bool firstActivation, ActivationType type)
-        {
-            if (type == ActivationType.AddedToHierarchy && firstActivation)
-            {
-                _searchKeyboardGO = Instantiate(Resources.FindObjectsOfTypeAll<UIKeyboard>().First(x => x.name != "CustomUIKeyboard"), rectTransform, false).gameObject;
-
-                Destroy(_searchKeyboardGO.GetComponent<UIKeyboard>());
-                _searchKeyboard = _searchKeyboardGO.AddComponent<CustomUIKeyboard>();
-
-                _searchKeyboard.textKeyWasPressedEvent += delegate (char input) { _inputString += input; UpdateInputText(); };
-                _searchKeyboard.deleteButtonWasPressedEvent += delegate () { _inputString = _inputString.Substring(0, _inputString.Length - 1); UpdateInputText(); };
-                _searchKeyboard.cancelButtonWasPressedEvent += () => { backButtonPressed?.Invoke(); };
-                _searchKeyboard.okButtonWasPressedEvent += () => { searchButtonPressed?.Invoke(_inputString); };
-
-                _inputText = BeatSaberUI.CreateText(rectTransform, "Search...", new Vector2(0f, 22f));
-                _inputText.alignment = TextAlignmentOptions.Center;
-                _inputText.fontSize = 6f;
-
-            }
-            else
-            {
-                _inputString = "";
-                UpdateInputText();
-            }
-
-        }
-
-        void UpdateInputText()
-        {
-            if (_inputText != null)
-            {
-                _inputText.text = _inputString?.ToUpper() ?? "";
-                if (string.IsNullOrEmpty(_inputString))
-                {
-                    _searchKeyboard.OkButtonInteractivity = false;
-                }
-                else
-                {
-                    _searchKeyboard.OkButtonInteractivity = true;
-                }
-            }
-        }
-
-        void ClearInput()
-        {
-            _inputString = "";
-        }
-
-        void Back()
-        {
-            _inputString = "";
-            backButtonPressed?.Invoke();
-        }
-    }
-}

+ 4 - 5
SongBrowserPlugin/UI/ProgressBar.cs

@@ -1,11 +1,10 @@
-using System.Collections.Generic;
+using SongCore.Utilities;
 using System.Collections;
+using System.Collections.Concurrent;
 using TMPro;
 using UnityEngine;
 using UnityEngine.SceneManagement;
 using UnityEngine.UI;
-using SongCore.Utilities;
-
 
 namespace SongBrowser.UI
 {
@@ -20,7 +19,7 @@ namespace SongBrowser.UI
         private TMP_Text _headerText;
         internal Image _loadingBackg;
 
-        private static readonly Vector3 Position = new Vector3(0, -0.85f, 2.5f);
+        private static readonly Vector3 Position = new Vector3(0, 0.0f, 2.5f);
         private static readonly Vector3 Rotation = new Vector3(0, 0, 0);
         private static readonly Vector3 Scale = new Vector3(0.01f, 0.01f, 0.01f);
 
@@ -95,7 +94,7 @@ namespace SongBrowser.UI
             }
         }
 
-        private void SongBrowserFinishedProcessingSongs(Dictionary<string, CustomPreviewBeatmapLevel> arg2)
+        private void SongBrowserFinishedProcessingSongs(ConcurrentDictionary<string, CustomPreviewBeatmapLevel> arg2)
         {
             StopAllCoroutines();
             _showingMessage = false;

+ 7 - 9
SongBrowserPlugin/manifest.json

@@ -1,19 +1,17 @@
 {
+  "$schema": "https://raw.githubusercontent.com/bsmg/BSIPA-MetadataFileSchema/master/Schema.json",
   "author": "Halsafar",
   "description": "Adds sort and filter features to the level selection UI.",
-  "gameVersion": "1.11.0",
+  "gameVersion": "1.12.1",
   "id": "SongBrowser",
   "name": "Song Browser",
-  "version": "6.0.7",
+  "version": "6.1.0",
   "dependsOn": {
-    "SongCore": "^2.9.10",
-    "SongDataCore": "^1.3.4"
+    "SongCore": "^3.0.0",
+    "SongDataCore": "^1.3.4",
+    "BSIPA": "^4.1.2",
+    "BeatSaberMarkupLanguage": "^1.4.0"
   },
-  "features": [
-    "print",
-    "debug",
-    "warn"
-  ],
   "misc": {
     "plugin-hint": "SongBrowser.Plugin"
   }