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