Skip to content

Commit 3abfc40

Browse files
committed
feat: 图片转 ab
1 parent d03d598 commit 3abfc40

13 files changed

Lines changed: 489 additions & 3 deletions

File tree

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
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+
}

MaiChartManager/Front/src/locales/en.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,8 @@ tools:
496496
convertSuccess: Conversion complete!
497497
convertFailed: Conversion failed
498498
videoConvertError: Video conversion error
499+
imageToAb: Image to AssetBundle
500+
imageToAbError: Image to AB conversion error
499501
videoOptions:
500502
processing: Still processing, please wait...
501503
error:

MaiChartManager/Front/src/locales/zh-TW.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,8 @@ tools:
459459
convertSuccess: 轉換完成!
460460
convertFailed: 轉換失敗
461461
videoConvertError: 影片轉換出錯
462+
imageToAb: 圖片轉 AssetBundle
463+
imageToAbError: 圖片轉 AB 出錯
462464
videoOptions:
463465
processing: 還在處理,別急…
464466
error:

MaiChartManager/Front/src/locales/zh.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,8 @@ tools:
453453
convertSuccess: 转换完成!
454454
convertFailed: 转换失败
455455
videoConvertError: 视频转换出错
456+
imageToAb: 图片转 AssetBundle
457+
imageToAbError: 图片转 AB 出错
456458
videoOptions:
457459
processing: 还在处理,别急…
458460
error:
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { getUrl } from '@/client/api';
2+
import { Modal, Progress, addToast } from '@munet/ui';
3+
import { defineComponent, ref } from 'vue';
4+
import { globalCapture } from '@/store/refs';
5+
import { fetchEventSource } from '@microsoft/fetch-event-source';
6+
import { handleSseOpen } from '@/utils/sseOpen';
7+
import { useI18n } from 'vue-i18n';
8+
9+
enum STEP {
10+
None,
11+
Progress,
12+
}
13+
14+
export default defineComponent({
15+
setup(props, { expose }) {
16+
const step = ref(STEP.None);
17+
const progress = ref(0);
18+
const { t } = useI18n();
19+
20+
const handleImageToAb = async () => {
21+
step.value = STEP.Progress;
22+
progress.value = 0;
23+
24+
const controller = new AbortController();
25+
26+
try {
27+
await new Promise<void>((resolve, reject) => {
28+
fetchEventSource(getUrl('ImageToAbToolApi'), {
29+
signal: controller.signal,
30+
method: 'POST',
31+
onopen: handleSseOpen,
32+
onerror(e) {
33+
reject(e);
34+
controller.abort();
35+
throw new Error("disable retry onerror");
36+
},
37+
onclose() {
38+
reject(new Error("EventSource Close"));
39+
controller.abort();
40+
throw new Error("disable retry onclose");
41+
},
42+
openWhenHidden: true,
43+
onmessage: (e) => {
44+
switch (e.event) {
45+
case 'Progress':
46+
progress.value = parseInt(e.data);
47+
break;
48+
case 'Success':
49+
console.log("success", e.data);
50+
controller.abort();
51+
addToast({message: t('tools.convertSuccess'), type: 'success'});
52+
resolve();
53+
break;
54+
case 'Error':
55+
controller.abort();
56+
reject(new Error(e.data));
57+
break;
58+
}
59+
}
60+
});
61+
});
62+
} catch (e: any) {
63+
if (e?.name === 'AbortError') return;
64+
console.log(e);
65+
if (e.message === t('error.file.notSelected')) return;
66+
globalCapture(e, t('tools.imageToAbError'));
67+
} finally {
68+
step.value = STEP.None;
69+
}
70+
};
71+
72+
const trigger = () => {
73+
handleImageToAb();
74+
};
75+
76+
expose({
77+
trigger,
78+
});
79+
80+
return () => <>
81+
<Modal
82+
width="min(40vw,40em)"
83+
title={t('tools.converting')}
84+
show={step.value === STEP.Progress}
85+
esc={false}
86+
>
87+
<Progress
88+
percentage={progress.value}
89+
status="success"
90+
showIndicator
91+
>
92+
{progress.value === 100 ? t('tools.videoOptions.processing') : `${progress.value}%`}
93+
</Progress>
94+
</Modal>
95+
</>;
96+
},
97+
});

MaiChartManager/Front/src/views/Tools/index.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import api from '@/client/api';
22
import { addToast } from '@munet/ui';
33
import { defineComponent, ref } from 'vue';
44
import VideoConvertButton from '@/views/Tools/VideoConvertModal';
5+
import ImageToAbModal from '@/views/Tools/ImageToAbModal';
56
import { useI18n } from 'vue-i18n';
67

