diff --git a/Epub/KoeBook.Epub/EpubDocumentException.cs b/Epub/KoeBook.Epub/EpubDocumentException.cs new file mode 100644 index 0000000..d83bb62 --- /dev/null +++ b/Epub/KoeBook.Epub/EpubDocumentException.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace KoeBook.Epub +{ + public class EpubDocumentException : Exception + { + public EpubDocumentException(string? message) : base(message) { } + } +} diff --git a/Epub/KoeBook.Epub/KoeBook.Epub.csproj b/Epub/KoeBook.Epub/KoeBook.Epub.csproj index 8737da9..8f3071f 100644 --- a/Epub/KoeBook.Epub/KoeBook.Epub.csproj +++ b/Epub/KoeBook.Epub/KoeBook.Epub.csproj @@ -7,6 +7,8 @@ + + diff --git a/Epub/KoeBook.Epub/ScrapingAozora.cs b/Epub/KoeBook.Epub/ScrapingAozora.cs new file mode 100644 index 0000000..a991c7e --- /dev/null +++ b/Epub/KoeBook.Epub/ScrapingAozora.cs @@ -0,0 +1,662 @@ +using AngleSharp; +using AngleSharp.Dom; +using AngleSharp.Html.Dom; +using AngleSharp.Io; +using KoeBook.Epub.Service; +using System.IO; +using static KoeBook.Epub.ScrapingHelper; + + +namespace KoeBook.Epub +{ + public partial class ScrapingAozora : IScrapingService + { + private int chapterNum; + private int sectionNum; + private bool chapterExist = false; + private bool sectionExist = false; + + + public async Task ScrapingAsync(string url, string coverFilePath, string imageDirectory, Guid id, CancellationToken ct) + { + var config = Configuration.Default.WithDefaultLoader(); + using var context = BrowsingContext.New(config); + var doc = await context.OpenAsync(url, ct).ConfigureAwait(false); + + // title の取得 + var bookTitle = doc.QuerySelector(".title"); + if (bookTitle is null) + { + throw new EpubDocumentException($"Failed to get title properly.\nYou may be able to get proper URL at {GetCardUrl(url)}"); + } + + // auther の取得 + var bookAuther = doc.QuerySelector(".author"); + if (bookAuther is null) + { + throw new EpubDocumentException($"Failed to get auther properly.\nYou may be able to get proper URL at {GetCardUrl(url)}"); + } + + // EpubDocument の生成 + var document = new EpubDocument(TextReplace(bookTitle.InnerHtml), TextReplace(bookAuther.InnerHtml), coverFilePath, id) + { + // EpubDocument.Chapters の生成 + Chapters = new List() + }; + + // 目次を取得 + var contents = doc.QuerySelectorAll(".midashi_anchor"); + + // 目次からEpubDocumentを構成 + List contentsIds = new List() { 0 }; + // Chapter, Section が存在するとき、それぞれtrue + chapterExist = false; + sectionExist = false; + if (contents.Length != 0) + { + int previousMidashiId = 0; + foreach (var midashi in contents) + { + if (midashi.Id != null) + { + var MidashiId = int.Parse(midashi.Id.Replace("midashi", "")); + if ((MidashiId - previousMidashiId) == 100) + { + document.Chapters.Add(new Chapter() { Title = TextProcess(midashi) }); + chapterExist = true; + } + if ((MidashiId - previousMidashiId) == 10) + { + checkChapter(document); + document.Chapters[^1].Sections.Add(new Section(TextProcess(midashi))); + sectionExist = true; + } + contentsIds.Add(MidashiId); + previousMidashiId = MidashiId; + } + } + } + else + { + document.Chapters.Add(new Chapter() { Title = null }); + document.Chapters[^1].Sections.Add(new Section(bookTitle.InnerHtml)); + } + + // 本文を取得 + var mainText = doc.QuerySelector(".main_text")!; + + + // 本文を分割しながらEpubDocumntに格納 + // 直前のNodeを確認した操作で、その内容をParagraphに追加した場合、true + bool previous = false; + // 各ChapterとSection のインデックス + chapterNum = -1; + sectionNum = -1; + + // 直前のimgタグにaltがなかったときtrueになる。 + bool skipCaption = false; + + foreach (var element in mainText.Children) + { + var nextNode = element.NextSibling; + if (element.TagName == "BR") + { + if (previous == true) + { + checkSection(document, chapterNum); + document.Chapters[chapterNum].Sections[sectionNum].Elements.Add(new Paragraph()); + } + + } + else if (element.TagName == "DIV") + { + var midashi = element.QuerySelector(".midashi_anchor"); + if (midashi != null) + { + if (midashi.Id != null) + { + if (int.TryParse(midashi.Id.Replace("midashi", ""), out var midashiId)) + { + if (contentsIds.Contains(midashiId)) + { + var contentsId = contentsIds.IndexOf(midashiId); + switch (contentsIds[contentsId] - contentsIds[contentsId - 1]) + { + case 100: + if (chapterNum >= 0 && sectionNum >= 0) + { + document.Chapters[chapterNum].Sections[sectionNum].Elements.RemoveAt(document.Chapters[chapterNum].Sections[sectionNum].Elements.Count - 1); + } + chapterNum++; + sectionNum = -1; + break; + case 10: + if (chapterNum == -1) + { + chapterNum++; + sectionNum = -1; + } + if (chapterNum >= 0 && sectionNum >= 0) + { + document.Chapters[chapterNum].Sections[sectionNum].Elements.RemoveAt(document.Chapters[chapterNum].Sections[sectionNum].Elements.Count - 1); + } + sectionNum++; + break; + default: + break; + } + } + else //小見出し、行中小見出しの処理 + { + if (chapterNum == -1) + { + if (chapterExist) + { + document.Chapters.Insert(0, new Chapter()); + } + chapterNum++; + sectionNum = -1; + } + if (sectionNum == -1) + { + if (sectionExist) + { + checkChapter(document); + document.Chapters[^1].Sections.Insert(0, new Section("___")); + } + sectionNum++; + } + checkParagraph(document, chapterNum, sectionNum); + if ((document.Chapters[chapterNum].Sections[sectionNum].Elements[^1] is Paragraph paragraph)) + { + paragraph.Text += TextProcess(midashi); + document.Chapters[chapterNum].Sections[sectionNum].Elements.Add(new Paragraph()); + + foreach (var splitText in SplitBrace(TextProcess(midashi))) + { + if (document.Chapters[chapterNum].Sections[sectionNum].Elements[^1] is Paragraph paragraph1) + { + paragraph1.Text += splitText; + } + document.Chapters[chapterNum].Sections[sectionNum].Elements.Add(new Paragraph()); + } + + } + } + } + else + { + throw new EpubDocumentException($"Unexpected id of Anchor tag was found: id = {midashi.Id}"); + } + } + else + { + throw new EpubDocumentException("Unecpected structure of HTML File: div tag with class=\"midashi_anchor\", but id=\"midashi___\" exist"); + } + } + else + { + if (element.ClassName == "caption") + { + // https://www.aozora.gr.jp/annotation/graphics.html#:~:text=%3Cdiv%20class%3D%22caption%22%3E を処理するための部分 + checkParagraph(document, chapterNum, sectionNum); + if ((document.Chapters[chapterNum].Sections[sectionNum].Elements[^1] is Paragraph paragraph)) + { + var split = SplitBrace(TextProcess(element)); + for (int i = 0; i < split.Count - 1; i++) + { + if (document.Chapters[chapterNum].Sections[sectionNum].Elements[^1] is Paragraph paragraph1) + { + paragraph1.Text += split[i]; + } + document.Chapters[chapterNum].Sections[sectionNum].Elements.Add(new Paragraph()); + } + if (document.Chapters[chapterNum].Sections[sectionNum].Elements[^1] is Paragraph paragraph2) + { + paragraph2.Text += split[^1]; + } + } + } + else + { + if (chapterNum == -1) + { + if (chapterExist) + { + document.Chapters.Insert(0, new Chapter()); + } + chapterNum++; + sectionNum = -1; + } + if (sectionNum == -1) + { + if (sectionExist) + { + checkChapter(document); + document.Chapters[^1].Sections.Insert(0, new Section("___")); + } + sectionNum++; + } + checkParagraph(document, chapterNum, sectionNum); + if ((document.Chapters[chapterNum].Sections[sectionNum].Elements[^1] is Paragraph paragraph)) + { + foreach (var splitText in SplitBrace(TextProcess(element))) + { + if (document.Chapters[chapterNum].Sections[sectionNum].Elements[^1] is Paragraph paragraph1) + { + paragraph1.Text += splitText; + } + document.Chapters[chapterNum].Sections[sectionNum].Elements.Add(new Paragraph()); + } + } + } + } + + } + else if (element.TagName == "IMG") + { + if (element is IHtmlImageElement img) + { + if (chapterNum == -1) + { + if (chapterExist) + { + document.Chapters.Insert(0, new Chapter()); + } + chapterNum++; + sectionNum = -1; + } + if (sectionNum == -1) + { + if (sectionExist) + { + checkChapter(document); + document.Chapters[^1].Sections.Insert(0, new Section("___")); + } + sectionNum++; + } + + if (element.ClassName != "gaiji") + { + if (img.Source != null) + { + // 画像のダウンロード + var loader = context.GetService(); + if (loader != null) + { + var downloading = loader.FetchAsync(new DocumentRequest(new Url(img.Source))); + ct.Register(() => downloading.Cancel()); + var response = await downloading.Task.ConfigureAwait(false); + using var ms = new MemoryStream(); + await response.Content.CopyToAsync(ms, ct).ConfigureAwait(false); + var filePass = imageDirectory + FileUrlToFileName().Replace(img.Source, "$1"); + File.WriteAllBytes(filePass, ms.ToArray()); + checkSection(document, chapterNum); + if (document.Chapters[chapterNum].Sections[sectionNum].Elements.Count > 1) + { + document.Chapters[chapterNum].Sections[sectionNum].Elements.Insert(document.Chapters[chapterNum].Sections[sectionNum].Elements.Count - 1, new Picture(filePass)); + } + } + } + if (img.AlternativeText != null) + { + checkParagraph(document, chapterNum, sectionNum); + if ((document.Chapters[chapterNum].Sections[sectionNum].Elements[^1] is Paragraph paragraph)) + { + paragraph.Text += TextReplace(img.AlternativeText); + document.Chapters[chapterNum].Sections[sectionNum].Elements.Add(new Paragraph()); + } + skipCaption = false; + } + else + { + skipCaption = true; + } + } + } + } + else if (element.TagName == "SPAN") + { + if (element.ClassName == "caption") + { + if (skipCaption) + { + if ((document.Chapters[chapterNum].Sections[sectionNum].Elements[^2] is Paragraph paragraph)) + { + paragraph.Text = TextProcess(element) + "の画像"; + } + } + else + { + if ((document.Chapters[chapterNum].Sections[sectionNum].Elements[^1] is Paragraph paragraph)) + { + paragraph.Text = TextProcess(element) + "の画像"; + } + } + + } + else if (element.ClassName == "notes") + { + switch (element.InnerHtml) + { + case "[#改丁]": + break; + case "[#改ページ]": + break; + case "[#改見開き]": + break; + case "[#改段]": + break; + case "[#ページの左右中央]": + break; + default: + checkParagraph(document, chapterNum, sectionNum); + if ((document.Chapters[chapterNum].Sections[sectionNum].Elements[^1] is Paragraph paragraph)) + { + foreach (var splitText in SplitBrace(TextProcess(element))) + { + if (document.Chapters[chapterNum].Sections[sectionNum].Elements[^1] is Paragraph paragraph1) + { + paragraph1.Text += splitText; + } + document.Chapters[chapterNum].Sections[sectionNum].Elements.Add(new Paragraph()); + } + } + break; + } + } + else + { + if (chapterNum == -1) + { + if (chapterExist) + { + document.Chapters.Insert(0, new Chapter()); + } + chapterNum++; + sectionNum = -1; + } + if (sectionNum == -1) + { + if (sectionExist) + { + checkChapter(document); + document.Chapters[^1].Sections.Insert(0, new Section("___")); + } + sectionNum++; + } + checkParagraph(document, chapterNum, sectionNum); + if ((document.Chapters[chapterNum].Sections[sectionNum].Elements[^1] is Paragraph paragraph)) + { + var split = SplitBrace(TextProcess(element)); + for (int i = 0; i < split.Count - 1; i++) + { + if (document.Chapters[chapterNum].Sections[sectionNum].Elements[^1] is Paragraph paragraph1) + { + paragraph1.Text += split[i]; + } + document.Chapters[chapterNum].Sections[sectionNum].Elements.Add(new Paragraph()); + } + if (document.Chapters[chapterNum].Sections[sectionNum].Elements[^1] is Paragraph paragraph2) + { + paragraph2.Text += split[^1]; + } + } + // 想定していない構造が見つかったことをログに出力した方が良い? + } + } + else + { + if (chapterNum == -1) + { + if (chapterExist) + { + document.Chapters.Insert(0, new Chapter()); + } + chapterNum++; + sectionNum = -1; + } + if (sectionNum == -1) + { + if (sectionExist) + { + checkChapter(document); + document.Chapters[^1].Sections.Insert(0, new Section("___")); + } + sectionNum++; + } + checkParagraph(document, chapterNum, sectionNum); + if ((document.Chapters[chapterNum].Sections[sectionNum].Elements[^1] is Paragraph paragraph)) + { + paragraph.Text += TextProcess(element); + + var split = SplitBrace(TextProcess(element)); + for (int i = 0; i < split.Count - 1; i++) + { + if (document.Chapters[chapterNum].Sections[sectionNum].Elements[^1] is Paragraph paragraph1) + { + paragraph1.Text += split[i]; + } + document.Chapters[chapterNum].Sections[sectionNum].Elements.Add(new Paragraph()); + } + if (document.Chapters[chapterNum].Sections[sectionNum].Elements[^1] is Paragraph paragraph2) + { + paragraph2.Text += split[^1]; + } + } + // 想定していない構造が見つかったことをログに出力した方が良い? + } + + if (nextNode != null) + { + if (nextNode.NodeType == NodeType.Text) + { + if (nextNode.Text() != "\n") + { + previous = true; + + if (chapterNum == -1) + { + if (chapterExist) + { + document.Chapters.Insert(0, new Chapter()); + } + chapterNum++; + sectionNum = -1; + } + if (sectionNum == -1) + { + if (sectionExist) + { + checkChapter(document); + document.Chapters[^1].Sections.Insert(0, new Section("___")); + } + sectionNum++; + } + checkParagraph(document, chapterNum, sectionNum); + if ((document.Chapters[chapterNum].Sections[sectionNum].Elements[^1] is Paragraph paragraph)) + { + var split = SplitBrace(TextReplace(nextNode.Text())); + for (int i = 0; i < split.Count - 1; i++) + { + if (document.Chapters[chapterNum].Sections[sectionNum].Elements[^1] is Paragraph paragraph1) + { + paragraph1.Text += split[i]; + } + document.Chapters[chapterNum].Sections[sectionNum].Elements.Add(new Paragraph()); + } + if (document.Chapters[chapterNum].Sections[sectionNum].Elements[^1] is Paragraph paragraph2) + { + paragraph2.Text += split[^1]; + } + } + + } + else + { + previous = false; + } + } + else + { + previous = false; + } + } + } + + document.Chapters[^1].Sections[^1].Elements.RemoveAt(document.Chapters[^1].Sections[^1].Elements.Count - 1); + + if (checkEpubDocument(document)) + { + Console.WriteLine("Success"); + } + else + { + Console.WriteLine("False"); + } + return document; + } + + private bool checkEpubDocument(EpubDocument document) + { + foreach (var chapter in document.Chapters) + { + foreach (var section in chapter.Sections) + { + foreach (var element in section.Elements) + { + if (element is Paragraph paragraph) + { + if (paragraph.Text == null) + { + Console.WriteLine($"{document.Chapters.IndexOf(chapter)}, {chapter.Sections.IndexOf(section)}, {section.Elements.IndexOf(element)}"); + return false; + } + } + else if (element is Picture picture) + { + if (picture.PictureFilePath == null) + { + Console.WriteLine($"{document.Chapters.IndexOf(chapter)}, {chapter.Sections.IndexOf(section)}, {section.Elements.IndexOf(element)}"); + return false; + } + } + } + } + } + return true; + } + + private static string TextProcess(IElement element) + { + string text = ""; + if (element.ChildElementCount == 0) + { + text += TextReplace(element.InnerHtml); + } + else + { + var rubies = element.QuerySelectorAll("ruby"); + if (rubies.Length > 0) + { + if (element.Children[0].PreviousSibling is INode node) + { + if (node.NodeType == NodeType.Text) + { + if (node.Text() != "\n") + { + text += TextReplace(node.Text()); + } + } + } + foreach (var item in element.Children) + { + if (item.TagName == "RUBY") + { + if (item.QuerySelectorAll("img").Length > 0) + { + if (item.QuerySelector("rt") != null) + { + text += TextReplace(item.QuerySelector("rt")!.TextContent); + } + } + else + { + text += TextReplace(item.OuterHtml); + } + } + else + { + if ((item.TextContent != "\n") && (!string.IsNullOrEmpty(item.TextContent))) + { + text += TextReplace(item.TextContent); + } + } + if (item.NextSibling != null) + { + if ((item.NextSibling.TextContent != "\n") && (!string.IsNullOrEmpty(item.NextSibling.TextContent))) + { + text += TextReplace(item.NextSibling.Text()); + } + } + } + } + else if (element.TagName == "RUBY") + { + if (element.QuerySelectorAll("img").Length > 0) + { + if (element.QuerySelector("rt") != null) + { + text += TextReplace(element.QuerySelector("rt")!.TextContent); + } + } + else + { + text += TextReplace(element.OuterHtml); + } + } + else + { + text += TextReplace(element.TextContent); + } + } + return text; + } + + + // ローマ数字、改行の置換をまとめて行う。 + private static string TextReplace(string text) + { + string returnText = text; + returnText = RomanNumImg().Replace(returnText, "$1"); + returnText = RomanNumText1().Replace(returnText, "$1"); + returnText = RomanNumText2().Replace(returnText, "$1"); + returnText = returnText.Replace("\n", ""); + return returnText; + } + + + private static string GetCardUrl(string url) + { + return UrlBookToCard().Replace(url, "$1card$2$3"); + } + + [System.Text.RegularExpressions.GeneratedRegex(@"(https://www\.aozora\.gr\.jp/cards/\d{6}/)files/(\d{1,})_\d{1,}(\.html)")] + private static partial System.Text.RegularExpressions.Regex UrlBookToCard(); + + [System.Text.RegularExpressions.GeneratedRegex(@"")] + private static partial System.Text.RegularExpressions.Regex RomanNumImg(); + + [System.Text.RegularExpressions.GeneratedRegex(@"※[#ローマ数字(\d{1,})、1-1\d.{0,}]")] + private static partial System.Text.RegularExpressions.Regex RomanNumText1(); + + [System.Text.RegularExpressions.GeneratedRegex(@"※(ローマ数字(\d.{1,})、1-1\d.{0,})")] + private static partial System.Text.RegularExpressions.Regex RomanNumText2(); + + [System.Text.RegularExpressions.GeneratedRegex(@"http.{1,}/([^/]{0,}\.[^/]{1,})")] + private static partial System.Text.RegularExpressions.Regex FileUrlToFileName(); + + [System.Text.RegularExpressions.GeneratedRegex(@"(.{1,})(.{1,})")] + private static partial System.Text.RegularExpressions.Regex RubyToText(); + } +} diff --git a/Epub/KoeBook.Epub/ScrapingHelper.cs b/Epub/KoeBook.Epub/ScrapingHelper.cs new file mode 100644 index 0000000..ec342f6 --- /dev/null +++ b/Epub/KoeBook.Epub/ScrapingHelper.cs @@ -0,0 +1,81 @@ +using System.Net; +using System.Reflection.Metadata; + +namespace KoeBook.Epub; + +internal static class ScrapingHelper +{ + internal static void checkChapter(EpubDocument document) + { + if (document.Chapters.Count == 0) + { + document.Chapters.Add(new Chapter() { Title = null }); + } + return; + } + + internal static void checkSection(EpubDocument document, int ChapterNum) + { + + checkChapter(document); + + if (document.Chapters[ChapterNum].Sections.Count == 0) + { + if (document.Chapters[ChapterNum].Title != null) + { + document.Chapters[ChapterNum].Sections.Add(new Section(document.Chapters[ChapterNum].Title!)); + } + else + { + document.Chapters[ChapterNum].Sections.Add(new Section(document.Title)); + } + + } + return; + } + + internal static void checkParagraph(EpubDocument document, int chapterNum, int sectionNum) + { + checkSection(document, chapterNum); + if (document.Chapters[chapterNum].Sections[sectionNum].Elements.Count == 0) + { + document.Chapters[chapterNum].Sections[sectionNum].Elements.Add(new Paragraph()); + } + return; + } + + public static List SplitBrace(string text) + { + var result = new List(); + int bracket = 0; + var brackets = new List(); + foreach (char c in text) + { + if (c == '「') bracket++; + if (c == '」') bracket--; + brackets.Add(bracket); + } + var mn = Math.Min(0, brackets.Min()); + int startIdx = 0; + for (int i = 0; i < brackets.Count; i++) + { + brackets[i] -= mn; + if (text[i] == '「' && brackets[i] == 1 && i != 0) + { + result.Add(text[startIdx..(i - startIdx)]); + startIdx = i; + } + if (text[i] == '」' && brackets[i] == 0 && i != 0) + { + result.Add(text[startIdx..(i - startIdx + 1)]); + startIdx = i + 1; + } + } + if (startIdx != text.Length - 1) + { + result.Add(text[startIdx..]); + } + + return result; + } +} diff --git a/Epub/KoeBook.Epub/Service/IScrapingService.cs b/Epub/KoeBook.Epub/Service/IScrapingService.cs index c66e568..c78791d 100644 --- a/Epub/KoeBook.Epub/Service/IScrapingService.cs +++ b/Epub/KoeBook.Epub/Service/IScrapingService.cs @@ -2,5 +2,5 @@ public interface IScrapingService { - public Task ScrapingAsync(string url, string coverFil9lePath, Guid id, CancellationToken ct); + public Task ScrapingAsync(string url, string coverFillePath, string imageDirectory, Guid id, CancellationToken ct); } diff --git a/KoeBook.Test/KoeBook.Test.csproj b/KoeBook.Test/KoeBook.Test.csproj index 1b7908a..c085021 100644 --- a/KoeBook.Test/KoeBook.Test.csproj +++ b/KoeBook.Test/KoeBook.Test.csproj @@ -18,7 +18,6 @@ - diff --git a/KoeBook.Common/KoeBook.Common.csproj b/KoeBook.Unsafe/KoeBook.Unsafe.csproj similarity index 54% rename from KoeBook.Common/KoeBook.Common.csproj rename to KoeBook.Unsafe/KoeBook.Unsafe.csproj index 683628f..f796119 100644 --- a/KoeBook.Common/KoeBook.Common.csproj +++ b/KoeBook.Unsafe/KoeBook.Unsafe.csproj @@ -4,12 +4,13 @@ net8.0 enable enable + true - - - + + all + diff --git a/KoeBook.Unsafe/NativeMethods.json b/KoeBook.Unsafe/NativeMethods.json new file mode 100644 index 0000000..7591544 --- /dev/null +++ b/KoeBook.Unsafe/NativeMethods.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://aka.ms/CsWin32.schema.json", + "public": true +} diff --git a/KoeBook.Unsafe/NativeMethods.txt b/KoeBook.Unsafe/NativeMethods.txt new file mode 100644 index 0000000..3428e7b --- /dev/null +++ b/KoeBook.Unsafe/NativeMethods.txt @@ -0,0 +1 @@ +MessageBox diff --git a/KoeBook.sln b/KoeBook.sln index c63240a..90e4b56 100644 --- a/KoeBook.sln +++ b/KoeBook.sln @@ -7,11 +7,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KoeBook", "KoeBook\KoeBook. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KoeBook.Core", "KoeBook.Core\KoeBook.Core.csproj", "{50444E76-C6A7-40AF-879C-0AD88A287510}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KoeBook.Common", "KoeBook.Common\KoeBook.Common.csproj", "{1E5B40AE-1D42-40D0-85D1-E921B17BFA69}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KoeBook.Epub", "Epub\KoeBook.Epub\KoeBook.Epub.csproj", "{1DE55F58-E4F3-4077-A241-AFFD736A2B01}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KoeBook.Test", "KoeBook.Test\KoeBook.Test.csproj", "{67CEB31C-B026-499A-83B4-528914EABDBF}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KoeBook.Test", "KoeBook.Test\KoeBook.Test.csproj", "{67CEB31C-B026-499A-83B4-528914EABDBF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KoeBook.Unsafe", "KoeBook.Unsafe\KoeBook.Unsafe.csproj", "{30AE8B29-D2D1-4D46-9A5E-68A3F4339892}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -65,22 +65,6 @@ Global {50444E76-C6A7-40AF-879C-0AD88A287510}.Release|x64.Build.0 = Release|x64 {50444E76-C6A7-40AF-879C-0AD88A287510}.Release|x86.ActiveCfg = Release|x86 {50444E76-C6A7-40AF-879C-0AD88A287510}.Release|x86.Build.0 = Release|x86 - {1E5B40AE-1D42-40D0-85D1-E921B17BFA69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1E5B40AE-1D42-40D0-85D1-E921B17BFA69}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1E5B40AE-1D42-40D0-85D1-E921B17BFA69}.Debug|arm64.ActiveCfg = Debug|Any CPU - {1E5B40AE-1D42-40D0-85D1-E921B17BFA69}.Debug|arm64.Build.0 = Debug|Any CPU - {1E5B40AE-1D42-40D0-85D1-E921B17BFA69}.Debug|x64.ActiveCfg = Debug|Any CPU - {1E5B40AE-1D42-40D0-85D1-E921B17BFA69}.Debug|x64.Build.0 = Debug|Any CPU - {1E5B40AE-1D42-40D0-85D1-E921B17BFA69}.Debug|x86.ActiveCfg = Debug|Any CPU - {1E5B40AE-1D42-40D0-85D1-E921B17BFA69}.Debug|x86.Build.0 = Debug|Any CPU - {1E5B40AE-1D42-40D0-85D1-E921B17BFA69}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1E5B40AE-1D42-40D0-85D1-E921B17BFA69}.Release|Any CPU.Build.0 = Release|Any CPU - {1E5B40AE-1D42-40D0-85D1-E921B17BFA69}.Release|arm64.ActiveCfg = Release|Any CPU - {1E5B40AE-1D42-40D0-85D1-E921B17BFA69}.Release|arm64.Build.0 = Release|Any CPU - {1E5B40AE-1D42-40D0-85D1-E921B17BFA69}.Release|x64.ActiveCfg = Release|Any CPU - {1E5B40AE-1D42-40D0-85D1-E921B17BFA69}.Release|x64.Build.0 = Release|Any CPU - {1E5B40AE-1D42-40D0-85D1-E921B17BFA69}.Release|x86.ActiveCfg = Release|Any CPU - {1E5B40AE-1D42-40D0-85D1-E921B17BFA69}.Release|x86.Build.0 = Release|Any CPU {1DE55F58-E4F3-4077-A241-AFFD736A2B01}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1DE55F58-E4F3-4077-A241-AFFD736A2B01}.Debug|Any CPU.Build.0 = Debug|Any CPU {1DE55F58-E4F3-4077-A241-AFFD736A2B01}.Debug|arm64.ActiveCfg = Debug|Any CPU @@ -113,6 +97,22 @@ Global {67CEB31C-B026-499A-83B4-528914EABDBF}.Release|x64.Build.0 = Release|Any CPU {67CEB31C-B026-499A-83B4-528914EABDBF}.Release|x86.ActiveCfg = Release|Any CPU {67CEB31C-B026-499A-83B4-528914EABDBF}.Release|x86.Build.0 = Release|Any CPU + {30AE8B29-D2D1-4D46-9A5E-68A3F4339892}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {30AE8B29-D2D1-4D46-9A5E-68A3F4339892}.Debug|Any CPU.Build.0 = Debug|Any CPU + {30AE8B29-D2D1-4D46-9A5E-68A3F4339892}.Debug|arm64.ActiveCfg = Debug|Any CPU + {30AE8B29-D2D1-4D46-9A5E-68A3F4339892}.Debug|arm64.Build.0 = Debug|Any CPU + {30AE8B29-D2D1-4D46-9A5E-68A3F4339892}.Debug|x64.ActiveCfg = Debug|Any CPU + {30AE8B29-D2D1-4D46-9A5E-68A3F4339892}.Debug|x64.Build.0 = Debug|Any CPU + {30AE8B29-D2D1-4D46-9A5E-68A3F4339892}.Debug|x86.ActiveCfg = Debug|Any CPU + {30AE8B29-D2D1-4D46-9A5E-68A3F4339892}.Debug|x86.Build.0 = Debug|Any CPU + {30AE8B29-D2D1-4D46-9A5E-68A3F4339892}.Release|Any CPU.ActiveCfg = Release|Any CPU + {30AE8B29-D2D1-4D46-9A5E-68A3F4339892}.Release|Any CPU.Build.0 = Release|Any CPU + {30AE8B29-D2D1-4D46-9A5E-68A3F4339892}.Release|arm64.ActiveCfg = Release|Any CPU + {30AE8B29-D2D1-4D46-9A5E-68A3F4339892}.Release|arm64.Build.0 = Release|Any CPU + {30AE8B29-D2D1-4D46-9A5E-68A3F4339892}.Release|x64.ActiveCfg = Release|Any CPU + {30AE8B29-D2D1-4D46-9A5E-68A3F4339892}.Release|x64.Build.0 = Release|Any CPU + {30AE8B29-D2D1-4D46-9A5E-68A3F4339892}.Release|x86.ActiveCfg = Release|Any CPU + {30AE8B29-D2D1-4D46-9A5E-68A3F4339892}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/KoeBook/App.xaml.cs b/KoeBook/App.xaml.cs index b41e3ea..3458991 100644 --- a/KoeBook/App.xaml.cs +++ b/KoeBook/App.xaml.cs @@ -1,6 +1,8 @@ -using KoeBook.Activation; +using FastEnumUtility; +using KoeBook.Activation; using KoeBook.Components.Dialog; using KoeBook.Contracts.Services; +using KoeBook.Core; using KoeBook.Core.Contracts.Services; using KoeBook.Core.Services; using KoeBook.Core.Services.Mocks; @@ -14,7 +16,10 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.UI.Xaml; -using Microsoft.Extensions.Http; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.UI.WindowsAndMessaging; +using WinRT.Interop; namespace KoeBook; @@ -122,8 +127,23 @@ public App() private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e) { - // TODO: Log and handle exceptions as appropriate. - // https://docs.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.application.unhandledexception. + var hwnd = WindowNative.GetWindowHandle(MainWindow); + if (e.Exception is EbookException ebookException) + { + PInvoke.MessageBox((HWND)hwnd, + $"ラーが発生しました。KoeBookを終了します。\n{ebookException.ExceptionType.GetEnumMemberValue()}", + "KoeBookからのお知らせ", + MESSAGEBOX_STYLE.MB_OK | MESSAGEBOX_STYLE.MB_ICONWARNING); + } + else + { + PInvoke.MessageBox((HWND)hwnd, + $"不明なエラーが発生しました。KoeBookを終了します。\n{e.Exception.Message}\n\n{e.Exception.StackTrace}", + "KoeBookからのお知らせ", + MESSAGEBOX_STYLE.MB_OK | MESSAGEBOX_STYLE.MB_ICONWARNING); + } + e.Handled = true; + Current.Exit(); } protected override async void OnLaunched(LaunchActivatedEventArgs args) diff --git a/KoeBook/Components/Dialog/DialogService.cs b/KoeBook/Components/Dialog/DialogService.cs index ccd31e5..02ee94d 100644 --- a/KoeBook/Components/Dialog/DialogService.cs +++ b/KoeBook/Components/Dialog/DialogService.cs @@ -42,4 +42,15 @@ public Task ShowAsync( { return ShowAsync(title, new DialogContentControl(content), primaryText, closeText, defaultButton, cancellationToken); } + + public Task ShowInfoAsync(string title, string content, string primaryText, CancellationToken cancellationToken) + { + var dialog = new SharedContentDialog(title, primaryText, null, ContentDialogButton.Primary) + { + XamlRoot = App.MainWindow.Content.XamlRoot, + Content = new DialogContentControl(content), + RequestedTheme = _themeSelectorService.Theme + }; + return dialog.ShowAsync().AsTask(cancellationToken); + } } diff --git a/KoeBook/Components/Dialog/SharedContentDialog.xaml.cs b/KoeBook/Components/Dialog/SharedContentDialog.xaml.cs index 51ad157..b1e58f3 100644 --- a/KoeBook/Components/Dialog/SharedContentDialog.xaml.cs +++ b/KoeBook/Components/Dialog/SharedContentDialog.xaml.cs @@ -4,7 +4,7 @@ namespace KoeBook.Components.Dialog; public sealed partial class SharedContentDialog : ContentDialog { - public SharedContentDialog(string title, string primaryText, string closeText, ContentDialogButton defaultButton) + public SharedContentDialog(string title, string primaryText, string? closeText, ContentDialogButton defaultButton) { Title = title; PrimaryButtonText = primaryText; diff --git a/KoeBook/Contracts/Services/IDialogService.cs b/KoeBook/Contracts/Services/IDialogService.cs index 2410c1a..36f771f 100644 --- a/KoeBook/Contracts/Services/IDialogService.cs +++ b/KoeBook/Contracts/Services/IDialogService.cs @@ -19,6 +19,12 @@ Task ShowAsync( string closeText, ContentDialogButton defaultButton, CancellationToken cancellationToken); + + Task ShowInfoAsync( + string title, + string content, + string primaryText, + CancellationToken cancellationToken); } public static class DialogServiceEx diff --git a/KoeBook/KoeBook.csproj b/KoeBook/KoeBook.csproj index 89918c3..a83ccf0 100644 --- a/KoeBook/KoeBook.csproj +++ b/KoeBook/KoeBook.csproj @@ -40,6 +40,7 @@ + diff --git a/KoeBook/Services/GenerationTaskRunnerService.cs b/KoeBook/Services/GenerationTaskRunnerService.cs index c7ed954..5669da4 100644 --- a/KoeBook/Services/GenerationTaskRunnerService.cs +++ b/KoeBook/Services/GenerationTaskRunnerService.cs @@ -1,4 +1,6 @@ -using KoeBook.Contracts.Services; +using FastEnumUtility; +using KoeBook.Contracts.Services; +using KoeBook.Core; using KoeBook.Core.Contracts.Services; using KoeBook.Core.Models; using KoeBook.Models; @@ -9,19 +11,22 @@ namespace KoeBook.Services; public class GenerationTaskRunnerService { private readonly IGenerationTaskService _taskService; - private readonly IAnalyzerService _analyzerService; - private readonly IEpubGenerateService _epubGenService; - + private readonly IDialogService _dialogService; private readonly string _tempFolder = ApplicationData.Current.TemporaryFolder.Path; - public GenerationTaskRunnerService(IGenerationTaskService taskService, IAnalyzerService analyzerService, IEpubGenerateService epubGenService) + public GenerationTaskRunnerService( + IGenerationTaskService taskService, + IAnalyzerService analyzerService, + IEpubGenerateService epubGenService, + IDialogService dialogService) { _taskService = taskService; _taskService.OnTasksChanged += TasksChanged; _analyzerService = analyzerService; _epubGenService = epubGenService; + _dialogService = dialogService; } private async void TasksChanged(GenerationTask task, ChangedEvents changedEvents) @@ -62,6 +67,11 @@ private async ValueTask RunAsync(GenerationTask task) { task.State = GenerationState.Failed; } + catch (EbookException e) + { + task.State = GenerationState.Failed; + await _dialogService.ShowInfoAsync("生成失敗", e.ExceptionType.GetEnumMemberValue()!, "OK", default); + } catch { task.State = GenerationState.Failed; @@ -83,6 +93,11 @@ public async void RunGenerateEpubAsync(GenerationTask task) { task.State = GenerationState.Failed; } + catch (EbookException e) + { + task.State = GenerationState.Failed; + await _dialogService.ShowInfoAsync("生成失敗", e.ExceptionType.GetEnumMemberValue()!, "OK", default); + } catch { task.State = GenerationState.Failed; diff --git a/KoeBook/ViewModels/GenerationTaskViewModel.cs b/KoeBook/ViewModels/GenerationTaskViewModel.cs index b6d2fa3..cae8c09 100644 --- a/KoeBook/ViewModels/GenerationTaskViewModel.cs +++ b/KoeBook/ViewModels/GenerationTaskViewModel.cs @@ -31,7 +31,7 @@ private async Task StopTask(CancellationToken cancellationToken) if (Task is null) return; - var result = await _dialogService.ShowAsync("KoeBookからのお知らせ", "実行中のタスクをキャンセルし、削除します。この操作は戻せません。", "削除", cancellationToken); + var result = await _dialogService.ShowAsync("タスクを削除します", "実行中のタスクをキャンセルし、削除します。この操作は戻せません。", "削除", cancellationToken); if (result != ContentDialogResult.Primary) return; _runner.StopTask(Task); diff --git a/KoeBook/ViewModels/MainViewModel.cs b/KoeBook/ViewModels/MainViewModel.cs index ce235f2..14f9ac4 100644 --- a/KoeBook/ViewModels/MainViewModel.cs +++ b/KoeBook/ViewModels/MainViewModel.cs @@ -114,7 +114,7 @@ public async void BeforeTextChanging(TextBox _, TextBoxBeforeTextChangingEventAr if (EbookFilePath is null || string.IsNullOrEmpty(args.NewText)) return; - var result = await _dialogService.ShowAsync("外部URLから使用する場合、ローカルファイルは無視されます。", default); + var result = await _dialogService.ShowAsync("ローカルファイルを無視します", "外部URLから使用する場合、ローカルファイルは無視されます。", default); if (result == ContentDialogResult.Primary) { EbookFilePath = null;