diff --git a/BSIPA-ModList/BSIPA-ModList.csproj b/BSIPA-ModList/BSIPA-ModList.csproj index f69147ae..48171cb8 100644 --- a/BSIPA-ModList/BSIPA-ModList.csproj +++ b/BSIPA-ModList/BSIPA-ModList.csproj @@ -67,14 +67,19 @@ + + + + + diff --git a/BSIPA-ModList/DownloadController.cs b/BSIPA-ModList/DownloadController.cs new file mode 100644 index 00000000..12ad9880 --- /dev/null +++ b/BSIPA-ModList/DownloadController.cs @@ -0,0 +1,216 @@ +using IPA.Config; +using IPA.Updating.BeatMods; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; +using static IPA.Updating.BeatMods.Updater; + +namespace BSIPA_ModList +{ + internal class DownloadObject + { + public enum States + { + ToDownload, Downloading, Installing, Failed, Completed + } + + public DependencyObject Mod; + public Sprite Icon; + public States State = States.ToDownload; + public double Progress = 0; + } + + internal class DownloadController : MonoBehaviour + { + private static DownloadController _instance; + public static DownloadController Instance + { + get + { + if (_instance == null) + _instance = Create(); + + return _instance; + } + } + + public static DownloadController Create() + { + var inst = new GameObject("BSIPA Modlist Download Controller").AddComponent(); + if (SelfConfig.SelfConfigRef.Value.Updates.AutoCheckUpdates) + inst.StartCoroutine(inst.StartUpdateCheck()); + return inst; + } + + private IEnumerator StartUpdateCheck() + { + yield return null; + CheckForUpdates(); + } + + private readonly List downloads = new List(); + private readonly Dictionary lookup = new Dictionary(); + + internal IReadOnlyList Downloads => downloads; + + public event Action OnCheckForUpdates; + public event Action OnCheckForUpdatesComplete; + public event Action OnDownloadStateChanged; + public event Action OnDownloaderListChanged; + + private enum States + { + Start, Checking, UpdatesFound, Downloading, Done + } + + private States _state = States.Start; + private States State + { + get => _state; + set + { + _state = value; + OnDownloadStateChanged?.Invoke(); + } + } + + public bool CanCheck => State == States.Start || State == States.Done; + public bool CanDownload => State == States.UpdatesFound; + public bool CanReset => State == States.UpdatesFound; + public bool IsChecking => State == States.Checking; + public bool IsDownloading => State == States.Downloading; + public bool IsDone => State == States.Done; + + public void Awake() => DontDestroyOnLoad(this); + + public void CheckForUpdates() + { + if (!CanCheck) + throw new InvalidOperationException("Invalid state for CheckForUpdates to be called"); + + State = States.Checking; + OnCheckForUpdates?.Invoke(); + Updater.Instance.CheckForUpdates(UpdateCheckComplete); + } + + public void ResetCheck(bool resetCache = false) + { + if (!CanReset) + throw new InvalidOperationException("Invalid state for ResetCheck to be called"); + + Clear(); + State = States.Start; + + if (resetCache) + ResetRequestCache(); + } + + private void Clear() + { + downloads.Clear(); + lookup.Clear(); + + OnDownloaderListChanged?.Invoke(); + } + + private void Add(DownloadObject obj) + { + downloads.Add(obj); + lookup.Add(obj.Mod, obj); + } + + private void Remove(DependencyObject obj) + { + downloads.Remove(lookup[obj]); + lookup.Remove(obj); + OnDownloaderListChanged?.Invoke(); + } + + private void UpdateCheckComplete(List found) + { + State = States.UpdatesFound; + OnCheckForUpdatesComplete?.Invoke(found.Count); + + foreach (var dep in found) + Add(new DownloadObject + { + Mod = dep, + Icon = Utilities.GetIcon(dep.LocalPluginMeta?.Metadata), + State = DownloadObject.States.ToDownload, + Progress = 0 + }); + + OnDownloaderListChanged?.Invoke(); + + if (SelfConfig.SelfConfigRef.Value.Updates.AutoUpdate) + StartDownloads(); + } + + public void StartDownloads() + { + if (!CanDownload) + throw new InvalidOperationException("Invalid state for StartDownloads to be called"); + + State = States.Downloading; + Updater.Instance.StartDownload(downloads.Select(d => d.Mod), _DownloadStart, _DownloadProgress, + _DownloadFailed, _DownloadFinished, _InstallFailed, _InstallFinished); + + if (downloads.Count == 0) + OnAllDownloadsCompleted(); + } + + private void _DownloadStart(DependencyObject obj) + { + var dl = lookup[obj]; + dl.Progress = 0; + dl.State = DownloadObject.States.Downloading; + } + + private void _DownloadProgress(DependencyObject obj, long totalBytes, long currentBytes, double progress) + { + lookup[obj].Progress = progress; + } + + private void _DownloadFailed(DependencyObject obj, string error) + { + lookup[obj].State = DownloadObject.States.Failed; + } + + private void _DownloadFinished(DependencyObject obj) + { + lookup[obj].State = DownloadObject.States.Installing; + } + + private void _InstallFailed(DependencyObject obj, Exception error) + { + lookup[obj].State = DownloadObject.States.Failed; + } + + private void _InstallFinished(DependencyObject obj, bool didError) + { + if (!didError) + lookup[obj].State = DownloadObject.States.Completed; + + StartCoroutine(RemoveModFromList(obj)); + } + + private IEnumerator RemoveModFromList(DependencyObject obj) + { + yield return new WaitForSeconds(0.25f); + + Remove(obj); + + if (downloads.Count == 0) + OnAllDownloadsCompleted(); + } + + private void OnAllDownloadsCompleted() + { + State = States.Done; + } + } +} diff --git a/BSIPA-ModList/Plugin.cs b/BSIPA-ModList/Plugin.cs index a6067db1..232d49ae 100644 --- a/BSIPA-ModList/Plugin.cs +++ b/BSIPA-ModList/Plugin.cs @@ -31,6 +31,7 @@ namespace BSIPA_ModList public void OnApplicationStart() { + } public void OnFixedUpdate() @@ -41,6 +42,8 @@ namespace BSIPA_ModList { if (scene.name == "MenuCore") { + FloatingNotification.Create(); + if (ButtonUI.Instance == null) { Logger.log.Debug("Creating Menu"); diff --git a/BSIPA-ModList/UI/ButtonUI.cs b/BSIPA-ModList/UI/ButtonUI.cs index e44803ce..d4c587a8 100644 --- a/BSIPA-ModList/UI/ButtonUI.cs +++ b/BSIPA-ModList/UI/ButtonUI.cs @@ -46,7 +46,6 @@ namespace BSIPA_ModList.UI StartCoroutine(AddModListButton()); } - private static MainFlowCoordinator mainFlow; private static ModListFlowCoordinator menuFlow; private static readonly WaitUntil _bottomPanelExists = new WaitUntil(() => GameObject.Find(ControllerPanel) != null); @@ -65,8 +64,6 @@ namespace BSIPA_ModList.UI lock (Instance) { - if (mainFlow == null) - mainFlow = Resources.FindObjectsOfTypeAll().First(); if (menuFlow == null) menuFlow = new GameObject("BSIPA Mod List Flow Controller").AddComponent(); if (panel == null) @@ -77,7 +74,7 @@ namespace BSIPA_ModList.UI button = BeatSaberUI.CreateUIButton(panel, CopyButton, () => { Logger.log.Debug("Presenting own flow controller"); - menuFlow.PresentOn(mainFlow); + menuFlow.Present(); }, "Mod List"); panel.Find(CopyButton).SetAsLastSibling(); diff --git a/BSIPA-ModList/UI/DownloadProgressCell.cs b/BSIPA-ModList/UI/DownloadProgressCell.cs new file mode 100644 index 00000000..08a37cd7 --- /dev/null +++ b/BSIPA-ModList/UI/DownloadProgressCell.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using TMPro; +using System.Threading.Tasks; +using IPA.Updating.BeatMods; +using UnityEngine; + +namespace BSIPA_ModList.UI +{ + + // originally ripped verbatim from Andruzz's BeatSaverDownloader + internal class DownloadProgressCell : LevelListTableCell + { + private DownloadObject mod; + + protected override void Awake() + { + base.Awake(); + } + + public void Init(DownloadObject mod) + { + Destroy(GetComponent()); + + reuseIdentifier = "DownloadCell"; + + this.mod = mod; + + _authorText = GetComponentsInChildren().First(x => x.name == "Author"); + _authorText.enableWordWrapping = false; + _authorText.overflowMode = TextOverflowModes.Overflow; + _songNameText = GetComponentsInChildren().First(x => x.name == "SongName"); + _songNameText.enableWordWrapping = false; + _songNameText.overflowMode = TextOverflowModes.Overflow; + _coverImage = GetComponentsInChildren().First(x => x.name == "CoverImage"); + _bgImage = GetComponentsInChildren().First(x => x.name == "BG"); + _highlightImage = GetComponentsInChildren().First(x => x.name == "Highlight"); + _beatmapCharacteristicAlphas = new float[0]; + _beatmapCharacteristicImages = new UnityEngine.UI.Image[0]; + _bought = true; + + foreach (var icon in GetComponentsInChildren().Where(x => x.name.StartsWith("LevelTypeIcon"))) + Destroy(icon.gameObject); + + _songNameText.text = $"{mod.Mod.Name} v{mod.Mod.ResolvedVersion}"; + _authorText.text = ""; + _coverImage.sprite = mod.Icon; + + _bgImage.enabled = true; + _bgImage.sprite = Sprite.Create(new Texture2D(1, 1), new Rect(0, 0, 1, 1), Vector2.one / 2f); + _bgImage.type = UnityEngine.UI.Image.Type.Filled; + _bgImage.fillMethod = UnityEngine.UI.Image.FillMethod.Horizontal; + + Update(); + } + + public void Update() + { + _bgImage.enabled = true; + switch (mod.State) + { + case DownloadObject.States.ToDownload: + { + _bgImage.color = new Color(1f, 1f, 1f, 0.35f); + _bgImage.fillAmount = 0; + } + break; + case DownloadObject.States.Downloading: + { + _bgImage.color = new Color(1f, 1f, 1f, 0.35f); + _bgImage.fillAmount = (float)mod.Progress; + } + break; + case DownloadObject.States.Installing: + { + _bgImage.color = new Color(0f, 1f, 1f, 0.35f); + _bgImage.fillAmount = 1f; + } + break; + case DownloadObject.States.Completed: + { + _bgImage.color = new Color(0f, 1f, 0f, 0.35f); + _bgImage.fillAmount = 1f; + } + break; + case DownloadObject.States.Failed: + { + _bgImage.color = new Color(1f, 0f, 0f, 0.35f); + _bgImage.fillAmount = 1f; + } + break; + } + } + } + +} diff --git a/BSIPA-ModList/UI/FloatingNotification.cs b/BSIPA-ModList/UI/FloatingNotification.cs new file mode 100644 index 00000000..006673bb --- /dev/null +++ b/BSIPA-ModList/UI/FloatingNotification.cs @@ -0,0 +1,218 @@ +using CustomUI.BeatSaber; +using System.Collections; +using TMPro; +using UnityEngine; +using UnityEngine.SceneManagement; +using UnityEngine.UI; + +namespace BSIPA_ModList.UI +{ + internal class FloatingNotification : MonoBehaviour + { + private Canvas _canvas; + private TMP_Text _authorNameText; + private TMP_Text _pluginNameText; + private TMP_Text _headerText; + private Image _loadingBackg; + private Image _loadingBar; + + private static readonly Vector3 Position = new Vector3(2.3f, 2.3f, 1.35f); + private static readonly Vector3 Rotation = new Vector3(0, 60, 0); + private static readonly Vector3 Scale = new Vector3(0.01f, 0.01f, 0.01f); + + private static readonly Vector2 CanvasSize = new Vector2(100, 50); + + private const string AuthorNameText = "BSIPA"; + private const float AuthorNameFontSize = 7f; + private static readonly Vector2 AuthorNamePosition = new Vector2(10, 31); + + private const string PluginNameText = "Mod Updater"; + private const float PluginNameFontSize = 9f; + private static readonly Vector2 PluginNamePosition = new Vector2(10, 23); + + private static readonly Vector2 HeaderPosition = new Vector2(10, 15); + private static readonly Vector2 HeaderSize = new Vector2(100, 20); + private const string HeaderText = "Checking for updates..."; + private const float HeaderFontSize = 15f; + + private static readonly Vector2 LoadingBarSize = new Vector2(100, 10); + private static readonly Color BackgroundColor = new Color(0, 0, 0, 0.2f); + + private bool _showingMessage; + + public static FloatingNotification Create() + { + return new GameObject("Mod List Floating Notification").AddComponent(); + } + + public void ShowMessage(string message, float time) + { + StopAllCoroutines(); + _showingMessage = true; + _headerText.text = message; + _loadingBar.enabled = false; + _loadingBackg.enabled = false; + _canvas.enabled = true; + StartCoroutine(DisableCanvasRoutine(time)); + } + + public void ShowMessage(string message) + { + StopAllCoroutines(); + _showingMessage = true; + _headerText.text = message; + _loadingBar.enabled = false; + _loadingBackg.enabled = false; + _canvas.enabled = true; + } + + protected void OnEnable() + { + SceneManager.activeSceneChanged += SceneManagerOnActiveSceneChanged; + DownloadController.Instance.OnDownloadStateChanged += DownloaderStateChanged; + DownloadController.Instance.OnCheckForUpdates += CheckForUpdatesStart; + DownloadController.Instance.OnCheckForUpdatesComplete += CheckForUpdatesDone; + } + + protected void OnDisable() + { + SceneManager.activeSceneChanged -= SceneManagerOnActiveSceneChanged; + DownloadController.Instance.OnDownloadStateChanged -= DownloaderStateChanged; + DownloadController.Instance.OnCheckForUpdates -= CheckForUpdatesStart; + DownloadController.Instance.OnCheckForUpdatesComplete -= CheckForUpdatesDone; + } + + private void CheckForUpdatesStart() + { + _showingMessage = false; + _headerText.text = HeaderText; + _loadingBar.enabled = false; + _loadingBackg.enabled = false; + _canvas.enabled = true; + } + private void CheckForUpdatesDone(int count) + { + _showingMessage = false; + _headerText.text = $"{count} updates found"; + _loadingBar.enabled = false; + _loadingBackg.enabled = false; + _canvas.enabled = true; + StartCoroutine(DisableCanvasRoutine(5f)); + } + + private void SceneManagerOnActiveSceneChanged(Scene oldScene, Scene newScene) + { + if (newScene.name == "MenuCore") + { + if (_showingMessage) + { + _canvas.enabled = true; + } + } + else + { + _canvas.enabled = false; + } + } + + private void DownloaderStateChanged() + { + if (DownloadController.Instance.IsDownloading) + { + StopAllCoroutines(); + _showingMessage = false; + _headerText.text = "Downloading updates..."; + _loadingBar.enabled = false; + _loadingBackg.enabled = false; + _canvas.enabled = true; + } + if (DownloadController.Instance.IsDone) + { + _showingMessage = false; + _headerText.text = "Updates complete"; + _loadingBar.enabled = false; + _loadingBackg.enabled = false; + StartCoroutine(DisableCanvasRoutine(5f)); + } + } + + private IEnumerator DisableCanvasRoutine(float time) + { + yield return new WaitForSecondsRealtime(time); + _canvas.enabled = false; + _showingMessage = false; + } + + private static FloatingNotification instance; + + protected void Awake() + { + if (instance != null) + { + Destroy(this); + return; + } + + instance = this; + + gameObject.transform.position = Position; + gameObject.transform.eulerAngles = Rotation; + gameObject.transform.localScale = Scale; + + _canvas = gameObject.AddComponent(); + _canvas.renderMode = RenderMode.WorldSpace; + _canvas.enabled = false; + var rectTransform = _canvas.transform as RectTransform; + rectTransform.sizeDelta = CanvasSize; + + _authorNameText = BeatSaberUI.CreateText(_canvas.transform as RectTransform, AuthorNameText, AuthorNamePosition); + rectTransform = _authorNameText.transform as RectTransform; + rectTransform.SetParent(_canvas.transform, false); + rectTransform.anchoredPosition = AuthorNamePosition; + rectTransform.sizeDelta = HeaderSize; + _authorNameText.text = AuthorNameText; + _authorNameText.fontSize = AuthorNameFontSize; + + _pluginNameText = BeatSaberUI.CreateText(_canvas.transform as RectTransform, PluginNameText, PluginNamePosition); + rectTransform = _pluginNameText.transform as RectTransform; + rectTransform.SetParent(_canvas.transform, false); + rectTransform.sizeDelta = HeaderSize; + rectTransform.anchoredPosition = PluginNamePosition; + _pluginNameText.text = PluginNameText; + _pluginNameText.fontSize = PluginNameFontSize; + + _headerText = BeatSaberUI.CreateText(_canvas.transform as RectTransform, HeaderText, HeaderPosition); + rectTransform = _headerText.transform as RectTransform; + rectTransform.SetParent(_canvas.transform, false); + rectTransform.anchoredPosition = HeaderPosition; + rectTransform.sizeDelta = HeaderSize; + _headerText.text = HeaderText; + _headerText.fontSize = HeaderFontSize; + + _loadingBackg = new GameObject("Background").AddComponent(); + rectTransform = _loadingBackg.transform as RectTransform; + rectTransform.SetParent(_canvas.transform, false); + rectTransform.sizeDelta = LoadingBarSize; + _loadingBackg.color = BackgroundColor; + + _loadingBar = new GameObject("Loading Bar").AddComponent(); + rectTransform = _loadingBar.transform as RectTransform; + rectTransform.SetParent(_canvas.transform, false); + rectTransform.sizeDelta = LoadingBarSize; + var tex = Texture2D.whiteTexture; + var sprite = Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), Vector2.one * 0.5f, 100, 1); + _loadingBar.sprite = sprite; + _loadingBar.type = Image.Type.Filled; + _loadingBar.fillMethod = Image.FillMethod.Horizontal; + _loadingBar.color = new Color(1, 1, 1, 0.5f); + + DontDestroyOnLoad(gameObject); + } + + /*private void Update() + { + if (!_canvas.enabled) return; + _loadingBar.fillAmount = SongLoader.LoadingProgress; + }*/ + } +} diff --git a/BSIPA-ModList/UI/ModListFlowCoordinator.cs b/BSIPA-ModList/UI/ModListFlowCoordinator.cs index 61f1482e..0dbb8ea5 100644 --- a/BSIPA-ModList/UI/ModListFlowCoordinator.cs +++ b/BSIPA-ModList/UI/ModListFlowCoordinator.cs @@ -1,4 +1,5 @@ -using CustomUI.BeatSaber; +using BSIPA_ModList.UI.ViewControllers; +using CustomUI.BeatSaber; using CustomUI.Utilities; using IPA.Loader; using System; @@ -13,6 +14,7 @@ namespace BSIPA_ModList.UI { private BackButtonNavigationController navigationController; private ModListController modList; + private DownloadProgressViewController downloads; #pragma warning disable CS0618 protected override void DidActivate(bool firstActivation, ActivationType activationType) @@ -27,17 +29,19 @@ namespace BSIPA_ModList.UI modList = BeatSaberUI.CreateViewController(); modList.Init(this, PluginManager.AllPlugins, PluginLoader.ignoredPlugins, PluginManager.Plugins); + downloads = BeatSaberUI.CreateViewController(); + PushViewControllerToNavigationController(navigationController, modList); } - ProvideInitialViewControllers(navigationController); + ProvideInitialViewControllers(navigationController, rightViewController: downloads); } #pragma warning restore private delegate void PresentFlowCoordDel(FlowCoordinator self, FlowCoordinator newF, Action finished, bool immediate, bool replaceTop); private static PresentFlowCoordDel presentFlow; - public void PresentOn(FlowCoordinator main, Action finished = null, bool immediate = false, bool replaceTop = false) + public void Present(Action finished = null, bool immediate = false, bool replaceTop = false) { if (presentFlow == null) { @@ -46,7 +50,8 @@ namespace BSIPA_ModList.UI presentFlow = (PresentFlowCoordDel)Delegate.CreateDelegate(typeof(PresentFlowCoordDel), m); } - presentFlow(main, this, finished, immediate, replaceTop); + MainFlowCoordinator mainFlow = Resources.FindObjectsOfTypeAll().First(); + presentFlow(mainFlow, this, finished, immediate, replaceTop); } public bool HasSelected { get; private set; } = false; diff --git a/BSIPA-ModList/UI/ViewControllers/DownloadProgressViewController.cs b/BSIPA-ModList/UI/ViewControllers/DownloadProgressViewController.cs new file mode 100644 index 00000000..358784b2 --- /dev/null +++ b/BSIPA-ModList/UI/ViewControllers/DownloadProgressViewController.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using CustomUI.BeatSaber; +using CustomUI.Utilities; +using HMUI; +using IPA.Updating.BeatMods; +using TMPro; +using UnityEngine; +using UnityEngine.UI; +using VRUI; + +namespace BSIPA_ModList.UI.ViewControllers +{ + // originally ripped verbatim from Andruzz's BeatSaverDownloader + internal class DownloadProgressViewController : VRUIViewController, TableView.IDataSource + { + private TextMeshProUGUI _titleText; + + private Button _checkForUpdates; + private Button _downloadUpdates; + private TableView _currentlyUpdatingTableView; + private LevelListTableCell _songListTableCellInstance; + private Button _pageUpButton; + private Button _pageDownButton; + + protected override void DidActivate(bool firstActivation, ActivationType type) + { + if (firstActivation && type == ActivationType.AddedToHierarchy) + { + DownloadController.Instance.OnDownloaderListChanged -= Refresh; + DownloadController.Instance.OnDownloadStateChanged -= DownloaderStateChanged; + + _songListTableCellInstance = Resources.FindObjectsOfTypeAll().First(x => (x.name == "LevelListTableCell")); + + _titleText = BeatSaberUI.CreateText(rectTransform, "DOWNLOAD QUEUE", new Vector2(0f, 35f)); + _titleText.alignment = TextAlignmentOptions.Top; + _titleText.fontSize = 6f; + + _pageUpButton = Instantiate(Resources.FindObjectsOfTypeAll