78
interface ToolCard {
@@ -14,6 +15,7 @@ interface ToolCard {
1415
export default defineComponent({
1516
setup() {
1617
const videoConvertRef = ref<{ trigger: () => void }>();
18+
const imageToAbRef = ref<{ trigger: () => void }>();
1719
const { t } = useI18n();
1820

1921
const handleAudioConvert = async () => {
@@ -40,6 +42,11 @@ export default defineComponent({
4042
labelKey: 'tools.videoConvert',
4143
action: () => videoConvertRef.value?.trigger(),
4244
},
45+
{
46+
icon: 'i-mdi-image',
47+
labelKey: 'tools.imageToAb',
48+
action: () => imageToAbRef.value?.trigger(),
49+
},
4350
];
4451

4552
return () => (
@@ -64,6 +71,7 @@ export default defineComponent({
6471
))}
6572
</div>
6673
<VideoConvertButton ref={videoConvertRef as any} />
74+
<ImageToAbModal ref={imageToAbRef as any} />
6775
</div>
6876
);
6977
},

MaiChartManager/Locale.Designer.cs

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

MaiChartManager/Locale.resx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,14 @@ If you notice any issues with the conversion result, you can try testing it in A
277277
<value>Video or Image|*.mp4;*.mov;*.avi;*.mkv;*.jpg;*.jpeg;*.png;*.gif;*.bmp;*.webp;*.svg;*.dat;*.usm</value>
278278
</data>
279279

280+
<!-- ImageToAbToolController -->
281+
<data name="SelectImageFolder" xml:space="preserve">
282+
<value>Please select folder containing jacket images</value>
283+
</data>
284+
<data name="NoValidImagesFound" xml:space="preserve">
285+
<value>No valid images found (expected: numeric filename or ui_jacket_* pattern, PNG/JPG format)</value>
286+
</data>
287+
280288
<!-- ModController -->
281289
<data name="UnsupportedConfigVersion" xml:space="preserve">
282290
<value>Incompatible configuration file version</value>
@@ -305,4 +313,4 @@ If you notice any issues with the conversion result, you can try testing it in A
305313
<data name="UnsupportedChartFormat" xml:space="preserve">
306314
<value>Unsupported file format</value>
307315
</data>
308-
</root>
316+
</root>

MaiChartManager/Locale.zh-hans.resx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,14 @@
269269
<value>视频或者图片|*.mp4;*.mov;*.avi;*.mkv;*.jpg;*.jpeg;*.png;*.gif;*.bmp;*.webp;*.svg;*.dat;*.usm</value>
270270
</data>
271271

272+
<!-- ImageToAbToolController -->
273+
<data name="SelectImageFolder" xml:space="preserve">
274+
<value>请选择包含封面图片的文件夹</value>
275+
</data>
276+
<data name="NoValidImagesFound" xml:space="preserve">
277+
<value>未找到有效的图片文件(需要:纯数字文件名或 ui_jacket_* 格式,PNG/JPG 格式)</value>
278+
</data>
279+
272280
<!-- ModController -->
273281
<data name="UnsupportedConfigVersion" xml:space="preserve">
274282
<value>无法兼容的配置文件版本</value>
@@ -297,4 +305,4 @@
297305
<data name="UnsupportedChartFormat" xml:space="preserve">
298306
<value>不支持的文件格式</value>
299307
</data>
300-
</root>
308+
</root>

MaiChartManager/Locale.zh-hant.resx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,14 @@
269269
<value>影片或者圖片|*.mp4;*.mov;*.avi;*.mkv;*.jpg;*.jpeg;*.png;*.gif;*.bmp;*.webp;*.svg;*.dat;*.usm</value>
270270
</data>
271271

272+
<!-- ImageToAbToolController -->
273+
<data name="SelectImageFolder" xml:space="preserve">
274+
<value>請選擇包含封面圖片的資料夾</value>
275+
</data>
276+
<data name="NoValidImagesFound" xml:space="preserve">
277+
<value>未找到有效的圖片檔案(需要:純數字檔名或 ui_jacket_* 格式,PNG/JPG 格式)</value>
278+
</data>
279+
272280
<!-- ModController -->
273281
<data name="UnsupportedConfigVersion" xml:space="preserve">
274282
<value>無法相容的設定檔案版本</value>
@@ -297,4 +305,4 @@
297305
<data name="UnsupportedChartFormat" xml:space="preserve">
298306
<value>不支援的檔案格式</value>
299307
</data>
300-
</root>
308+
</root>

0 commit comments

Comments
 (0)