diff --git a/Chruch.Net/Chruch.Net.sln b/Chruch.Net/Chruch.Net.sln new file mode 100644 index 0000000..c022d0b --- /dev/null +++ b/Chruch.Net/Chruch.Net.sln @@ -0,0 +1,37 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.4.33205.214 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Chruch.Net", "Chruch.Net.csproj", "{755399EA-BCFB-48A7-8A85-923054F979CA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestProject1", "..\TestProject1\TestProject1.csproj", "{F75C4820-424C-4AA9-8C0E-B6527AB76C2D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PropresentorScript", "..\PropresentorScript\PropresentorScript.csproj", "{D9C65F50-0828-4E88-9C8F-250A0AB57C98}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {755399EA-BCFB-48A7-8A85-923054F979CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {755399EA-BCFB-48A7-8A85-923054F979CA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {755399EA-BCFB-48A7-8A85-923054F979CA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {755399EA-BCFB-48A7-8A85-923054F979CA}.Release|Any CPU.Build.0 = Release|Any CPU + {F75C4820-424C-4AA9-8C0E-B6527AB76C2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F75C4820-424C-4AA9-8C0E-B6527AB76C2D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F75C4820-424C-4AA9-8C0E-B6527AB76C2D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F75C4820-424C-4AA9-8C0E-B6527AB76C2D}.Release|Any CPU.Build.0 = Release|Any CPU + {D9C65F50-0828-4E88-9C8F-250A0AB57C98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D9C65F50-0828-4E88-9C8F-250A0AB57C98}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D9C65F50-0828-4E88-9C8F-250A0AB57C98}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D9C65F50-0828-4E88-9C8F-250A0AB57C98}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {CA8E6F4D-EBB4-4298-B882-FDB841B667BE} + EndGlobalSection +EndGlobal diff --git a/Church.Net.DAL.EFCoreDBF/D2MobInfoDAL.cs b/Church.Net.DAL.EFCoreDBF/D2MobInfoDAL.cs new file mode 100644 index 0000000..11282e6 --- /dev/null +++ b/Church.Net.DAL.EFCoreDBF/D2MobInfoDAL.cs @@ -0,0 +1,28 @@ +using Church.Net.DAL.EF; +using Church.Net.DAL.EFCoreDBF.Core; +using Church.Net.DAL.EFCoreDBF.Interface; +using Church.Net.Entity.Games.MD2; +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Church.Net.DAL.EFCoreDBF +{ + public class D2MobInfoDAL : CrudDALCBase, ICrudDAL + { + public D2MobInfoDAL(DatabaseOptions databaseOptions) : base(databaseOptions) + { + } + public override IQueryable InitQuery(ChurchNetContext dbContext) + { + return dbContext.Md2MobInfos + .Include(m=>m.MobLevelInfos).ThenInclude(s=>s.AttackInfo) + .Include(m => m.MobLevelInfos).ThenInclude(s => s.DefenceInfo) + .Include(m => m.MobLevelInfos).ThenInclude(s => s.Skills) + .Include(m => m.Skills); + } + } +} diff --git a/Church.Net.Entity2/Games/MD2/MobInfo.cs b/Church.Net.Entity2/Games/MD2/MobInfo.cs index e08105c..fcbc0e4 100644 --- a/Church.Net.Entity2/Games/MD2/MobInfo.cs +++ b/Church.Net.Entity2/Games/MD2/MobInfo.cs @@ -32,10 +32,14 @@ namespace Church.Net.Entity.Games.MD2 public enum MobSkillTarget { - LeastHp, - LeastMaxHp, - LeastMana, - LeastMaxMana, + Random = 40, + LeastHp = 50, + LeastMp = 60, + HighestHp = 70, + HighestMp = 80, + LowestLevel = 90, + MostCorruption = 200, + LeastCorruption = 201 } public enum MD2Icon { @@ -129,6 +133,8 @@ namespace Church.Net.Entity.Games.MD2 public virtual MobLevelInfo MobLevelInfo { get; set; } public MobSkillType Type { get; set; } + public MobSkillTarget? SkillTarget { get; set; } + public int ClawRoll { get; set; } public int SkillRoll { get; set; } public string Name { get; set; } diff --git a/Church.Net.WebAPI/Bindings/APIBinding.cs b/Church.Net.WebAPI/Bindings/APIBinding.cs new file mode 100644 index 0000000..3be8a56 --- /dev/null +++ b/Church.Net.WebAPI/Bindings/APIBinding.cs @@ -0,0 +1,58 @@ +using Church.Net.DAL.EF; +using Church.Net.DAL.EFCoreDBF.Core; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using System; +using WebAPI.Hubs; +using WebAPI; +using Newtonsoft.Json.Serialization; +using WebAPI.Services; +using Microsoft.AspNetCore.Authorization; +using WebAPI.Handlers; + +namespace Church.Net.WebAPI.Bindings +{ + public class APIBinding : IBinding + { + public void Binding(IServiceCollection services) + { + services.AddControllers().AddNewtonsoftJson(options => + { + // Use the default property (Pascal) casing + options.SerializerSettings.ContractResolver = new DefaultContractResolver() + { + NamingStrategy = new CamelCaseNamingStrategy(), + + }; + //options.SerializerSettings.DateTimeZoneHandling = Newtonsoft.Json.DateTimeZoneHandling.Unspecified; + options.SerializerSettings.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore; + options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore; + }); + + services.AddSignalR(); + + services.AddSingleton(); + + services.AddSingleton(); + + //Localted at \\ArkNAS\docker\ChurchAPI\docker-compose.yaml + string databaseConnString = Environment.GetEnvironmentVariable("DB_CONN_STRING"); +#if DEBUG + databaseConnString = "Host=192.168.68.55;Port=49154;Database=Church;Username=chris;Password=1124"; +#endif + //services.AddSingleton(_ => new DatabaseOptions { ConnectionString = databaseConnString }); + services.AddSingleton(_ => new DatabaseOptions { ConnectionString = databaseConnString }); + //services.AddSingleton(new ChurchNetContext()); + services.AddDbContext(options => + options.UseNpgsql( + //Configuration.GetConnectionString() + //"Host=192.168.68.55;Port=49154;Database=ChurchSandbox;Username=chris;Password=1124" + databaseConnString + )); + + services.AddHostedService(); + services.AddSingleton(); + + } + } +} diff --git a/Church.Net.WebAPI/Bindings/DALBinding.cs b/Church.Net.WebAPI/Bindings/DALBinding.cs new file mode 100644 index 0000000..0f589e6 --- /dev/null +++ b/Church.Net.WebAPI/Bindings/DALBinding.cs @@ -0,0 +1,25 @@ +using Church.Net.DAL.EFCoreDBF; +using Church.Net.DAL.EFCoreDBF.Core; +using Church.Net.DAL.EFCoreDBF.Interface; +using Church.Net.Entity; +using Church.Net.Entity.Games.MD2; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using WebAPI.Logics.Interface; +using WebAPI.Logics; + +namespace Church.Net.WebAPI.Bindings +{ + public class DALBinding : IBinding + { + public void Binding(IServiceCollection services) + { + services.AddScoped(typeof(ICrudDAL<>), typeof(CrudDALCBase<>)); + services.AddScoped(typeof(ICombinedKeyCrudDAL<>), typeof(CombinedKeyCrudDALCBase<>)); + + services.AddScoped, D2MobInfoDAL>(); + //D2MobInfoDAL: CrudDALCBase, ICrudDAL + + } + } +} diff --git a/Church.Net.WebAPI/Bindings/IBinding.cs b/Church.Net.WebAPI/Bindings/IBinding.cs new file mode 100644 index 0000000..b9868d6 --- /dev/null +++ b/Church.Net.WebAPI/Bindings/IBinding.cs @@ -0,0 +1,9 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Church.Net.WebAPI.Bindings +{ + public interface IBinding + { + void Binding(IServiceCollection services); + } +} \ No newline at end of file diff --git a/Church.Net.WebAPI/Bindings/LogicBinding.cs b/Church.Net.WebAPI/Bindings/LogicBinding.cs new file mode 100644 index 0000000..27945cb --- /dev/null +++ b/Church.Net.WebAPI/Bindings/LogicBinding.cs @@ -0,0 +1,40 @@ +using Microsoft.Extensions.DependencyInjection; +using WebAPI.Logics.Core; +using WebAPI.Logics.Interface; +using WebAPI.Logics; +using WebAPI.Services.AutoReplyCommands; +using WebAPI.Services.Interfaces; +using WebAPI.Services; +using Church.Net.Entity; + +namespace Church.Net.WebAPI.Bindings +{ + public class LogicBinding : IBinding + { + public void Binding(IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + //services.AddScoped(); + + + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(typeof(ICrudLogic<>), typeof(LogicBase<>)); + services.AddScoped(typeof(ICombinedKeyCrudLogic<>), typeof(CombinedKeyLogicBase<>)); + + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped, MemberLogic>(); + + } + } +} diff --git a/Church.Net.WebAPI/Controllers/MD2Controller.cs b/Church.Net.WebAPI/Controllers/MD2Controller.cs index e458a26..54d422e 100644 --- a/Church.Net.WebAPI/Controllers/MD2Controller.cs +++ b/Church.Net.WebAPI/Controllers/MD2Controller.cs @@ -16,4 +16,22 @@ namespace WebAPI.Controllers { } } + + + [Route("[controller]/[action]")] + [ApiController] + public class MobLevelInfoController : ApiControllerBase + { + public MobLevelInfoController(ICrudLogic logic) : base(logic) + { + } + } + [Route("[controller]/[action]")] + [ApiController] + public class MobSkillController : ApiControllerBase + { + public MobSkillController(ICrudLogic logic) : base(logic) + { + } + } } diff --git a/Church.Net.WebAPI/Extensions/ServiceCollectionExtensions.cs b/Church.Net.WebAPI/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..ca3c788 --- /dev/null +++ b/Church.Net.WebAPI/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,98 @@ +using Microsoft.Extensions.DependencyInjection; +using System.Collections.Generic; +using System.Data; +using System; +using System.Linq; + +namespace Church.Net.WebAPI.Extensions +{ + public static class ServiceCollectionExtensions + { + public static IServiceCollection Remove(this IServiceCollection services) + { + if (services.IsReadOnly) + { + throw new ReadOnlyException($"{nameof(services)} is read only"); + } + + var serviceDescriptors = services.Where(descriptor => descriptor.ServiceType == typeof(T)); + if (serviceDescriptors.Any()) + { + foreach (var service in serviceDescriptors) + { + services.Remove(service); + + } + } + + return services; + } + + public static IServiceCollection OverWriteTransient(this IServiceCollection services) where TService : class where TImplementation : class, TService + { + services.Remove(); + services.AddTransient(); + return services; + } + + public static IServiceCollection OverWriteScoped(this IServiceCollection services) where TService : class where TImplementation : class, TService + { + services.Remove(); + services.AddScoped(); + return services; + } + public static IServiceCollection OverWriteSingleton(this IServiceCollection services) where TService : class where TImplementation : class, TService + { + services.Remove(); + services.AddSingleton(); + return services; + } + + + //public static void AddFactory(this IServiceCollection services) + //where TService : class + //where TImplementation : class, TService + //{ + // services.AddTransient(); + // services.AddSingleton>(x => () => x.GetService()); + // services.AddSingleton, Factory>(); + //} + + + public static IServiceCollection AddMultiScoped(this IServiceCollection services) where IService : class where TBaseImplementation : class, IService + { + foreach (var type in GetImplementedTypes()) + { + services.AddScoped(h => (IService)Activator.CreateInstance(type)); + } + return services; + } + public static IServiceCollection AddMultiTransient(this IServiceCollection services) where IService : class where TBaseImplementation : class, IService + { + foreach (var type in GetImplementedTypes()) + { + services.AddTransient(h => (IService)Activator.CreateInstance(type)); + } + return services; + } + public static IServiceCollection AddMultiSingleton(this IServiceCollection services) where IService : class where TBaseImplementation : class, IService + { + foreach (var type in GetImplementedTypes()) + { + services.AddSingleton(h => (IService)Activator.CreateInstance(type)); + } + return services; + } + + private static IEnumerable GetImplementedTypes() where TBaseImplementation : class, TService + { + + Type interfaceType = typeof(TService); + Type baseType = typeof(TBaseImplementation); + IEnumerable types = System.Reflection.Assembly.GetAssembly(baseType).GetTypes() + .Where(p => !p.IsInterface && p != baseType && interfaceType.IsAssignableFrom(p)); + return types; + } + } + +} diff --git a/Church.Net.WebAPI/Startup.cs b/Church.Net.WebAPI/Startup.cs index 74beed5..c38bd1f 100644 --- a/Church.Net.WebAPI/Startup.cs +++ b/Church.Net.WebAPI/Startup.cs @@ -7,6 +7,8 @@ using Church.Net.DAL.EF; using Church.Net.DAL.EFCoreDBF.Core; using Church.Net.DAL.EFCoreDBF.Interface; using Church.Net.Entity; +using Church.Net.Entity.Games.MD2; +using Church.Net.WebAPI.Bindings; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics; @@ -17,6 +19,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Newtonsoft.Json.Serialization; @@ -48,73 +51,12 @@ namespace WebAPI services.AddHttpContextAccessor(); - services.AddControllers().AddNewtonsoftJson(options => - { - // Use the default property (Pascal) casing - options.SerializerSettings.ContractResolver = new DefaultContractResolver() - { - NamingStrategy = new CamelCaseNamingStrategy(), - - }; - //options.SerializerSettings.DateTimeZoneHandling = Newtonsoft.Json.DateTimeZoneHandling.Unspecified; - options.SerializerSettings.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore; - options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore; - }); - services.AddSignalR(); + new APIBinding().Binding(services); + new LogicBinding().Binding(services); + new DALBinding().Binding(services); - services.AddSingleton(); - - services.AddSingleton(); - - //Localted at \\ArkNAS\docker\ChurchAPI\docker-compose.yaml - string databaseConnString = Environment.GetEnvironmentVariable("DB_CONN_STRING"); -#if DEBUG - databaseConnString = "Host=192.168.68.55;Port=49154;Database=Church;Username=chris;Password=1124"; -#endif - //services.AddSingleton(_ => new DatabaseOptions { ConnectionString = databaseConnString }); - services.AddSingleton(_ => new DatabaseOptions { ConnectionString = databaseConnString }); - //services.AddSingleton(new ChurchNetContext()); - services.AddDbContext(options => - options.UseNpgsql( - //Configuration.GetConnectionString() - //"Host=192.168.68.55;Port=49154;Database=ChurchSandbox;Username=chris;Password=1124" - databaseConnString - )); - - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - - //services.AddScoped(); - - - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(typeof(ICrudLogic<>), typeof(LogicBase<>)); - services.AddScoped(typeof(ICrudDAL<>), typeof(CrudDALCBase<>)); - services.AddScoped(typeof(ICombinedKeyCrudLogic<>), typeof(CombinedKeyLogicBase<>)); - services.AddScoped(typeof(ICombinedKeyCrudDAL<>), typeof(CombinedKeyCrudDALCBase<>)); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped, MemberLogic>(); - services.AddHostedService(); - //services.AddMvc(o=>o.Filters.Add(typeof(HandleExceptionFilter))); - //services.AddMvc(o => o.Filters.Add(new HandleExceptionFilter(services.BuildServiceProvider().GetService()))); - //services.BuildServiceProvider().GetService(); - services.AddSingleton(); - - //ObjectFactory.Initialize(x => - // x.For() - // .HybridHttpOrThreadLocalScoped() - // .Use(() => new HttpContextWrapper(HttpContext.Current)); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. diff --git a/Church.Net.sln b/Church.Net.sln index cc9f590..83de301 100644 --- a/Church.Net.sln +++ b/Church.Net.sln @@ -21,6 +21,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestProject", "TestProject\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LineMessaging", "LineMessaging\LineMessaging.csproj", "{EF90BF58-C73D-408D-8ECE-F45425ADB81E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PropresentorScript", "PropresentorScript\PropresentorScript.csproj", "{D9C65F50-0828-4E88-9C8F-250A0AB57C98}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -51,6 +53,10 @@ Global {EF90BF58-C73D-408D-8ECE-F45425ADB81E}.Debug|Any CPU.Build.0 = Debug|Any CPU {EF90BF58-C73D-408D-8ECE-F45425ADB81E}.Release|Any CPU.ActiveCfg = Release|Any CPU {EF90BF58-C73D-408D-8ECE-F45425ADB81E}.Release|Any CPU.Build.0 = Release|Any CPU + {D9C65F50-0828-4E88-9C8F-250A0AB57C98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D9C65F50-0828-4E88-9C8F-250A0AB57C98}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D9C65F50-0828-4E88-9C8F-250A0AB57C98}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D9C65F50-0828-4E88-9C8F-250A0AB57C98}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/PropresentorScript/App.config b/PropresentorScript/App.config new file mode 100644 index 0000000..56efbc7 --- /dev/null +++ b/PropresentorScript/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/PropresentorScript/Program.cs b/PropresentorScript/Program.cs new file mode 100644 index 0000000..397f2d1 --- /dev/null +++ b/PropresentorScript/Program.cs @@ -0,0 +1,647 @@ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Xml.Linq; + + +namespace PropresentorScript +{ +#nullable enable + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Text; + using System.Xml.Linq; + + class Program + { + static void Main(string[] args) + { + // 用法:dotnet run "path/to/song.txt" + string inputText; + if (args.Length > 0 && File.Exists(args[0])) + { + inputText = File.ReadAllText(args[0]); + } + else + { + // 沒給檔案就用內建範例(可自行刪除) + inputText = @"向我神 +To Our God + +[Verse 1] +從塵土灰燼 祢愛贖回我 +Up from the ashes Your love has brought us +脫離黑暗中 進入光明 +Out of the darkness into the light; +挪去我憂傷 +Lifting our sorrows +承受我重擔 醫治我心 +Bearing our burdens healing our hearts; + + +[Chorus] +向我神 我揚聲歌唱 +To our God we lift up one voice +向我神 我高舉雙手 +To our God we lift up one song; +向我神 同心來宣揚 +To our God we lift up one voice +高唱 哈利路亞 +Singing Hallelujah;"; + } + + Song song = ParseSong(inputText); + + XDocument pro6 = BuildPro6(song, 1920, 1080); + string safeTitle = MakeSafeFileName(song.TitleZh ?? song.Title); + string outputPath = Path.Combine(Directory.GetCurrentDirectory(), safeTitle + ".pro6"); + pro6.Save(outputPath); + Console.WriteLine("Wrote: " + outputPath); + } + + // ---------------- Models ---------------- + + class Section + { + public string Name { get; set; } = ""; + // 每個元素為一張「原始」投影片的多行文字(以 \n 分隔);後續會再自動分頁 + public List Slides { get; set; } = new List(); + } + + class Song + { + public string Title { get; set; } = "Untitled"; // 後備 + public string? TitleZh { get; set; } // 中文歌名(若有) + public string? TitleEn { get; set; } // 英文歌名(若有) + public List
Sections { get; set; } = new List
(); + + public string FooterText(string sectionName) + { + string baseTitle = !string.IsNullOrWhiteSpace(TitleEn) && !string.IsNullOrWhiteSpace(TitleZh) + ? (TitleZh + " / " + TitleEn) + : (TitleZh ?? Title); + return baseTitle + " - " + sectionName; + } + } + + // ---------------- Parsing ---------------- + + // 規則: + // - 第一個非空行 = 標題1(可能是中或英) + // - 第二個非空行若不是 [Header],則當作標題2 + // - [xxx] 開頭代表段落標頭;其後文字累積,遇到 ; 或 ; 就分成新投影片 + static Song ParseSong(string raw) + { + string[] lines = raw.Replace("\r\n", "\n").Replace('\r', '\n').Split('\n'); + int i = 0; + + // 標題1 + while (i < lines.Length && string.IsNullOrWhiteSpace(lines[i])) i++; + string title1 = (i < lines.Length) ? lines[i].Trim() : "Untitled"; + i++; + + // 標題2(若下一個非空行不是 header) + while (i < lines.Length && string.IsNullOrWhiteSpace(lines[i])) i++; + string? title2 = null; + if (i < lines.Length && !IsHeader(lines[i].Trim(), out _)) + { + title2 = lines[i].Trim(); + i++; + } + + Song song = new Song(); + if (ContainsCjk(title1)) + { + song.TitleZh = title1; + song.Title = title1; + if (!string.IsNullOrWhiteSpace(title2)) song.TitleEn = title2; + } + else + { + song.Title = title1; + if (!string.IsNullOrWhiteSpace(title2)) + { + if (ContainsCjk(title2)) song.TitleZh = title2; + else song.TitleEn = title2; + } + } + + List
sections = new List
(); + Section? current = null; + List slideBuffer = new List(); + + void FlushSlide() + { + if (current == null) return; + string joined = string.Join("\n", slideBuffer).Trim(); + if (!string.IsNullOrWhiteSpace(joined)) + current.Slides.Add(joined); + slideBuffer.Clear(); + } + + void FlushSection() + { + if (current == null) return; + if (slideBuffer.Count > 0) FlushSlide(); + sections.Add(current); + current = null; + } + + // 跳過 header 前的空白 + while (i < lines.Length && string.IsNullOrWhiteSpace(lines[i])) i++; + + for (; i < lines.Length; i++) + { + string rawLine = lines[i] ?? ""; + string line = rawLine.TrimEnd(); + + string header; + if (IsHeader(line, out header)) + { + FlushSection(); + current = new Section { Name = header }; + slideBuffer.Clear(); + continue; + } + + if (current == null) continue; // 忽略 header 之前的散行 + + // 支援多個分號;每遇到 ; 或 ; 分頁 + string remaining = line; + while (true) + { + int idx = IndexOfBreak(remaining); // ; 或 ; + if (idx == -1) + { + if (!string.IsNullOrWhiteSpace(remaining)) + slideBuffer.Add(remaining); + break; + } + else + { + string before = remaining.Substring(0, idx); + if (!string.IsNullOrWhiteSpace(before)) + slideBuffer.Add(before); + + // 分頁 + FlushSlide(); + + // 繼續處理分號之後的內容 + remaining = remaining.Substring(idx + 1); + } + } + } + + // 收尾 + FlushSection(); + + song.Sections = sections; + return song; + } + + static bool IsHeader(string line, out string name) + { + name = ""; + if (string.IsNullOrWhiteSpace(line)) return false; + line = line.Trim(); + if (line.Length >= 3 && line.StartsWith("[") && line.EndsWith("]")) + { + name = line.Substring(1, line.Length - 2).Trim(); + return name.Length > 0; + } + return false; + } + + static int IndexOfBreak(string s) + { + if (string.IsNullOrEmpty(s)) return -1; + int a = s.IndexOf(';'); // 半形 + int b = s.IndexOf(';'); // 全形 + if (a == -1) return b; + if (b == -1) return a; + return Math.Min(a, b); + } + + static bool ContainsCjk(string s) + { + foreach (char ch in s) + { + // 基本 CJK 區段(足夠處理歌詞) + if (ch >= 0x4E00 && ch <= 0x9FFF) return true; + } + return false; + } + + static string MakeSafeFileName(string s) + { + char[] invalid = Path.GetInvalidFileNameChars(); + char[] arr = s.Select(c => invalid.Contains(c) ? '_' : c).ToArray(); + string cleaned = new string(arr); + return string.IsNullOrWhiteSpace(cleaned) ? "Presentation" : cleaned; + } + + // ---------------- Pro6 XML building ---------------- + + static XDocument BuildPro6(Song song, int width, int height) + { + XElement root = new XElement("RVPresentationDocument", + new XAttribute("height", height), + new XAttribute("width", width), + new XAttribute("docType", "0"), + new XAttribute("versionNumber", "600"), + new XAttribute("usedCount", "0"), + new XAttribute("backgroundColor", "0 0 0 1"), + new XAttribute("drawingBackgroundColor", "false"), + new XAttribute("CCLIDisplay", "false"), + new XAttribute("lastDateUsed", ""), + new XAttribute("selectedArrangementID", ""), + new XAttribute("category", "Presentation"), + new XAttribute("resourcesDirectory", ""), + new XAttribute("notes", ""), + new XAttribute("CCLISongTitle", song.TitleZh ?? song.Title), + new XAttribute("chordChartPath", ""), + new XAttribute("os", "1"), + new XAttribute("buildNumber", "6016") + ); + + // Minimal timeline + root.Add(new XElement("RVTimeline", + new XAttribute("timeOffset", "0"), + new XAttribute("duration", "0"), + new XAttribute("selectedMediaTrackIndex", "-1"), + new XAttribute("loop", "false"), + new XAttribute("rvXMLIvarName", "timeline"), + new XElement("array", new XAttribute("rvXMLIvarName", "timeCues")), + new XElement("array", new XAttribute("rvXMLIvarName", "mediaTracks")) + )); + + XElement groupsArray = new XElement("array", new XAttribute("rvXMLIvarName", "groups")); + root.Add(groupsArray); + + // Intro group at top + groupsArray.Add(BuildIntroGroup(song, width, height)); + + // Section groups + foreach (Section sec in song.Sections) + { + XElement group = new XElement("RVSlideGrouping", + new XAttribute("name", sec.Name), + new XAttribute("color", "1 1 1 0"), + new XAttribute("uuid", Guid.NewGuid().ToString()) + ); + + XElement slidesArray = new XElement("array", new XAttribute("rvXMLIvarName", "slides")); + group.Add(slidesArray); + + foreach (string slideText in sec.Slides) + { + // ⬇️ 自動分頁:可能回傳 1..N 張投影片 + foreach (XElement pageSlide in BuildSlidesAutoPaginate(song, sec.Name, slideText, width, height)) + { + slidesArray.Add(pageSlide); + } + } + + groupsArray.Add(group); + } + + // Arrangements (empty) + root.Add(new XElement("array", new XAttribute("rvXMLIvarName", "arrangements"))); + + return new XDocument(new XDeclaration("1.0", "utf-8", null), root); + } + + // ---------------- Slides & Pagination ---------------- + + static XElement BuildIntroGroup(Song song, int width, int height) + { + XElement group = new XElement("RVSlideGrouping", + new XAttribute("name", "Intro"), + new XAttribute("color", "1 1 1 0"), + new XAttribute("uuid", Guid.NewGuid().ToString()) + ); + + XElement slidesArray = new XElement("array", new XAttribute("rvXMLIvarName", "slides")); + group.Add(slidesArray); + + slidesArray.Add(BuildIntroSlide(song, width, height)); + return group; + } + + static XElement BuildIntroSlide(Song song, int docWidth, int docHeight) + { + XElement slide = NewEmptySlide(); + XElement elements = new XElement("array", new XAttribute("rvXMLIvarName", "displayElements")); + + string titleZh = song.TitleZh ?? song.Title; + string? titleEn = song.TitleEn; + + int marginX = 80; + int width = docWidth - marginX * 2; + int gap = 20; + + // 150pt / 105pt → RTF \fs (half-points) + int fsCn = 300; + int fsEn = 210; + + int cnHeight = 260; + int enHeight = 180; + + if (!string.IsNullOrWhiteSpace(titleEn)) + { + int totalH = cnHeight + gap + enHeight; + int startY = (docHeight - totalH) / 2; + + string cnRect = "{" + marginX + " " + startY + " 0 " + width + " " + cnHeight + "}"; + elements.Add(BuildTextElement(titleZh, cnRect, fsCn, TextAlign.Center)); + + string enRect = "{" + marginX + " " + (startY + cnHeight + gap) + " 0 " + width + " " + enHeight + "}"; + elements.Add(BuildTextElement(titleEn, enRect, fsEn, TextAlign.Center)); + } + else + { + int startY = (docHeight - cnHeight) / 2; + string rect = "{" + marginX + " " + startY + " 0 " + width + " " + cnHeight + "}"; + elements.Add(BuildTextElement(titleZh, rect, fsCn, TextAlign.Center)); + } + + slide.Add(elements); + return slide; + } + + // 自動分頁:把一段 slideText 依版面自動切成多張投影片 + static IEnumerable BuildSlidesAutoPaginate(Song song, string sectionName, string slideText, int docWidth, int docHeight) + { + // 版面參數(與單頁版一致) + int marginX = 80; + int marginTop = 120; + int marginBottom = 120; + int innerGap = 12; // 同組中英行距 + int width = docWidth - marginX * 2; + + // 字級(half-points):100pt=200、90pt=180 + int fsCn = 200; + int fsEn = 180; + + // 高度估值(像素) + int cnHeight = 190; + int enHeight = 160; + + // 一組(中+英)所需高度(含一點額外間距) + int pairSlot = cnHeight + innerGap + enHeight + 20; + + // 可用高度 + int usableHeight = docHeight - marginTop - marginBottom; + + // 先做中英配對 + List rawLines = (slideText ?? "") + .Replace("\r\n", "\n").Replace("\r", "\n") + .Split('\n') + .Select(s => s.TrimEnd()) + .Where(s => !string.IsNullOrWhiteSpace(s)) + .ToList(); + + List> pairs = new List>(); + for (int i = 0; i < rawLines.Count;) + { + string line = rawLines[i]; + if (ContainsCjk(line)) + { + string? en = null; + if (i + 1 < rawLines.Count && !ContainsCjk(rawLines[i + 1])) + { + en = rawLines[i + 1]; + i += 2; + } + else + { + i += 1; + } + pairs.Add(new Tuple(line, en)); + } + else + { + pairs.Add(new Tuple(null, line)); + i += 1; + } + } + + // 開始分頁 + int idx = 0; + while (idx < pairs.Count) + { + XElement slide = NewEmptySlide(); + XElement elementsArray = new XElement("array", new XAttribute("rvXMLIvarName", "displayElements")); + int y = marginTop; + + while (idx < pairs.Count) + { + string? cn = pairs[idx].Item1; + string? en = pairs[idx].Item2; + bool isPair = (cn != null && en != null); + + int needed = isPair ? pairSlot : (ContainsCjk(cn ?? en ?? "") ? (cnHeight + 20) : (enHeight + 20)); + + // 如果放不下,先結束這一頁 + if (y + needed > marginTop + usableHeight) + { + // 若這頁還沒放任何元素,強制塞一個以避免死循環 + if (y == marginTop) + { + if (isPair) + { + string cnRect0 = "{" + marginX + " " + y + " 0 " + width + " " + cnHeight + "}"; + elementsArray.Add(BuildTextElement(cn!, cnRect0, fsCn, TextAlign.Left)); + + string enRect0 = "{" + marginX + " " + (y + cnHeight + innerGap) + " 0 " + width + " " + enHeight + "}"; + elementsArray.Add(BuildTextElement(en!, enRect0, fsEn, TextAlign.Left)); + + y += needed; + } + else + { + bool isCn = ContainsCjk(cn ?? en ?? ""); + int fs = isCn ? fsCn : fsEn; + int boxH = isCn ? cnHeight : enHeight; + string rect0 = "{" + marginX + " " + y + " 0 " + width + " " + boxH + "}"; + elementsArray.Add(BuildTextElement(cn ?? en ?? "", rect0, fs, TextAlign.Left)); + y += (boxH + 20); + } + idx++; // 放了一個,移到下一筆 + } + break; // 結束這頁 + } + + // 放進這頁 + if (isPair) + { + string cnRect = "{" + marginX + " " + y + " 0 " + width + " " + cnHeight + "}"; + elementsArray.Add(BuildTextElement(cn!, cnRect, fsCn, TextAlign.Left)); + + string enRect = "{" + marginX + " " + (y + cnHeight + innerGap) + " 0 " + width + " " + enHeight + "}"; + elementsArray.Add(BuildTextElement(en!, enRect, fsEn, TextAlign.Left)); + + y += pairSlot; + } + else + { + bool isCn = ContainsCjk(cn ?? en ?? ""); + int fs = isCn ? fsCn : fsEn; + int boxH = isCn ? cnHeight : enHeight; + + string rect = "{" + marginX + " " + y + " 0 " + width + " " + boxH + "}"; + elementsArray.Add(BuildTextElement(cn ?? en ?? "", rect, fs, TextAlign.Left)); + + y += (boxH + 20); + } + + idx++; + } + + // 加入右下角頁尾 + string footerText = song.FooterText(sectionName); + elementsArray.Add(BuildTextElement( + footerText, + rect: FooterRect(docWidth, docHeight), + fontSizeHalfPoints: 28, // ≈14pt + align: TextAlign.Right + )); + + slide.Add(elementsArray); + yield return slide; + } + } + + static XElement NewEmptySlide() + { + return new XElement("RVDisplaySlide", + new XAttribute("backgroundColor", "1 1 1 0"), + new XAttribute("highlightColor", ""), + new XAttribute("drawingBackgroundColor", "false"), + new XAttribute("enabled", "true"), + new XAttribute("hotKey", ""), + new XAttribute("label", ""), + new XAttribute("notes", ""), + new XAttribute("UUID", Guid.NewGuid().ToString()), + new XAttribute("chordChartPath", ""), + new XElement("array", new XAttribute("rvXMLIvarName", "cues")) + ); + } + + enum TextAlign { Left, Center, Right } + + static string FooterRect(int docWidth, int docHeight) + { + int pad = 30; + int footerWidth = 900; // 雙語標題較長,留寬一點 + int footerHeight = 50; + int x = docWidth - pad - footerWidth; + int y = docHeight - pad - footerHeight; + return "{" + x + " " + y + " 0 " + footerWidth + " " + footerHeight + "}"; + } + + // 建立一個 RVTextElement(RTF 內用白色、Unicode \uNNNN、可選對齊) + static XElement BuildTextElement(string text, string rect, int fontSizeHalfPoints, TextAlign align) + { + string rtf = BuildRtf(text, fontSizeHalfPoints, align); + string rtfB64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(rtf)); + string plainB64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(text ?? "")); + + return new XElement("RVTextElement", + new XAttribute("displayName", "Text"), + new XAttribute("UUID", Guid.NewGuid().ToString()), + new XAttribute("typeID", "0"), + new XAttribute("displayDelay", "0"), + new XAttribute("locked", "false"), + new XAttribute("persistent", "0"), + new XAttribute("fromTemplate", "false"), + new XAttribute("opacity", "1"), + new XAttribute("source", ""), + new XAttribute("bezelRadius", "0"), + new XAttribute("rotation", "0"), + new XAttribute("drawingFill", "false"), + new XAttribute("drawingShadow", "false"), + new XAttribute("drawingStroke", "false"), + new XAttribute("fillColor", "1 1 1 1"), + new XAttribute("adjustsHeightToFit", "false"), + new XAttribute("verticalAlignment", "0"), + new XAttribute("revealType", "0"), + + new XElement("RVRect3D", + new XAttribute("rvXMLIvarName", "position"), + rect + ), + + new XElement("dictionary", new XAttribute("rvXMLIvarName", "stroke"), + new XElement("NSColor", + new XAttribute("rvXMLIvarName", "RVShapeElementStrokeColorKey"), + "1 1 1 1" + ), + new XElement("NSNumber", + new XAttribute("rvXMLIvarName", "RVShapeElementStrokeWidthKey"), + new XAttribute("serialization", "double"), + "0" + ) + ), + + new XElement("NSString", new XAttribute("rvXMLIvarName", "PlainText"), plainB64), + new XElement("NSString", new XAttribute("rvXMLIvarName", "RTFData"), rtfB64) + ); + } + + // 產生白字 + Unicode 的 RTF(全 ASCII,非 ASCII 用 \uNNNN) + static string BuildRtf(string text, int fontSizeHalfPoints, TextAlign align) + { + // 將換行先標準化,之後轉成 \line + string normalized = (text ?? "").Replace("\r\n", "\n").Replace("\r", "\n"); + string content = ToRtfUnicode(normalized).Replace("\n", @"\line "); + + string alignCode = + align == TextAlign.Right ? @"\qr " : + (align == TextAlign.Center ? @"\qc " : @"\ql "); + + // 注意:\fs 是 half-points;\uc0 表示 \u 後不帶替代字元 + // 字型表:f0=Arial(備用西文字型)、f1=Microsoft JhengHei(CJK 友善) + return @"{\rtf1\ansi\deff0" + + @"{\fonttbl{\f0 Arial;}{\f1 Microsoft JhengHei;}}" + + @"{\colortbl ;\red255\green255\blue255;}" + + @"\uc0\f1\fs" + fontSizeHalfPoints + " " + alignCode + @"\cf1 " + + content + + "}"; + } + + // 把所有非 ASCII 字元轉成 RTF Unicode:\uNNNN(以 16 位有號整數輸出),並處理 RTF 轉義 + static string ToRtfUnicode(string s) + { + StringBuilder sb = new StringBuilder(s.Length * 6); + foreach (char ch in s) + { + switch (ch) + { + case '\\': sb.Append(@"\\"); break; + case '{': sb.Append(@"\{"); break; + case '}': sb.Append(@"\}"); break; + case '\n': sb.Append('\n'); break; // 之後替換成 \line + default: + if (ch <= 0x7F) + { + sb.Append(ch); + } + else + { + // \uNNNN 使用 16 位帶正負號的數值 + sb.Append(@"\u").Append(unchecked((short)ch)).Append(' '); + } + break; + } + } + return sb.ToString(); + } + } + +} \ No newline at end of file diff --git a/PropresentorScript/Properties/AssemblyInfo.cs b/PropresentorScript/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..f87919a --- /dev/null +++ b/PropresentorScript/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("PropresentorScript")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("PropresentorScript")] +[assembly: AssemblyCopyright("Copyright © 2025")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("d9c65f50-0828-4e88-9c8f-250a0ab57c98")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/PropresentorScript/PropresentorScript.csproj b/PropresentorScript/PropresentorScript.csproj new file mode 100644 index 0000000..51cfda3 --- /dev/null +++ b/PropresentorScript/PropresentorScript.csproj @@ -0,0 +1,55 @@ + + + + + 9.0 + + Debug + AnyCPU + {D9C65F50-0828-4E88-9C8F-250A0AB57C98} + Exe + PropresentorScript + PropresentorScript + v4.7.2 + 512 + true + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TestProject1/TestProject1.csproj b/TestProject1/TestProject1.csproj new file mode 100644 index 0000000..3103d62 --- /dev/null +++ b/TestProject1/TestProject1.csproj @@ -0,0 +1,19 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + + + + diff --git a/TestProject1/UnitTest1.cs b/TestProject1/UnitTest1.cs new file mode 100644 index 0000000..79b64c0 --- /dev/null +++ b/TestProject1/UnitTest1.cs @@ -0,0 +1,16 @@ +namespace TestProject1 +{ + public class Tests + { + [SetUp] + public void Setup() + { + } + + [Test] + public void Test1() + { + Assert.Pass(); + } + } +} \ No newline at end of file diff --git a/TestProject1/Usings.cs b/TestProject1/Usings.cs new file mode 100644 index 0000000..cefced4 --- /dev/null +++ b/TestProject1/Usings.cs @@ -0,0 +1 @@ +global using NUnit.Framework; \ No newline at end of file