SongBrowserUI.cs 47 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164
  1. using UnityEngine;
  2. using System.Linq;
  3. using System;
  4. using System.Collections.Generic;
  5. using UnityEngine.UI;
  6. using HMUI;
  7. using VRUI;
  8. using SongBrowserPlugin.DataAccess;
  9. using System.IO;
  10. using SongLoaderPlugin;
  11. using System.Security.Cryptography;
  12. using System.Text;
  13. using System.Text.RegularExpressions;
  14. using TMPro;
  15. namespace SongBrowserPlugin.UI
  16. {
  17. /// <summary>
  18. /// Hijack the flow coordinator. Have access to all StandardLevel easily.
  19. /// </summary>
  20. public class SongBrowserUI : MonoBehaviour
  21. {
  22. // Logging
  23. public const String Name = "SongBrowserUI";
  24. private const float SEGMENT_PERCENT = 0.1f;
  25. private const int LIST_ITEMS_VISIBLE_AT_ONCE = 6;
  26. private Logger _log = new Logger(Name);
  27. // Beat Saber UI Elements
  28. private StandardLevelSelectionFlowCoordinator _levelSelectionFlowCoordinator;
  29. private StandardLevelListViewController _levelListViewController;
  30. private StandardLevelDetailViewController _levelDetailViewController;
  31. private StandardLevelDifficultyViewController _levelDifficultyViewController;
  32. private StandardLevelSelectionNavigationController _levelSelectionNavigationController;
  33. private StandardLevelListTableView _levelListTableView;
  34. private RectTransform _tableViewRectTransform;
  35. private Button _tableViewPageUpButton;
  36. private Button _tableViewPageDownButton;
  37. private Button _playButton;
  38. // New UI Elements
  39. private List<SongSortButton> _sortButtonGroup;
  40. private List<SongFilterButton> _filterButtonGroup;
  41. private Button _addFavoriteButton;
  42. private SimpleDialogPromptViewController _simpleDialogPromptViewControllerPrefab;
  43. private SimpleDialogPromptViewController _deleteDialog;
  44. private Button _deleteButton;
  45. private Button _pageUpFastButton;
  46. private Button _pageDownFastButton;
  47. private Button _enterFolderButton;
  48. private Button _upFolderButton;
  49. private SearchKeyboardViewController _searchViewController;
  50. private PlaylistFlowCoordinator _playListFlowCoordinator;
  51. private TextMeshProUGUI _ppText;
  52. private TextMeshProUGUI _starText;
  53. // Cached items
  54. private Sprite _addFavoriteSprite;
  55. private Sprite _removeFavoriteSprite;
  56. private Sprite _currentAddFavoriteButtonSprite;
  57. // Plugin Compat checks
  58. private bool _detectedTwitchPluginQueue = false;
  59. private bool _checkedForTwitchPlugin = false;
  60. // Debug
  61. private int _sortButtonLastPushedIndex = 0;
  62. private int _lastRow = 0;
  63. // Model
  64. private SongBrowserModel _model;
  65. /// <summary>
  66. /// Constructor
  67. /// </summary>
  68. public SongBrowserUI() : base()
  69. {
  70. if (_model == null)
  71. {
  72. _model = new SongBrowserModel();
  73. }
  74. _model.Init();
  75. _sortButtonLastPushedIndex = (int)(_model.Settings.sortMode);
  76. }
  77. /// <summary>
  78. /// Builds the UI for this plugin.
  79. /// </summary>
  80. public void CreateUI()
  81. {
  82. _log.Trace("CreateUI()");
  83. try
  84. {
  85. if (_levelSelectionFlowCoordinator == null)
  86. {
  87. _levelSelectionFlowCoordinator = Resources.FindObjectsOfTypeAll<StandardLevelSelectionFlowCoordinator>().First();
  88. }
  89. if (_levelListViewController == null)
  90. {
  91. _levelListViewController = _levelSelectionFlowCoordinator.GetPrivateField<StandardLevelListViewController>("_levelListViewController");
  92. }
  93. if (_levelDetailViewController == null)
  94. {
  95. _levelDetailViewController = _levelSelectionFlowCoordinator.GetPrivateField<StandardLevelDetailViewController>("_levelDetailViewController");
  96. }
  97. if (_levelSelectionNavigationController == null)
  98. {
  99. _levelSelectionNavigationController = _levelSelectionFlowCoordinator.GetPrivateField<StandardLevelSelectionNavigationController>("_levelSelectionNavigationController");
  100. }
  101. if (_levelDifficultyViewController == null)
  102. {
  103. _levelDifficultyViewController = _levelSelectionFlowCoordinator.GetPrivateField<StandardLevelDifficultyViewController>("_levelDifficultyViewController");
  104. }
  105. if (_levelListTableView == null)
  106. {
  107. _levelListTableView = this._levelListViewController.GetComponentInChildren<StandardLevelListTableView>();
  108. }
  109. _playButton = _levelDetailViewController.GetComponentsInChildren<Button>().FirstOrDefault(x => x.name == "PlayButton");
  110. _simpleDialogPromptViewControllerPrefab = Resources.FindObjectsOfTypeAll<SimpleDialogPromptViewController>().First();
  111. this._deleteDialog = UnityEngine.Object.Instantiate<SimpleDialogPromptViewController>(this._simpleDialogPromptViewControllerPrefab);
  112. this._deleteDialog.gameObject.SetActive(false);
  113. this._addFavoriteSprite = Base64Sprites.Base64ToSprite(Base64Sprites.AddToFavoritesIcon);
  114. this._removeFavoriteSprite = Base64Sprites.Base64ToSprite(Base64Sprites.RemoveFromFavoritesIcon);
  115. this.CreateUIElements();
  116. _levelListViewController.didSelectLevelEvent += OnDidSelectLevelEvent;
  117. TableView tableView = ReflectionUtil.GetPrivateField<TableView>(_levelListTableView, "_tableView");
  118. tableView.didSelectRowEvent += HandleDidSelectTableViewRow;
  119. }
  120. catch (Exception e)
  121. {
  122. _log.Exception("Exception during CreateUI: ", e);
  123. }
  124. }
  125. /// <summary>
  126. /// Builds the SongBrowser UI
  127. /// </summary>
  128. private void CreateUIElements()
  129. {
  130. _log.Trace("CreateUIElements");
  131. try
  132. {
  133. // Gather some transforms and templates to use.
  134. RectTransform sortButtonTransform = this._levelSelectionNavigationController.transform as RectTransform;
  135. RectTransform otherButtonTransform = this._levelDetailViewController.transform as RectTransform;
  136. Button sortButtonTemplate = _playButton;
  137. Button otherButtonTemplate = Resources.FindObjectsOfTypeAll<Button>().First(x => (x.name == "QuitButton"));
  138. Sprite arrowIcon = SongBrowserApplication.Instance.CachedIcons["ArrowIcon"];
  139. // Resize some of the UI
  140. _tableViewRectTransform = _levelListViewController.GetComponentsInChildren<RectTransform>().First(x => x.name == "TableViewContainer");
  141. _tableViewRectTransform.sizeDelta = new Vector2(0f, -20f);
  142. _tableViewRectTransform.anchoredPosition = new Vector2(0f, -2.5f);
  143. _tableViewPageUpButton = _tableViewRectTransform.GetComponentsInChildren<Button>().First(x => x.name == "PageUpButton");
  144. (_tableViewPageUpButton.transform as RectTransform).anchoredPosition = new Vector2(0f, -1f);
  145. _tableViewPageDownButton = _tableViewRectTransform.GetComponentsInChildren<Button>().First(x => x.name == "PageDownButton");
  146. (_tableViewPageDownButton.transform as RectTransform).anchoredPosition = new Vector2(0f, 1f);
  147. // Create Sorting Songs By-Buttons
  148. _log.Debug("Creating sort by buttons...");
  149. float fontSize = 2.35f;
  150. float buttonWidth = 13.50f;
  151. float buttonHeight = 5.5f;
  152. float buttonX = -61;
  153. float buttonY = 74.0f;
  154. string[] sortButtonNames = new string[]
  155. {
  156. "Song", "Author", "Original", "Newest", "Plays", "PP", "Difficult", "Random"
  157. };
  158. SongSortMode[] sortModes = new SongSortMode[]
  159. {
  160. SongSortMode.Default, SongSortMode.Author, SongSortMode.Original, SongSortMode.Newest, SongSortMode.PlayCount, SongSortMode.PP, SongSortMode.Difficulty, SongSortMode.Random
  161. };
  162. _sortButtonGroup = new List<SongSortButton>();
  163. for (int i = 0; i < sortButtonNames.Length; i++)
  164. {
  165. _sortButtonGroup.Add(UIBuilder.CreateSortButton(sortButtonTransform, sortButtonTemplate, arrowIcon,
  166. sortButtonNames[i],
  167. fontSize,
  168. buttonX + (buttonWidth * i),
  169. buttonY,
  170. buttonWidth,
  171. buttonHeight,
  172. sortModes[i],
  173. onSortButtonClickEvent));
  174. }
  175. // Create filter buttons
  176. float filterButtonX = buttonX + (buttonWidth * (sortButtonNames.Length - 1)) + (buttonWidth / 2.0f) + 2.5f;
  177. Vector2 iconButtonSize = new Vector2(5.5f, buttonHeight);
  178. Sprite playlistSprite = Base64Sprites.Base64ToSprite(Base64Sprites.PlaylistIcon);
  179. Sprite searchSprite = Base64Sprites.Base64ToSprite(Base64Sprites.SearchIcon);
  180. List<Tuple<SongFilterMode, UnityEngine.Events.UnityAction, Sprite>> filterButtonSetup = new List<Tuple<SongFilterMode, UnityEngine.Events.UnityAction, Sprite>>()
  181. {
  182. Tuple.Create<SongFilterMode, UnityEngine.Events.UnityAction, Sprite>(SongFilterMode.Favorites, onFavoriteFilterButtonClickEvent, _addFavoriteSprite),
  183. Tuple.Create<SongFilterMode, UnityEngine.Events.UnityAction, Sprite>(SongFilterMode.Playlist, onPlaylistButtonClickEvent, playlistSprite),
  184. Tuple.Create<SongFilterMode, UnityEngine.Events.UnityAction, Sprite>(SongFilterMode.Search, onSearchButtonClickEvent, searchSprite),
  185. };
  186. _filterButtonGroup = new List<SongFilterButton>();
  187. for (int i = 0; i < filterButtonSetup.Count; i++)
  188. {
  189. Tuple<SongFilterMode, UnityEngine.Events.UnityAction, Sprite> t = filterButtonSetup[i];
  190. Button b = UIBuilder.CreateIconButton(sortButtonTransform, sortButtonTemplate,
  191. t.Item3,
  192. new Vector2(filterButtonX + (iconButtonSize.x * i), buttonY),
  193. new Vector2(iconButtonSize.x, iconButtonSize.y),
  194. new Vector2(0, 0),
  195. new Vector2(3.5f, 3.5f),
  196. new Vector2(1.0f, 1.0f),
  197. 0);
  198. SongFilterButton filterButton = new SongFilterButton();
  199. filterButton.Button = b;
  200. filterButton.FilterMode = t.Item1;
  201. b.onClick.AddListener(t.Item2);
  202. _filterButtonGroup.Add(filterButton);
  203. }
  204. // Create Add to Favorites Button
  205. Vector2 addFavoritePos = new Vector2(40f, (sortButtonTemplate.transform as RectTransform).anchoredPosition.y);
  206. _addFavoriteButton = UIBuilder.CreateIconButton(otherButtonTransform, otherButtonTemplate, null,
  207. new Vector2(addFavoritePos.x, addFavoritePos.y),
  208. new Vector2(10.0f, 10.0f),
  209. new Vector2(2f, -1.5f),
  210. new Vector2(7.0f, 7.0f),
  211. new Vector2(1.0f, 1.0f),
  212. 0.0f);
  213. _addFavoriteButton.onClick.AddListener(delegate () {
  214. ToggleSongInFavorites();
  215. });
  216. if (_currentAddFavoriteButtonSprite == null)
  217. {
  218. IStandardLevel level = this._levelListViewController.selectedLevel;
  219. if (level != null)
  220. {
  221. RefreshAddFavoriteButton(level.levelID);
  222. }
  223. }
  224. // Create delete button
  225. _deleteButton = UIBuilder.CreateButton(otherButtonTransform, otherButtonTemplate, "Delete", fontSize, 46f, 0f, 15f, 5f);
  226. _deleteButton.onClick.AddListener(delegate () {
  227. HandleDeleteSelectedLevel();
  228. });
  229. // Create fast scroll buttons
  230. _pageUpFastButton = UIBuilder.CreateIconButton(sortButtonTransform, otherButtonTemplate, arrowIcon,
  231. new Vector2(15, 67.5f),
  232. new Vector2(6.0f, 5.5f),
  233. new Vector2(0f, 0f),
  234. new Vector2(1.5f, 1.5f),
  235. new Vector2(2.0f, 2.0f),
  236. 180);
  237. _pageUpFastButton.onClick.AddListener(delegate () {
  238. this.JumpSongList(-1, SEGMENT_PERCENT);
  239. });
  240. _pageDownFastButton = UIBuilder.CreateIconButton(sortButtonTransform, otherButtonTemplate, arrowIcon,
  241. new Vector2(15, 0.5f),
  242. new Vector2(6.0f, 5.5f),
  243. new Vector2(0f, 0f),
  244. new Vector2(1.5f, 1.5f),
  245. new Vector2(2.0f, 2.0f),
  246. 0);
  247. _pageDownFastButton.onClick.AddListener(delegate () {
  248. this.JumpSongList(1, SEGMENT_PERCENT);
  249. });
  250. // Create enter folder button
  251. if (_model.Settings.folderSupportEnabled)
  252. {
  253. _enterFolderButton = UIBuilder.CreateUIButton(otherButtonTransform, _playButton);
  254. _enterFolderButton.onClick.AddListener(delegate ()
  255. {
  256. _model.PushDirectory(_levelListViewController.selectedLevel);
  257. this.RefreshSongList();
  258. this.RefreshDirectoryButtons();
  259. });
  260. UIBuilder.SetButtonText(ref _enterFolderButton, "Enter");
  261. // Create up folder button
  262. _upFolderButton = UIBuilder.CreateIconButton(sortButtonTransform, sortButtonTemplate, arrowIcon,
  263. new Vector2(filterButtonX + (iconButtonSize.x* filterButtonSetup.Count), buttonY),
  264. new Vector2(iconButtonSize.x, iconButtonSize.y),
  265. new Vector2(0f, 0f),
  266. new Vector2(0.85f, 0.85f),
  267. new Vector2(2.0f, 2.0f),
  268. 180);
  269. _upFolderButton.onClick.RemoveAllListeners();
  270. _upFolderButton.onClick.AddListener(delegate ()
  271. {
  272. _model.PopDirectory();
  273. this.RefreshSongList();
  274. this.RefreshDirectoryButtons();
  275. });
  276. }
  277. RefreshSortButtonUI();
  278. RefreshDirectoryButtons();
  279. }
  280. catch (Exception e)
  281. {
  282. _log.Exception("Exception CreateUIElements:", e);
  283. }
  284. }
  285. /// <summary>
  286. /// Sort button clicked.
  287. /// </summary>
  288. private void onSortButtonClickEvent(SongSortMode sortMode)
  289. {
  290. _log.Debug("Sort button - {0} - pressed.", sortMode.ToString());
  291. _model.LastSelectedLevelId = null;
  292. if (_model.Settings.sortMode == sortMode)
  293. {
  294. _model.ToggleInverting();
  295. }
  296. _model.Settings.sortMode = sortMode;
  297. _model.Settings.Save();
  298. //this._model.ProcessSongList();
  299. UpdateSongList();
  300. RefreshSongList();
  301. // Handle instant queue logic, avoid picking a folder.
  302. if (_model.Settings.sortMode == SongSortMode.Random && _model.Settings.randomInstantQueue)
  303. {
  304. for (int i = 0; i < _model.SortedSongList.Count; i++)
  305. {
  306. if (!_model.SortedSongList[i].levelID.StartsWith("Folder_"))
  307. {
  308. this.SelectAndScrollToLevel(_levelListTableView, _model.SortedSongList[i].levelID);
  309. this._levelDifficultyViewController.HandleDifficultyTableViewDidSelectRow(null, _model.SortedSongList[i].difficultyBeatmaps.Length-1);
  310. _playButton.onClick.Invoke();
  311. break;
  312. }
  313. }
  314. }
  315. }
  316. /// <summary>
  317. /// Filter by favorites.
  318. /// </summary>
  319. private void onFavoriteFilterButtonClickEvent()
  320. {
  321. _log.Debug("Filter button - {0} - pressed.", SongFilterMode.Favorites.ToString());
  322. if (_model.Settings.filterMode != SongFilterMode.Favorites)
  323. {
  324. _model.Settings.filterMode = SongFilterMode.Favorites;
  325. }
  326. else
  327. {
  328. _model.Settings.filterMode = SongFilterMode.None;
  329. }
  330. _model.Settings.Save();
  331. UpdateSongList();
  332. RefreshSongList();
  333. }
  334. /// <summary>
  335. /// Filter button clicked.
  336. /// </summary>
  337. /// <param name="sortMode"></param>
  338. private void onSearchButtonClickEvent()
  339. {
  340. _log.Debug("Filter button - {0} - pressed.", SongFilterMode.Search.ToString());
  341. if (_model.Settings.filterMode != SongFilterMode.Search)
  342. {
  343. _model.Settings.filterMode = SongFilterMode.Search;
  344. this.ShowSearchKeyboard();
  345. }
  346. else
  347. {
  348. _model.Settings.filterMode = SongFilterMode.None;
  349. UpdateSongList();
  350. RefreshSongList();
  351. }
  352. _model.Settings.Save();
  353. }
  354. /// <summary>
  355. /// Display the playlist selector.
  356. /// </summary>
  357. /// <param name="sortMode"></param>
  358. private void onPlaylistButtonClickEvent()
  359. {
  360. _log.Debug("Filter button - {0} - pressed.", SongFilterMode.Playlist.ToString());
  361. _model.LastSelectedLevelId = null;
  362. if (_model.Settings.filterMode != SongFilterMode.Playlist)
  363. {
  364. if (_playListFlowCoordinator == null || !_playListFlowCoordinator.isActiveAndEnabled)
  365. {
  366. _playListFlowCoordinator = UIBuilder.CreateFlowCoordinator<PlaylistFlowCoordinator>("PlaylistFlowCoordinator");
  367. _playListFlowCoordinator.didSelectPlaylist += HandleDidSelectPlaylist;
  368. _playListFlowCoordinator.Present(_levelSelectionNavigationController);
  369. }
  370. }
  371. else
  372. {
  373. _model.Settings.filterMode = SongFilterMode.None;
  374. _model.Settings.Save();
  375. UpdateSongList();
  376. RefreshSongList();
  377. }
  378. }
  379. /// <summary>
  380. /// Adjust UI based on level selected.
  381. /// Various ways of detecting if a level is not properly selected. Seems most hit the first one.
  382. /// </summary>
  383. private void OnDidSelectLevelEvent(StandardLevelListViewController view, IStandardLevel level)
  384. {
  385. try
  386. {
  387. _log.Trace("OnDidSelectLevelEvent()");
  388. if (level == null)
  389. {
  390. _log.Debug("No level selected?");
  391. return;
  392. }
  393. if (_model.Settings == null)
  394. {
  395. _log.Debug("Settings not instantiated yet?");
  396. return;
  397. }
  398. _model.LastSelectedLevelId = level.levelID;
  399. RefreshAddFavoriteButton(level.levelID);
  400. RefreshQuickScrollButtons();
  401. if (level.levelID.StartsWith("Folder_"))
  402. {
  403. _log.Debug("Folder selected! Adjust PlayButton logic...");
  404. HandleDidSelectFolderRow(level);
  405. }
  406. else
  407. {
  408. HandleDidSelectLevelRow(level);
  409. }
  410. }
  411. catch (Exception e)
  412. {
  413. _log.Exception("Exception selecting song:", e);
  414. }
  415. }
  416. /// <summary>
  417. /// Turn play button into enter folder button.
  418. /// </summary>
  419. private void HandleDidSelectFolderRow(IStandardLevel level)
  420. {
  421. _enterFolderButton.gameObject.SetActive(true);
  422. _playButton.gameObject.SetActive(false);
  423. }
  424. /// <summary>
  425. /// Turn enter folder button into play button.
  426. /// </summary>
  427. /// <param name="level"></param>
  428. private void HandleDidSelectLevelRow(IStandardLevel level)
  429. {
  430. // deal with enter folder button
  431. if (_enterFolderButton != null)
  432. {
  433. _enterFolderButton.gameObject.SetActive(false);
  434. }
  435. _playButton.gameObject.SetActive(true);
  436. // display pp potentially
  437. if (this._model.LevelIdToScoreSaberData != null && this._levelDifficultyViewController.selectedDifficultyLevel != null)
  438. {
  439. if (this._ppText == null)
  440. {
  441. // Create the PP and Star rating labels
  442. //RectTransform bmpTextRect = Resources.FindObjectsOfTypeAll<RectTransform>().First(x => x.name == "BPMText");
  443. var text = UIBuilder.CreateText(this._levelDetailViewController.rectTransform, "PP", new Vector2(-5, -41), new Vector2(20f, 10f));
  444. text.fontSize = 3.5f;
  445. text.alignment = TextAlignmentOptions.Left;
  446. text = UIBuilder.CreateText(this._levelDetailViewController.rectTransform, "STAR", new Vector2(-5, -22), new Vector2(20f, 10f));
  447. text.fontSize = 3.5f;
  448. text.alignment = TextAlignmentOptions.Left;
  449. RectTransform bmpValueTextRect = Resources.FindObjectsOfTypeAll<RectTransform>().First(x => x.name == "BPMValueText");
  450. _ppText = UIBuilder.CreateText(this._levelDetailViewController.rectTransform, "?", new Vector2(bmpValueTextRect.anchoredPosition.x, -41), new Vector2(39f, 10f));
  451. _ppText.fontSize = 3.5f;
  452. _ppText.alignment = TextAlignmentOptions.Right;
  453. _starText = UIBuilder.CreateText(this._levelDetailViewController.rectTransform, "", new Vector2(bmpValueTextRect.anchoredPosition.x, -22), new Vector2(39f, 10f));
  454. _starText.fontSize = 3.5f;
  455. _starText.alignment = TextAlignmentOptions.Right;
  456. }
  457. LevelDifficulty difficulty = this._levelDifficultyViewController.selectedDifficultyLevel.difficulty;
  458. string difficultyString = difficulty.ToString();
  459. _log.Debug("Checking if have info for song {0}", level.songName);
  460. if (this._model.LevelIdToScoreSaberData.ContainsKey(level.levelID))
  461. {
  462. _log.Debug("Checking if have difficulty for song {0} difficulty {1}", level.songName, difficultyString);
  463. ScoreSaberData ppData = this._model.LevelIdToScoreSaberData[level.levelID];
  464. if (ppData.difficultyToSaberDifficulty.ContainsKey(difficultyString))
  465. {
  466. _log.Debug("Display pp for song.");
  467. float pp = ppData.difficultyToSaberDifficulty[difficultyString].pp;
  468. float star = ppData.difficultyToSaberDifficulty[difficultyString].star;
  469. _ppText.SetText(String.Format("{0:0.##}", pp));
  470. _starText.SetText(String.Format("{0:0.##}", star));
  471. }
  472. else
  473. {
  474. _ppText.SetText("?");
  475. _starText.SetText("?");
  476. }
  477. }
  478. else
  479. {
  480. _ppText.SetText("?");
  481. _starText.SetText("?");
  482. }
  483. }
  484. }
  485. /// <summary>
  486. /// Track the current row.
  487. /// </summary>
  488. /// <param name="tableView"></param>
  489. /// <param name="row"></param>
  490. private void HandleDidSelectTableViewRow(TableView tableView, int row)
  491. {
  492. _log.Trace("HandleDidSelectTableViewRow({0})", row);
  493. _lastRow = row;
  494. }
  495. /// <summary>
  496. /// Pop up a delete dialog.
  497. /// </summary>
  498. private void HandleDeleteSelectedLevel()
  499. {
  500. IStandardLevel level = this._levelListViewController.selectedLevel;
  501. if (level == null)
  502. {
  503. _log.Info("No level selected, cannot delete nothing...");
  504. return;
  505. }
  506. if (level.levelID.StartsWith("Level"))
  507. {
  508. _log.Debug("Cannot delete non-custom levels.");
  509. return;
  510. }
  511. if (level.levelID.StartsWith("Folder"))
  512. {
  513. _log.Debug("Cannot delete folders.");
  514. return;
  515. }
  516. SongLoaderPlugin.OverrideClasses.CustomLevel customLevel = _model.LevelIdToCustomSongInfos[level.levelID];
  517. this._deleteDialog.Init("Delete level warning!", String.Format("<color=#00AAFF>Permanently delete level: {0}</color>\n Do you want to continue?", customLevel.songName), "YES", "NO");
  518. this._deleteDialog.didFinishEvent += this.HandleDeleteDialogPromptViewControllerDidFinish;
  519. this._levelSelectionNavigationController.PresentModalViewController(this._deleteDialog, null, false);
  520. }
  521. /// <summary>
  522. /// Handle delete dialog resolution.
  523. /// </summary>
  524. /// <param name="viewController"></param>
  525. /// <param name="ok"></param>
  526. public void HandleDeleteDialogPromptViewControllerDidFinish(SimpleDialogPromptViewController viewController, bool ok)
  527. {
  528. viewController.didFinishEvent -= this.HandleDeleteDialogPromptViewControllerDidFinish;
  529. if (!ok)
  530. {
  531. viewController.DismissModalViewController(null, false);
  532. }
  533. else
  534. {
  535. string customSongsPath = Path.Combine(Environment.CurrentDirectory, "CustomSongs");
  536. IStandardLevel level = this._levelListViewController.selectedLevel;
  537. SongLoaderPlugin.OverrideClasses.CustomLevel customLevel = _model.LevelIdToCustomSongInfos[level.levelID];
  538. string songPath = customLevel.customSongInfo.path;
  539. bool isZippedSong = false;
  540. viewController.DismissModalViewController(null, false);
  541. _log.Debug("Deleting: {0}", songPath);
  542. if (!string.IsNullOrEmpty(songPath) && songPath.Contains("/.cache/"))
  543. {
  544. isZippedSong = true;
  545. }
  546. if (isZippedSong)
  547. {
  548. DirectoryInfo songHashDir = Directory.GetParent(songPath);
  549. _log.Debug("Deleting zipped song cache: {0}", songHashDir.FullName);
  550. Directory.Delete(songHashDir.FullName, true);
  551. foreach (string file in Directory.GetFiles(customSongsPath, "*.zip"))
  552. {
  553. string hash = CreateMD5FromFile(file);
  554. if (hash != null)
  555. {
  556. if (hash == songHashDir.Name)
  557. {
  558. _log.Debug("Deleting zipped song: {0}", file);
  559. File.Delete(file);
  560. break;
  561. }
  562. }
  563. }
  564. }
  565. else
  566. {
  567. // Just delete the song we know about.
  568. FileAttributes attr = File.GetAttributes(songPath);
  569. if (attr.HasFlag(FileAttributes.Directory))
  570. {
  571. _log.Debug("Deleting song: {0}", songPath);
  572. Directory.Delete(songPath, true);
  573. }
  574. // check if this is in the BeatSaberDownloader format
  575. if (_model.Settings.deleteNumberedSongFolder)
  576. {
  577. String[] splitPath = songPath.Split('/');
  578. if (splitPath.Length > 2)
  579. {
  580. String numberedDir = splitPath[splitPath.Length - 2];
  581. Regex r = new Regex(@"^\d{1,}-\d{1,}");
  582. if (r.Match(numberedDir).Success)
  583. {
  584. DirectoryInfo songNumberedDirPath = Directory.GetParent(songPath);
  585. _log.Debug("Deleting song numbered folder: {0}", songNumberedDirPath.FullName);
  586. Directory.Delete(songNumberedDirPath.FullName, true);
  587. }
  588. }
  589. }
  590. }
  591. int newRow = _model.SortedSongList.FindIndex(x => x.levelID == level.levelID) - 1;
  592. if (newRow > 0 && newRow < _model.SortedSongList.Count)
  593. {
  594. _model.LastSelectedLevelId = _model.SortedSongList[newRow].levelID;
  595. }
  596. else
  597. {
  598. _model.LastSelectedLevelId = null;
  599. }
  600. SongLoaderPlugin.SongLoader.Instance.RemoveSongWithPath(songPath);
  601. this.UpdateSongList();
  602. this.RefreshSongList();
  603. }
  604. }
  605. /// <summary>
  606. /// Create MD5 of a file.
  607. /// </summary>
  608. /// <param name="path"></param>
  609. /// <returns></returns>
  610. public static string CreateMD5FromFile(string path)
  611. {
  612. string hash = "";
  613. if (!File.Exists(path)) return null;
  614. using (MD5 md5 = MD5.Create())
  615. {
  616. using (var stream = File.OpenRead(path))
  617. {
  618. byte[] hashBytes = md5.ComputeHash(stream);
  619. StringBuilder sb = new StringBuilder();
  620. foreach (byte hashByte in hashBytes)
  621. {
  622. sb.Append(hashByte.ToString("X2"));
  623. }
  624. hash = sb.ToString();
  625. return hash;
  626. }
  627. }
  628. }
  629. /// <summary>
  630. /// Handle selection of a playlist. Show just the songs in the playlist.
  631. /// </summary>
  632. /// <param name="p"></param>
  633. private void HandleDidSelectPlaylist(Playlist p)
  634. {
  635. if (_playListFlowCoordinator != null)
  636. {
  637. _levelSelectionNavigationController.PopViewControllerImmediately();
  638. _playListFlowCoordinator.gameObject.SetActive(false);
  639. UnityEngine.Object.DestroyImmediate(_playListFlowCoordinator);
  640. }
  641. if (p != null)
  642. {
  643. _log.Debug("Showing songs for playlist: {0}", p.playlistTitle);
  644. _model.Settings.filterMode = SongFilterMode.Playlist;
  645. _model.CurrentPlaylist = p;
  646. _model.Settings.Save();
  647. }
  648. else
  649. {
  650. _log.Debug("No playlist selected");
  651. }
  652. this.UpdateSongList();
  653. this.RefreshSongList();
  654. }
  655. /// <summary>
  656. /// Display the search keyboard
  657. /// </summary>
  658. void ShowSearchKeyboard()
  659. {
  660. if (_searchViewController == null)
  661. {
  662. _searchViewController = UIBuilder.CreateViewController<SearchKeyboardViewController>("SearchKeyboardViewController");
  663. _searchViewController.searchButtonPressed += SearchViewControllerSearchButtonPressed;
  664. _searchViewController.backButtonPressed += SearchViewControllerbackButtonPressed;
  665. }
  666. _log.Debug("Presenting keyboard");
  667. _levelListViewController.navigationController.PresentModalViewController(_searchViewController, null, false);
  668. }
  669. /// <summary>
  670. /// Handle back button event from search keyboard.
  671. /// </summary>
  672. private void SearchViewControllerbackButtonPressed()
  673. {
  674. }
  675. /// <summary>
  676. /// Handle search.
  677. /// </summary>
  678. /// <param name="searchFor"></param>
  679. private void SearchViewControllerSearchButtonPressed(string searchFor)
  680. {
  681. _log.Debug("Searching for \"{0}\"...", searchFor);
  682. _model.Settings.searchTerms.Insert(0, searchFor);
  683. _model.Settings.Save();
  684. _model.LastSelectedLevelId = null;
  685. this.UpdateSongList();
  686. this.RefreshSongList();
  687. }
  688. /// <summary>
  689. /// Make big jumps in the song list.
  690. /// </summary>
  691. /// <param name="numJumps"></param>
  692. private void JumpSongList(int numJumps, float segmentPercent)
  693. {
  694. int totalSize = _model.SortedSongList.Count;
  695. int segmentSize = (int)(totalSize * segmentPercent);
  696. // Jump at least one scree size.
  697. if (segmentSize < LIST_ITEMS_VISIBLE_AT_ONCE)
  698. {
  699. segmentSize = LIST_ITEMS_VISIBLE_AT_ONCE;
  700. }
  701. TableView tableView = ReflectionUtil.GetPrivateField<TableView>(_levelListTableView, "_tableView");
  702. int jumpDirection = Math.Sign(numJumps);
  703. int newRow = _lastRow + (jumpDirection * segmentSize);
  704. if (newRow <= 0)
  705. {
  706. newRow = 0;
  707. }
  708. else if (newRow >= totalSize)
  709. {
  710. newRow = totalSize - 1;
  711. }
  712. _log.Debug("jumpDirection: {0}, newRow: {1}", jumpDirection, newRow);
  713. _lastRow = newRow;
  714. this.SelectAndScrollToLevel(_levelListTableView, _model.SortedSongList[newRow].levelID);
  715. }
  716. /// <summary>
  717. /// Add/Remove song from favorites depending on if it already exists.
  718. /// </summary>
  719. private void ToggleSongInFavorites()
  720. {
  721. IStandardLevel songInfo = this._levelListViewController.selectedLevel;
  722. if (_model.Settings.Favorites.Contains(songInfo.levelID))
  723. {
  724. _log.Info("Remove {0} from favorites", songInfo.songName);
  725. _model.Settings.Favorites.Remove(songInfo.levelID);
  726. }
  727. else
  728. {
  729. _log.Info("Add {0} to favorites", songInfo.songName);
  730. _model.Settings.Favorites.Add(songInfo.levelID);
  731. }
  732. RefreshAddFavoriteButton(songInfo.levelID);
  733. _model.Settings.Save();
  734. }
  735. /// <summary>
  736. /// Update interactive state of the quick scroll buttons.
  737. /// </summary>
  738. private void RefreshQuickScrollButtons()
  739. {
  740. // Refresh the fast scroll buttons
  741. _pageUpFastButton.interactable = _tableViewPageUpButton.interactable;
  742. _pageUpFastButton.gameObject.SetActive(_tableViewPageUpButton.IsActive());
  743. _pageDownFastButton.interactable = _tableViewPageDownButton.interactable;
  744. _pageDownFastButton.gameObject.SetActive(_tableViewPageDownButton.IsActive());
  745. }
  746. /// <summary>
  747. /// Helper to quickly refresh add to favorites button
  748. /// </summary>
  749. /// <param name="levelId"></param>
  750. private void RefreshAddFavoriteButton(String levelId)
  751. {
  752. if (levelId == null)
  753. {
  754. _currentAddFavoriteButtonSprite = null;
  755. }
  756. else
  757. {
  758. if (_model.Settings.Favorites.Contains(levelId))
  759. {
  760. _currentAddFavoriteButtonSprite = _removeFavoriteSprite;
  761. }
  762. else
  763. {
  764. _currentAddFavoriteButtonSprite = _addFavoriteSprite;
  765. }
  766. }
  767. UIBuilder.SetButtonIcon(ref _addFavoriteButton, _currentAddFavoriteButtonSprite);
  768. }
  769. /// <summary>
  770. /// Adjust the UI colors.
  771. /// </summary>
  772. public void RefreshSortButtonUI()
  773. {
  774. // So far all we need to refresh is the sort buttons.
  775. foreach (SongSortButton sortButton in _sortButtonGroup)
  776. {
  777. UIBuilder.SetButtonBorder(ref sortButton.Button, Color.black);
  778. if (sortButton.SortMode == _model.Settings.sortMode)
  779. {
  780. if (_model.InvertingResults)
  781. {
  782. UIBuilder.SetButtonBorder(ref sortButton.Button, Color.red);
  783. }
  784. else
  785. {
  786. UIBuilder.SetButtonBorder(ref sortButton.Button, Color.green);
  787. }
  788. }
  789. }
  790. // refresh filter buttons
  791. foreach (SongFilterButton filterButton in _filterButtonGroup)
  792. {
  793. UIBuilder.SetButtonBorder(ref filterButton.Button, Color.clear);
  794. if (filterButton.FilterMode == _model.Settings.filterMode)
  795. {
  796. UIBuilder.SetButtonBorder(ref filterButton.Button, Color.green);
  797. }
  798. }
  799. }
  800. /// <summary>
  801. /// Refresh the UI state of any directory buttons.
  802. /// </summary>
  803. public void RefreshDirectoryButtons()
  804. {
  805. // bail if no button, likely folder support not enabled.
  806. if (_upFolderButton == null)
  807. {
  808. return;
  809. }
  810. if (_model.DirStackSize > 1)
  811. {
  812. _upFolderButton.interactable = true;
  813. }
  814. else
  815. {
  816. _upFolderButton.interactable = false;
  817. }
  818. }
  819. /// <summary>
  820. /// Try to refresh the song list. Broken for now.
  821. /// </summary>
  822. public void RefreshSongList()
  823. {
  824. _log.Info("Refreshing the song list view.");
  825. try
  826. {
  827. if (_model.SortedSongList == null)
  828. {
  829. _log.Debug("Songs are not sorted yet, nothing to refresh.");
  830. return;
  831. }
  832. StandardLevelSO[] levels = _model.SortedSongList.ToArray();
  833. StandardLevelListViewController songListViewController = this._levelSelectionFlowCoordinator.GetPrivateField<StandardLevelListViewController>("_levelListViewController");
  834. ReflectionUtil.SetPrivateField(_levelListTableView, "_levels", levels);
  835. ReflectionUtil.SetPrivateField(songListViewController, "_levels", levels);
  836. TableView tableView = ReflectionUtil.GetPrivateField<TableView>(_levelListTableView, "_tableView");
  837. tableView.ReloadData();
  838. String selectedLevelID = null;
  839. if (_model.LastSelectedLevelId != null)
  840. {
  841. selectedLevelID = _model.LastSelectedLevelId;
  842. _log.Debug("Scrolling to row for level ID: {0}", selectedLevelID);
  843. }
  844. else
  845. {
  846. if (levels.Length > 0)
  847. {
  848. selectedLevelID = levels.FirstOrDefault().levelID;
  849. }
  850. }
  851. // HACK, seems like if 6 or less items scrolling to row causes the song list to disappear.
  852. if (levels.Length > 6 && !String.IsNullOrEmpty(selectedLevelID) && levels.Any(x => x.levelID == selectedLevelID))
  853. {
  854. SelectAndScrollToLevel(_levelListTableView, selectedLevelID);
  855. }
  856. RefreshSortButtonUI();
  857. RefreshQuickScrollButtons();
  858. }
  859. catch (Exception e)
  860. {
  861. _log.Exception("Exception refreshing song list:", e);
  862. }
  863. }
  864. /// <summary>
  865. /// Scroll TableView to proper row, fire events.
  866. /// </summary>
  867. /// <param name="table"></param>
  868. /// <param name="levelID"></param>
  869. private void SelectAndScrollToLevel(StandardLevelListTableView table, string levelID)
  870. {
  871. // Check once per load
  872. if (!_checkedForTwitchPlugin)
  873. {
  874. _log.Info("Checking for BeatSaber Twitch Integration Plugin...");
  875. // Try to detect BeatSaber Twitch Integration Plugin
  876. _detectedTwitchPluginQueue = Resources.FindObjectsOfTypeAll<VRUIViewController>().Any(x => x.name == "RequestInfo");
  877. _log.Info("BeatSaber Twitch Integration plugin detected: " + _detectedTwitchPluginQueue);
  878. _checkedForTwitchPlugin = true;
  879. }
  880. // Skip scrolling to level if twitch plugin has queue active.
  881. if (_detectedTwitchPluginQueue)
  882. {
  883. _log.Debug("Skipping SelectAndScrollToLevel() because we detected Twitch Integrtion Plugin has a Queue active...");
  884. return;
  885. }
  886. int row = table.RowNumberForLevelID(levelID);
  887. TableView tableView = table.GetComponentInChildren<TableView>();
  888. tableView.SelectRow(row, true);
  889. tableView.ScrollToRow(row, true);
  890. _lastRow = row;
  891. }
  892. /// <summary>
  893. /// Helper for updating the model (which updates the song list)c
  894. /// </summary>
  895. public void UpdateSongList()
  896. {
  897. _log.Trace("UpdateSongList()");
  898. GameplayMode gameplayMode = _levelSelectionFlowCoordinator.GetPrivateField<GameplayMode>("_gameplayMode");
  899. _model.UpdateSongLists(gameplayMode);
  900. this.RefreshDirectoryButtons();
  901. }
  902. /// <summary>
  903. /// Not normally called by the game-engine. Dependent on SongBrowserApplication to call it.
  904. /// </summary>
  905. public void LateUpdate()
  906. {
  907. CheckDebugUserInput();
  908. }
  909. /// <summary>
  910. /// Map some key presses directly to UI interactions to make testing easier.
  911. /// </summary>
  912. private void CheckDebugUserInput()
  913. {
  914. try
  915. {
  916. if (this._levelListViewController != null && this._levelListViewController.isActiveAndEnabled)
  917. {
  918. bool isShiftKeyDown = Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift);
  919. // back
  920. if (Input.GetKeyDown(KeyCode.Escape))
  921. {
  922. this._levelSelectionNavigationController.DismissButtonWasPressed();
  923. }
  924. // cycle sort modes
  925. if (Input.GetKeyDown(KeyCode.T))
  926. {
  927. _sortButtonLastPushedIndex = (_sortButtonLastPushedIndex + 1) % _sortButtonGroup.Count;
  928. _sortButtonGroup[_sortButtonLastPushedIndex].Button.onClick.Invoke();
  929. }
  930. // select current sort mode again (toggle inverting)
  931. if (Input.GetKeyDown(KeyCode.Y))
  932. {
  933. _sortButtonGroup[_sortButtonLastPushedIndex].Button.onClick.Invoke();
  934. }
  935. // filter playlists
  936. if (Input.GetKeyDown(KeyCode.P))
  937. {
  938. onPlaylistButtonClickEvent();
  939. }
  940. // filter search
  941. if (Input.GetKeyDown(KeyCode.S))
  942. {
  943. onSearchButtonClickEvent();
  944. }
  945. // filter favorites
  946. if (Input.GetKeyDown(KeyCode.F))
  947. {
  948. onFavoriteFilterButtonClickEvent();
  949. }
  950. // delete
  951. if (Input.GetKeyDown(KeyCode.D))
  952. {
  953. _deleteButton.onClick.Invoke();
  954. }
  955. // c - select difficulty for top song
  956. if (Input.GetKeyDown(KeyCode.C))
  957. {
  958. this.SelectAndScrollToLevel(_levelListTableView, _model.SortedSongList[0].levelID);
  959. this._levelDifficultyViewController.HandleDifficultyTableViewDidSelectRow(null, 0);
  960. this._levelSelectionFlowCoordinator.HandleDifficultyViewControllerDidSelectDifficulty(_levelDifficultyViewController, _model.SortedSongList[0].GetDifficultyLevel(LevelDifficulty.Easy));
  961. }
  962. // v start a song or enter a folder
  963. if (Input.GetKeyDown(KeyCode.Return))
  964. {
  965. if (_playButton.isActiveAndEnabled)
  966. {
  967. _playButton.onClick.Invoke();
  968. }
  969. else if (_enterFolderButton.isActiveAndEnabled)
  970. {
  971. _enterFolderButton.onClick.Invoke();
  972. }
  973. }
  974. // backspace - up a folder
  975. if (Input.GetKeyDown(KeyCode.Backspace))
  976. {
  977. _upFolderButton.onClick.Invoke();
  978. }
  979. // change song index
  980. if (isShiftKeyDown && Input.GetKeyDown(KeyCode.N))
  981. {
  982. _pageUpFastButton.onClick.Invoke();
  983. }
  984. else if (Input.GetKeyDown(KeyCode.N))
  985. {
  986. _lastRow = (_lastRow - 1) != -1 ? (_lastRow - 1) % this._model.SortedSongList.Count : 0;
  987. this.SelectAndScrollToLevel(_levelListTableView, _model.SortedSongList[_lastRow].levelID);
  988. }
  989. if (isShiftKeyDown && Input.GetKeyDown(KeyCode.M))
  990. {
  991. _pageDownFastButton.onClick.Invoke();
  992. }
  993. else if (Input.GetKeyDown(KeyCode.M))
  994. {
  995. _lastRow = (_lastRow + 1) % this._model.SortedSongList.Count;
  996. this.SelectAndScrollToLevel(_levelListTableView, _model.SortedSongList[_lastRow].levelID);
  997. }
  998. // add to favorites
  999. if (Input.GetKeyDown(KeyCode.KeypadPlus))
  1000. {
  1001. ToggleSongInFavorites();
  1002. }
  1003. }
  1004. else if (_deleteDialog != null && _deleteDialog.isInViewControllerHierarchy)
  1005. {
  1006. // accept delete
  1007. if (Input.GetKeyDown(KeyCode.Return))
  1008. {
  1009. _deleteDialog.GetPrivateField<TextMeshProButton>("_okButton").button.onClick.Invoke();
  1010. }
  1011. if (Input.GetKeyDown(KeyCode.Escape))
  1012. {
  1013. _deleteDialog.GetPrivateField<TextMeshProButton>("_cancelButton").button.onClick.Invoke();
  1014. }
  1015. }
  1016. }
  1017. catch (Exception e)
  1018. {
  1019. _log.Exception("Debug Input caused Exception: ", e);
  1020. }
  1021. }
  1022. }
  1023. }