using UnityEngine; using System.Linq; using System; using System.Collections.Generic; using UnityEngine.UI; using HMUI; using VRUI; using SongBrowser.DataAccess; using TMPro; using Logger = SongBrowser.Logging.Logger; using SongBrowser.DataAccess.BeatSaverApi; using System.Collections; using SongCore.Utilities; using SongBrowser.Internals; using CustomUI.BeatSaber; using SongDataCore.ScoreSaber; namespace SongBrowser.UI { public enum UIState { Disabled, Main, SortBy, FilterBy } /// /// 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; // BeatSaber Internal UI structures DataAccess.BeatSaberUIController _beatUi; // New UI Elements 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 Button _addFavoriteButton; private SimpleDialogPromptViewController _deleteDialog; private Button _deleteButton; private Button _pageUpFastButton; private Button _pageDownFastButton; private SearchKeyboardViewController _searchViewController; private PlaylistFlowCoordinator _playListFlowCoordinator; private RectTransform _ppStatButton; private RectTransform _starStatButton; private RectTransform _njsStatButton; private Sprite _currentAddFavoriteButtonSprite; private IBeatmapLevelPack _lastLevelPack; // Model private SongBrowserModel _model; public SongBrowserModel Model { set { _model = value; } } // UI Created private bool _uiCreated = false; private UIState _currentUiState = UIState.Disabled; /// /// Builds the UI for this plugin. /// public void CreateUI(MainMenuViewController.MenuButton mode) { Logger.Trace("CreateUI()"); // Determine the flow controller to use FlowCoordinator flowCoordinator = null; 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.Debug("Entering SOLO CAMPAIGN mode..."); flowCoordinator = Resources.FindObjectsOfTypeAll().First(); return; } _beatUi = new DataAccess.BeatSaberUIController(flowCoordinator); // returning to the menu and switching modes could trigger this. if (_uiCreated) { return; } try { if (_playListFlowCoordinator == null) { _playListFlowCoordinator = UIBuilder.CreateFlowCoordinator("PlaylistFlowCoordinator"); _playListFlowCoordinator.didFinishEvent += HandleDidSelectPlaylist; } // 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(); CreateAddFavoritesButton(); 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 = -32.5f; float clearButtonY = 34.5f; float buttonY = 37f; float buttonHeight = 5.0f; float sortByButtonX = -22.5f + buttonHeight; float outerButtonFontSize = 3.0f; float displayButtonFontSize = 2.5f; float outerButtonWidth = 24.0f; float randomButtonWidth = 8.0f; // clear button _clearSortFilterButton = CreateClearButton(clearButtonX, clearButtonY, buttonHeight, () => { if (_currentUiState == UIState.FilterBy || _currentUiState == UIState.SortBy) { RefreshOuterUIState(UIState.Main); } else { OnClearButtonClickEvent(); } }); // create SortBy button and its display float curX = sortByButtonX; _sortByButton = _beatUi.LevelPackLevelsViewController.CreateUIButton("ApplyButton", new Vector2(curX, buttonY), new Vector2(outerButtonWidth, buttonHeight), () => { RefreshOuterUIState(UIState.SortBy); }, "Sort By"); _sortByButton.SetButtonTextSize(outerButtonFontSize); _sortByButton.ToggleWordWrapping(false); curX += outerButtonWidth; _sortByDisplay = _beatUi.LevelPackLevelsViewController.CreateUIButton("ApplyButton", 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 _filterByButton = _beatUi.LevelPackLevelsViewController.CreateUIButton("ApplyButton", new Vector2(curX, buttonY), new Vector2(outerButtonWidth, buttonHeight), () => { RefreshOuterUIState(UIState.FilterBy); }, "Filter By"); _filterByButton.SetButtonTextSize(outerButtonFontSize); _filterByButton.ToggleWordWrapping(false); curX += outerButtonWidth; _filterByDisplay = _beatUi.LevelPackLevelsViewController.CreateUIButton("ApplyButton", new Vector2(curX, buttonY), new Vector2(outerButtonWidth, buttonHeight), () => { _model.Settings.filterMode = SongFilterMode.None; CancelFilter(); RefreshSongUI(); }, ""); _filterByDisplay.SetButtonTextSize(displayButtonFontSize); _filterByDisplay.ToggleWordWrapping(false); // random button _randomButton = _beatUi.LevelPackLevelsViewController.CreateUIButton("HowToPlayButton", new Vector2(curX + (outerButtonWidth / 2.0f) + (randomButtonWidth / 2.0f), clearButtonY), new Vector2(randomButtonWidth, buttonHeight), () => { OnSortButtonClickEvent(SongSortMode.Random); }, "", Base64Sprites.RandomIcon); _randomButton.GetComponentsInChildren().First(btn => btn.name == "Content").padding = new RectOffset(0, 0, 0, 0); var textRect = _randomButton.GetComponentsInChildren(true).FirstOrDefault(c => c.name == "Text"); if (textRect != null) { UnityEngine.Object.Destroy(textRect.gameObject); } UIBuilder.SetButtonBorderActive(_randomButton, false); } /// /// Create the back button /// /// private Button CreateClearButton(float x, float y, float h, UnityEngine.Events.UnityAction callback) { Button b = _beatUi.LevelPackLevelsViewController.CreateUIButton("HowToPlayButton", new Vector2(x, y), new Vector2(h, h), callback, "", Base64Sprites.XIcon); b.GetComponentsInChildren().First(btn => btn.name == "Content").padding = new RectOffset(1, 1, 0, 0); RectTransform textRect = b.GetComponentsInChildren(true).FirstOrDefault(c => c.name == "Text"); if (textRect != null) { UnityEngine.Object.Destroy(textRect.gameObject); } UIBuilder.SetButtonBorderActive(b, false); return b; } /// /// Create the sort button ribbon /// private void CreateSortButtons() { Logger.Debug("Create sort buttons..."); float sortButtonFontSize = 2.15f; float sortButtonX = -23.0f; float sortButtonWidth = 12.0f; float buttonSpacing = 0.25f; float buttonY = 37f; float buttonHeight = 5.0f; string[] sortButtonNames = new string[] { "Title", "Author", "Newest", "YourPlays", "PP", "Stars", "UpVotes", "PlayCount", "Rating", "Heat" }; SongSortMode[] sortModes = new SongSortMode[] { SongSortMode.Default, SongSortMode.Author, SongSortMode.Newest, SongSortMode.YourPlayCount, SongSortMode.PP, SongSortMode.Stars, SongSortMode.UpVotes, SongSortMode.PlayCount, 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 = _beatUi.LevelPackLevelsViewController.CreateUIButton("ApplyButton", new Vector2(curButtonX, buttonY), new Vector2(sortButtonWidth, buttonHeight), () => { OnSortButtonClickEvent(sortButton.SortMode); RefreshOuterUIState(UIState.Main); }, sortButtonNames[i]); sortButton.Button.SetButtonTextSize(sortButtonFontSize); sortButton.Button.GetComponentsInChildren().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); } } /// /// Create the filter by buttons /// private void CreateFilterButtons() { Logger.Debug("Creating filter buttons..."); float filterButtonFontSize = 2.25f; float filterButtonX = -23.0f; float filterButtonWidth = 12.25f; float buttonSpacing = 0.5f; float buttonY = 37f; float buttonHeight = 5.0f; string[] filterButtonNames = new string[] { "Favorites", "Playlist", "Search", "Ranked", "Unranked" }; SongFilterMode[] filterModes = new SongFilterMode[] { SongFilterMode.Favorites, SongFilterMode.Playlist, 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 = _beatUi.LevelPackLevelsViewController.CreateUIButton("ApplyButton", new Vector2(curButtonX, buttonY), new Vector2(filterButtonWidth, buttonHeight), () => { OnFilterButtonClickEvent(filterButton.FilterMode); RefreshOuterUIState(UIState.Main); }, filterButtonNames[i]); filterButton.Button.SetButtonTextSize(filterButtonFontSize); filterButton.Button.GetComponentsInChildren().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); } } /// /// Create the fast page up and down buttons /// private void CreateFastPageButtons() { Logger.Debug("Creating fast scroll button..."); _pageUpFastButton = Instantiate(_beatUi.TableViewPageUpButton, _beatUi.LevelPackLevelsTableViewRectTransform, 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, 0.25f); (_pageUpFastButton.transform as RectTransform).sizeDelta = new Vector2(8f, 6f); _pageUpFastButton.GetComponentsInChildren().First(x => x.name == "BG").sizeDelta = new Vector2(8f, 6f); _pageUpFastButton.GetComponentsInChildren().First(x => x.name == "Arrow").sprite = Base64Sprites.DoubleArrow; _pageUpFastButton.onClick.AddListener(delegate () { this.JumpSongList(-1, SEGMENT_PERCENT); }); _pageDownFastButton = Instantiate(_beatUi.TableViewPageDownButton, _beatUi.LevelPackLevelsTableViewRectTransform, 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().First(x => x.name == "BG").sizeDelta = new Vector2(8f, 6f); _pageDownFastButton.GetComponentsInChildren().First(x => x.name == "Arrow").sprite = Base64Sprites.DoubleArrow; _pageDownFastButton.onClick.AddListener(delegate () { this.JumpSongList(1, SEGMENT_PERCENT); }); } /// /// Create the +/- favorite button in the play button container. /// private void CreateAddFavoritesButton() { // Create add favorite button Logger.Debug("Creating Add to favorites button..."); _addFavoriteButton = UIBuilder.CreateIconButton(_beatUi.PlayButtons, _beatUi.PracticeButton, Base64Sprites.AddToFavoritesIcon); _addFavoriteButton.onClick.AddListener(delegate () { ToggleSongInPlaylist(); }); } /// /// Create the delete button in the play button container /// private void CreateDeleteButton() { // Create delete button Logger.Debug("Creating delete button..."); _deleteButton = UIBuilder.CreateIconButton(_beatUi.PlayButtons, _beatUi.PracticeButton, Base64Sprites.DeleteIcon); _deleteButton.onClick.AddListener(delegate () { HandleDeleteSelectedLevel(); }); } /// /// Resize the stats panel to fit more stats. /// private void ModifySongStatsPanel() { // modify details view Logger.Debug("Resizing Stats Panel..."); var statsPanel = _beatUi.StandardLevelDetailView.GetPrivateField("_levelParamsPanel"); var statTransforms = statsPanel.GetComponentsInChildren(); var valueTexts = statsPanel.GetComponentsInChildren().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.75f, r.sizeDelta.y * 0.75f); } for (int i = 0; i < valueTexts.Count; i++) { var text = valueTexts[i]; text.fontSize = 3.25f; } // inject our components _ppStatButton = UnityEngine.Object.Instantiate(statTransforms[1], statsPanel.transform, false); UIBuilder.SetStatButtonIcon(_ppStatButton, Base64Sprites.GraphIcon); _starStatButton = UnityEngine.Object.Instantiate(statTransforms[1], statsPanel.transform, false); UIBuilder.SetStatButtonIcon(_starStatButton, Base64Sprites.StarFullIcon); _njsStatButton = UnityEngine.Object.Instantiate(statTransforms[1], statsPanel.transform, false); UIBuilder.SetStatButtonIcon(_njsStatButton, Base64Sprites.SpeedIcon); // shrink title var titleText = _beatUi.LevelDetailViewController.GetComponentsInChildren(true).First(x => x.name == "SongNameText"); titleText.fontSize = 5.0f; } /// /// Resize some of the song table elements. /// public void ResizeSongUI() { // Reposition the table view a bit _beatUi.LevelPackLevelsTableViewRectTransform.anchoredPosition = new Vector2(0f, -2.5f); // Move the page up/down buttons a bit TableView tableView = ReflectionUtil.GetPrivateField(_beatUi.LevelPackLevelsTableView, "_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().First(x => x.name == "PlayContainer"); RectTransform playButtonsRect = playContainerRect.GetComponentsInChildren().First(x => x.name == "PlayButtons"); playButtonsRect.localScale = new Vector3(0.825f, 0.825f, 0.825f); } /// /// Add our handlers into BeatSaber. /// private void InstallHandlers() { // level pack, level, difficulty handlers, characteristics TableView tableView = ReflectionUtil.GetPrivateField(_beatUi.LevelPackLevelsTableView, "_tableView"); _beatUi.LevelPackLevelsViewController.didSelectLevelEvent -= OnDidSelectLevelEvent; _beatUi.LevelPackLevelsViewController.didSelectLevelEvent += OnDidSelectLevelEvent; _beatUi.LevelDetailViewController.didPresentContentEvent -= OnDidPresentContentEvent; _beatUi.LevelDetailViewController.didPresentContentEvent += OnDidPresentContentEvent; _beatUi.LevelDetailViewController.didChangeDifficultyBeatmapEvent -= OnDidChangeDifficultyEvent; _beatUi.LevelDetailViewController.didChangeDifficultyBeatmapEvent += OnDidChangeDifficultyEvent; //_beatUi.LevelPacksTableView.didSelectPackEvent -= _levelPacksTableView_didSelectPackEvent; //_beatUi.LevelPacksTableView.didSelectPackEvent += _levelPacksTableView_didSelectPackEvent; _beatUi.LevelPackViewController.didSelectPackEvent -= _levelPackViewController_didSelectPackEvent; _beatUi.LevelPackViewController.didSelectPackEvent += _levelPackViewController_didSelectPackEvent; _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()); }); // finished level ResultsViewController resultsViewController = _beatUi.LevelSelectionFlowCoordinator.GetPrivateField("_resultsViewController"); resultsViewController.continueButtonPressedEvent += ResultsViewController_continueButtonPressedEvent; } /// /// Helper to reduce code duplication... /// public void RefreshSongUI(bool scrollToLevel=true) { if (!_uiCreated) { return; } RefreshSongList(scrollToLevel); RefreshSortButtonUI(); if (!scrollToLevel) { _beatUi.ScrollToLevelByRow(0); } RefreshQuickScrollButtons(); RefreshCurrentSelectionDisplay(); } /// /// External helper. /// public void ProcessSongList() { if (!_uiCreated) { return; } this._model.ProcessSongList(_beatUi.LevelPackLevelsViewController); } /// /// Helper for common filter cancellation logic. /// public void CancelFilter() { Logger.Debug($"Cancelling filter, levelPack {_lastLevelPack}"); _model.Settings.filterMode = SongFilterMode.None; _beatUi.LevelPackLevelsViewController.SetData(_lastLevelPack); } /// /// Handle updating the level pack selection after returning from a song. /// /// private void ResultsViewController_continueButtonPressedEvent(ResultsViewController obj) { StartCoroutine(this.UpdateLevelPackSelectionEndOfFrame()); } /// /// TODO - evaluate this sillyness... /// /// public IEnumerator UpdateLevelPackSelectionEndOfFrame() { yield return new WaitForEndOfFrame(); try { UpdateLevelPackSelection(); _beatUi.SelectAndScrollToLevel(_beatUi.LevelPackLevelsTableView, _model.LastSelectedLevelId); RefreshQuickScrollButtons(); } catch (Exception e) { Logger.Exception("Exception:", e); } } /// /// Handler for level pack selection. /// /// /// private void _levelPacksTableView_didSelectPackEvent(LevelPacksTableView arg1, IBeatmapLevelPack arg2) { Logger.Trace("_levelPacksTableView_didSelectPackEvent(arg2={0})", arg2); try { RefreshSortButtonUI(); RefreshQuickScrollButtons(); } catch (Exception e) { Logger.Exception("Exception handling didSelectPackEvent...", e); } } /// /// Handler for level pack selection, controller. /// Sets the current level pack into the model and updates. /// /// /// private void _levelPackViewController_didSelectPackEvent(LevelPacksViewController arg1, IBeatmapLevelPack levelPack) { Logger.Trace("_levelPackViewController_didSelectPackEvent(levelPack={0})", levelPack); try { // store the real level pack if (levelPack.packID != SongBrowserModel.FilteredSongsPackId) { _lastLevelPack = levelPack; } // reset level selection _model.LastSelectedLevelId = null; // save level packs this._model.Settings.currentLevelPackId = levelPack.packID; this._model.Settings.Save(); ProcessSongList(); // trickery to handle Downloader playlist level packs // We need to avoid scrolling to a level and then select the header bool scrollToLevel = true; if (levelPack.packID.Contains("Playlist_")) { scrollToLevel = false; } RefreshSongUI(scrollToLevel); } catch (Exception e) { Logger.Exception("Exception handling didSelectPackEvent...", e); } } /// /// 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.ScoreSaber.IsDataAvailable()) || (sortMode.NeedsBeatSaverData() && !SongDataCore.Plugin.BeatSaver.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."); if (_lastLevelPack == null || _beatUi.LevelPackLevelsViewController.levelPack.packID != SongBrowserModel.FilteredSongsPackId) { _lastLevelPack = _beatUi.LevelPackLevelsViewController.levelPack; } if (mode == SongFilterMode.Favorites || mode == SongFilterMode.Playlist) { _beatUi.SelectLevelPack(PluginConfig.CUSTOM_SONG_LEVEL_PACK_ID); } else { _beatUi.LevelPackLevelsViewController.SetData(_lastLevelPack); } // 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.Playlist: OnPlaylistButtonClickEvent(); break; 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(); } /// /// Display the playlist selector. /// /// private void OnPlaylistButtonClickEvent() { Logger.Debug("Filter button - {0} - pressed.", SongFilterMode.Playlist.ToString()); _model.LastSelectedLevelId = null; _playListFlowCoordinator.parentFlowCoordinator = _beatUi.LevelSelectionFlowCoordinator; _beatUi.LevelSelectionFlowCoordinator.InvokePrivateMethod("PresentFlowCoordinator", new object[] { _playListFlowCoordinator, null, false, false }); } /// /// 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(LevelPackLevelsViewController 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) { Logger.Trace("OnDidSelectBeatmapCharacteristic({0}", bc.name); _model.CurrentBeatmapCharacteristicSO = bc; _model.UpdateLevelRecords(); this.RefreshSongList(); } /// /// Handle difficulty level selection. /// private void OnDidChangeDifficultyEvent(StandardLevelDetailViewController view, IDifficultyBeatmap beatmap) { Logger.Trace("OnDidChangeDifficultyEvent({0})", beatmap); if (view.selectedDifficultyBeatmap == null) { return; } _deleteButton.interactable = (view.selectedDifficultyBeatmap.level.levelID.Length >= 32); RefreshScoreSaberData(view.selectedDifficultyBeatmap.level, beatmap.difficulty); RefreshNoteJumpSpeed(beatmap.difficulty); } /// /// BeatSaber finished loading content. This is when the difficulty is finally updated. /// /// /// private void OnDidPresentContentEvent(StandardLevelDetailViewController view, StandardLevelDetailViewController.ContentType type) { Logger.Trace("OnDidPresentContentEvent()"); if (view.selectedDifficultyBeatmap == null) { return; } _deleteButton.interactable = (_beatUi.LevelDetailViewController.selectedDifficultyBeatmap.level.levelID.Length >= 32); RefreshScoreSaberData(view.selectedDifficultyBeatmap.level, view.selectedDifficultyBeatmap.difficulty); RefreshNoteJumpSpeed(view.selectedDifficultyBeatmap.difficulty); } /// /// Refresh stats panel. /// /// private void HandleDidSelectLevelRow(IPreviewBeatmapLevel level) { Logger.Trace("HandleDidSelectLevelRow({0})", level); _deleteButton.interactable = (level.levelID.Length >= 32); RefreshQuickScrollButtons(); RefreshAddFavoriteButton(level.levelID); } /// /// Call Downloader delete. /// private void CallDownloaderDelete() { BeatSaverDownloader.UI.SongListTweaks.Instance.DeletePressed(); } /// /// Pop up a delete dialog. /// private void HandleDeleteSelectedLevel() { bool DownloaderInstalled = CustomHelpers.IsModInstalled("BeatSaverDownloader"); if (DownloaderInstalled) { CallDownloaderDelete(); return; } 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.LevelPackLevelsViewController.GetPrivateField("_levelPackLevelsTableView"); List levels = levelsTableView.GetPrivateField("_pack").beatmapLevelCollection.beatmapLevels.ToList(); int selectedIndex = levels.FindIndex(x => x.levelID == _beatUi.StandardLevelDetailView.selectedDifficultyBeatmap.level.levelID); if (selectedIndex > -1) { var song = new Song(SongCore.Loader.CustomLevels.First(x => x.Value.levelID == _beatUi.LevelDetailViewController.selectedDifficultyBeatmap.level.levelID).Value); SongCore.Loader.Instance.DeleteSong(song.path); this._model.RemoveSongFromLevelPack(_beatUi.GetCurrentSelectedLevelPack(), _beatUi.LevelDetailViewController.selectedDifficultyBeatmap.level.levelID); this.UpdateLevelDataModel(); this.RefreshSongList(); int removedLevels = levels.RemoveAll(x => x.levelID == _beatUi.StandardLevelDetailView.selectedDifficultyBeatmap.level.levelID); Logger.Info("Removed " + removedLevels + " level(s) from song list!"); TableView listTableView = levelsTableView.GetPrivateField("_tableView"); listTableView.ScrollToCellWithIdx(selectedIndex, TableView.ScrollPositionType.Beginning, false); levelsTableView.SetPrivateField("_selectedRow", selectedIndex); listTableView.SelectCellWithIdx(selectedIndex, true); } } catch (Exception e) { Logger.Error("Unable to delete song! Exception: " + e); } } }); _beatUi.LevelSelectionFlowCoordinator.InvokePrivateMethod("PresentViewController", new object[] { _deleteDialog, null, false }); } /// /// Handle selection of a playlist. Show just the songs in the playlist. /// /// private void HandleDidSelectPlaylist(Playlist p) { if (p != null) { Logger.Debug("Showing songs for playlist: {0}", p.playlistTitle); _model.Settings.filterMode = SongFilterMode.Playlist; _model.CurrentPlaylist = p; _model.Settings.Save(); ProcessSongList(); RefreshSongUI(); } else { Logger.Debug("No playlist selected"); } } /// /// Display the search keyboard /// void ShowSearchKeyboard() { if (_searchViewController == null) { _searchViewController = UIBuilder.CreateViewController("SearchKeyboardViewController"); _searchViewController.searchButtonPressed += SearchViewControllerSearchButtonPressed; _searchViewController.backButtonPressed += SearchViewControllerbackButtonPressed; } Logger.Debug("Presenting search keyboard"); _beatUi.LevelSelectionFlowCoordinator.InvokePrivateMethod("PresentViewController", new object[] { _searchViewController, null, false }); } /// /// Handle back button event from search keyboard. /// private void SearchViewControllerbackButtonPressed() { _beatUi.LevelSelectionFlowCoordinator.InvokePrivateMethod("DismissViewController", new object[] { _searchViewController, null, false }); this._model.Settings.filterMode = SongFilterMode.None; this._model.Settings.Save(); RefreshSongUI(); } /// /// Handle search. /// /// 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; _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.GetCurrentLevelPackLevels(); if (levels == null) { return; } int totalSize = _beatUi.GetLevelPackLevelCount(); int segmentSize = (int)(totalSize * segmentPercent); // Jump at least one scree size. if (segmentSize < LIST_ITEMS_VISIBLE_AT_ONCE) { segmentSize = LIST_ITEMS_VISIBLE_AT_ONCE; } TableView tableView = ReflectionUtil.GetPrivateField(_beatUi.LevelPackLevelsTableView, "_tableView"); int currentRow = _beatUi.LevelPackLevelsTableView.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(_beatUi.LevelPackLevelsTableView, levels[newRow].levelID); RefreshQuickScrollButtons(); } /// /// Add/Remove song from favorites depending on if it already exists. /// private void ToggleSongInPlaylist() { IBeatmapLevel songInfo = _beatUi.LevelDetailViewController.selectedDifficultyBeatmap.level; if (_model.CurrentEditingPlaylist != null) { if (_model.CurrentEditingPlaylistLevelIds.Contains(songInfo.levelID)) { Logger.Info("Remove {0} from editing playlist", songInfo.songName); _model.RemoveSongFromEditingPlaylist(songInfo); if (_model.Settings.filterMode == SongFilterMode.Favorites) { ProcessSongList(); this.RefreshSongList(); } } else { Logger.Info("Add {0} to editing playlist", songInfo.songName); _model.AddSongToEditingPlaylist(songInfo); } } RefreshAddFavoriteButton(songInfo.levelID); _model.Settings.Save(); } /// /// Update GUI elements that show score saber data. /// public void RefreshScoreSaberData(IPreviewBeatmapLevel level, BeatmapDifficulty vdifficulty) { Logger.Trace("RefreshScoreSaberData({0})", level.levelID); if (!SongDataCore.Plugin.ScoreSaber.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 = CustomHelpers.GetSongHash(level.levelID); if (SongDataCore.Plugin.ScoreSaber.Data.Songs.ContainsKey(hash)) { Logger.Debug("Checking if have difficulty for song {0} difficulty {1}", level.songName, difficultyString); ScoreSaberSong scoreSaberSong = SongDataCore.Plugin.ScoreSaber.Data.Songs[hash]; ScoreSaberSongDifficultyStats 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; UIBuilder.SetStatButtonText(_ppStatButton, String.Format("{0:0.#}", pp)); UIBuilder.SetStatButtonText(_starStatButton, String.Format("{0:0.#}", star)); } else { UIBuilder.SetStatButtonText(_ppStatButton, "NA"); UIBuilder.SetStatButtonText(_starStatButton, "NA"); } } else { UIBuilder.SetStatButtonText(_ppStatButton, "NA"); UIBuilder.SetStatButtonText(_starStatButton, "NA"); } Logger.Debug("Done refreshing score saber stats."); } /// /// Helper to refresh the NJS widget. /// /// private void RefreshNoteJumpSpeed(BeatmapDifficulty beatmap) { UIBuilder.SetStatButtonText(_njsStatButton, String.Format("{0}", beatmap.NoteJumpMovementSpeed())); } /// /// 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); _addFavoriteButton.gameObject.SetActive(visible); _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 = null; 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()); } } /// /// Helper to quickly refresh add to favorites button /// /// private void RefreshAddFavoriteButton(String levelId) { if (levelId == null) { _currentAddFavoriteButtonSprite = null; } else { if (_model.CurrentEditingPlaylistLevelIds.Contains(levelId)) { _currentAddFavoriteButtonSprite = Base64Sprites.RemoveFromFavoritesIcon; } else { _currentAddFavoriteButtonSprite = Base64Sprites.AddToFavoritesIcon; } } _addFavoriteButton.SetButtonIcon(_currentAddFavoriteButtonSprite); } /// /// Adjust the UI colors. /// public void RefreshSortButtonUI() { if (!_uiCreated) { return; } // So far all we need to refresh is the sort buttons. foreach (SongSortButton sortButton in _sortButtonGroup) { if (sortButton.SortMode.NeedsBeatSaverData() && !SongDataCore.Plugin.BeatSaver.IsDataAvailable()) { UIBuilder.SetButtonBorder(sortButton.Button, Color.gray); } else if (sortButton.SortMode.NeedsScoreSaberData() && !SongDataCore.Plugin.ScoreSaber.IsDataAvailable()) { UIBuilder.SetButtonBorder(sortButton.Button, Color.gray); } else { UIBuilder.SetButtonBorder(sortButton.Button, Color.white); } if (sortButton.SortMode == _model.Settings.sortMode) { if (this._model.Settings.invertSortResults) { UIBuilder.SetButtonBorder(sortButton.Button, Color.red); } else { UIBuilder.SetButtonBorder(sortButton.Button, Color.green); } } } // refresh filter buttons foreach (SongFilterButton filterButton in _filterButtonGroup) { UIBuilder.SetButtonBorder(filterButton.Button, Color.white); if (filterButton.FilterMode == _model.Settings.filterMode) { UIBuilder.SetButtonBorder(filterButton.Button, Color.green); } } if (this._model.Settings.invertSortResults) { UIBuilder.SetButtonBorder(_sortByDisplay, Color.red); } else { UIBuilder.SetButtonBorder(_sortByDisplay, Color.green); } if (this._model.Settings.filterMode != SongFilterMode.None) { UIBuilder.SetButtonBorder(_filterByDisplay, Color.green); } else { UIBuilder.SetButtonBorder(_filterByDisplay, Color.white); } } /// /// /// public void RefreshSongList(bool scrollToLevel = true) { 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 UpdateLevelPackSelection() { if (_uiCreated) { IBeatmapLevelPack currentSelected = _beatUi.GetCurrentSelectedLevelPack(); Logger.Debug("Current selected level pack: {0}", currentSelected); if (String.IsNullOrEmpty(_model.Settings.currentLevelPackId)) { if (currentSelected == null) { Logger.Debug("No level pack selected, acquiring the first available..."); var levelPackCollection = _beatUi.LevelPackViewController.GetPrivateField("_levelPackCollection"); currentSelected = levelPackCollection.beatmapLevelPacks[0]; } } else if (currentSelected == null || (currentSelected.packID != _model.Settings.currentLevelPackId)) { Logger.Debug("Automatically selecting level pack: {0}", _model.Settings.currentLevelPackId); _beatUi.LevelPackViewController.didSelectPackEvent -= _levelPackViewController_didSelectPackEvent; _lastLevelPack = _beatUi.GetLevelPackByPackId(_model.Settings.currentLevelPackId); _beatUi.SelectLevelPack(_model.Settings.currentLevelPackId); _beatUi.LevelPackViewController.didSelectPackEvent += _levelPackViewController_didSelectPackEvent; ProcessSongList(); return true; } } return false; } } }