private void RefreshUI(bool init = true, bool refreshValue = false)
        {
            if (init)
            {
                if (!refreshValue)
                {
                    // disable applyImmediately temporarily, otherwise Init() triggers a call to SetValue, which also calls RefreshUI()
                    _minViewController.applyImmediately = false;
                    _maxViewController.applyImmediately = false;
                }

                _minViewController.Init();
                _maxViewController.Init();

                if (!refreshValue)
                {
                    _minViewController.applyImmediately = true;
                    _maxViewController.applyImmediately = true;
                }
            }

            // reset button state
            _minViewController.GetComponentsInChildren <Button>().First(x => x.name == "DecButton").interactable = _minEnabledStagingValue && _minStagingValue > MinValue;
            _minViewController.GetComponentsInChildren <Button>().First(x => x.name == "IncButton").interactable = _minEnabledStagingValue && (!_maxEnabledStagingValue || _minStagingValue < _maxStagingValue) && _minStagingValue < MaxValue;
            _maxViewController.GetComponentsInChildren <Button>().First(x => x.name == "DecButton").interactable = _maxEnabledStagingValue && (!_minEnabledStagingValue || _maxStagingValue > _minStagingValue) && _maxStagingValue > MinValue;
            _maxViewController.GetComponentsInChildren <Button>().First(x => x.name == "IncButton").interactable = _maxEnabledStagingValue && _maxStagingValue < MaxValue;
        }
        private void RefreshUI(bool init = true, bool refreshValue = false)
        {
            if (init)
            {
                if (!refreshValue)
                {
                    // disable applyImmediately temporarily, otherwise Init() triggers a call to SetValue, which also calls RefreshUI()
                    _minViewController.applyImmediately = false;
                    _maxViewController.applyImmediately = false;
                }
                // ranked view controller should never have its value refreshed, since it doesn't have an enable toggle
                // and refreshing modifies the enable toggles for the other view controllers
                _rankedViewController.applyImmediately = false;

                _rankedViewController.Init();
                _minViewController.Init();
                _maxViewController.Init();

                if (!refreshValue)
                {
                    _minViewController.applyImmediately = true;
                    _maxViewController.applyImmediately = true;
                }
                _rankedViewController.applyImmediately = true;
            }

            // reset button state
            _minViewController.GetComponentsInChildren <Button>().First(x => x.name == "DecButton").interactable = _minEnabledStagingValue && _minStagingValue > MinValue;
            _minViewController.GetComponentsInChildren <Button>().First(x => x.name == "IncButton").interactable = _minEnabledStagingValue && (!_maxEnabledStagingValue || _minStagingValue < _maxStagingValue) && _minStagingValue < MaxValue;
            _maxViewController.GetComponentsInChildren <Button>().First(x => x.name == "DecButton").interactable = _maxEnabledStagingValue && (!_minEnabledStagingValue || _maxStagingValue > _minStagingValue) && _maxStagingValue > MinValue;
            _maxViewController.GetComponentsInChildren <Button>().First(x => x.name == "IncButton").interactable = _maxEnabledStagingValue && _maxStagingValue < MaxValue;
        }
        public void Init()
        {
            if (_isInitialized)
            {
                return;
            }

            // difficulties
            var difficultiesContainer = new GameObject("DifficultiesContainer");

            Controls[0] = new FilterControl(difficultiesContainer, new Vector2(0f, 0.95f), new Vector2(1f, 0.95f), new Vector2(0.5f, 1f), new Vector2(0f, 26f), Vector2.zero,
                                            delegate()
            {
                for (int i = 0; i < 5; ++i)
                {
                    _difficultyToggles[i].isOn = _difficultiesStagingValue[i];
                }
            });

            // the container needs some graphical component to have the Transform to RectTransform cast work
            var unused = difficultiesContainer.AddComponent <Image>();

            unused.color = new Color(0f, 0f, 0f, 0f);

            var divider = Utilities.CreateHorizontalDivider(difficultiesContainer.transform);

            divider.color = new Color(1f, 1f, 1f, 0.4f);
            divider.rectTransform.sizeDelta = new Vector2(0f, 0.2f);

            CreateDifficultyToggles(difficultiesContainer.transform);

            // minimum value setting
            float[] values = Enumerable.Range(MinValue, MaxValue).Select(x => (float)x).ToArray();
            _minViewController = Utilities.CreateListViewController("Minimum NJS", values, "Filter out songs that have a smaller NJS than this value");
            _minViewController.GetTextForValue += x => ((int)x).ToString();
            _minViewController.GetValue        += () => _minStagingValue;
            _minViewController.SetValue        += delegate(float value)
            {
                if (_maxEnabledStagingValue && value > _maxStagingValue)
                {
                    _minStagingValue = _maxStagingValue;
                    RefreshUI();
                    return;
                }

                _minStagingValue = (int)value;

                RefreshUI(false);

                SettingChanged?.Invoke();
            };
            _minViewController.Init();
            _minViewController.applyImmediately = true;

            var minToggle = CreateEnableToggle(_minViewController);

            minToggle.name = "MinValueToggle";
            minToggle.onValueChanged.AddListener(delegate(bool value)
            {
                _minEnabledStagingValue = value;

                if (value && _maxEnabledStagingValue && _minStagingValue > _maxStagingValue)
                {
                    _minStagingValue = _maxStagingValue;
                }

                RefreshUI(true, true);

                SettingChanged?.Invoke();
            });

            Utilities.MoveListViewControllerElements(_minViewController);
            Utilities.CreateHorizontalDivider(_minViewController.transform);

            Controls[1] = new FilterControl(_minViewController.gameObject, new Vector2(0f, 0.95f), new Vector2(1f, 0.95f), new Vector2(0.5f, 1f), new Vector2(0f, 12f), new Vector2(0f, -26f),
                                            delegate()
            {
                // disabling buttons needs to be done after the view controller is enabled to override the interactable assignments of ListSettingsController:OnEnable()
                _minViewController.GetComponentsInChildren <Button>().First(x => x.name == "DecButton").interactable = _minEnabledStagingValue && _minStagingValue > MinValue;
                _minViewController.GetComponentsInChildren <Button>().First(x => x.name == "IncButton").interactable = _minEnabledStagingValue && (!_maxEnabledStagingValue || _minStagingValue < _maxStagingValue) && _minStagingValue < MaxValue;

                minToggle.isOn = _minEnabledStagingValue;
            });

            // maximum value setting
            _maxViewController = Utilities.CreateListViewController("Maximum NJS", values, "Filter out songs that have a larger NJS than this value");
            _maxViewController.GetTextForValue += x => ((int)x).ToString();
            _maxViewController.GetValue        += () => _maxStagingValue;
            _maxViewController.SetValue        += delegate(float value)
            {
                if (_minEnabledStagingValue && value < _minStagingValue)
                {
                    _maxStagingValue = _minStagingValue;
                    RefreshUI();
                    return;
                }

                _maxStagingValue = (int)value;

                RefreshUI(false);

                SettingChanged?.Invoke();
            };
            _maxViewController.Init();
            _maxViewController.applyImmediately = true;

            var maxToggle = CreateEnableToggle(_maxViewController);

            maxToggle.name = "MaxValueToggle";
            maxToggle.onValueChanged.AddListener(delegate(bool value)
            {
                _maxEnabledStagingValue = value;

                if (value && _minEnabledStagingValue && _maxStagingValue < _minStagingValue)
                {
                    _maxStagingValue = _minStagingValue;
                }

                RefreshUI(true, true);

                SettingChanged?.Invoke();
            });

            Utilities.MoveListViewControllerElements(_maxViewController);

            Controls[2] = new FilterControl(_maxViewController.gameObject, new Vector2(0f, 0.95f), new Vector2(1f, 0.95f), new Vector2(0.5f, 1f), new Vector2(0f, 12f), new Vector2(0f, -38f),
                                            delegate()
            {
                _maxViewController.GetComponentsInChildren <Button>().First(x => x.name == "DecButton").interactable = _maxEnabledStagingValue && (!_minEnabledStagingValue || _maxStagingValue > _minStagingValue) && _maxStagingValue < MinValue;
                _maxViewController.GetComponentsInChildren <Button>().First(x => x.name == "IncButton").interactable = _maxEnabledStagingValue && _maxStagingValue < MaxValue;

                maxToggle.isOn = _maxEnabledStagingValue;
            });

            _isInitialized = true;
        }
        public void Init()
        {
            if (_isInitialized)
            {
                return;
            }

            // we're using SongDataCore's ScoreSaber data storage to find out the star rating
            if (!Tweaks.SongDataCoreTweaks.ModLoaded)
            {
                Controls = new FilterControl[1];

                var noModMessage = BeatSaberUI.CreateText(null, "<color=#FFAAAA>Sorry!\n\n<size=80%>This filter requires the SongDataCore mod to be\n installed.</size></color>", Vector2.zero);
                noModMessage.alignment = TextAlignmentOptions.Center;
                noModMessage.fontSize  = 5.5f;

                Controls[0] = new FilterControl(noModMessage.gameObject, new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(80f, 50f), new Vector2(0f, 10f));

                _isInitialized = true;
                return;
            }

            var togglePrefab = Utilities.GetTogglePrefab();

            // title text
            var text = BeatSaberUI.CreateText(null, "Keep Songs Between Some Star Rating", Vector2.zero, Vector2.zero);

            text.fontSize = 5.5f;
            var rt = text.rectTransform;

            rt.anchorMin = new Vector2(0f, 1f);
            rt.anchorMax = new Vector2(0f, 1f);
            rt.pivot     = new Vector2(0f, 1f);

            Controls[0] = new FilterControl(text.gameObject, new Vector2(0f, 0.95f), new Vector2(0f, 0.95f), new Vector2(0f, 1f), new Vector2(50f, 6f), Vector2.zero);

            // min view controller
            float[] values = Enumerable.Range((int)MinValue, (int)((MaxValue - MinValue) / IncrementValue) + 1).Select(x => x * IncrementValue).ToArray();
            _minViewController = Utilities.CreateListViewController("Minimum Star Rating", values, "Filters out songs that have a lesser star difficulty rating than this value");
            _minViewController.GetTextForValue += x => x.ToString("0.00");
            _minViewController.GetValue        += () => _minStagingValue;
            _minViewController.SetValue        += delegate(float value)
            {
                if (_maxEnabledStagingValue && value > _maxStagingValue)
                {
                    _minStagingValue = _maxStagingValue;
                    RefreshUI();
                    return;
                }

                _minStagingValue = value;

                RefreshUI(false);

                SettingChanged?.Invoke();
            };
            _minViewController.Init();
            _minViewController.applyImmediately = true;

            var minToggle = Utilities.CreateToggleFromPrefab(togglePrefab.toggle, _minViewController.transform.Find("Value"));

            minToggle.name = "MinValueToggle";
            minToggle.onValueChanged.AddListener(delegate(bool value)
            {
                _minEnabledStagingValue = value;

                if (value && _maxEnabledStagingValue && _minStagingValue > _maxStagingValue)
                {
                    _minStagingValue = _maxStagingValue;
                }

                RefreshUI(true, true);

                SettingChanged?.Invoke();
            });

            Utilities.MoveListViewControllerElements(_minViewController);
            Utilities.CreateHorizontalDivider(_minViewController.transform);

            Controls[1] = new FilterControl(_minViewController.gameObject, new Vector2(0f, 0.95f), new Vector2(1f, 0.95f), new Vector2(0.5f, 1f), new Vector2(0f, 12f), new Vector2(0f, -8f),
                                            delegate()
            {
                // disabling buttons needs to be done after the view controller is enabled to override the interactable assignments of ListSettingsController:OnEnable()
                _minViewController.GetComponentsInChildren <Button>().First(x => x.name == "DecButton").interactable = _minEnabledStagingValue && _minStagingValue > MinValue;
                _maxViewController.GetComponentsInChildren <Button>().First(x => x.name == "IncButton").interactable = _minEnabledStagingValue && (!_maxEnabledStagingValue || _minStagingValue < _maxStagingValue) && _minStagingValue < MaxValue;
            });

            // max view controller
            _maxViewController = Utilities.CreateListViewController("Maximum Star Rating", values, "Filters out songs that have a greater star difficulty rating than this value");
            _maxViewController.GetTextForValue += x => x.ToString("0.00");
            _maxViewController.GetValue        += () => _maxStagingValue;
            _maxViewController.SetValue        += delegate(float value)
            {
                if (_minEnabledStagingValue && value < _minStagingValue)
                {
                    _maxStagingValue = _minStagingValue;
                    RefreshUI();
                    return;
                }

                _maxStagingValue = value;

                RefreshUI(false);

                SettingChanged?.Invoke();
            };
            _maxViewController.Init();
            _maxViewController.applyImmediately = true;

            var maxToggle = Utilities.CreateToggleFromPrefab(togglePrefab.toggle, _maxViewController.transform.Find("Value"));

            maxToggle.name = "MaxValueToggle";
            maxToggle.onValueChanged.AddListener(delegate(bool value)
            {
                _maxEnabledStagingValue = value;

                if (value && _minEnabledStagingValue && _maxStagingValue < _minStagingValue)
                {
                    _maxStagingValue = _minStagingValue;
                }

                RefreshUI(true, true);

                SettingChanged?.Invoke();
            });

            Utilities.MoveListViewControllerElements(_maxViewController);
            Utilities.CreateHorizontalDivider(_maxViewController.transform);

            Controls[2] = new FilterControl(_maxViewController.gameObject, new Vector2(0f, 0.95f), new Vector2(1f, 0.95f), new Vector2(0.5f, 1f), new Vector2(0f, 12f), new Vector2(0f, -20f),
                                            delegate()
            {
                // disabling buttons needs to be done after the view controller is enabled to override the interactable assignments of ListSettingsController:OnEnable()
                _maxViewController.GetComponentsInChildren <Button>().First(x => x.name == "DecButton").interactable = _maxEnabledStagingValue && (!_minEnabledStagingValue || _maxStagingValue > _minStagingValue) && _maxStagingValue > MinValue;
                _maxViewController.GetComponentsInChildren <Button>().First(x => x.name == "IncButton").interactable = _maxEnabledStagingValue && _maxStagingValue < MaxValue;
            });

            // include unrated songs toggle
            _includeUnratedViewController           = Utilities.CreateBoolViewController("Include Unrated Songs", "Do not filter out songs that do not have a star rating provided by ScoreSaber");
            _includeUnratedViewController.GetValue += () => _includeUnratedStagingValue;
            _includeUnratedViewController.SetValue += delegate(bool value)
            {
                _includeUnratedStagingValue = value;

                SettingChanged?.Invoke();
            };
            _includeUnratedViewController.Init();
            _includeUnratedViewController.applyImmediately = true;

            Utilities.MoveIncDecViewControllerElements(_includeUnratedViewController);

            Controls[3] = new FilterControl(_includeUnratedViewController.gameObject, new Vector2(0f, 0.95f), new Vector2(1f, 0.95f), new Vector2(0.5f, 1f), new Vector2(0f, 12f), new Vector2(0f, -32f));

            Object.Destroy(togglePrefab);

            _isInitialized = true;
        }
        public void Init()
        {
            if (_isInitialized)
            {
                return;
            }

            var togglePrefab = Utilities.GetTogglePrefab();

            // title text
            var text = BeatSaberUI.CreateText(null, "Keep Songs Between Some Length", Vector2.zero, Vector2.zero);

            text.fontSize = 5.5f;
            var rt = text.rectTransform;

            rt.anchorMin = new Vector2(0f, 1f);
            rt.anchorMax = new Vector2(0f, 1f);
            rt.pivot     = new Vector2(0f, 1f);

            Controls[0] = new FilterControl(text.gameObject, new Vector2(0f, 0.95f), new Vector2(0f, 0.95f), new Vector2(0f, 1f), new Vector2(50f, 6f), Vector2.zero);

            // min view controller
            float[] values = Enumerable.Range((int)MinValue, (int)((MaxValue - MinValue) / IncrementValue) + 1).Select(x => x * IncrementValue).ToArray();
            _minViewController = Utilities.CreateListViewController("Minimum Duration", values, "Filters out songs that are shorter than this value");
            _minViewController.GetTextForValue += x => ConvertFloatToTimeString(x);
            _minViewController.GetValue        += () => _minStagingValue;
            _minViewController.SetValue        += delegate(float value)
            {
                if (_maxEnabledStagingValue && value > _maxStagingValue)
                {
                    _minStagingValue = _maxStagingValue;
                    RefreshUI();
                    return;
                }

                _minStagingValue = value;

                RefreshUI(false);

                SettingChanged?.Invoke();
            };
            _minViewController.Init();
            _minViewController.applyImmediately = true;

            var minToggle = Utilities.CreateToggleFromPrefab(togglePrefab.toggle, _minViewController.transform.Find("Value"));

            minToggle.name = "MinValueToggle";
            minToggle.onValueChanged.AddListener(delegate(bool value)
            {
                _minEnabledStagingValue = value;

                if (value && _maxEnabledStagingValue && _minStagingValue > _maxStagingValue)
                {
                    _minStagingValue = _maxStagingValue;
                }

                RefreshUI(true, true);

                SettingChanged?.Invoke();
            });

            Utilities.MoveListViewControllerElements(_minViewController);
            Utilities.CreateHorizontalDivider(_minViewController.transform);

            Controls[1] = new FilterControl(_minViewController.gameObject, new Vector2(0f, 0.95f), new Vector2(1f, 0.95f), new Vector2(0.5f, 1f), new Vector2(0f, 12f), new Vector2(0f, -8f),
                                            delegate()
            {
                // disabling buttons needs to be done after the view controller is enabled to override the interactable assignments of ListSettingsController:OnEnable()
                _minViewController.GetComponentsInChildren <Button>().First(x => x.name == "DecButton").interactable = _minEnabledStagingValue && _minStagingValue > MinValue;
                _maxViewController.GetComponentsInChildren <Button>().First(x => x.name == "IncButton").interactable = _minEnabledStagingValue && (!_maxEnabledStagingValue || _minStagingValue < _maxStagingValue) && _minStagingValue < MaxValue;
            });

            // max view controller
            _maxViewController = Utilities.CreateListViewController("Maximum Duration", values, "Filters out songs that are longer than this value");
            _maxViewController.GetTextForValue += x => ConvertFloatToTimeString(x);
            _maxViewController.GetValue        += () => _maxStagingValue;
            _maxViewController.SetValue        += delegate(float value)
            {
                if (_minEnabledStagingValue && value < _minStagingValue)
                {
                    _maxStagingValue = _minStagingValue;
                    RefreshUI();
                    return;
                }

                _maxStagingValue = value;

                RefreshUI(false);

                SettingChanged?.Invoke();
            };
            _maxViewController.Init();
            _maxViewController.applyImmediately = true;

            var maxToggle = Utilities.CreateToggleFromPrefab(togglePrefab.toggle, _maxViewController.transform.Find("Value"));

            maxToggle.name = "MaxValueToggle";
            maxToggle.onValueChanged.AddListener(delegate(bool value)
            {
                _maxEnabledStagingValue = value;

                if (value && _minEnabledStagingValue && _maxStagingValue < _minStagingValue)
                {
                    _maxStagingValue = _minStagingValue;
                }

                RefreshUI(true, true);

                SettingChanged?.Invoke();
            });

            Utilities.MoveListViewControllerElements(_maxViewController);

            Controls[2] = new FilterControl(_maxViewController.gameObject, new Vector2(0f, 0.95f), new Vector2(1f, 0.95f), new Vector2(0.5f, 1f), new Vector2(0f, 12f), new Vector2(0f, -20f),
                                            delegate()
            {
                // disabling buttons needs to be done after the view controller is enabled to override the interactable assignments of ListSettingsController:OnEnable()
                _maxViewController.GetComponentsInChildren <Button>().First(x => x.name == "DecButton").interactable = _maxEnabledStagingValue && (!_minEnabledStagingValue || _maxStagingValue > _minStagingValue) && _maxStagingValue > MinValue;
                _maxViewController.GetComponentsInChildren <Button>().First(x => x.name == "IncButton").interactable = _maxEnabledStagingValue && _maxStagingValue < MaxValue;
            });

            Object.Destroy(togglePrefab);

            _isInitialized = true;
        }