SongBrowserUI.cs 54 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388
  1. using BeatSaberMarkupLanguage.Components;
  2. using HMUI;
  3. using SongBrowser.DataAccess;
  4. using SongBrowser.Internals;
  5. using SongCore.Utilities;
  6. using SongDataCore.BeatStar;
  7. using System;
  8. using System.Collections;
  9. using System.Collections.Generic;
  10. using System.Linq;
  11. using UnityEngine;
  12. using UnityEngine.UI;
  13. using Logger = SongBrowser.Logging.Logger;
  14. namespace SongBrowser.UI
  15. {
  16. public enum UIState
  17. {
  18. Disabled,
  19. Main,
  20. SortBy,
  21. FilterBy
  22. }
  23. public class SongBrowserViewController : ViewController
  24. {
  25. // Named instance
  26. }
  27. /// <summary>
  28. /// Hijack the flow coordinator. Have access to all StandardLevel easily.
  29. /// </summary>
  30. public class SongBrowserUI : MonoBehaviour
  31. {
  32. // Logging
  33. public const String Name = "SongBrowserUI";
  34. private const float SEGMENT_PERCENT = 0.1f;
  35. private const int LIST_ITEMS_VISIBLE_AT_ONCE = 6;
  36. private const float CLEAR_BUTTON_Y = -32.5f;
  37. private const float BUTTON_ROW_Y = -32.5f;
  38. // BeatSaber Internal UI structures
  39. DataAccess.BeatSaberUIController _beatUi;
  40. // New UI Elements
  41. private SongBrowserViewController _viewController;
  42. private List<SongSortButton> _sortButtonGroup;
  43. private List<SongFilterButton> _filterButtonGroup;
  44. private Button _sortByButton;
  45. private Button _sortByDisplay;
  46. private Button _filterByButton;
  47. private Button _filterByDisplay;
  48. private Button _randomButton;
  49. private Button _clearSortFilterButton;
  50. private SimpleDialogPromptViewController _deleteDialog;
  51. private Button _deleteButton;
  52. private Button _pageUpFastButton;
  53. private Button _pageDownFastButton;
  54. private RectTransform _ppStatButton;
  55. private RectTransform _starStatButton;
  56. private RectTransform _njsStatButton;
  57. private IAnnotatedBeatmapLevelCollection _lastLevelCollection;
  58. private SongBrowserModel _model;
  59. public SongBrowserModel Model
  60. {
  61. set
  62. {
  63. _model = value;
  64. }
  65. get
  66. {
  67. return _model;
  68. }
  69. }
  70. private bool _uiCreated = false;
  71. private UIState _currentUiState = UIState.Disabled;
  72. private bool _asyncUpdating = false;
  73. /// <summary>
  74. /// Builds the UI for this plugin.
  75. /// </summary>
  76. public void CreateUI(MainMenuViewController.MenuButton mode)
  77. {
  78. Logger.Trace("CreateUI()");
  79. // Determine the flow controller to use
  80. FlowCoordinator flowCoordinator;
  81. if (mode == MainMenuViewController.MenuButton.SoloFreePlay)
  82. {
  83. Logger.Debug("Entering SOLO mode...");
  84. flowCoordinator = Resources.FindObjectsOfTypeAll<SoloFreePlayFlowCoordinator>().First();
  85. }
  86. else if (mode == MainMenuViewController.MenuButton.Party)
  87. {
  88. Logger.Debug("Entering PARTY mode...");
  89. flowCoordinator = Resources.FindObjectsOfTypeAll<PartyFreePlayFlowCoordinator>().First();
  90. }
  91. else
  92. {
  93. Logger.Info("Entering Unsupported mode...");
  94. return;
  95. }
  96. Logger.Debug("Done fetching Flow Coordinator for the appropriate mode...");
  97. _beatUi = new DataAccess.BeatSaberUIController(flowCoordinator);
  98. _lastLevelCollection = null;
  99. // returning to the menu and switching modes could trigger this.
  100. if (_uiCreated)
  101. {
  102. return;
  103. }
  104. try
  105. {
  106. // Create a view controller to store all SongBrowser elements
  107. if (_viewController)
  108. {
  109. UnityEngine.GameObject.Destroy(_viewController);
  110. }
  111. _viewController = BeatSaberUI.CreateCurvedViewController<SongBrowserViewController>("SongBrowserViewController", 125.0f);
  112. _viewController.rectTransform.SetParent(_beatUi.LevelCollectionNavigationController.rectTransform, false);
  113. _viewController.rectTransform.anchorMin = new Vector2(0f, 0f);
  114. _viewController.rectTransform.anchorMax = new Vector2(1f, 1f);
  115. _viewController.rectTransform.anchoredPosition = new Vector2(0, 0);
  116. _viewController.rectTransform.sizeDelta = new Vector2(125, 25);
  117. _viewController.gameObject.SetActive(true);
  118. // delete dialog
  119. this._deleteDialog = UnityEngine.Object.Instantiate<SimpleDialogPromptViewController>(_beatUi.SimpleDialogPromptViewControllerPrefab);
  120. this._deleteDialog.name = "DeleteDialogPromptViewController";
  121. this._deleteDialog.gameObject.SetActive(false);
  122. // create song browser main ui
  123. CreateOuterUi();
  124. CreateSortButtons();
  125. CreateFilterButtons();
  126. CreateDeleteButton();
  127. CreateFastPageButtons();
  128. this.InstallHandlers();
  129. this.ModifySongStatsPanel();
  130. this.ResizeSongUI();
  131. _uiCreated = true;
  132. RefreshSortButtonUI();
  133. Logger.Debug("Done Creating UI...");
  134. }
  135. catch (Exception e)
  136. {
  137. Logger.Exception("Exception during CreateUI: ", e);
  138. }
  139. }
  140. /// <summary>
  141. /// Create the outer ui.
  142. /// </summary>
  143. private void CreateOuterUi()
  144. {
  145. Logger.Debug("Creating outer UI...");
  146. float clearButtonX = -72.5f;
  147. float clearButtonY = CLEAR_BUTTON_Y;
  148. float buttonY = BUTTON_ROW_Y;
  149. float buttonHeight = 5.0f;
  150. float sortByButtonX = -62.5f + buttonHeight;
  151. float outerButtonFontSize = 3.0f;
  152. float displayButtonFontSize = 2.5f;
  153. float outerButtonWidth = 24.0f;
  154. float randomButtonWidth = 10.0f;
  155. // clear button
  156. _clearSortFilterButton = _viewController.CreateIconButton(
  157. "ClearSortAndFilterButton",
  158. "PracticeButton",
  159. new Vector2(clearButtonX, clearButtonY),
  160. new Vector2(randomButtonWidth, randomButtonWidth),
  161. () =>
  162. {
  163. if (_currentUiState == UIState.FilterBy || _currentUiState == UIState.SortBy)
  164. {
  165. RefreshOuterUIState(UIState.Main);
  166. }
  167. else
  168. {
  169. OnClearButtonClickEvent();
  170. }
  171. },
  172. Base64Sprites.XIcon);
  173. _clearSortFilterButton.SetButtonBackgroundActive(false);
  174. // create SortBy button and its display
  175. float curX = sortByButtonX;
  176. Logger.Debug("Creating Sort By...");
  177. _sortByButton = _viewController.CreateUIButton("sortBy", "PracticeButton", new Vector2(curX, buttonY), new Vector2(outerButtonWidth, buttonHeight), () =>
  178. {
  179. RefreshOuterUIState(UIState.SortBy);
  180. }, "Sort By");
  181. _sortByButton.SetButtonTextSize(outerButtonFontSize);
  182. _sortByButton.ToggleWordWrapping(false);
  183. curX += outerButtonWidth;
  184. Logger.Debug("Creating Sort By Display...");
  185. _sortByDisplay = _viewController.CreateUIButton("sortByValue", "PracticeButton", new Vector2(curX, buttonY), new Vector2(outerButtonWidth, buttonHeight), () =>
  186. {
  187. OnSortButtonClickEvent(_model.Settings.sortMode);
  188. }, "");
  189. _sortByDisplay.SetButtonTextSize(displayButtonFontSize);
  190. _sortByDisplay.ToggleWordWrapping(false);
  191. _sortByDisplay.SetButtonBackgroundActive(false);
  192. curX += outerButtonWidth;
  193. // create FilterBy button and its display
  194. Logger.Debug("Creating Filter By...");
  195. _filterByButton = _viewController.CreateUIButton("filterBy", "PracticeButton", new Vector2(curX, buttonY), new Vector2(outerButtonWidth, buttonHeight), () =>
  196. {
  197. RefreshOuterUIState(UIState.FilterBy);
  198. }, "Filter By");
  199. _filterByButton.SetButtonTextSize(outerButtonFontSize);
  200. _filterByButton.ToggleWordWrapping(false);
  201. curX += outerButtonWidth;
  202. Logger.Debug("Creating Filter By Display...");
  203. _filterByDisplay = _viewController.CreateUIButton("filterValue", "PracticeButton", new Vector2(curX, buttonY), new Vector2(outerButtonWidth, buttonHeight), () =>
  204. {
  205. _model.Settings.filterMode = SongFilterMode.None;
  206. CancelFilter();
  207. ProcessSongList();
  208. RefreshSongUI();
  209. }, "");
  210. _filterByDisplay.SetButtonTextSize(displayButtonFontSize);
  211. _filterByDisplay.ToggleWordWrapping(false);
  212. _filterByDisplay.SetButtonBackgroundActive(false);
  213. // random button
  214. Logger.Debug("Creating Random Button...");
  215. _randomButton = _viewController.CreateIconButton("randomButton", "PracticeButton", new Vector2(curX + (outerButtonWidth / 2.0f) + (randomButtonWidth / 2.0f), clearButtonY), new Vector2(randomButtonWidth, randomButtonWidth), () =>
  216. {
  217. OnSortButtonClickEvent(SongSortMode.Random);
  218. }, Base64Sprites.RandomIcon);
  219. _randomButton.SetButtonBackgroundActive(false);
  220. }
  221. /// <summary>
  222. /// Create the sort button ribbon
  223. /// </summary>
  224. private void CreateSortButtons()
  225. {
  226. Logger.Debug("Create sort buttons...");
  227. float sortButtonFontSize = 2.0f;
  228. float sortButtonX = -63.0f;
  229. float sortButtonWidth = 12.0f;
  230. float buttonSpacing = 0.25f;
  231. float buttonY = BUTTON_ROW_Y;
  232. float buttonHeight = 5.0f;
  233. string[] sortButtonNames = new string[]
  234. {
  235. "Title", "Author", "Newest", "#Plays", "PP", "Stars", "UpVotes", "Rating", "Heat"
  236. };
  237. SongSortMode[] sortModes = new SongSortMode[]
  238. {
  239. SongSortMode.Default, SongSortMode.Author, SongSortMode.Newest, SongSortMode.YourPlayCount, SongSortMode.PP, SongSortMode.Stars, SongSortMode.UpVotes, SongSortMode.Rating, SongSortMode.Heat
  240. };
  241. _sortButtonGroup = new List<SongSortButton>();
  242. for (int i = 0; i < sortButtonNames.Length; i++)
  243. {
  244. float curButtonX = sortButtonX + (sortButtonWidth * i) + (buttonSpacing * i);
  245. SongSortButton sortButton = new SongSortButton();
  246. sortButton.SortMode = sortModes[i];
  247. sortButton.Button = _viewController.CreateUIButton(String.Format("Sort{0}Button", sortButton.SortMode), "PracticeButton",
  248. new Vector2(curButtonX, buttonY), new Vector2(sortButtonWidth, buttonHeight),
  249. () =>
  250. {
  251. OnSortButtonClickEvent(sortButton.SortMode);
  252. RefreshOuterUIState(UIState.Main);
  253. },
  254. sortButtonNames[i]);
  255. sortButton.Button.SetButtonTextSize(sortButtonFontSize);
  256. sortButton.Button.ToggleWordWrapping(false);
  257. _sortButtonGroup.Add(sortButton);
  258. }
  259. }
  260. /// <summary>
  261. /// Create the filter by buttons
  262. /// </summary>
  263. private void CreateFilterButtons()
  264. {
  265. Logger.Debug("Creating filter buttons...");
  266. float filterButtonFontSize = 2.25f;
  267. float filterButtonX = -63.0f;
  268. float filterButtonWidth = 12.25f;
  269. float buttonSpacing = 0.5f;
  270. float buttonY = BUTTON_ROW_Y;
  271. float buttonHeight = 5.0f;
  272. string[] filterButtonNames = new string[]
  273. {
  274. "Search", "Ranked", "Unranked"
  275. };
  276. SongFilterMode[] filterModes = new SongFilterMode[]
  277. {
  278. SongFilterMode.Search, SongFilterMode.Ranked, SongFilterMode.Unranked
  279. };
  280. _filterButtonGroup = new List<SongFilterButton>();
  281. for (int i = 0; i < filterButtonNames.Length; i++)
  282. {
  283. float curButtonX = filterButtonX + (filterButtonWidth * i) + (buttonSpacing * i);
  284. SongFilterButton filterButton = new SongFilterButton();
  285. filterButton.FilterMode = filterModes[i];
  286. filterButton.Button = _viewController.CreateUIButton(String.Format("Filter{0}Button", filterButton.FilterMode), "PracticeButton",
  287. new Vector2(curButtonX, buttonY), new Vector2(filterButtonWidth, buttonHeight),
  288. () =>
  289. {
  290. OnFilterButtonClickEvent(filterButton.FilterMode);
  291. RefreshOuterUIState(UIState.Main);
  292. },
  293. filterButtonNames[i]);
  294. filterButton.Button.SetButtonTextSize(filterButtonFontSize);
  295. filterButton.Button.ToggleWordWrapping(false);
  296. _filterButtonGroup.Add(filterButton);
  297. }
  298. }
  299. /// <summary>
  300. /// Create the fast page up and down buttons
  301. /// </summary>
  302. private void CreateFastPageButtons()
  303. {
  304. Logger.Debug("Creating fast scroll button...");
  305. _pageUpFastButton = BeatSaberUI.CreateIconButton("PageUpFast",
  306. _beatUi.LevelCollectionTableViewTransform, "PracticeButton",
  307. new Vector2(42f, 24f),
  308. new Vector2(10f, 10f),
  309. delegate ()
  310. {
  311. this.JumpSongList(-1, SEGMENT_PERCENT);
  312. }, Base64Sprites.DoubleArrow);
  313. _pageUpFastButton.SetButtonBackgroundActive(false);
  314. (_pageUpFastButton.transform as RectTransform).Rotate(new Vector3(0, 0, 180));
  315. _pageDownFastButton = BeatSaberUI.CreateIconButton("PageDownFast",
  316. _beatUi.LevelCollectionTableViewTransform, "PracticeButton",
  317. new Vector2(42f, -24f),
  318. new Vector2(10f, 10f),
  319. delegate ()
  320. {
  321. this.JumpSongList(1, SEGMENT_PERCENT);
  322. }, Base64Sprites.DoubleArrow);
  323. _pageDownFastButton.SetButtonBackgroundActive(false);
  324. }
  325. /// <summary>
  326. /// Create the delete button in the play button container
  327. /// </summary>
  328. private void CreateDeleteButton()
  329. {
  330. // Create delete button
  331. /*Logger.Debug("Creating delete button...");
  332. _deleteButton = BeatSaberUI.CreateIconButton(_beatUi.PlayButtons, _beatUi.PracticeButton, Base64Sprites.DeleteIcon);
  333. _deleteButton.onClick.AddListener(delegate () {
  334. HandleDeleteSelectedLevel();
  335. });
  336. BeatSaberUI.DestroyHoverHint(_deleteButton.transform as RectTransform);*/
  337. }
  338. /// <summary>
  339. /// Resize the stats panel to fit more stats.
  340. /// </summary>
  341. private void ModifySongStatsPanel()
  342. {
  343. // modify stat panel, inject extra row of stats
  344. Logger.Debug("Resizing Stats Panel...");
  345. var statsPanel = _beatUi.StandardLevelDetailView.GetPrivateField<LevelParamsPanel>("_levelParamsPanel");
  346. (statsPanel.transform as RectTransform).Translate(0, 0.05f, 0);
  347. _ppStatButton = UnityEngine.Object.Instantiate(statsPanel.GetComponentsInChildren<RectTransform>().First(x => x.name == "NPS"), statsPanel.transform, false);
  348. _ppStatButton.name = "PPStatLabel";
  349. (_ppStatButton.transform as RectTransform).Translate(0, -0.1f, 0);
  350. BeatSaberUI.SetStatButtonIcon(_ppStatButton, Base64Sprites.GraphIcon);
  351. BeatSaberUI.DestroyHoverHint(_ppStatButton);
  352. BeatSaberUI.SetHoverHint(_ppStatButton, "songBrowser_ppValue", "PP Value");
  353. _starStatButton = UnityEngine.Object.Instantiate(statsPanel.GetComponentsInChildren<RectTransform>().First(x => x.name == "NotesCount"), statsPanel.transform, false);
  354. _starStatButton.name = "StarStatLabel";
  355. (_starStatButton.transform as RectTransform).Translate(0, -0.1f, 0);
  356. BeatSaberUI.SetStatButtonIcon(_starStatButton, Base64Sprites.StarFullIcon);
  357. BeatSaberUI.DestroyHoverHint(_starStatButton);
  358. BeatSaberUI.SetHoverHint(_starStatButton, "songBrowser_starValue", "Star Difficulty Rating");
  359. _njsStatButton = UnityEngine.Object.Instantiate(statsPanel.GetComponentsInChildren<RectTransform>().First(x => x.name == "ObstaclesCount"), statsPanel.transform, false);
  360. _njsStatButton.name = "NoteJumpSpeedLabel";
  361. (_njsStatButton.transform as RectTransform).Translate(0, -0.1f, 0);
  362. BeatSaberUI.SetStatButtonIcon(_njsStatButton, Base64Sprites.SpeedIcon);
  363. BeatSaberUI.DestroyHoverHint(_njsStatButton);
  364. BeatSaberUI.SetHoverHint(_njsStatButton, "songBrowser_njsValue", "Note Jump Speed");
  365. }
  366. /// <summary>
  367. /// Resize some of the song table elements.
  368. /// </summary>
  369. public void ResizeSongUI()
  370. {
  371. // shrink play button container
  372. //RectTransform playButtonsRect = Resources.FindObjectsOfTypeAll<RectTransform>().First(x => x.name == "ActionButtons");
  373. //playButtonsRect.localScale = new Vector3(0.825f, 0.825f, 0.825f);
  374. }
  375. /// <summary>
  376. /// Add our handlers into BeatSaber.
  377. /// </summary>
  378. private void InstallHandlers()
  379. {
  380. // level collection, level, difficulty handlers, characteristics
  381. TableView tableView = ReflectionUtil.GetPrivateField<TableView>(_beatUi.LevelCollectionTableView, "_tableView");
  382. // update stats
  383. _beatUi.LevelCollectionViewController.didSelectLevelEvent -= OnDidSelectLevelEvent;
  384. _beatUi.LevelCollectionViewController.didSelectLevelEvent += OnDidSelectLevelEvent;
  385. _beatUi.LevelDetailViewController.didChangeContentEvent -= OnDidPresentContentEvent;
  386. _beatUi.LevelDetailViewController.didChangeContentEvent += OnDidPresentContentEvent;
  387. _beatUi.LevelDetailViewController.didChangeDifficultyBeatmapEvent -= OnDidChangeDifficultyEvent;
  388. _beatUi.LevelDetailViewController.didChangeDifficultyBeatmapEvent += OnDidChangeDifficultyEvent;
  389. // update our view of the game state
  390. _beatUi.LevelFilteringNavigationController.didSelectAnnotatedBeatmapLevelCollectionEvent -= _levelFilteringNavController_didSelectAnnotatedBeatmapLevelCollectionEvent;
  391. _beatUi.LevelFilteringNavigationController.didSelectAnnotatedBeatmapLevelCollectionEvent += _levelFilteringNavController_didSelectAnnotatedBeatmapLevelCollectionEvent;
  392. _beatUi.AnnotatedBeatmapLevelCollectionsViewController.didSelectAnnotatedBeatmapLevelCollectionEvent -= handleDidSelectAnnotatedBeatmapLevelCollection;
  393. _beatUi.AnnotatedBeatmapLevelCollectionsViewController.didSelectAnnotatedBeatmapLevelCollectionEvent += handleDidSelectAnnotatedBeatmapLevelCollection;
  394. // Respond to characteristics changes
  395. _beatUi.BeatmapCharacteristicSelectionViewController.didSelectBeatmapCharacteristicEvent -= OnDidSelectBeatmapCharacteristic;
  396. _beatUi.BeatmapCharacteristicSelectionViewController.didSelectBeatmapCharacteristicEvent += OnDidSelectBeatmapCharacteristic;
  397. // make sure the quick scroll buttons don't desync with regular scrolling
  398. _beatUi.TableViewPageDownButton.onClick.AddListener(delegate ()
  399. {
  400. StartCoroutine(RefreshQuickScrollButtonsAsync());
  401. });
  402. _beatUi.TableViewPageUpButton.onClick.AddListener(delegate ()
  403. {
  404. StartCoroutine(RefreshQuickScrollButtonsAsync());
  405. });
  406. }
  407. /// <summary>
  408. /// Waits for the song UI to be available before trying to update.
  409. /// </summary>
  410. /// <returns></returns>
  411. public IEnumerator AsyncWaitForSongUIUpdate()
  412. {
  413. if (_asyncUpdating)
  414. {
  415. yield break;
  416. }
  417. if (!_uiCreated)
  418. {
  419. yield break;
  420. }
  421. if (!_model.SortWasMissingData)
  422. {
  423. yield break;
  424. }
  425. _asyncUpdating = true;
  426. while (_beatUi.LevelSelectionNavigationController.GetPrivateField<bool>("_isInTransition") ||
  427. _beatUi.LevelDetailViewController.GetPrivateField<bool>("_isInTransition") ||
  428. !_beatUi.LevelSelectionNavigationController.isInViewControllerHierarchy ||
  429. !_beatUi.LevelDetailViewController.isInViewControllerHierarchy ||
  430. !_beatUi.LevelSelectionNavigationController.isActiveAndEnabled ||
  431. !_beatUi.LevelDetailViewController.isActiveAndEnabled)
  432. {
  433. yield return null;
  434. }
  435. //yield return new WaitForEndOfFrame();
  436. if (_model.Settings.sortMode.NeedsScoreSaberData() && SongDataCore.Plugin.Songs.IsDataAvailable())
  437. {
  438. ProcessSongList();
  439. RefreshSongUI();
  440. }
  441. _asyncUpdating = false;
  442. }
  443. /// <summary>
  444. /// Helper to reduce code duplication...
  445. /// </summary>
  446. public void RefreshSongUI(bool scrollToLevel = true)
  447. {
  448. if (!_uiCreated)
  449. {
  450. return;
  451. }
  452. RefreshSongList();
  453. RefreshSortButtonUI();
  454. if (!scrollToLevel)
  455. {
  456. _beatUi.ScrollToLevelByRow(0);
  457. }
  458. RefreshQuickScrollButtons();
  459. RefreshCurrentSelectionDisplay();
  460. }
  461. /// <summary>
  462. /// External helper.
  463. /// </summary>
  464. public void ProcessSongList()
  465. {
  466. if (!_uiCreated)
  467. {
  468. return;
  469. }
  470. this._model.ProcessSongList(_lastLevelCollection, _beatUi.LevelSelectionNavigationController);
  471. }
  472. /// <summary>
  473. /// Helper for common filter cancellation logic.
  474. /// </summary>
  475. public void CancelFilter()
  476. {
  477. Logger.Debug($"Cancelling filter, levelCollection {_lastLevelCollection}");
  478. _model.Settings.filterMode = SongFilterMode.None;
  479. GameObject _noDataGO = _beatUi.LevelCollectionViewController.GetPrivateField<GameObject>("_noDataInfoGO");
  480. string _headerText = _beatUi.LevelCollectionTableView.GetPrivateField<string>("_headerText");
  481. Sprite _headerSprite = _beatUi.LevelCollectionTableView.GetPrivateField<Sprite>("_headerSprite");
  482. IBeatmapLevelCollection levelCollection = _beatUi.GetCurrentSelectedAnnotatedBeatmapLevelCollection().beatmapLevelCollection;
  483. _beatUi.LevelCollectionViewController.SetData(levelCollection, _headerText, _headerSprite, false, _noDataGO);
  484. }
  485. /// <summary>
  486. /// Playlists (fancy name for AnnotatedBeatmapLevelCollection)
  487. /// </summary>
  488. /// <param name="annotatedBeatmapLevelCollection"></param>
  489. public virtual void handleDidSelectAnnotatedBeatmapLevelCollection(IAnnotatedBeatmapLevelCollection annotatedBeatmapLevelCollection)
  490. {
  491. Logger.Trace("handleDidSelectAnnotatedBeatmapLevelCollection()");
  492. _lastLevelCollection = annotatedBeatmapLevelCollection;
  493. Model.Settings.currentLevelCategoryName = _beatUi.LevelFilteringNavigationController.selectedLevelCategory.ToString();
  494. Model.Settings.Save();
  495. Logger.Debug("Selected Level Collection={0}", _lastLevelCollection);
  496. }
  497. /// <summary>
  498. /// Handler for level collection selection, controller.
  499. /// Sets the current level collection into the model and updates.
  500. /// </summary>
  501. /// <param name="arg1"></param>
  502. /// <param name="arg2"></param>
  503. /// <param name="arg3"></param>
  504. /// <param name="arg4"></param>
  505. private void _levelFilteringNavController_didSelectAnnotatedBeatmapLevelCollectionEvent(LevelFilteringNavigationController arg1, IAnnotatedBeatmapLevelCollection arg2,
  506. GameObject arg3, BeatmapCharacteristicSO arg4)
  507. {
  508. Logger.Trace("_levelFilteringNavController_didSelectAnnotatedBeatmapLevelCollectionEvent(levelCollection={0})", arg2);
  509. if (arg2 == null)
  510. {
  511. // Probably means we transitioned between Music Packs and Playlists
  512. arg2 = _beatUi.GetCurrentSelectedAnnotatedBeatmapLevelCollection();
  513. if (arg2 == null)
  514. {
  515. Logger.Warning("Nothing selected. This is likely an error.");
  516. return;
  517. }
  518. }
  519. Logger.Debug("Selected Level Collection={0}", arg2);
  520. // Do something about preview level packs, they can't be used past this point
  521. if (arg2 as PreviewBeatmapLevelPackSO)
  522. {
  523. Logger.Info("Hiding SongBrowser, previewing a song pack.");
  524. //CancelFilter();
  525. Hide();
  526. return;
  527. }
  528. else
  529. {
  530. Show();
  531. }
  532. // Skip the first time - Effectively ignores BeatSaber forcing OST1 on us on first load.
  533. // Skip when we have a playlist
  534. if (_lastLevelCollection == null)
  535. {
  536. return;
  537. }
  538. SelectLevelCollection(arg2);
  539. }
  540. /// <summary>
  541. /// Logic for selecting a level collection.
  542. /// </summary>
  543. /// <param name="levelPack"></param>
  544. public void SelectLevelCollection(IAnnotatedBeatmapLevelCollection levelCollection)
  545. {
  546. try
  547. {
  548. if (levelCollection == null)
  549. {
  550. Logger.Debug("No level collection selected...");
  551. return;
  552. }
  553. // store the real level collection
  554. if (levelCollection.collectionName != SongBrowserModel.FilteredSongsCollectionName && _lastLevelCollection != null)
  555. {
  556. Logger.Debug("Recording levelCollection: {0}", levelCollection.collectionName);
  557. _lastLevelCollection = levelCollection;
  558. Model.Settings.currentLevelCategoryName = _beatUi.LevelFilteringNavigationController.selectedLevelCategory.ToString();
  559. }
  560. // reset level selection
  561. _model.LastSelectedLevelId = null;
  562. // save level collection
  563. this._model.Settings.currentLevelCollectionName = levelCollection.collectionName;
  564. this._model.Settings.Save();
  565. StartCoroutine(ProcessSongListEndOfFrame());
  566. }
  567. catch (Exception e)
  568. {
  569. Logger.Exception("Exception handling SelectLevelCollection...", e);
  570. }
  571. }
  572. /// <summary>
  573. /// End of frame update the song list, the game seems to stomp on us sometimes otherwise
  574. /// TODO - Might not be nice to other plugins
  575. /// </summary>
  576. /// <returns></returns>
  577. public IEnumerator ProcessSongListEndOfFrame()
  578. {
  579. yield return new WaitForEndOfFrame();
  580. ProcessSongList();
  581. RefreshSongUI();
  582. }
  583. /// <summary>
  584. /// Remove all filters, update song list, save.
  585. /// </summary>
  586. private void OnClearButtonClickEvent()
  587. {
  588. Logger.Debug("Clearing all sorts and filters.");
  589. _model.Settings.sortMode = SongSortMode.Original;
  590. _model.Settings.invertSortResults = false;
  591. _model.Settings.filterMode = SongFilterMode.None;
  592. _model.Settings.Save();
  593. CancelFilter();
  594. ProcessSongList();
  595. RefreshSongUI();
  596. }
  597. /// <summary>
  598. /// Sort button clicked.
  599. /// </summary>
  600. private void OnSortButtonClickEvent(SongSortMode sortMode)
  601. {
  602. Logger.Debug("Sort button - {0} - pressed.", sortMode.ToString());
  603. if ((sortMode.NeedsScoreSaberData() && !SongDataCore.Plugin.Songs.IsDataAvailable()))
  604. {
  605. Logger.Info("Data for sort type is not available.");
  606. return;
  607. }
  608. // Clear current selected level id so our song list jumps to the start
  609. _model.LastSelectedLevelId = null;
  610. if (_model.Settings.sortMode == sortMode)
  611. {
  612. _model.ToggleInverting();
  613. }
  614. _model.Settings.sortMode = sortMode;
  615. // update the seed
  616. if (_model.Settings.sortMode == SongSortMode.Random)
  617. {
  618. _model.Settings.randomSongSeed = Guid.NewGuid().GetHashCode();
  619. }
  620. _model.Settings.Save();
  621. ProcessSongList();
  622. RefreshSongUI();
  623. }
  624. /// <summary>
  625. /// Handle filter button logic. Some filters have sub menus that need special logic.
  626. /// </summary>
  627. /// <param name="mode"></param>
  628. private void OnFilterButtonClickEvent(SongFilterMode mode)
  629. {
  630. Logger.Debug($"FilterButton {mode} clicked.");
  631. var curCollection = _beatUi.GetCurrentSelectedAnnotatedBeatmapLevelCollection();
  632. if (_lastLevelCollection == null ||
  633. (curCollection != null &&
  634. curCollection.collectionName != SongBrowserModel.FilteredSongsCollectionName &&
  635. curCollection.collectionName != SongBrowserModel.PlaylistSongsCollectionName))
  636. {
  637. _lastLevelCollection = _beatUi.GetCurrentSelectedAnnotatedBeatmapLevelCollection();
  638. }
  639. if (mode == SongFilterMode.Favorites)
  640. {
  641. _beatUi.SelectLevelCollection(SelectLevelCategoryViewController.LevelCategory.Favorites.ToString(), SongBrowserSettings.CUSTOM_SONGS_LEVEL_COLLECTION_NAME);
  642. }
  643. else
  644. {
  645. GameObject _noDataGO = _beatUi.LevelCollectionViewController.GetPrivateField<GameObject>("_noDataInfoGO");
  646. string _headerText = _beatUi.LevelCollectionTableView.GetPrivateField<string>("_headerText");
  647. Sprite _headerSprite = _beatUi.LevelCollectionTableView.GetPrivateField<Sprite>("_headerSprite");
  648. IBeatmapLevelCollection levelCollection = _beatUi.GetCurrentSelectedAnnotatedBeatmapLevelCollection().beatmapLevelCollection;
  649. _beatUi.LevelCollectionViewController.SetData(levelCollection, _headerText, _headerSprite, false, _noDataGO);
  650. }
  651. // If selecting the same filter, cancel
  652. if (_model.Settings.filterMode == mode)
  653. {
  654. _model.Settings.filterMode = SongFilterMode.None;
  655. }
  656. else
  657. {
  658. _model.Settings.filterMode = mode;
  659. }
  660. switch (mode)
  661. {
  662. case SongFilterMode.Search:
  663. OnSearchButtonClickEvent();
  664. break;
  665. default:
  666. _model.Settings.Save();
  667. ProcessSongList();
  668. RefreshSongUI();
  669. break;
  670. }
  671. }
  672. /// <summary>
  673. /// Display the keyboard.
  674. /// </summary>
  675. /// <param name="sortMode"></param>
  676. private void OnSearchButtonClickEvent()
  677. {
  678. Logger.Debug("Filter button - {0} - pressed.", SongFilterMode.Search.ToString());
  679. this.ShowSearchKeyboard();
  680. }
  681. /// <summary>
  682. /// Adjust UI based on level selected.
  683. /// Various ways of detecting if a level is not properly selected. Seems most hit the first one.
  684. /// </summary>
  685. private void OnDidSelectLevelEvent(LevelCollectionViewController view, IPreviewBeatmapLevel level)
  686. {
  687. try
  688. {
  689. Logger.Trace("OnDidSelectLevelEvent()");
  690. if (level == null)
  691. {
  692. Logger.Debug("No level selected?");
  693. return;
  694. }
  695. if (_model.Settings == null)
  696. {
  697. Logger.Debug("Settings not instantiated yet?");
  698. return;
  699. }
  700. _model.LastSelectedLevelId = level.levelID;
  701. HandleDidSelectLevelRow(level);
  702. }
  703. catch (Exception e)
  704. {
  705. Logger.Exception("Exception selecting song:", e);
  706. }
  707. }
  708. /// <summary>
  709. /// Switching one-saber modes for example.
  710. /// </summary>
  711. /// <param name="view"></param>
  712. /// <param name="bc"></param>
  713. private void OnDidSelectBeatmapCharacteristic(BeatmapCharacteristicSegmentedControlController view, BeatmapCharacteristicSO bc)
  714. {
  715. try
  716. {
  717. Logger.Trace("OnDidSelectBeatmapCharacteristic({0})", bc.compoundIdPartName);
  718. _model.CurrentBeatmapCharacteristicSO = bc;
  719. if (_beatUi.StandardLevelDetailView != null)
  720. {
  721. RefreshScoreSaberData(_beatUi.StandardLevelDetailView.selectedDifficultyBeatmap.level);
  722. RefreshNoteJumpSpeed(_beatUi.StandardLevelDetailView.selectedDifficultyBeatmap.noteJumpMovementSpeed);
  723. }
  724. }
  725. catch (Exception e)
  726. {
  727. Logger.Exception(e);
  728. }
  729. }
  730. /// <summary>
  731. /// Handle difficulty level selection.
  732. /// </summary>
  733. private void OnDidChangeDifficultyEvent(StandardLevelDetailViewController view, IDifficultyBeatmap beatmap)
  734. {
  735. Logger.Trace("OnDidChangeDifficultyEvent({0})", beatmap);
  736. if (view.selectedDifficultyBeatmap == null)
  737. {
  738. return;
  739. }
  740. if (_deleteButton != null)
  741. {
  742. _deleteButton.interactable = (view.selectedDifficultyBeatmap.level.levelID.Length >= 32);
  743. }
  744. RefreshScoreSaberData(view.selectedDifficultyBeatmap.level);
  745. RefreshNoteJumpSpeed(beatmap.noteJumpMovementSpeed);
  746. }
  747. /// <summary>
  748. /// BeatSaber finished loading content. This is when the difficulty is finally updated.
  749. /// </summary>
  750. /// <param name="view"></param>
  751. /// <param name="type"></param>
  752. private void OnDidPresentContentEvent(StandardLevelDetailViewController view, StandardLevelDetailViewController.ContentType type)
  753. {
  754. Logger.Trace("OnDidPresentContentEvent()");
  755. // v1.12.2 - TODO - is this safe to prevent us from trying to lookup empty/dead content?
  756. if (type != StandardLevelDetailViewController.ContentType.OwnedAndReady)
  757. {
  758. return;
  759. }
  760. if (view.selectedDifficultyBeatmap == null)
  761. {
  762. return;
  763. }
  764. if (_deleteButton != null)
  765. {
  766. _deleteButton.interactable = (_beatUi.LevelDetailViewController.selectedDifficultyBeatmap.level.levelID.Length >= 32);
  767. }
  768. RefreshScoreSaberData(view.selectedDifficultyBeatmap.level);
  769. RefreshNoteJumpSpeed(view.selectedDifficultyBeatmap.noteJumpMovementSpeed);
  770. }
  771. /// <summary>
  772. /// Refresh stats panel.
  773. /// </summary>
  774. /// <param name="level"></param>
  775. private void HandleDidSelectLevelRow(IPreviewBeatmapLevel level)
  776. {
  777. Logger.Trace("HandleDidSelectLevelRow({0})", level);
  778. if (_deleteButton != null)
  779. {
  780. _deleteButton.interactable = (level.levelID.Length >= 32);
  781. }
  782. RefreshQuickScrollButtons();
  783. }
  784. /// <summary>
  785. /// Pop up a delete dialog.
  786. /// </summary>
  787. private void HandleDeleteSelectedLevel()
  788. {
  789. IBeatmapLevel level = _beatUi.LevelDetailViewController.selectedDifficultyBeatmap.level;
  790. _deleteDialog.Init("Delete song", $"Do you really want to delete \"{ level.songName} {level.songSubName}\"?", "Delete", "Cancel",
  791. (selectedButton) =>
  792. {
  793. _beatUi.LevelSelectionFlowCoordinator.InvokePrivateMethod("DismissViewController", new object[] { _deleteDialog, null, false });
  794. if (selectedButton == 0)
  795. {
  796. try
  797. {
  798. // determine the index we are deleting so we can keep the cursor near the same spot after
  799. // the header counts as an index, so if the index came from the level array we have to add 1.
  800. var levelsTableView = _beatUi.LevelCollectionTableView;
  801. List<IPreviewBeatmapLevel> levels = _beatUi.GetCurrentLevelCollectionLevels().ToList();
  802. int selectedIndex = levels.FindIndex(x => x.levelID == _beatUi.StandardLevelDetailView.selectedDifficultyBeatmap.level.levelID);
  803. if (selectedIndex > -1)
  804. {
  805. var song = SongCore.Loader.CustomLevels.First(x => x.Value.levelID == _beatUi.LevelDetailViewController.selectedDifficultyBeatmap.level.levelID).Value;
  806. Logger.Info($"Deleting song: {song.customLevelPath}");
  807. SongCore.Loader.Instance.DeleteSong(song.customLevelPath);
  808. this._model.RemoveSongFromLevelCollection(_beatUi.GetCurrentSelectedAnnotatedBeatmapLevelCollection(), _beatUi.LevelDetailViewController.selectedDifficultyBeatmap.level.levelID);
  809. int removedLevels = levels.RemoveAll(x => x.levelID == _beatUi.StandardLevelDetailView.selectedDifficultyBeatmap.level.levelID);
  810. Logger.Info("Removed " + removedLevels + " level(s) from song list!");
  811. this.UpdateLevelDataModel();
  812. // if we have a song to select at the same index, set the last selected level id, UI updates takes care of the rest.
  813. if (selectedIndex < levels.Count)
  814. {
  815. if (levels[selectedIndex].levelID != null)
  816. {
  817. _model.LastSelectedLevelId = levels[selectedIndex].levelID;
  818. }
  819. }
  820. this.RefreshSongList();
  821. }
  822. }
  823. catch (Exception e)
  824. {
  825. Logger.Error("Unable to delete song! Exception: " + e);
  826. }
  827. }
  828. });
  829. _beatUi.LevelSelectionFlowCoordinator.InvokePrivateMethod("PresentViewController", new object[] { _deleteDialog, null, false });
  830. }
  831. /// <summary>
  832. /// Display the search keyboard
  833. /// </summary>
  834. void ShowSearchKeyboard()
  835. {
  836. var modalKbTag = new BeatSaberMarkupLanguage.Tags.ModalKeyboardTag();
  837. var modalKbView = modalKbTag.CreateObject(_beatUi.LevelSelectionNavigationController.rectTransform);
  838. modalKbView.gameObject.SetActive(true);
  839. var modalKb = modalKbView.GetComponent<ModalKeyboard>();
  840. modalKb.gameObject.SetActive(true);
  841. modalKb.keyboard.EnterPressed += SearchViewControllerSearchButtonPressed;
  842. modalKb.modalView.Show(true, true);
  843. }
  844. /// <summary>
  845. /// Handle search.
  846. /// </summary>
  847. /// <param name="searchFor"></param>
  848. private void SearchViewControllerSearchButtonPressed(string searchFor)
  849. {
  850. Logger.Debug("Searching for \"{0}\"...", searchFor);
  851. _model.Settings.filterMode = SongFilterMode.Search;
  852. _model.Settings.searchTerms.Insert(0, searchFor);
  853. _model.Settings.Save();
  854. _model.LastSelectedLevelId = null;
  855. ProcessSongList();
  856. RefreshSongUI();
  857. }
  858. /// <summary>
  859. /// Make big jumps in the song list.
  860. /// </summary>
  861. /// <param name="numJumps"></param>
  862. private void JumpSongList(int numJumps, float segmentPercent)
  863. {
  864. var levels = _beatUi.GetCurrentLevelCollectionLevels();
  865. if (levels == null)
  866. {
  867. return;
  868. }
  869. int totalSize = levels.Count();
  870. int segmentSize = (int)(totalSize * segmentPercent);
  871. // Jump at least one scree size.
  872. if (segmentSize < LIST_ITEMS_VISIBLE_AT_ONCE)
  873. {
  874. segmentSize = LIST_ITEMS_VISIBLE_AT_ONCE;
  875. }
  876. int currentRow = _beatUi.LevelCollectionTableView.GetPrivateField<int>("_selectedRow");
  877. int jumpDirection = Math.Sign(numJumps);
  878. int newRow = currentRow + (jumpDirection * segmentSize);
  879. if (newRow <= 0)
  880. {
  881. newRow = 0;
  882. }
  883. else if (newRow >= totalSize)
  884. {
  885. newRow = totalSize - 1;
  886. }
  887. Logger.Debug("jumpDirection: {0}, newRow: {1}", jumpDirection, newRow);
  888. _beatUi.SelectAndScrollToLevel(levels[newRow].levelID);
  889. RefreshQuickScrollButtons();
  890. }
  891. /// <summary>
  892. /// Update GUI elements that show score saber data.
  893. /// </summary>
  894. public void RefreshScoreSaberData(IPreviewBeatmapLevel level)
  895. {
  896. Logger.Trace("RefreshScoreSaberData({0})", level.levelID);
  897. if (!SongDataCore.Plugin.Songs.IsDataAvailable())
  898. {
  899. return;
  900. }
  901. BeatmapDifficulty difficulty = _beatUi.LevelDifficultyViewController.selectedDifficulty;
  902. string difficultyString = difficulty.ToString();
  903. if (difficultyString.Equals("ExpertPlus"))
  904. {
  905. difficultyString = "Expert+";
  906. }
  907. Logger.Debug(difficultyString);
  908. // Check if we have data for this song
  909. Logger.Debug("Checking if have info for song {0}", level.songName);
  910. var hash = SongBrowserModel.GetSongHash(level.levelID);
  911. if (SongDataCore.Plugin.Songs.Data.Songs.ContainsKey(hash))
  912. {
  913. Logger.Debug("Checking if have difficulty for song {0} difficulty {1}", level.songName, difficultyString);
  914. BeatStarSong scoreSaberSong = SongDataCore.Plugin.Songs.Data.Songs[hash];
  915. BeatStarSongDifficultyStats scoreSaberSongDifficulty = scoreSaberSong.diffs.FirstOrDefault(x => String.Equals(x.diff, difficultyString));
  916. if (scoreSaberSongDifficulty != null)
  917. {
  918. Logger.Debug("Display pp for song.");
  919. double pp = scoreSaberSongDifficulty.pp;
  920. double star = scoreSaberSongDifficulty.star;
  921. BeatSaberUI.SetStatButtonText(_ppStatButton, String.Format("{0:0.#}", pp));
  922. BeatSaberUI.SetStatButtonText(_starStatButton, String.Format("{0:0.#}", star));
  923. }
  924. else
  925. {
  926. BeatSaberUI.SetStatButtonText(_ppStatButton, "NA");
  927. BeatSaberUI.SetStatButtonText(_starStatButton, "NA");
  928. }
  929. }
  930. else
  931. {
  932. BeatSaberUI.SetStatButtonText(_ppStatButton, "NA");
  933. BeatSaberUI.SetStatButtonText(_starStatButton, "NA");
  934. }
  935. Logger.Debug("Done refreshing score saber stats.");
  936. }
  937. /// <summary>
  938. /// Helper to refresh the NJS widget.
  939. /// </summary>
  940. /// <param name="noteJumpMovementSpeed"></param>
  941. private void RefreshNoteJumpSpeed(float noteJumpMovementSpeed)
  942. {
  943. BeatSaberUI.SetStatButtonText(_njsStatButton, String.Format("{0}", noteJumpMovementSpeed));
  944. }
  945. /// <summary>
  946. /// Update interactive state of the quick scroll buttons.
  947. /// </summary>
  948. private void RefreshQuickScrollButtons()
  949. {
  950. if (!_uiCreated)
  951. {
  952. return;
  953. }
  954. _pageUpFastButton.interactable = _beatUi.TableViewPageUpButton.interactable;
  955. _pageUpFastButton.gameObject.SetActive(_beatUi.TableViewPageUpButton.IsActive());
  956. _pageDownFastButton.interactable = _beatUi.TableViewPageDownButton.interactable;
  957. _pageDownFastButton.gameObject.SetActive(_beatUi.TableViewPageDownButton.IsActive());
  958. }
  959. /// <summary>
  960. /// TODO - evaluate this sillyness...
  961. /// </summary>
  962. /// <returns></returns>
  963. public IEnumerator RefreshQuickScrollButtonsAsync()
  964. {
  965. yield return new WaitForEndOfFrame();
  966. RefreshQuickScrollButtons();
  967. }
  968. /// <summary>
  969. /// Show the UI.
  970. /// </summary>
  971. public void Show()
  972. {
  973. Logger.Trace("Show SongBrowserUI()");
  974. this.SetVisibility(true);
  975. }
  976. /// <summary>
  977. /// Hide the UI.
  978. /// </summary>
  979. public void Hide()
  980. {
  981. Logger.Trace("Hide SongBrowserUI()");
  982. this.SetVisibility(false);
  983. }
  984. /// <summary>
  985. /// Handle showing or hiding UI logic.
  986. /// </summary>
  987. /// <param name="visible"></param>
  988. private void SetVisibility(bool visible)
  989. {
  990. // UI not created, nothing visible to hide...
  991. if (!_uiCreated)
  992. {
  993. return;
  994. }
  995. _ppStatButton?.gameObject.SetActive(visible);
  996. _starStatButton?.gameObject.SetActive(visible);
  997. _njsStatButton?.gameObject.SetActive(visible);
  998. RefreshOuterUIState(visible == true ? UIState.Main : UIState.Disabled);
  999. _deleteButton?.gameObject.SetActive(visible);
  1000. _pageUpFastButton?.gameObject.SetActive(visible);
  1001. _pageDownFastButton?.gameObject.SetActive(visible);
  1002. }
  1003. /// <summary>
  1004. /// Update the top UI state.
  1005. /// Hides the outer ui, sort, and filter buttons depending on the state.
  1006. /// </summary>
  1007. private void RefreshOuterUIState(UIState state)
  1008. {
  1009. bool sortButtons = false;
  1010. bool filterButtons = false;
  1011. bool outerButtons = false;
  1012. bool clearButton = true;
  1013. if (state == UIState.SortBy)
  1014. {
  1015. sortButtons = true;
  1016. }
  1017. else if (state == UIState.FilterBy)
  1018. {
  1019. filterButtons = true;
  1020. }
  1021. else if (state == UIState.Main)
  1022. {
  1023. outerButtons = true;
  1024. }
  1025. else
  1026. {
  1027. clearButton = false;
  1028. }
  1029. _sortButtonGroup.ForEach(x => x.Button.gameObject.SetActive(sortButtons));
  1030. _filterButtonGroup.ForEach(x => x.Button.gameObject.SetActive(filterButtons));
  1031. _sortByButton?.gameObject.SetActive(outerButtons);
  1032. _sortByDisplay?.gameObject.SetActive(outerButtons);
  1033. _filterByButton?.gameObject.SetActive(outerButtons);
  1034. _filterByDisplay?.gameObject.SetActive(outerButtons);
  1035. _clearSortFilterButton?.gameObject.SetActive(clearButton);
  1036. _randomButton?.gameObject.SetActive(outerButtons);
  1037. RefreshCurrentSelectionDisplay();
  1038. _currentUiState = state;
  1039. }
  1040. /// <summary>
  1041. /// Adjust the text field of the sort by and filter by displays.
  1042. /// </summary>
  1043. private void RefreshCurrentSelectionDisplay()
  1044. {
  1045. string sortByDisplay;
  1046. if (_model.Settings.sortMode == SongSortMode.Default)
  1047. {
  1048. sortByDisplay = "Title";
  1049. }
  1050. else
  1051. {
  1052. sortByDisplay = _model.Settings.sortMode.ToString();
  1053. }
  1054. _sortByDisplay.SetButtonText(sortByDisplay);
  1055. if (_model.Settings.filterMode != SongFilterMode.Custom)
  1056. {
  1057. // Custom SongFilterMod implies that another mod has modified the text of this button (do not overwrite)
  1058. _filterByDisplay.SetButtonText(_model.Settings.filterMode.ToString());
  1059. }
  1060. }
  1061. /// <summary>
  1062. /// Adjust the UI colors.
  1063. /// </summary>
  1064. public void RefreshSortButtonUI()
  1065. {
  1066. if (!_uiCreated)
  1067. {
  1068. return;
  1069. }
  1070. foreach (SongSortButton sortButton in _sortButtonGroup)
  1071. {
  1072. if (sortButton.SortMode.NeedsScoreSaberData() && !SongDataCore.Plugin.Songs.IsDataAvailable())
  1073. {
  1074. sortButton.Button.SetButtonUnderlineColor(Color.gray);
  1075. }
  1076. else
  1077. {
  1078. sortButton.Button.SetButtonUnderlineColor(Color.white);
  1079. }
  1080. if (sortButton.SortMode == _model.Settings.sortMode)
  1081. {
  1082. if (this._model.Settings.invertSortResults)
  1083. {
  1084. sortButton.Button.SetButtonUnderlineColor(Color.red);
  1085. }
  1086. else
  1087. {
  1088. sortButton.Button.SetButtonUnderlineColor(Color.green);
  1089. }
  1090. }
  1091. }
  1092. foreach (SongFilterButton filterButton in _filterButtonGroup)
  1093. {
  1094. filterButton.Button.SetButtonUnderlineColor(Color.white);
  1095. if (filterButton.FilterMode == _model.Settings.filterMode)
  1096. {
  1097. filterButton.Button.SetButtonUnderlineColor(Color.green);
  1098. }
  1099. }
  1100. if (this._model.Settings.invertSortResults)
  1101. {
  1102. _sortByDisplay.SetButtonUnderlineColor(Color.red);
  1103. }
  1104. else
  1105. {
  1106. _sortByDisplay.SetButtonUnderlineColor(Color.green);
  1107. }
  1108. if (this._model.Settings.filterMode != SongFilterMode.None)
  1109. {
  1110. _filterByDisplay.SetButtonUnderlineColor(Color.green);
  1111. }
  1112. else
  1113. {
  1114. _filterByDisplay.SetButtonUnderlineColor(Color.white);
  1115. }
  1116. }
  1117. /// <summary>
  1118. ///
  1119. /// </summary>
  1120. public void RefreshSongList()
  1121. {
  1122. if (!_uiCreated)
  1123. {
  1124. return;
  1125. }
  1126. _beatUi.RefreshSongList(_model.LastSelectedLevelId);
  1127. }
  1128. /// <summary>
  1129. /// Helper for updating the model (which updates the song list)
  1130. /// </summary>
  1131. public void UpdateLevelDataModel()
  1132. {
  1133. try
  1134. {
  1135. Logger.Trace("UpdateLevelDataModel()");
  1136. // get a current beatmap characteristic...
  1137. if (_model.CurrentBeatmapCharacteristicSO == null && _uiCreated)
  1138. {
  1139. _model.CurrentBeatmapCharacteristicSO = _beatUi.BeatmapCharacteristicSelectionViewController.GetPrivateField<BeatmapCharacteristicSO>("_selectedBeatmapCharacteristic");
  1140. }
  1141. _model.UpdateLevelRecords();
  1142. }
  1143. catch (Exception e)
  1144. {
  1145. Logger.Exception("SongBrowser UI crashed trying to update the internal song lists: ", e);
  1146. }
  1147. }
  1148. /// <summary>
  1149. /// Logic for fixing BeatSaber's level pack selection bugs.
  1150. /// </summary>
  1151. public bool UpdateLevelCollectionSelection()
  1152. {
  1153. if (_uiCreated)
  1154. {
  1155. IAnnotatedBeatmapLevelCollection currentSelected = _beatUi.GetCurrentSelectedAnnotatedBeatmapLevelCollection();
  1156. Logger.Debug("Current selected level collection: {0}", currentSelected);
  1157. if (String.IsNullOrEmpty(_model.Settings.currentLevelCollectionName))
  1158. {
  1159. if (currentSelected == null)
  1160. {
  1161. Logger.Debug("No level collection selected, acquiring the first available, likely OST1...");
  1162. currentSelected = _beatUi.BeatmapLevelsModel.allLoadedBeatmapLevelPackCollection.beatmapLevelPacks[0];
  1163. }
  1164. }
  1165. else if (currentSelected == null || (currentSelected.collectionName != _model.Settings.currentLevelCollectionName))
  1166. {
  1167. Logger.Debug("Automatically selecting level collection: {0}", _model.Settings.currentLevelCollectionName);
  1168. _beatUi.LevelFilteringNavigationController.didSelectAnnotatedBeatmapLevelCollectionEvent -= _levelFilteringNavController_didSelectAnnotatedBeatmapLevelCollectionEvent;
  1169. _lastLevelCollection = _beatUi.GetLevelCollectionByName(_model.Settings.currentLevelCollectionName);
  1170. if (_lastLevelCollection as PreviewBeatmapLevelPackSO)
  1171. {
  1172. Hide();
  1173. }
  1174. _beatUi.SelectLevelCollection(_model.Settings.currentLevelCategoryName, _model.Settings.currentLevelCollectionName);
  1175. _beatUi.LevelFilteringNavigationController.didSelectAnnotatedBeatmapLevelCollectionEvent += _levelFilteringNavController_didSelectAnnotatedBeatmapLevelCollectionEvent;
  1176. }
  1177. if (_lastLevelCollection == null)
  1178. {
  1179. if (currentSelected.collectionName != SongBrowserModel.FilteredSongsCollectionName && currentSelected.collectionName != SongBrowserModel.PlaylistSongsCollectionName)
  1180. {
  1181. _lastLevelCollection = currentSelected;
  1182. }
  1183. }
  1184. Logger.Debug("Current Level Collection is: {0}", _lastLevelCollection);
  1185. ProcessSongList();
  1186. }
  1187. return false;
  1188. }
  1189. }
  1190. }