diff --git a/MaiChartManager/Controllers/App/AppVersionController.cs b/MaiChartManager/Controllers/App/AppVersionController.cs index 01e1454b..ae8e47c9 100644 --- a/MaiChartManager/Controllers/App/AppVersionController.cs +++ b/MaiChartManager/Controllers/App/AppVersionController.cs @@ -8,16 +8,24 @@ namespace MaiChartManager.Controllers.App; public class AppVersionController(StaticSettings settings, ILogger logger) : ControllerBase { #if WINDOWS - public record AppVersionResult(string Version, int GameVersion, IapManager.LicenseStatus License, VideoConvert.HardwareAccelerationStatus HardwareAcceleration, string H264Encoder, string Locale); + public record AppVersionResult(string Version, int GameVersion, IapManager.LicenseStatus License, VideoConvert.HardwareAccelerationStatus HardwareAcceleration, string H264Encoder, string Locale, string Platform, bool Export); [HttpGet] public AppVersionResult GetAppVersion() { - return new AppVersionResult(Application.ProductVersion, settings.gameVersion, IapManager.License, VideoConvert.HardwareAcceleration, VideoConvert.H264Encoder, StaticSettings.CurrentLocale); + return new AppVersionResult( + Application.ProductVersion, + settings.gameVersion, + IapManager.License, + VideoConvert.HardwareAcceleration, + VideoConvert.H264Encoder, + StaticSettings.CurrentLocale, + OperatingSystem.IsWindows() ? "Windows" : "Linux", + StaticSettings.Config.Export); } #else public enum LicenseStatus { Pending, Active, Inactive } - public record AppVersionResult(string Version, int GameVersion, LicenseStatus License, VideoConvert.HardwareAccelerationStatus HardwareAcceleration, string H264Encoder, string Locale); + public record AppVersionResult(string Version, int GameVersion, LicenseStatus License, VideoConvert.HardwareAccelerationStatus HardwareAcceleration, string H264Encoder, string Locale, string Platform, bool Export); [HttpGet] public AppVersionResult GetAppVersion() @@ -29,7 +37,15 @@ public AppVersionResult GetAppVersion() var info = (System.Reflection.AssemblyInformationalVersionAttribute?)System.Attribute .GetCustomAttribute(asm, typeof(System.Reflection.AssemblyInformationalVersionAttribute)); var version = info?.InformationalVersion?.Split('+')[0] ?? "linux"; - return new AppVersionResult(version, settings.gameVersion, LicenseStatus.Active, VideoConvert.HardwareAcceleration, VideoConvert.H264Encoder, StaticSettings.CurrentLocale); + return new AppVersionResult( + version, + settings.gameVersion, + LicenseStatus.Active, + VideoConvert.HardwareAcceleration, + VideoConvert.H264Encoder, + StaticSettings.CurrentLocale, + OperatingSystem.IsWindows() ? "Windows" : "Linux", + StaticSettings.Config.Export); } #endif } diff --git a/MaiChartManager/Controllers/AssetDir/ImportBrowseController.cs b/MaiChartManager/Controllers/AssetDir/ImportBrowseController.cs index c547a223..51c2477d 100644 --- a/MaiChartManager/Controllers/AssetDir/ImportBrowseController.cs +++ b/MaiChartManager/Controllers/AssetDir/ImportBrowseController.cs @@ -23,7 +23,7 @@ public record ImportDirEntry(string Name, string Path, bool IsDirectory); public ActionResult PickImportFolder() { // 仅本地桌面场景,export / 远程模式下禁止 - if (StaticSettings.Config.Export) return Forbid(); + if (StaticSettings.Config.Export) return StatusCode(StatusCodes.Status403Forbidden); // 如果直接 return Forbid(); 的话,会由于框架内部的某些authentication scheme机制造成抛异常,导致500 var path = dialogService.PickFolder(); logger.LogInformation("PickImportFolder: {path}", path); // 取消时 PickFolder 返回 null,这里原样返回(前端按取消处理) @@ -35,7 +35,7 @@ public record ImportDirEntry(string Name, string Path, bool IsDirectory); public ActionResult> ListImportDir([FromQuery] string path) { // 仅本地桌面场景,export / 远程模式下禁止 - if (StaticSettings.Config.Export) return Forbid(); + if (StaticSettings.Config.Export) return StatusCode(StatusCodes.Status403Forbidden); if (string.IsNullOrEmpty(path) || !Directory.Exists(path)) { return Ok(Array.Empty()); @@ -64,7 +64,7 @@ public ActionResult> ListImportDir([FromQuery] strin public IActionResult ReadImportFile([FromQuery] string path, [FromQuery] string? name = null) { // 仅本地桌面场景,export / 远程模式下禁止 - if (StaticSettings.Config.Export) return Forbid(); + if (StaticSettings.Config.Export) return StatusCode(StatusCodes.Status403Forbidden); var fullPath = string.IsNullOrEmpty(name) ? path : Path.Combine(path, name); if (string.IsNullOrEmpty(fullPath) || !System.IO.File.Exists(fullPath)) { diff --git a/MaiChartManager/Front/src/client/api.ts b/MaiChartManager/Front/src/client/api.ts index 2655781a..fe94a0a8 100644 --- a/MaiChartManager/Front/src/client/api.ts +++ b/MaiChartManager/Front/src/client/api.ts @@ -43,9 +43,12 @@ export const getUrl = (suffix: string) => { return `${base}/MaiChartManagerServlet/${suffix}`; } -// 是否运行在 Photino(WebKitGTK) 宿主:本地宿主但不是 Windows WebView2。 -// WebKitGTK 不支持 window.open 弹新窗口,需要走后端用系统浏览器打开。 -export const isPhotino = isLocalHost && !isWebView; +// 是否运行在 Photino(WebKitGTK) 宿主。 +// 不能仅用 isLocalHost && !isWebView:因为Export 模式下用本机浏览器访问 localhost 也满足该条件。 +// Photino 暴露 window.external.sendMessage(见 PreviewChartButton),普通浏览器没有。 +export const isPhotino = + isLocalHost && !isWebView && + typeof (window as any).external?.sendMessage === 'function'; // 用系统浏览器打开一个 http/https URL(后端 xdg-open 等)。 // 给 Photino 用:WebKitGTK 弹不出 window.open 的新窗口,预览谱面等改为外部浏览器打开。 diff --git a/MaiChartManager/Front/src/client/apiGen.ts b/MaiChartManager/Front/src/client/apiGen.ts index ae3b8c81..0b44405e 100644 --- a/MaiChartManager/Front/src/client/apiGen.ts +++ b/MaiChartManager/Front/src/client/apiGen.ts @@ -17,6 +17,14 @@ export enum VerifyStatus { Valid = "Valid", } +export enum StorePurchaseStatus { + Succeeded = "Succeeded", + AlreadyPurchased = "AlreadyPurchased", + NotPurchased = "NotPurchased", + NetworkError = "NetworkError", + ServerError = "ServerError", +} + export enum ShiftMethod { Legacy = "Legacy", Bar = "Bar", @@ -70,6 +78,8 @@ export interface AppVersionResult { hardwareAcceleration?: HardwareAccelerationStatus; h264Encoder?: string | null; locale?: string | null; + platform?: string | null; + export?: boolean; } export interface AudioPreviewTime { @@ -385,8 +395,7 @@ export interface RequestExportMaidataRequest { export interface RequestPurchaseResult { errorMessage?: string | null; - /** @format int32 */ - status?: number; + status?: StorePurchaseStatus; } export interface Section { diff --git a/MaiChartManager/Front/src/views/Charts/ImportCreateChartButton/ImportChartButton/ErrorDisplayIdInput.tsx b/MaiChartManager/Front/src/views/Charts/ImportCreateChartButton/ImportChartButton/ErrorDisplayIdInput.tsx index 524a3de4..18b2b6c8 100644 --- a/MaiChartManager/Front/src/views/Charts/ImportCreateChartButton/ImportChartButton/ErrorDisplayIdInput.tsx +++ b/MaiChartManager/Front/src/views/Charts/ImportCreateChartButton/ImportChartButton/ErrorDisplayIdInput.tsx @@ -2,7 +2,7 @@ import { computed, defineComponent, PropType, useId, watch } from "vue"; import { Button, CheckBox, Modal, NumberInput, Popover, Section, Select } from "@munet/ui"; import type { ImportChartMessageEx, ImportMeta, SavedOptions, TempOptions } from "./types"; import noJacket from '@/assets/noJacket.webp'; -import { addVersionList, genreList, showNeedPurchaseDialog } from "@/store/refs"; +import { addVersionList, genreList, showNeedPurchaseDialog, version } from "@/store/refs"; import GenreInput from "@/components/GenreInput"; import VersionInput from "@/components/VersionInput"; import { UTAGE_GENRE } from "@/consts"; @@ -76,7 +76,7 @@ export default defineComponent({
-
+ {version.value?.platform === 'Windows' &&
{t('chart.import.option.ignoreGapless')} {{ @@ -84,7 +84,7 @@ export default defineComponent({ default: () =>
{t('chart.import.option.ignoreGaplessTip')}
}}
-
+
}
} , diff --git a/MaiChartManager/Front/src/views/Charts/ImportCreateChartButton/ImportChartButton/importErrors.ts b/MaiChartManager/Front/src/views/Charts/ImportCreateChartButton/ImportChartButton/importErrors.ts index b9873df8..bb472904 100644 --- a/MaiChartManager/Front/src/views/Charts/ImportCreateChartButton/ImportChartButton/importErrors.ts +++ b/MaiChartManager/Front/src/views/Charts/ImportCreateChartButton/ImportChartButton/importErrors.ts @@ -33,6 +33,4 @@ export const getCaptureTarget = (errorValue: unknown): unknown => { return errorValue.error; }; -export const isAbortError = (errorValue: unknown): boolean => - errorValue instanceof DOMException && errorValue.name === "AbortError" || - errorValue instanceof Error && errorValue.name === "AbortError"; +export const isAbortError = (errorValue: any): boolean => errorValue?.name === "AbortError" diff --git a/MaiChartManager/Locale.Designer.cs b/MaiChartManager/Locale.Designer.cs index c1819e57..14ac8a42 100644 --- a/MaiChartManager/Locale.Designer.cs +++ b/MaiChartManager/Locale.Designer.cs @@ -140,6 +140,33 @@ internal static string AwbNotFound { } } + /// + /// Looks up a localized string similar to Selected folder does not exist. + /// + internal static string BatchConvertPvFolderNotFound { + get { + return ResourceManager.GetString("BatchConvertPvFolderNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Batch PV conversion requires sponsor activation. + /// + internal static string BatchConvertPvNeedLicense { + get { + return ResourceManager.GetString("BatchConvertPvNeedLicense", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No PV files found in the matching format. + /// + internal static string BatchConvertPvNoFiles { + get { + return ResourceManager.GetString("BatchConvertPvNoFiles", resourceCulture); + } + } + /// /// Looks up a localized string similar to Browser window not initialized. /// @@ -667,7 +694,7 @@ internal static string MusicIdExists { } /// - /// Looks up a localized string similar to Music has no charts. + /// Looks up a localized string similar to Music has no valid charts. /// internal static string MusicNoCharts { get { @@ -801,33 +828,6 @@ internal static string SelectVideoToConvert { } } - /// - /// Looks up a localized string similar to Batch PV conversion requires sponsor activation. - /// - internal static string BatchConvertPvNeedLicense { - get { - return ResourceManager.GetString("BatchConvertPvNeedLicense", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to No PV files found in the matching format. - /// - internal static string BatchConvertPvNoFiles { - get { - return ResourceManager.GetString("BatchConvertPvNoFiles", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Selected folder does not exist. - /// - internal static string BatchConvertPvFolderNotFound { - get { - return ResourceManager.GetString("BatchConvertPvFolderNotFound", resourceCulture); - } - } - /// /// Looks up a localized string similar to Unknown error. /// diff --git a/MaiChartManager/Locale.resx b/MaiChartManager/Locale.resx index 6c7f2324..12ea3470 100644 --- a/MaiChartManager/Locale.resx +++ b/MaiChartManager/Locale.resx @@ -199,7 +199,7 @@ If you notice any issues with the conversion result, you can try testing it in A A chart with difficulty {0} will be ignored - Music has no charts + Music has no valid charts Chart difficulty {0} contains {1}-note division, this value cannot exceed 384. Most cases can be fixed by modifying the chart diff --git a/MaiChartManager/Locale.zh-Hans.resx b/MaiChartManager/Locale.zh-Hans.resx index c8536ba3..c12c0efd 100644 --- a/MaiChartManager/Locale.zh-Hans.resx +++ b/MaiChartManager/Locale.zh-Hans.resx @@ -191,7 +191,7 @@ 有一个难度为 {0} 的谱面将被忽略 - 乐曲没有谱面 + 乐曲中没有有效的谱面 谱面难度 {0} 存在 {1} 分音符,这个数值不能大于 384。绝大多数这样的情况都是可以修改谱面解决的 diff --git a/MaiChartManager/Locale.zh-Hant.resx b/MaiChartManager/Locale.zh-Hant.resx index 72dbff24..94349f91 100644 --- a/MaiChartManager/Locale.zh-Hant.resx +++ b/MaiChartManager/Locale.zh-Hant.resx @@ -191,7 +191,7 @@ 有一個難度為 {0} 的譜面將被忽略 - 樂曲沒有譜面 + 樂曲中沒有有效的譜面 譜面難度 {0} 存在 {1} 分音符,這個數值不能大於 384。絕大多數這樣的情況都是可以修改譜面解決的 diff --git a/MaiChartManager/Utils/Audio.cs b/MaiChartManager/Utils/Audio.cs index e592bd27..ba875a09 100644 --- a/MaiChartManager/Utils/Audio.cs +++ b/MaiChartManager/Utils/Audio.cs @@ -67,9 +67,11 @@ public static Stream ConvertToWav(Stream src, string extension, float padding = using WaveStream reader = extension switch { ".ogg" => new NAudio.Vorbis.VorbisWaveReader(src, true), - ".mp3" when !forceUseNAudio => new WaveFileReader(ConvertToWavViaFfmpeg(src, ".mp3")), // 默认情况下,优先使用ffmpeg - // WAV / WMA / AAC(以及 MP3+forceUseNAudio 的兼容模式)原本走 Windows-only 的 MediaFoundation, - // 跨平台改为用 ffmpeg 把任意输入解码成 16bit PCM wav,再用 NAudio WaveFileReader 读取。 + // MP3 兼容模式(ignoreGapless):仅在 Windows 上用 MediaFoundation 解码, + // 这会忽略 MP3 Gapless 元数据,从而表现与 Visual Maimai 等软件一致的行为。 + // Linux 上无 MediaFoundation,因此兼容模式不可用(不管开不开启兼容模式,都一定会落到下面的ffmpeg逻辑中) + ".mp3" when forceUseNAudio && SupportsMp3CompatibilityMode => new StreamMediaFoundationReader(src), + // 一般情况(MP3 默认、WAV、WMA、AAC 等):走 ffmpeg 解码为 16bit PCM wav。 _ => new WaveFileReader(ConvertToWavViaFfmpeg(src, extension)), }; // 关于上述MP3 Gapless问题的影响等具体讨论,详见 https://github.com/MuNET-OSS/MaiChartManager/issues/40 @@ -99,16 +101,21 @@ public static Stream ConvertToWav(Stream src, string extension, float padding = stream.Position = 0; return stream; } + + /// MP3 兼容模式(ignoreGapless)依赖 Windows MediaFoundation,Linux 上不可用。 + public static bool SupportsMp3CompatibilityMode => OperatingSystem.IsWindows(); // 用 ffmpeg 把任意输入流(按 ext 写到临时文件)解码成 16bit PCM wav,返回 wav 的内存流。 // 替代 Windows-only 的 MediaFoundation,跨平台可用(系统 ffmpeg 已配好)。 private static MemoryStream ConvertToWavViaFfmpeg(Stream src, string ext) { - var tempFileGuid = Guid.NewGuid(); // ext 形如 ".mp3"/".wav"/".aac" 等;去掉前导点用作临时输入文件后缀 var inputExt = string.IsNullOrEmpty(ext) ? "" : (ext.StartsWith('.') ? ext : "." + ext); + + var tempFileGuid = Guid.NewGuid(); + // 输入/输出须用不同文件名,以防输入也是 .wav时,输入输出相同文件名导致报错。 var inputPath = Path.Combine(StaticSettings.tempPath, $"ConvertToWav_{tempFileGuid:N}{inputExt}"); - var outputPath = Path.Combine(StaticSettings.tempPath, $"ConvertToWav_{tempFileGuid:N}.wav"); + var outputPath = Path.Combine(StaticSettings.tempPath, $"ConvertToWav_{tempFileGuid:N}_out.wav"); try { Directory.CreateDirectory(StaticSettings.tempPath); diff --git a/MaiChartManager/Utils/ImageConvert.cs b/MaiChartManager/Utils/ImageConvert.cs index 223b0996..8c4f4c97 100644 --- a/MaiChartManager/Utils/ImageConvert.cs +++ b/MaiChartManager/Utils/ImageConvert.cs @@ -23,31 +23,40 @@ public static class ImageConvert public static byte[]? GetTextureAsPngData(string inputAbPath) { var am = new AssetsManager(); - var bunInst = am.LoadBundleFile(inputAbPath, true); - var afileInst = am.LoadAssetsFileFromBundle(bunInst, 0, false); - - foreach (var info in afileInst.file.Metadata.AssetInfos) + BundleFileInstance? bunInst = null; + try { - var baseField = am.GetBaseField(afileInst, info); - if (baseField.IsDummy || baseField.TypeName != "Texture2D") continue; + bunInst = am.LoadBundleFile(inputAbPath, true); + var afileInst = am.LoadAssetsFileFromBundle(bunInst, 0, false); + + foreach (var info in afileInst.file.Metadata.AssetInfos) + { + var baseField = am.GetBaseField(afileInst, info); + if (baseField.IsDummy || baseField.TypeName != "Texture2D") continue; - var tex = TextureFile.ReadTextureFile(baseField); - // 从 bundle 内解析贴图像素数据:同时覆盖 inline "image data" 与 bundle 内部的 .resS streamData。 - // 游戏原始封面的像素数据存在 bundle 内部的 .resS 里,必须用 bundle 解析, - // 不能只读文件系统目录(之前用 FillPictureData(目录) 会导致原始封面解析失败、接口返回 404)。 - tex.SetPictureDataFromBundle(bunInst); - if (tex.pictureData is null || tex.pictureData.Length == 0) return null; + var tex = TextureFile.ReadTextureFile(baseField); + // 从 bundle 内解析贴图像素数据:同时覆盖 inline "image data" 与 bundle 内部的 .resS streamData。 + // 游戏原始封面的像素数据存在 bundle 内部的 .resS 里,必须用 bundle 解析, + // 不能只读文件系统目录(之前用 FillPictureData(目录) 会导致原始封面解析失败、接口返回 404)。 + tex.SetPictureDataFromBundle(bunInst); + if (tex.pictureData is null || tex.pictureData.Length == 0) return null; - var bgra = TextureFile.DecodeManagedData( - tex.pictureData, (TextureFormat)tex.m_TextureFormat, tex.m_Width, tex.m_Height, true); - if (bgra is null || bgra.Length == 0) return null; + var bgra = TextureFile.DecodeManagedData( + tex.pictureData, (TextureFormat)tex.m_TextureFormat, tex.m_Width, tex.m_Height, true); + if (bgra is null || bgra.Length == 0) return null; - using var image = Image.LoadPixelData(bgra, tex.m_Width, tex.m_Height); - image.Mutate(x => x.Flip(FlipMode.Vertical)); - using var ms = new MemoryStream(); - image.SaveAsPng(ms); - return ms.ToArray(); + using var image = Image.LoadPixelData(bgra, tex.m_Width, tex.m_Height); + image.Mutate(x => x.Flip(FlipMode.Vertical)); + using var ms = new MemoryStream(); + image.SaveAsPng(ms); + return ms.ToArray(); + } + return null; + } + finally + { + if (bunInst is not null) + am.UnloadBundleFile(bunInst); } - return null; } } diff --git a/MaiLib b/MaiLib deleted file mode 160000 index c76e94e1..00000000 --- a/MaiLib +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c76e94e16592db4510e6f3b5f86cd2b7b360a350 diff --git a/SimaiSharp b/SimaiSharp deleted file mode 160000 index bcaf9387..00000000 --- a/SimaiSharp +++ /dev/null @@ -1 +0,0 @@ -Subproject commit bcaf9387fb8dce045edba2415d8a8cca1d5057a7 diff --git a/Sitreamai.slnx b/Sitreamai.slnx index 084d400c..20dc41b5 100644 --- a/Sitreamai.slnx +++ b/Sitreamai.slnx @@ -19,10 +19,7 @@ - - - - +