|
| 1 | +using System.Text.RegularExpressions; |
| 2 | +using MaiChartManager.Utils; |
| 3 | +using Microsoft.AspNetCore.Mvc; |
| 4 | + |
| 5 | +namespace MaiChartManager.Controllers.Tools; |
| 6 | + |
| 7 | +[ApiController] |
| 8 | +[Route("MaiChartManagerServlet/[action]Api")] |
| 9 | +public partial class ImageToAbToolController(ILogger<ImageToAbToolController> logger) : ControllerBase |
| 10 | +{ |
| 11 | + [GeneratedRegex(@"^(?<id>\d+)\.(png|jpg|jpeg)$", RegexOptions.IgnoreCase)] |
| 12 | + private static partial Regex NumericFileRegex(); |
| 13 | + |
| 14 | + [GeneratedRegex(@"^ui_jacket_(?<id>\d+)\.(png|jpg|jpeg)$", RegexOptions.IgnoreCase)] |
| 15 | + private static partial Regex UiJacketFileRegex(); |
| 16 | + |
| 17 | + public enum ImageToAbEventType |
| 18 | + { |
| 19 | + Progress, |
| 20 | + Success, |
| 21 | + Error |
| 22 | + } |
| 23 | + |
| 24 | + [HttpPost] |
| 25 | + public async Task ImageToAbTool() |
| 26 | + { |
| 27 | + Response.Headers.Append("Content-Type", "text/event-stream"); |
| 28 | + |
| 29 | + var dialog = new FolderBrowserDialog |
| 30 | + { |
| 31 | + Description = Locale.SelectImageFolder, |
| 32 | + ShowNewFolderButton = false, |
| 33 | + }; |
| 34 | + |
| 35 | + if (WinUtils.ShowDialog(dialog) != DialogResult.OK) |
| 36 | + { |
| 37 | + await WriteEvent(ImageToAbEventType.Error, Locale.FileNotSelected); |
| 38 | + return; |
| 39 | + } |
| 40 | + |
| 41 | + var selectedPath = dialog.SelectedPath; |
| 42 | + if (string.IsNullOrWhiteSpace(selectedPath) || !Directory.Exists(selectedPath)) |
| 43 | + { |
| 44 | + await WriteEvent(ImageToAbEventType.Error, Locale.FileNotSelected); |
| 45 | + return; |
| 46 | + } |
| 47 | + |
| 48 | + var candidates = Directory.EnumerateFiles(selectedPath) |
| 49 | + .Select(path => new |
| 50 | + { |
| 51 | + Path = path, |
| 52 | + Name = Path.GetFileName(path), |
| 53 | + }) |
| 54 | + .Select(x => |
| 55 | + { |
| 56 | + var numericMatch = NumericFileRegex().Match(x.Name); |
| 57 | + if (numericMatch.Success) |
| 58 | + { |
| 59 | + return new ImageTaskItem(x.Path, numericMatch.Groups["id"].Value); |
| 60 | + } |
| 61 | + |
| 62 | + var uiJacketMatch = UiJacketFileRegex().Match(x.Name); |
| 63 | + if (uiJacketMatch.Success) |
| 64 | + { |
| 65 | + return new ImageTaskItem(x.Path, uiJacketMatch.Groups["id"].Value); |
| 66 | + } |
| 67 | + |
| 68 | + return null; |
| 69 | + }) |
| 70 | + .Where(x => x is not null) |
| 71 | + .Select(x => x!) |
| 72 | + .ToList(); |
| 73 | + |
| 74 | + if (candidates.Count == 0) |
| 75 | + { |
| 76 | + await WriteEvent( |
| 77 | + ImageToAbEventType.Error, |
| 78 | + Locale.NoValidImagesFound); |
| 79 | + return; |
| 80 | + } |
| 81 | + |
| 82 | + var jacketDir = Path.Combine(selectedPath, "jacket"); |
| 83 | + var jacketSmallDir = Path.Combine(selectedPath, "jacket_s"); |
| 84 | + Directory.CreateDirectory(jacketDir); |
| 85 | + Directory.CreateDirectory(jacketSmallDir); |
| 86 | + |
| 87 | + var failures = new List<string>(); |
| 88 | + var total = candidates.Count; |
| 89 | + |
| 90 | + for (var i = 0; i < total; i++) |
| 91 | + { |
| 92 | + var item = candidates[i]; |
| 93 | + var id = item.Id; |
| 94 | + |
| 95 | + try |
| 96 | + { |
| 97 | + var fullAbPath = Path.Combine(jacketDir, $"ui_jacket_{id}.ab"); |
| 98 | + AssetBundleCreator.CreateTextureAssetBundle( |
| 99 | + item.FilePath, |
| 100 | + fullAbPath, |
| 101 | + $"UI_Jacket_{id}", |
| 102 | + $"assets/assetbundle/jacket/ui_jacket_{id}.png", |
| 103 | + $"jacket/ui_jacket_{id}.ab"); |
| 104 | + |
| 105 | + var smallAbPath = Path.Combine(jacketSmallDir, $"ui_jacket_{id}_s.ab"); |
| 106 | + AssetBundleCreator.CreateTextureAssetBundle( |
| 107 | + item.FilePath, |
| 108 | + smallAbPath, |
| 109 | + $"UI_Jacket_{id}_s", |
| 110 | + $"assets/assetbundle/jacket_s/ui_jacket_{id}_s.png", |
| 111 | + $"jacket_s/ui_jacket_{id}_s.ab", |
| 112 | + resizeWidth: 200, |
| 113 | + resizeHeight: 200); |
| 114 | + |
| 115 | + var percent = (int)((i + 1) * 100.0 / total); |
| 116 | + await WriteEvent(ImageToAbEventType.Progress, percent.ToString()); |
| 117 | + } |
| 118 | + catch (Exception ex) |
| 119 | + { |
| 120 | + logger.LogError(ex, "Failed to create AB for image {ImagePath}", item.FilePath); |
| 121 | + failures.Add($"{Path.GetFileName(item.FilePath)}: {ex.Message}"); |
| 122 | + } |
| 123 | + } |
| 124 | + |
| 125 | + if (failures.Count > 0) |
| 126 | + { |
| 127 | + await WriteEvent( |
| 128 | + ImageToAbEventType.Error, |
| 129 | + $"{string.Format(Locale.ConvertFailed, $"{failures.Count}/{total}")}\n{string.Join("\n", failures)}"); |
| 130 | + return; |
| 131 | + } |
| 132 | + |
| 133 | + await WriteEvent(ImageToAbEventType.Success, selectedPath); |
| 134 | + } |
| 135 | + |
| 136 | + private async Task WriteEvent(ImageToAbEventType eventType, string data) |
| 137 | + { |
| 138 | + await Response.WriteAsync($"event: {eventType}\ndata: {data}\n\n"); |
| 139 | + await Response.Body.FlushAsync(); |
| 140 | + } |
| 141 | + |
| 142 | + private sealed record ImageTaskItem(string FilePath, string Id); |
| 143 | +} |
0 commit comments