using BeatSaberMarkupLanguage.Components; using HMUI; using SongBrowser.DataAccess; 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 { public enum UIState { Disabled, Main, SortBy, FilterBy } public class SongBrowserViewController : ViewController { // Named instance } /// /// Hijack the flow coordinator. Have access to all StandardLevel easily. /// public class SongBrowserUI : MonoBehaviour { // Logging public const String Name = "SongBrowserUI"; 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 _sortButtonGroup; private List _filterButtonGroup; private Button _sortByButton; private Button _sortByDisplay; private Button _filterByButton; private Button _filterByDisplay; private Button _randomButton; private Button _clearSortFilterButton; private SimpleDialogPromptViewController _deleteDialog; private Button _deleteButton; private Button _pageUpFastButton; private Button _pageDownFastButton; private RectTransform _ppStatButton; private RectTransform _starStatButton; private RectTransform _njsStatButton; private RectTransform _noteJumpStartBeatOffsetLabel; private IAnnotatedBeatmapLevelCollection _lastLevelCollection; bool _selectingCategory = false; private SongBrowserModel _model; public SongBrowserModel Model { set { _model = value; } get { return _model; } } private bool _uiCreated = false; private UIState _currentUiState = UIState.Disabled; private bool _asyncUpdating = false; /// /// Builds the UI for this plugin. /// public void CreateUI(MainMenuViewController.MenuButton mode) { Logger.Trace("CreateUI()"); // Determine the flow controller to use FlowCoordinator flowCoordinator; if (mode == MainMenuViewController.MenuButton.SoloFreePlay) { Logger.Debug("Entering SOLO mode..."); flowCoordinator = Resources.FindObjectsOfTypeAll().First(); } else if (mode == MainMenuViewController.MenuButton.Party) { Logger.Debug("Entering PARTY mode..."); flowCoordinator = Resources.FindObjectsOfTypeAll().First(); } else { Logger.Info("Entering Unsupported mode..."); return; } Logger.Debug("Done fetching Flow Coordinator for the appropriate mode..."); _beatUi = new DataAccess.BeatSaberUIController(flowCoordinator); _lastLevelCollection = null; // returning to the menu and switching modes could trigger this. if (_uiCreated) { return; } try { // Create a view controller to store all SongBrowser elements if (_viewController) { UnityEngine.GameObject.Destroy(_viewController); } _viewController = BeatSaberUI.CreateCurvedViewController("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(_beatUi.SimpleDialogPromptViewControllerPrefab); this._deleteDialog.name = "DeleteDialogPromptViewController"; this._deleteDialog.gameObject.SetActive(false); // create song browser main ui CreateOuterUi(); CreateSortButtons(); CreateFilterButtons(); CreateDeleteButton(); CreateFastPageButtons(); this.InstallHandlers(); this.ModifySongStatsPanel(); this.ResizeSongUI(); _uiCreated = true; RefreshSortButtonUI(); Logger.Debug("Done Creating UI..."); } catch (Exception e) { Logger.Exception("Exception during CreateUI: ", e); } } /// /// Create the outer ui. /// private void CreateOuterUi() { Logger.Debug("Creating outer UI..."); float clearButtonX = -72.5f; float clearButtonY = CLEAR_BUTTON_Y; float buttonY = BUTTON_ROW_Y; float buttonHeight = 5.0f; float sortByButtonX = -62.5f + buttonHeight; float outerButtonFontSize = 3.0f; float displayButtonFontSize = 2.5f; float outerButtonWidth = 24.0f; float randomButtonWidth = 10.0f; // clear button _clearSortFilterButton = _viewController.CreateIconButton( "ClearSortAndFilterButton", "PracticeButton", new Vector2(clearButtonX, clearButtonY), new Vector2(randomButtonWidth, randomButtonWidth), () => { 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; Logger.Debug("Creating Sort By..."); _sortByButton = _viewController.CreateUIButton("sortBy", "PracticeButton", new Vector2(curX, buttonY), new Vector2(outerButtonWidth, buttonHeight), () => { RefreshOuterUIState(UIState.SortBy); }, "Sort By"); _sortByButton.SetButtonTextSize(outerButtonFontSize); _sortByButton.ToggleWordWrapping(false); curX += outerButtonWidth; 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); curX += outerButtonWidth; // create FilterBy button and its display Logger.Debug("Creating Filter By..."); _filterByButton = _viewController.CreateUIButton("filterBy", "PracticeButton", new Vector2(curX, buttonY), new Vector2(outerButtonWidth, buttonHeight), () => { RefreshOuterUIState(UIState.FilterBy); }, "Filter By"); _filterByButton.SetButtonTextSize(outerButtonFontSize); _filterByButton.ToggleWordWrapping(false); curX += outerButtonWidth; 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(); ProcessSongList(); RefreshSongUI(); }, ""); _filterByDisplay.SetButtonTextSize(displayButtonFontSize); _filterByDisplay.ToggleWordWrapping(false); // random button Logger.Debug("Creating Random Button..."); _randomButton = _viewController.CreateIconButton("randomButton", "PracticeButton", new Vector2(curX + (outerButtonWidth / 2.0f) + (randomButtonWidth / 4.0f), clearButtonY), new Vector2(randomButtonWidth, randomButtonWidth), () => { OnSortButtonClickEvent(SongSortMode.Random); }, Base64Sprites.RandomIcon); _randomButton.SetButtonBackgroundActive(false); } /// /// Create the sort button ribbon /// private void CreateSortButtons() { Logger.Debug("Create sort buttons..."); float sortButtonFontSize = 2.0f; float sortButtonX = -63.0f; float sortButtonWidth = 12.0f; float buttonSpacing = 0.25f; float buttonY = BUTTON_ROW_Y; float buttonHeight = 5.0f; string[] sortButtonNames = new string[] { "Title", "Author", "Newest", "#Plays", "PP", "Stars", "UpVotes", "Rating", "Heat" }; SongSortMode[] sortModes = new SongSortMode[] { SongSortMode.Default, SongSortMode.Author, SongSortMode.Newest, SongSortMode.YourPlayCount, SongSortMode.PP, SongSortMode.Stars, SongSortMode.UpVotes, SongSortMode.Rating, SongSortMode.Heat }; _sortButtonGroup = new List(); for (int i = 0; i < sortButtonNames.Length; i++) { float curButtonX = sortButtonX + (sortButtonWidth * i) + (buttonSpacing * i); SongSortButton sortButton = new SongSortButton(); sortButton.SortMode = sortModes[i]; sortButton.Button = _viewController.CreateUIButton(String.Format("Sort{0}Button", sortButton.SortMode), "PracticeButton", new Vector2(curButtonX, buttonY), new Vector2(sortButtonWidth, buttonHeight), () => { OnSortButtonClickEvent(sortButton.SortMode); RefreshOuterUIState(UIState.Main); }, sortButtonNames[i]); sortButton.Button.SetButtonTextSize(sortButtonFontSize); sortButton.Button.ToggleWordWrapping(false); _sortButtonGroup.Add(sortButton); } } /// /// Create the filter by buttons /// private void CreateFilterButtons() { Logger.Debug("Creating filter buttons..."); float filterButtonFontSize = 2.25f; float filterButtonX = -63.0f; float filterButtonWidth = 14.25f; float buttonSpacing = 0.5f; float buttonY = BUTTON_ROW_Y; float buttonHeight = 5.0f; string[] filterButtonNames = new string[] { "Search", "Ranked", "Unranked" }; SongFilterMode[] filterModes = new SongFilterMode[] { SongFilterMode.Search, SongFilterMode.Ranked, SongFilterMode.Unranked }; _filterButtonGroup = new List(); for (int i = 0; i < filterButtonNames.Length; i++) { float curButtonX = filterButtonX + (filterButtonWidth * i) + (buttonSpacing * i); SongFilterButton filterButton = new SongFilterButton(); filterButton.FilterMode = filterModes[i]; filterButton.Button = _viewController.CreateUIButton(String.Format("Filter{0}Button", filterButton.FilterMode), "PracticeButton", new Vector2(curButtonX, buttonY), new Vector2(filterButtonWidth, buttonHeight), () => { OnFilterButtonClickEvent(filterButton.FilterMode); RefreshOuterUIState(UIState.Main); }, filterButtonNames[i]); filterButton.Button.SetButtonTextSize(filterButtonFontSize); filterButton.Button.ToggleWordWrapping(false); _filterButtonGroup.Add(filterButton); } } /// /// Create the fast page up and down buttons /// private void CreateFastPageButtons() { Logger.Debug("Creating fast scroll button..."); _pageUpFastButton = BeatSaberUI.CreateIconButton("PageUpFast", _beatUi.LevelCollectionNavigationController.transform as RectTransform, "PracticeButton", new Vector2(0.75f, 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.LevelCollectionNavigationController.transform as RectTransform, "PracticeButton", new Vector2(0.75f, -24f), new Vector2(10f, 10f), delegate () { this.JumpSongList(1, SEGMENT_PERCENT); }, Base64Sprites.DoubleArrow); _pageDownFastButton.SetButtonBackgroundActive(false); } /// /// Create the delete button in the play button container /// private void CreateDeleteButton() { // Create 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);*/ } /// /// Resize the stats panel to fit more stats. /// private void ModifySongStatsPanel() { // modify stat panel, inject extra row of stats Logger.Debug("Resizing Stats Panel..."); var statsPanel = _beatUi.StandardLevelDetailView.GetPrivateField("_levelParamsPanel"); (statsPanel.transform as RectTransform).Translate(0, 0.05f, 0); _ppStatButton = BeatSaberUI.CreateStatIcon("PPStatLabel", statsPanel.GetComponentsInChildren().First(x => x.name == "NPS"), statsPanel.transform, Base64Sprites.GraphIcon, "PP Value"); _starStatButton = BeatSaberUI.CreateStatIcon("StarStatLabel", statsPanel.GetComponentsInChildren().First(x => x.name == "NotesCount"), statsPanel.transform, Base64Sprites.StarFullIcon, "Star Difficulty Rating"); _njsStatButton = BeatSaberUI.CreateStatIcon("NoteJumpSpeedLabel", statsPanel.GetComponentsInChildren().First(x => x.name == "ObstaclesCount"), statsPanel.transform, Base64Sprites.SpeedIcon, "Note Jump Speed"); _noteJumpStartBeatOffsetLabel = BeatSaberUI.CreateStatIcon("NoteJumpStartBeatOffsetLabel", statsPanel.GetComponentsInChildren().First(x => x.name == "BombsCount"), statsPanel.transform, Base64Sprites.NoteStartOffsetIcon, "Note Jump Start Beat Offset"); } /// /// Resize some of the song table elements. /// public void ResizeSongUI() { // shrink play button container //RectTransform playButtonsRect = Resources.FindObjectsOfTypeAll().First(x => x.name == "ActionButtons"); //playButtonsRect.localScale = new Vector3(0.825f, 0.825f, 0.825f); } /// /// Add our handlers into BeatSaber. /// private void InstallHandlers() { // level collection, level, difficulty handlers, characteristics TableView tableView = ReflectionUtil.GetPrivateField(_beatUi.LevelCollectionTableView, "_tableView"); // update stats _beatUi.LevelCollectionViewController.didSelectLevelEvent -= OnDidSelectLevelEvent; _beatUi.LevelCollectionViewController.didSelectLevelEvent += OnDidSelectLevelEvent; _beatUi.LevelDetailViewController.didChangeContentEvent -= OnDidPresentContentEvent; _beatUi.LevelDetailViewController.didChangeContentEvent += OnDidPresentContentEvent; _beatUi.LevelDetailViewController.didChangeDifficultyBeatmapEvent -= OnDidChangeDifficultyEvent; _beatUi.LevelDetailViewController.didChangeDifficultyBeatmapEvent += OnDidChangeDifficultyEvent; // update our view of the game state _beatUi.LevelFilteringNavigationController.didSelectAnnotatedBeatmapLevelCollectionEvent -= _levelFilteringNavController_didSelectAnnotatedBeatmapLevelCollectionEvent; _beatUi.LevelFilteringNavigationController.didSelectAnnotatedBeatmapLevelCollectionEvent += _levelFilteringNavController_didSelectAnnotatedBeatmapLevelCollectionEvent; _beatUi.AnnotatedBeatmapLevelCollectionsViewController.didSelectAnnotatedBeatmapLevelCollectionEvent -= handleDidSelectAnnotatedBeatmapLevelCollection; _beatUi.AnnotatedBeatmapLevelCollectionsViewController.didSelectAnnotatedBeatmapLevelCollectionEvent += handleDidSelectAnnotatedBeatmapLevelCollection; // Respond to characteristics changes _beatUi.BeatmapCharacteristicSelectionViewController.didSelectBeatmapCharacteristicEvent -= OnDidSelectBeatmapCharacteristic; _beatUi.BeatmapCharacteristicSelectionViewController.didSelectBeatmapCharacteristicEvent += OnDidSelectBeatmapCharacteristic; // make sure the quick scroll buttons don't desync with regular scrolling _beatUi.TableViewPageDownButton.onClick.AddListener(delegate () { StartCoroutine(RefreshQuickScrollButtonsAsync()); }); _beatUi.TableViewPageUpButton.onClick.AddListener(delegate () { StartCoroutine(RefreshQuickScrollButtonsAsync()); }); } /// /// Waits for the song UI to be available before trying to update. /// /// public IEnumerator AsyncWaitForSongUIUpdate() { if (_asyncUpdating) { yield break; } if (!_uiCreated) { yield break; } if (!_model.SortWasMissingData) { yield break; } _asyncUpdating = true; while (_beatUi.LevelSelectionNavigationController.GetPrivateField("_isInTransition") || _beatUi.LevelDetailViewController.GetPrivateField("_isInTransition") || !_beatUi.LevelSelectionNavigationController.isInViewControllerHierarchy || !_beatUi.LevelDetailViewController.isInViewControllerHierarchy || !_beatUi.LevelSelectionNavigationController.isActiveAndEnabled || !_beatUi.LevelDetailViewController.isActiveAndEnabled) { yield return null; } //yield return new WaitForEndOfFrame(); if (_model.Settings.sortMode.NeedsScoreSaberData() && SongDataCore.Plugin.Songs.IsDataAvailable()) { ProcessSongList(); RefreshSongUI(); } _asyncUpdating = false; } /// /// Helper to reduce code duplication... /// public void RefreshSongUI(bool scrollToLevel = true) { if (!_uiCreated) { return; } RefreshSongList(); RefreshSortButtonUI(); if (!scrollToLevel) { _beatUi.ScrollToLevelByRow(0); } RefreshQuickScrollButtons(); RefreshCurrentSelectionDisplay(); } /// /// External helper. /// public void ProcessSongList() { if (!_uiCreated) { return; } this._model.ProcessSongList(_lastLevelCollection, _beatUi.LevelSelectionNavigationController); } /// /// Helper for common filter cancellation logic. /// public void CancelFilter() { Logger.Debug($"Cancelling filter, levelCollection {_lastLevelCollection}"); _model.Settings.filterMode = SongFilterMode.None; GameObject _noDataGO = _beatUi.LevelCollectionViewController.GetPrivateField("_noDataInfoGO"); string _headerText = _beatUi.LevelCollectionTableView.GetPrivateField("_headerText"); Sprite _headerSprite = _beatUi.LevelCollectionTableView.GetPrivateField("_headerSprite"); IBeatmapLevelCollection levelCollection = _beatUi.GetCurrentSelectedAnnotatedBeatmapLevelCollection().beatmapLevelCollection; _beatUi.LevelCollectionViewController.SetData(levelCollection, _headerText, _headerSprite, false, _noDataGO); } /// /// Playlists (fancy name for AnnotatedBeatmapLevelCollection) /// /// public virtual void handleDidSelectAnnotatedBeatmapLevelCollection(IAnnotatedBeatmapLevelCollection annotatedBeatmapLevelCollection) { Logger.Trace("handleDidSelectAnnotatedBeatmapLevelCollection()"); _lastLevelCollection = annotatedBeatmapLevelCollection; Model.Settings.currentLevelCategoryName = _beatUi.LevelFilteringNavigationController.selectedLevelCategory.ToString(); Model.Settings.Save(); Logger.Debug("AnnotatedBeatmapLevelCollection, Selected Level Collection={0}", _lastLevelCollection); } /// /// Handler for level collection selection, controller. /// Sets the current level collection into the model and updates. /// /// /// /// /// private void _levelFilteringNavController_didSelectAnnotatedBeatmapLevelCollectionEvent(LevelFilteringNavigationController arg1, IAnnotatedBeatmapLevelCollection arg2, GameObject arg3, BeatmapCharacteristicSO arg4) { Logger.Trace("_levelFilteringNavController_didSelectAnnotatedBeatmapLevelCollectionEvent(levelCollection={0})", arg2); if (arg2 == null) { // Probably means we transitioned between Music Packs and Playlists arg2 = _beatUi.GetCurrentSelectedAnnotatedBeatmapLevelCollection(); if (arg2 == null) { Logger.Warning("Nothing selected. This is likely an error."); return; } } Logger.Debug("Selected Level Collection={0}", arg2); // Do something about preview level packs, they can't be used past this point if (arg2 as PreviewBeatmapLevelPackSO) { Logger.Info("Hiding SongBrowser, previewing a song pack."); Hide(); return; } Show(); // category transition, just record the new collection if (_selectingCategory) { Logger.Info("Transitioning level category"); _lastLevelCollection = arg2; StartCoroutine(RefreshSongListEndOfFrame()); return; } // Skip the first time - prevents a bunch of reload content spam if (_lastLevelCollection == null) { return; } SelectLevelCollection(arg2); } /// /// Logic for selecting a level collection. /// /// public void SelectLevelCollection(IAnnotatedBeatmapLevelCollection levelCollection) { try { if (levelCollection == null) { Logger.Debug("No level collection selected..."); return; } // store the real level collection if (levelCollection.collectionName != SongBrowserModel.FilteredSongsCollectionName && _lastLevelCollection != null) { Logger.Debug("Recording levelCollection: {0}", levelCollection.collectionName); _lastLevelCollection = levelCollection; Model.Settings.currentLevelCategoryName = _beatUi.LevelFilteringNavigationController.selectedLevelCategory.ToString(); } // reset level selection _model.LastSelectedLevelId = null; // save level collection this._model.Settings.currentLevelCollectionName = levelCollection.collectionName; this._model.Settings.Save(); StartCoroutine(ProcessSongListEndOfFrame()); } catch (Exception e) { Logger.Exception("Exception handling SelectLevelCollection...", e); } } /// /// End of frame update the song list, the game seems to stomp on us sometimes otherwise /// TODO - Might not be nice to other plugins /// /// public IEnumerator ProcessSongListEndOfFrame() { yield return new WaitForEndOfFrame(); ProcessSongList(); RefreshSongUI(); } public IEnumerator RefreshSongListEndOfFrame() { yield return new WaitForEndOfFrame(); RefreshSongUI(); } /// /// Remove all filters, update song list, save. /// private void OnClearButtonClickEvent() { Logger.Debug("Clearing all sorts and filters."); _model.Settings.sortMode = SongSortMode.Original; _model.Settings.invertSortResults = false; _model.Settings.filterMode = SongFilterMode.None; _model.Settings.Save(); CancelFilter(); ProcessSongList(); RefreshSongUI(); } /// /// Sort button clicked. /// private void OnSortButtonClickEvent(SongSortMode sortMode) { Logger.Debug("Sort button - {0} - pressed.", sortMode.ToString()); if ((sortMode.NeedsScoreSaberData() && !SongDataCore.Plugin.Songs.IsDataAvailable())) { Logger.Info("Data for sort type is not available."); return; } // Clear current selected level id so our song list jumps to the start _model.LastSelectedLevelId = null; if (_model.Settings.sortMode == sortMode) { _model.ToggleInverting(); } _model.Settings.sortMode = sortMode; // update the seed if (_model.Settings.sortMode == SongSortMode.Random) { _model.Settings.randomSongSeed = Guid.NewGuid().GetHashCode(); } _model.Settings.Save(); ProcessSongList(); RefreshSongUI(); } /// /// Handle filter button logic. Some filters have sub menus that need special logic. /// /// private void OnFilterButtonClickEvent(SongFilterMode mode) { Logger.Debug($"FilterButton {mode} clicked."); var curCollection = _beatUi.GetCurrentSelectedAnnotatedBeatmapLevelCollection(); if (_lastLevelCollection == null || (curCollection != null && curCollection.collectionName != SongBrowserModel.FilteredSongsCollectionName && curCollection.collectionName != SongBrowserModel.PlaylistSongsCollectionName)) { _lastLevelCollection = _beatUi.GetCurrentSelectedAnnotatedBeatmapLevelCollection(); } if (mode == SongFilterMode.Favorites) { _beatUi.SelectLevelCategory(SelectLevelCategoryViewController.LevelCategory.Favorites.ToString()); } else { GameObject _noDataGO = _beatUi.LevelCollectionViewController.GetPrivateField("_noDataInfoGO"); string _headerText = _beatUi.LevelCollectionTableView.GetPrivateField("_headerText"); Sprite _headerSprite = _beatUi.LevelCollectionTableView.GetPrivateField("_headerSprite"); IBeatmapLevelCollection levelCollection = _beatUi.GetCurrentSelectedAnnotatedBeatmapLevelCollection().beatmapLevelCollection; _beatUi.LevelCollectionViewController.SetData(levelCollection, _headerText, _headerSprite, false, _noDataGO); } // If selecting the same filter, cancel if (_model.Settings.filterMode == mode) { _model.Settings.filterMode = SongFilterMode.None; } else { _model.Settings.filterMode = mode; } switch (mode) { case SongFilterMode.Search: OnSearchButtonClickEvent(); break; default: _model.Settings.Save(); ProcessSongList(); RefreshSongUI(); break; } } /// /// Display the keyboard. /// /// private void OnSearchButtonClickEvent() { Logger.Debug("Filter button - {0} - pressed.", SongFilterMode.Search.ToString()); this.ShowSearchKeyboard(); } /// /// Adjust UI based on level selected. /// Various ways of detecting if a level is not properly selected. Seems most hit the first one. /// private void OnDidSelectLevelEvent(LevelCollectionViewController view, IPreviewBeatmapLevel level) { try { Logger.Trace("OnDidSelectLevelEvent()"); if (level == null) { Logger.Debug("No level selected?"); return; } if (_model.Settings == null) { Logger.Debug("Settings not instantiated yet?"); return; } _model.LastSelectedLevelId = level.levelID; HandleDidSelectLevelRow(level); } catch (Exception e) { Logger.Exception("Exception selecting song:", e); } } /// /// Switching one-saber modes for example. /// /// /// private void OnDidSelectBeatmapCharacteristic(BeatmapCharacteristicSegmentedControlController view, BeatmapCharacteristicSO bc) { try { Logger.Trace("OnDidSelectBeatmapCharacteristic({0})", bc.compoundIdPartName); _model.CurrentBeatmapCharacteristicSO = bc; if (_beatUi.StandardLevelDetailView != null) { RefreshScoreSaberData(_beatUi.StandardLevelDetailView.selectedDifficultyBeatmap.level); RefreshNoteJumpSpeed(_beatUi.StandardLevelDetailView.selectedDifficultyBeatmap.noteJumpMovementSpeed, _beatUi.StandardLevelDetailView.selectedDifficultyBeatmap.noteJumpStartBeatOffset); } } catch (Exception e) { Logger.Exception(e); } } /// /// Handle difficulty level selection. /// private void OnDidChangeDifficultyEvent(StandardLevelDetailViewController view, IDifficultyBeatmap beatmap) { Logger.Trace("OnDidChangeDifficultyEvent({0})", beatmap); if (view.selectedDifficultyBeatmap == null) { return; } if (_deleteButton != null) { _deleteButton.interactable = (view.selectedDifficultyBeatmap.level.levelID.Length >= 32); } RefreshScoreSaberData(view.selectedDifficultyBeatmap.level); RefreshNoteJumpSpeed(beatmap.noteJumpMovementSpeed, beatmap.noteJumpStartBeatOffset); } /// /// BeatSaber finished loading content. This is when the difficulty is finally updated. /// /// /// private void OnDidPresentContentEvent(StandardLevelDetailViewController view, StandardLevelDetailViewController.ContentType type) { 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; } if (_deleteButton != null) { _deleteButton.interactable = (_beatUi.LevelDetailViewController.selectedDifficultyBeatmap.level.levelID.Length >= 32); } RefreshScoreSaberData(view.selectedDifficultyBeatmap.level); RefreshNoteJumpSpeed(view.selectedDifficultyBeatmap.noteJumpMovementSpeed, view.selectedDifficultyBeatmap.noteJumpStartBeatOffset); } /// /// Refresh stats panel. /// /// private void HandleDidSelectLevelRow(IPreviewBeatmapLevel level) { Logger.Trace("HandleDidSelectLevelRow({0})", level); if (_deleteButton != null) { _deleteButton.interactable = (level.levelID.Length >= 32); } RefreshQuickScrollButtons(); } /// /// Pop up a delete dialog. /// private void HandleDeleteSelectedLevel() { IBeatmapLevel level = _beatUi.LevelDetailViewController.selectedDifficultyBeatmap.level; _deleteDialog.Init("Delete song", $"Do you really want to delete \"{ level.songName} {level.songSubName}\"?", "Delete", "Cancel", (selectedButton) => { _beatUi.LevelSelectionFlowCoordinator.InvokePrivateMethod("DismissViewController", new object[] { _deleteDialog, null, false }); if (selectedButton == 0) { try { // determine the index we are deleting so we can keep the cursor near the same spot after // the header counts as an index, so if the index came from the level array we have to add 1. var levelsTableView = _beatUi.LevelCollectionTableView; List levels = _beatUi.GetCurrentLevelCollectionLevels().ToList(); int selectedIndex = levels.FindIndex(x => x.levelID == _beatUi.StandardLevelDetailView.selectedDifficultyBeatmap.level.levelID); if (selectedIndex > -1) { var song = SongCore.Loader.CustomLevels.First(x => x.Value.levelID == _beatUi.LevelDetailViewController.selectedDifficultyBeatmap.level.levelID).Value; Logger.Info($"Deleting song: {song.customLevelPath}"); SongCore.Loader.Instance.DeleteSong(song.customLevelPath); this._model.RemoveSongFromLevelCollection(_beatUi.GetCurrentSelectedAnnotatedBeatmapLevelCollection(), _beatUi.LevelDetailViewController.selectedDifficultyBeatmap.level.levelID); int removedLevels = levels.RemoveAll(x => x.levelID == _beatUi.StandardLevelDetailView.selectedDifficultyBeatmap.level.levelID); Logger.Info("Removed " + removedLevels + " level(s) from song list!"); this.UpdateLevelDataModel(); // if we have a song to select at the same index, set the last selected level id, UI updates takes care of the rest. if (selectedIndex < levels.Count) { if (levels[selectedIndex].levelID != null) { _model.LastSelectedLevelId = levels[selectedIndex].levelID; } } this.RefreshSongList(); } } catch (Exception e) { Logger.Error("Unable to delete song! Exception: " + e); } } }); _beatUi.LevelSelectionFlowCoordinator.InvokePrivateMethod("PresentViewController", new object[] { _deleteDialog, null, false }); } /// /// Display the search keyboard /// void ShowSearchKeyboard() { var modalKbTag = new BeatSaberMarkupLanguage.Tags.ModalKeyboardTag(); var modalKbView = modalKbTag.CreateObject(_beatUi.LevelSelectionNavigationController.rectTransform); modalKbView.gameObject.SetActive(true); var modalKb = modalKbView.GetComponent(); modalKb.gameObject.SetActive(true); modalKb.keyboard.EnterPressed += SearchViewControllerSearchButtonPressed; modalKb.modalView.Show(true, true); } /// /// Handle search. /// /// private void SearchViewControllerSearchButtonPressed(string searchFor) { Logger.Debug("Searching for \"{0}\"...", searchFor); _model.Settings.filterMode = SongFilterMode.Search; _model.Settings.searchTerms.Insert(0, searchFor); _model.Settings.Save(); _model.LastSelectedLevelId = null; ProcessSongList(); RefreshSongUI(); } /// /// Make big jumps in the song list. /// /// private void JumpSongList(int numJumps, float segmentPercent) { var levels = _beatUi.GetCurrentLevelCollectionLevels(); if (levels == null) { return; } int totalSize = levels.Count(); int segmentSize = (int)(totalSize * segmentPercent); // Jump at least one scree size. if (segmentSize < LIST_ITEMS_VISIBLE_AT_ONCE) { segmentSize = LIST_ITEMS_VISIBLE_AT_ONCE; } int currentRow = _beatUi.LevelCollectionTableView.GetPrivateField("_selectedRow"); int jumpDirection = Math.Sign(numJumps); int newRow = currentRow + (jumpDirection * segmentSize); if (newRow <= 0) { newRow = 0; } else if (newRow >= totalSize) { newRow = totalSize - 1; } Logger.Debug("jumpDirection: {0}, newRow: {1}", jumpDirection, newRow); _beatUi.SelectAndScrollToLevel(levels[newRow].levelID); RefreshQuickScrollButtons(); } /// /// Update GUI elements that show score saber data. /// public void RefreshScoreSaberData(IPreviewBeatmapLevel level) { Logger.Trace("RefreshScoreSaberData({0})", level.levelID); if (!SongDataCore.Plugin.Songs.IsDataAvailable()) { return; } BeatmapDifficulty difficulty = _beatUi.LevelDifficultyViewController.selectedDifficulty; string difficultyString = difficulty.ToString(); if (difficultyString.Equals("ExpertPlus")) { difficultyString = "Expert+"; } Logger.Debug(difficultyString); // Check if we have data for this song Logger.Debug("Checking if have info for song {0}", level.songName); var hash = SongBrowserModel.GetSongHash(level.levelID); if (SongDataCore.Plugin.Songs.Data.Songs.ContainsKey(hash)) { Logger.Debug("Checking if have difficulty for song {0} difficulty {1}", level.songName, difficultyString); BeatStarSong scoreSaberSong = SongDataCore.Plugin.Songs.Data.Songs[hash]; BeatStarSongDifficultyStats scoreSaberSongDifficulty = scoreSaberSong.diffs.FirstOrDefault(x => String.Equals(x.diff, difficultyString)); if (scoreSaberSongDifficulty != null) { Logger.Debug("Display pp for song."); double pp = scoreSaberSongDifficulty.pp; double star = scoreSaberSongDifficulty.star; BeatSaberUI.SetStatButtonText(_ppStatButton, String.Format("{0:0.#}", pp)); BeatSaberUI.SetStatButtonText(_starStatButton, String.Format("{0:0.#}", star)); } else { BeatSaberUI.SetStatButtonText(_ppStatButton, "NA"); BeatSaberUI.SetStatButtonText(_starStatButton, "NA"); } } else { BeatSaberUI.SetStatButtonText(_ppStatButton, "NA"); BeatSaberUI.SetStatButtonText(_starStatButton, "NA"); } Logger.Debug("Done refreshing score saber stats."); } /// /// Helper to refresh the NJS widget. /// /// private void RefreshNoteJumpSpeed(float noteJumpMovementSpeed, float noteJumpStartBeatOffset) { BeatSaberUI.SetStatButtonText(_njsStatButton, String.Format("{0}", noteJumpMovementSpeed)); BeatSaberUI.SetStatButtonText(_noteJumpStartBeatOffsetLabel, String.Format("{0}", noteJumpStartBeatOffset)); } /// /// Update interactive state of the quick scroll buttons. /// private void RefreshQuickScrollButtons() { if (!_uiCreated) { return; } _pageUpFastButton.interactable = _beatUi.TableViewPageUpButton.interactable; _pageUpFastButton.gameObject.SetActive(_beatUi.TableViewPageUpButton.IsActive()); _pageDownFastButton.interactable = _beatUi.TableViewPageDownButton.interactable; _pageDownFastButton.gameObject.SetActive(_beatUi.TableViewPageDownButton.IsActive()); } /// /// TODO - evaluate this sillyness... /// /// public IEnumerator RefreshQuickScrollButtonsAsync() { yield return new WaitForEndOfFrame(); RefreshQuickScrollButtons(); } /// /// Show the UI. /// public void Show() { Logger.Trace("Show SongBrowserUI()"); this.SetVisibility(true); } /// /// Hide the UI. /// public void Hide() { Logger.Trace("Hide SongBrowserUI()"); this.SetVisibility(false); } /// /// Handle showing or hiding UI logic. /// /// private void SetVisibility(bool visible) { // UI not created, nothing visible to hide... if (!_uiCreated) { return; } _ppStatButton?.gameObject.SetActive(visible); _starStatButton?.gameObject.SetActive(visible); _njsStatButton?.gameObject.SetActive(visible); RefreshOuterUIState(visible == true ? UIState.Main : UIState.Disabled); _deleteButton?.gameObject.SetActive(visible); _pageUpFastButton?.gameObject.SetActive(visible); _pageDownFastButton?.gameObject.SetActive(visible); } /// /// Update the top UI state. /// Hides the outer ui, sort, and filter buttons depending on the state. /// private void RefreshOuterUIState(UIState state) { bool sortButtons = false; bool filterButtons = false; bool outerButtons = false; bool clearButton = true; if (state == UIState.SortBy) { sortButtons = true; } else if (state == UIState.FilterBy) { filterButtons = true; } else if (state == UIState.Main) { outerButtons = true; } else { clearButton = false; } _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); RefreshCurrentSelectionDisplay(); _currentUiState = state; } /// /// Adjust the text field of the sort by and filter by displays. /// private void RefreshCurrentSelectionDisplay() { string sortByDisplay; if (_model.Settings.sortMode == SongSortMode.Default) { sortByDisplay = "Title"; } else { sortByDisplay = _model.Settings.sortMode.ToString(); } _sortByDisplay.SetButtonText(sortByDisplay); if (_model.Settings.filterMode != SongFilterMode.Custom) { // Custom SongFilterMod implies that another mod has modified the text of this button (do not overwrite) _filterByDisplay.SetButtonText(_model.Settings.filterMode.ToString()); } } /// /// Adjust the UI colors. /// public void RefreshSortButtonUI() { if (!_uiCreated) { return; } foreach (SongSortButton sortButton in _sortButtonGroup) { if (sortButton.SortMode.NeedsScoreSaberData() && !SongDataCore.Plugin.Songs.IsDataAvailable()) { sortButton.Button.SetButtonUnderlineColor(Color.gray); } else { sortButton.Button.SetButtonUnderlineColor(Color.white); } if (sortButton.SortMode == _model.Settings.sortMode) { if (this._model.Settings.invertSortResults) { sortButton.Button.SetButtonUnderlineColor(Color.red); } else { sortButton.Button.SetButtonUnderlineColor(Color.green); } } } foreach (SongFilterButton filterButton in _filterButtonGroup) { filterButton.Button.SetButtonUnderlineColor(Color.white); if (filterButton.FilterMode == _model.Settings.filterMode) { filterButton.Button.SetButtonUnderlineColor(Color.green); } } if (this._model.Settings.invertSortResults) { _sortByDisplay.SetButtonUnderlineColor(Color.red); } else { _sortByDisplay.SetButtonUnderlineColor(Color.green); } if (this._model.Settings.filterMode != SongFilterMode.None) { _filterByDisplay.SetButtonUnderlineColor(Color.green); } else { _filterByDisplay.SetButtonUnderlineColor(Color.white); } } /// /// /// public void RefreshSongList() { if (!_uiCreated) { return; } _beatUi.RefreshSongList(_model.LastSelectedLevelId); } /// /// Helper for updating the model (which updates the song list) /// public void UpdateLevelDataModel() { try { Logger.Trace("UpdateLevelDataModel()"); // get a current beatmap characteristic... if (_model.CurrentBeatmapCharacteristicSO == null && _uiCreated) { _model.CurrentBeatmapCharacteristicSO = _beatUi.BeatmapCharacteristicSelectionViewController.GetPrivateField("_selectedBeatmapCharacteristic"); } _model.UpdateLevelRecords(); } catch (Exception e) { Logger.Exception("SongBrowser UI crashed trying to update the internal song lists: ", e); } } /// /// Logic for fixing BeatSaber's level pack selection bugs. /// public bool UpdateLevelCollectionSelection() { if (_uiCreated) { IAnnotatedBeatmapLevelCollection currentSelected = _beatUi.GetCurrentSelectedAnnotatedBeatmapLevelCollection(); Logger.Debug("Updating level collection, current selected level collection: {0}", currentSelected); // select category if (!String.IsNullOrEmpty(_model.Settings.currentLevelCategoryName)) { _selectingCategory = true; _beatUi.SelectLevelCategory(_model.Settings.currentLevelCategoryName); _selectingCategory = false; } // select collection if (String.IsNullOrEmpty(_model.Settings.currentLevelCollectionName)) { if (currentSelected == null && String.IsNullOrEmpty(_model.Settings.currentLevelCategoryName)) { Logger.Debug("No level collection selected, acquiring the first available, likely OST1..."); currentSelected = _beatUi.BeatmapLevelsModel.allLoadedBeatmapLevelPackCollection.beatmapLevelPacks[0]; } } else if (currentSelected == null || (currentSelected.collectionName != _model.Settings.currentLevelCollectionName)) { Logger.Debug("Automatically selecting level collection: {0}", _model.Settings.currentLevelCollectionName); _beatUi.LevelFilteringNavigationController.didSelectAnnotatedBeatmapLevelCollectionEvent -= _levelFilteringNavController_didSelectAnnotatedBeatmapLevelCollectionEvent; _lastLevelCollection = _beatUi.GetLevelCollectionByName(_model.Settings.currentLevelCollectionName); if (_lastLevelCollection as PreviewBeatmapLevelPackSO) { Hide(); } _beatUi.SelectLevelCollection(_model.Settings.currentLevelCollectionName); _beatUi.LevelFilteringNavigationController.didSelectAnnotatedBeatmapLevelCollectionEvent += _levelFilteringNavController_didSelectAnnotatedBeatmapLevelCollectionEvent; } if (_lastLevelCollection == null) { if (currentSelected != null && currentSelected.collectionName != SongBrowserModel.FilteredSongsCollectionName && currentSelected.collectionName != SongBrowserModel.PlaylistSongsCollectionName) { _lastLevelCollection = currentSelected; } } Logger.Debug("Current Level Collection is: {0}", _lastLevelCollection); ProcessSongList(); } return false; } } }