using System; using System.IO; using System.IO.Compression; using System.Net.Http; using System.Text; using System.Threading.Tasks; using TMPro; using UnityEngine; using UnityEngine.UI; using MelonLoader; using MelonLoader.Utils; namespace ADOFAIRenderer.UI; internal class FFmpegDownloadUI : MonoBehaviour { private GameObject _buttonContainerObj = null!; private GameObject _progressBarObj = null!; private GameObject _progressBarFillObj = null!; private RectTransform _fillRect = null!; private TextMeshProUGUI _progressText = null!; private GameObject _statusTextObj = null!; private TextMeshProUGUI _statusText = null!; private bool _isDownloading; private string DownloadUrl => Application.platform switch { RuntimePlatform.WindowsPlayer => "https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2026-05-31-13-22/ffmpeg-N-124714-g49a77d37be-win64-gpl-shared.zip", _ => "https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2026-05-31-13-22/ffmpeg-N-124714-g49a77d37be-linux64-gpl-shared.tar.xz" }; private System.Threading.CancellationTokenSource? _cts; private readonly System.Collections.Concurrent.ConcurrentQueue _mainThreadQueue = new(); public static bool CheckFFmpegExists() { var userLibs = MelonEnvironment.UserLibsDirectory; if (!Directory.Exists(userLibs)) return false; string[] searchPatterns = Application.platform switch { RuntimePlatform.WindowsPlayer => new[] { "avcodec-*.dll", "avformat-*.dll", "avutil-*.dll", "swscale-*.dll", "swresample-*.dll" }, RuntimePlatform.OSXPlayer => new[] { "libavcodec.*.dylib", "libavformat.*.dylib", "libavutil.*.dylib", "libswscale.*.dylib", "libswresample.*.dylib" }, _ => new[] { "libavcodec.so*", "libavformat.so*", "libavutil.so*", "libswscale.so*", "libswresample.so*" } }; foreach (var pattern in searchPatterns) { if (Directory.GetFiles(userLibs, pattern).Length == 0) { return false; } } return true; } public static void ShowDownloadUI() { if (CheckFFmpegExists()) return; var obj = new GameObject("FFmpegDownloadUI"); DontDestroyOnLoad(obj); obj.AddComponent(); } public static FFmpegDownloadUI? Instance { get; private set; } private void Awake() { Instance = this; if (CheckFFmpegExists()) { Destroy(gameObject); } } private static TMP_FontAsset GetNanumSquareNeoBdFontAsset() { TMP_FontAsset? font = null; if (ModAssets.FontBundle) { try { font = ModAssets.FontBundle.LoadAsset("NanumSquareNeoOTF-Bd SDF"); } catch { // Loading failed } } // Fallback processing if (font == null) { try { font = RDString.fontData.fontTMP; } catch { // ignored } } if (font == null) { // ReSharper disable once Unity.UnknownResource font = Resources.Load("Fonts & Materials/LiberationSans SDF"); } if (font) return font; var fonts = Resources.FindObjectsOfTypeAll(); if (fonts.Length > 0) font = fonts[0]; return font!; } private void Start() { // Set UI layer (5) gameObject.layer = 5; // Canvas setup var canvas = gameObject.AddComponent(); canvas.renderMode = RenderMode.ScreenSpaceOverlay; canvas.sortingOrder = 32767; // Topmost // Responsive Canvas Scaler setup var scaler = gameObject.AddComponent(); scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; scaler.referenceResolution = new Vector2(1920, 1080); scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight; scaler.matchWidthOrHeight = 0.5f; gameObject.AddComponent(); // 1. Create full screen overlay background var bgPanelObj = new GameObject("BackgroundPanel"); bgPanelObj.transform.SetParent(transform, false); bgPanelObj.layer = 5; var bgImage = bgPanelObj.AddComponent(); bgImage.color = new Color(0f, 0f, 0f, 0.65f); // Dark transparent overlay bgImage.raycastTarget = true; var bgRect = bgPanelObj.GetComponent(); bgRect.anchorMin = Vector2.zero; bgRect.anchorMax = Vector2.one; bgRect.offsetMin = Vector2.zero; bgRect.offsetMax = Vector2.zero; var font = GetNanumSquareNeoBdFontAsset(); // 2. Create modal container var modalContainerObj = new GameObject("ModalContainer"); modalContainerObj.transform.SetParent(bgPanelObj.transform, false); modalContainerObj.layer = 5; var modalImage = modalContainerObj.AddComponent(); modalImage.color = new Color(0.08f, 0.08f, 0.1f, 0.88f); var modalOutline = modalContainerObj.AddComponent(); modalOutline.effectColor = Color.white; modalOutline.effectDistance = new Vector2(3.0f, -3.0f); var modalRect = modalContainerObj.GetComponent(); modalRect.anchorMin = new Vector2(0.5f, 0.5f); modalRect.anchorMax = new Vector2(0.5f, 0.5f); modalRect.pivot = new Vector2(0.5f, 0.5f); modalRect.sizeDelta = new Vector2(600, 320); // 4. Create title text var titleObj = new GameObject("TitleText"); titleObj.transform.SetParent(modalContainerObj.transform, false); titleObj.layer = 5; var titleText = titleObj.AddComponent(); titleText.text = "FFmpeg Library Installation Required"; titleText.fontSize = 26; titleText.font = font; titleText.color = Color.white; titleText.alignment = TextAlignmentOptions.Center; var titleRect = titleObj.GetComponent(); titleRect.anchorMin = new Vector2(0, 1); titleRect.anchorMax = new Vector2(1, 1); titleRect.pivot = new Vector2(0.5f, 1); titleRect.anchoredPosition = new Vector2(0, -35); titleRect.sizeDelta = new Vector2(0, 40); // 5. Create description text var descObj = new GameObject("DescriptionText"); descObj.transform.SetParent(modalContainerObj.transform, false); descObj.layer = 5; var descText = descObj.AddComponent(); var descStrBuilder = new StringBuilder(); descStrBuilder.AppendLine("FFmpeg shared libraries are required to render video."); if (Application.platform == RuntimePlatform.OSXPlayer) { descStrBuilder.AppendLine("For macOS, reliable public builds are unavailable, so FFmpeg 8.1 must be built manually."); descStrBuilder.AppendLine("Click the button below to open the official FFmpeg website."); } else { descStrBuilder.AppendLine("Click the button below to automatically download and install from the internet."); descStrBuilder.AppendLine("(Download size: ~70MB, extracted to UserLibs folder)"); } descText.text = descStrBuilder.ToString(); descText.fontSize = 16; descText.font = font; descText.color = new Color(0.85f, 0.85f, 0.85f, 1f); descText.alignment = TextAlignmentOptions.Center; descText.lineSpacing = 10; var descRect = descObj.GetComponent(); descRect.anchorMin = new Vector2(0, 0.5f); descRect.anchorMax = new Vector2(1, 1); descRect.pivot = new Vector2(0.5f, 0.5f); descRect.anchoredPosition = new Vector2(0, -60); descRect.sizeDelta = new Vector2(-40, 100); // 6. Create button container _buttonContainerObj = new GameObject("ButtonContainer", typeof(RectTransform)); _buttonContainerObj.transform.SetParent(modalContainerObj.transform, false); _buttonContainerObj.layer = 5; var btnContainerRect = _buttonContainerObj.GetComponent(); btnContainerRect.anchorMin = new Vector2(0, 0); btnContainerRect.anchorMax = new Vector2(1, 0); btnContainerRect.pivot = new Vector2(0.5f, 0); btnContainerRect.anchoredPosition = new Vector2(0, 35); btnContainerRect.sizeDelta = new Vector2(0, 50); // 7. Create download button var downloadBtnObj = new GameObject("DownloadButton"); downloadBtnObj.transform.SetParent(_buttonContainerObj.transform, false); downloadBtnObj.layer = 5; var downloadImg = downloadBtnObj.AddComponent(); downloadImg.color = new Color(0.0f, 0.8f, 1.0f, 1.0f); // Sleek cyan/blue downloadImg.raycastTarget = true; var downloadBtn = downloadBtnObj.AddComponent