diff --git a/MangaReader.Core/Extensions/ServiceCollectionExtensions.cs b/MangaReader.Core/Extensions/ServiceCollectionExtensions.cs index 4d2bb89..ea08459 100644 --- a/MangaReader.Core/Extensions/ServiceCollectionExtensions.cs +++ b/MangaReader.Core/Extensions/ServiceCollectionExtensions.cs @@ -1,5 +1,11 @@ -using MangaReader.Core.Search; +using MangaReader.Core.HttpService; +using MangaReader.Core.Metadata; +using MangaReader.Core.Search; +using MangaReader.Core.Sources.MangaDex.Api; +using MangaReader.Core.Sources.MangaDex.Metadata; using MangaReader.Core.Sources.MangaDex.Search; +using MangaReader.Core.Sources.NatoManga.Api; +using MangaReader.Core.Sources.NatoManga.Metadata; using MangaReader.Core.Sources.NatoManga.Search; #pragma warning disable IDE0130 // Namespace does not match folder structure @@ -10,10 +16,24 @@ public static class ServiceCollectionExtensions { public static IServiceCollection AddMangaReader(this IServiceCollection services) { - services.AddScoped(); + // Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:139.0) Gecko/20100101 Firefox/139.0 + services.AddHttpClient(client => + { + client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:139.0) Gecko/20100101 Firefox/139.0"); + }); + + services.AddScoped(); + + //services.AddScoped(); + services.AddScoped(); + + //services.AddScoped(); services.AddScoped(); services.AddScoped(); + //services.AddScoped(); + services.AddScoped(); + return services; } } \ No newline at end of file diff --git a/MangaReader.Core/HttpService/HttpService.cs b/MangaReader.Core/HttpService/HttpService.cs index 1470dda..54abc12 100644 --- a/MangaReader.Core/HttpService/HttpService.cs +++ b/MangaReader.Core/HttpService/HttpService.cs @@ -1,7 +1,16 @@ namespace MangaReader.Core.HttpService; -public class HttpService(HttpClient httpClient) : IHttpService +public class HttpService : IHttpService { + private readonly HttpClient _httpClient; + + public HttpService(HttpClient httpClient) + { + httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:139.0) Gecko/20100101 Firefox/139.0"); + + _httpClient = httpClient; + } + public Task GetStringAsync(string url, CancellationToken cancellationToken) - => httpClient.GetStringAsync(url, cancellationToken); + => _httpClient.GetStringAsync(url, cancellationToken); } \ No newline at end of file diff --git a/MangaReader.Core/MangaReader.Core.csproj b/MangaReader.Core/MangaReader.Core.csproj index bc7f4ce..2e8d1f8 100644 --- a/MangaReader.Core/MangaReader.Core.csproj +++ b/MangaReader.Core/MangaReader.Core.csproj @@ -9,6 +9,7 @@ + diff --git a/MangaReader.Core/Sources/MangaDex/Api/IMangaDexClient.cs b/MangaReader.Core/Sources/MangaDex/Api/IMangaDexClient.cs index 733b321..ef43c58 100644 --- a/MangaReader.Core/Sources/MangaDex/Api/IMangaDexClient.cs +++ b/MangaReader.Core/Sources/MangaDex/Api/IMangaDexClient.cs @@ -9,4 +9,5 @@ public interface IMangaDexClient Task GetFeedAsync(Guid mangaGuid, CancellationToken cancellationToken); Task GetChapterAsync(Guid chapterGuid, CancellationToken cancellationToken); Task GetCoverArtAsync(Guid mangaGuid, CancellationToken cancellationToken); + Task GetCoverArtAsync(Guid[] mangaGuid, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/MangaReader.Core/Sources/MangaDex/Api/MangaDexClient.cs b/MangaReader.Core/Sources/MangaDex/Api/MangaDexClient.cs index f9fc9b2..2fbc41e 100644 --- a/MangaReader.Core/Sources/MangaDex/Api/MangaDexClient.cs +++ b/MangaReader.Core/Sources/MangaDex/Api/MangaDexClient.cs @@ -20,17 +20,28 @@ namespace MangaReader.Core.Sources.MangaDex.Api } private async Task GetAsync(string url, CancellationToken cancellationToken) + { + //string response = await httpService.GetStringAsync(url, cancellationToken); + + //return JsonSerializer.Deserialize(response, _jsonSerializerOptions); + + return await GetAsync(url, cancellationToken); + } + + private async Task GetAsync(string url, CancellationToken cancellationToken) { string response = await httpService.GetStringAsync(url, cancellationToken); - return JsonSerializer.Deserialize(response, _jsonSerializerOptions); + return JsonSerializer.Deserialize(response, _jsonSerializerOptions); } public async Task SearchMangaByTitleAsync(string title, CancellationToken cancellationToken) { string normalizedKeyword = GetNormalizedKeyword(title); - return await GetAsync($"https://api.mangadex.org/manga?title={normalizedKeyword}&limit=5", cancellationToken); + //return await GetAsync($"https://api.mangadex.org/manga?title={normalizedKeyword}&limit=5", cancellationToken); + return await GetAsync($"https://api.mangadex.org/manga?title={normalizedKeyword}&limit=10&contentRating[]=safe&contentRating[]=suggestive&contentRating[]=erotica&includes[]=cover_art&order[followedCount]=desc&order[relevance]=desc", cancellationToken); + // } public async Task SearchMangaByAuthorAsync(string author, CancellationToken cancellationToken) @@ -64,15 +75,24 @@ namespace MangaReader.Core.Sources.MangaDex.Api public async Task GetChapterAsync(Guid chapterGuid, CancellationToken cancellationToken) { - string url = $"https://api.mangadex.org/at-home/server/{chapterGuid}?forcePort443=false"; - string response = await httpService.GetStringAsync(url, cancellationToken); + //string url = $"https://api.mangadex.org/at-home/server/{chapterGuid}?forcePort443=false"; + //string response = await httpService.GetStringAsync(url, cancellationToken); - return JsonSerializer.Deserialize(response, _jsonSerializerOptions); + //return JsonSerializer.Deserialize(response, _jsonSerializerOptions); + + return await GetAsync($"https://api.mangadex.org/at-home/server/{chapterGuid}?forcePort443=false", cancellationToken); } public async Task GetCoverArtAsync(Guid mangaGuid, CancellationToken cancellationToken) { - return await GetAsync($"https://api.mangadex.org/cover?order[volume]=asc&manga[]={mangaGuid}&limit=100&offset=0", cancellationToken); + return await GetCoverArtAsync([mangaGuid], cancellationToken); + //return await GetAsync($"https://api.mangadex.org/cover?order[volume]=asc&manga[]={mangaGuid}&limit=100&offset=0", cancellationToken); + } + + public async Task GetCoverArtAsync(Guid[] mangaGuids, CancellationToken cancellationToken) + { + string mangaGuidQuery = string.Join("&manga[]=", mangaGuids); + return await GetAsync($"https://api.mangadex.org/cover?order[volume]=asc&manga[]={mangaGuidQuery}&limit=100&offset=0", cancellationToken); } } } \ No newline at end of file diff --git a/MangaReader.Core/Sources/MangaDex/Search/MangaDexSearchProvider.cs b/MangaReader.Core/Sources/MangaDex/Search/MangaDexSearchProvider.cs index f9b14ac..a01fb69 100644 --- a/MangaReader.Core/Sources/MangaDex/Search/MangaDexSearchProvider.cs +++ b/MangaReader.Core/Sources/MangaDex/Search/MangaDexSearchProvider.cs @@ -21,43 +21,83 @@ public partial class MangaDexSearchProvider(IMangaDexClient mangaDexClient) : IM if (response == null || (response is not MangaDexCollectionResponse collectionResponse)) return []; - List mangaSearchResults = []; + MangaEntity[] mangaEntities = [.. collectionResponse.Data.Where(entity => entity is MangaEntity).Cast()]; - foreach (MangaDexEntity entity in collectionResponse.Data) + if (mangaEntities.Length == 0) + return []; + + Dictionary> mangaCoverArtMap = await GetCoverArtFileNamesAsync(mangaEntities, cancellationToken); + + List mangaSearchResults = []; + Dictionary thing = []; + + foreach (MangaEntity mangaEntity in mangaEntities) { - MangaSearchResult? mangaSearchResult = GetMangaSearchResult(entity); + CoverArtEntity[] coverArtEntites = [.. mangaCoverArtMap[mangaEntity.Id]]; + + MangaSearchResult? mangaSearchResult = GetMangaSearchResult(mangaEntity, coverArtEntites); if (mangaSearchResult == null) continue; mangaSearchResults.Add(mangaSearchResult); + } + if (thing.Count > 0) + { + Guid[] mangaGuids = thing.Select(x => x.Key).ToArray(); + var reults = await GetCoverArtFileNamesAsync(mangaGuids, cancellationToken); + //var reults = await mangaDexClient.GetCoverArtAsync(mangaGuids, cancellationToken); } return [.. mangaSearchResults]; } - private static MangaSearchResult? GetMangaSearchResult(MangaDexEntity entity) + private static MangaSearchResult? GetMangaSearchResult(MangaEntity mangaEntity, CoverArtEntity[] coverArtEntites) { - if (entity is not MangaEntity mangaEntity) + MangaAttributes? mangaAttributes = mangaEntity.Attributes; + + if (mangaAttributes == null) return null; - if (mangaEntity.Attributes == null) - return null; - - string title = mangaEntity.Attributes.Title.FirstOrDefault().Value; + string title = GetTitle(mangaAttributes); string slug = GenerateSlug(title); MangaSearchResult mangaSearchResult = new() { Title = title, + Description = GetDescription(mangaAttributes), Url = $"https://mangadex.org/title/{mangaEntity.Id}/{slug}", - Thumbnail = GetThumbnail(mangaEntity) + Thumbnail = GetThumbnail(mangaEntity, coverArtEntites) }; return mangaSearchResult; } + private static string GetTitle(MangaAttributes attributes) + { + var alternateTitle = attributes.AltTitles.Where(x => x.ContainsKey("en")).FirstOrDefault(); + + if (alternateTitle?.Count > 0) + return alternateTitle["en"]; + + if (attributes.Title.TryGetValue("en", out string? title)) + return title; + + if (attributes.Title.Count > 0) + return attributes.Title.ElementAt(0).Value; + + return string.Empty; + } + + private static string GetDescription(MangaAttributes attributes) + { + if (attributes.Description.TryGetValue("en", out string? description)) + return description; + + return string.Empty; + } + public static string GenerateSlug(string title) { // title.ToLowerInvariant().Normalize(NormalizationForm.FormD); @@ -70,7 +110,18 @@ public partial class MangaDexSearchProvider(IMangaDexClient mangaDexClient) : IM return title.Trim('-'); } - private static string? GetThumbnail(MangaDexEntity mangaDexEntity) + private static string? GetThumbnail(MangaDexEntity mangaDexEntity, CoverArtEntity[] coverArtEntites) + { + string? fileName = GetCoverArtFileNameFromMangaEntity(mangaDexEntity) + ?? GetCoverArtFileNameFromCoverArtEntities(coverArtEntites); + + if (string.IsNullOrWhiteSpace(fileName)) + return null; + + return $"https://mangadex.org/covers/{mangaDexEntity.Id}/{fileName}"; + } + + private static string? GetCoverArtFileNameFromMangaEntity(MangaDexEntity mangaDexEntity) { CoverArtEntity? coverArtEntity = (CoverArtEntity?)mangaDexEntity.Relationships.FirstOrDefault(entity => entity is CoverArtEntity); @@ -78,11 +129,59 @@ public partial class MangaDexSearchProvider(IMangaDexClient mangaDexClient) : IM if (coverArtEntity == null || string.IsNullOrWhiteSpace(coverArtEntity.Attributes?.FileName)) return null; - string? fileName = coverArtEntity.Attributes?.FileName; + return coverArtEntity.Attributes?.FileName; + } - if (string.IsNullOrWhiteSpace(coverArtEntity.Attributes?.FileName)) - return null; + private static string? GetCoverArtFileNameFromCoverArtEntities(CoverArtEntity[] coverArtEntites) + { + return coverArtEntites.Where(coverArtEntity => + string.IsNullOrWhiteSpace(coverArtEntity.Attributes?.FileName) == false).FirstOrDefault()?.Attributes!.FileName; + } - return $"https://mangadex.org/covers/{mangaDexEntity.Id}/{fileName}"; + private async Task>> GetCoverArtFileNamesAsync(MangaEntity[] mangaEntities, CancellationToken cancellationToken) + { + Guid[] mangaGuids = [.. mangaEntities.Select(entity => entity.Id)]; + + return await GetCoverArtFileNamesAsync(mangaGuids, cancellationToken); + } + + private async Task>> GetCoverArtFileNamesAsync(Guid[] mangaGuids, CancellationToken cancellationToken) + { + Dictionary> result = []; + + foreach (Guid mangaGuid in mangaGuids) + { + result.Add(mangaGuid, []); + } + + MangaDexResponse? response = await mangaDexClient.GetCoverArtAsync(mangaGuids, cancellationToken); + + if (response == null || (response is not MangaDexCollectionResponse collectionResponse)) + return result; + + CoverArtEntity[] coverArtEntities = [.. collectionResponse.Data.Where(entity => entity is CoverArtEntity).Cast()]; + + if (coverArtEntities.Length == 0) + return result; + + CoverArtEntity[] orderedCoverArtEntities = [.. coverArtEntities.OrderBy(x => x.Attributes?.Volume)]; + + foreach (var coverArtEntity in orderedCoverArtEntities) + { + if (coverArtEntity.Attributes == null) + continue; + + MangaEntity? mangaEntity = (MangaEntity?)coverArtEntity.Relationships.FirstOrDefault(relationship => relationship is MangaEntity); + + if (mangaEntity == null) + continue; + + if (result.ContainsKey(mangaEntity.Id) == false) + continue; + + result[mangaEntity.Id].Add(coverArtEntity); + } + + return result; } } \ No newline at end of file diff --git a/MangaReader.Tests/MangaReader.Tests.csproj b/MangaReader.Tests/MangaReader.Tests.csproj index 9e94d5e..6ef018a 100644 --- a/MangaReader.Tests/MangaReader.Tests.csproj +++ b/MangaReader.Tests/MangaReader.Tests.csproj @@ -12,6 +12,7 @@ + @@ -27,6 +28,7 @@ + diff --git a/MangaReader.Tests/Sources/MangaDex/Api/Manga-Search-Response-2.json b/MangaReader.Tests/Sources/MangaDex/Api/Manga-Search-Response-2.json new file mode 100644 index 0000000..949593d --- /dev/null +++ b/MangaReader.Tests/Sources/MangaDex/Api/Manga-Search-Response-2.json @@ -0,0 +1,890 @@ +{ + "result": "ok", + "response": "collection", + "data": [ + { + "id": "e78a489b-6632-4d61-b00b-5206f5b8b22b", + "type": "manga", + "attributes": { + "title": { "en": "Tensei Shitara Slime Datta Ken" }, + "altTitles": [ + { "en": "That Time I Got Reincarnated as a Slime" }, + { "fr": "Moi, quand je me r\u00e9incarne en Slime" }, + { "pl": "Odrodzony jako galareta" }, + { "en": "Regarding Reincarnated to Slime" }, + { "ja-ro": "Tensei Slime" }, + { "ja-ro": "TenSli" }, + { "ja-ro": "TenSura" }, + { "en": "In Regards to My Reincarnation as a Slime" }, + { "it": "Vita da Slime" }, + { "ru": "\u041e \u043c\u043e\u0451\u043c \u043f\u0435\u0440\u0435\u0440\u043e\u0436\u0434\u0435\u043d\u0438\u0438 \u0432 \u0441\u043b\u0438\u0437\u044c" }, + { "th": "\u0e40\u0e01\u0e34\u0e14\u0e43\u0e2b\u0e21\u0e48\u0e17\u0e31\u0e49\u0e07\u0e17\u0e35\u0e01\u0e47\u0e40\u0e1b\u0e47\u0e19\u0e2a\u0e44\u0e25\u0e21\u0e4c\u0e44\u0e1b\u0e0b\u0e30\u0e41\u0e25\u0e49\u0e27" }, + { "zh-hk": "\u5173\u4e8e\u6211\u8f6c\u751f\u540e\u6210\u4e3a\u53f2\u83b1\u59c6\u7684\u90a3\u4ef6\u4e8b" }, + { "ja": "\u8ee2\u751f\u3057\u305f\u3089\u30b9\u30e9\u30a4\u30e0\u3060\u3063\u305f\u4ef6" }, + { "ko": "\uc804\uc0dd\ud588\ub354\ub2c8 \uc2ac\ub77c\uc784\uc774\uc5c8\ub358 \uac74\uc5d0 \ub300\ud558\uc5ec" }, + { "es-la": "Aquella vez que me convert\u00ed en Slime" }, + { "ar": "\u0630\u0644\u0643 \u0627\u0644\u0648\u0642\u062a \u0627\u0644\u0630\u064a \u062a\u062c\u0633\u062f\u062a \u0641\u064a\u0647 \u0643\u0633\u0644\u0627\u064a\u0645" }, + { "fi": "Kun j\u00e4lleensynnyin hirvi\u00f6n\u00e4" }, + { "tr": "O zaman bir bal\u00e7\u0131k olarak reenkarne oldum" }, + { "tr": "O zaman bir slime olarak reenkarne oldum" }, + { "de": "Meine Wiedergeburt als Schleim in einer anderen Welt" } + ], + "description": { + "en": "The ordinary Mikami Satoru found himself dying after being stabbed by a slasher. It should have been the end of his meager 37 years, but he found himself deaf and blind after hearing a mysterious voice. \nHe had been reincarnated into a slime! \n \nWhile complaining about becoming the weak but famous slime and enjoying the life of a slime at the same time, Mikami Satoru met with the Catastrophe-level monster \u201cStorm Dragon Veldora\u201d, and his fate began to move.\n\n---\n**Links:**\n- Alternative Official English - [K MANGA](https:\/\/kmanga.kodansha.com\/title\/10044\/episode\/317350) (U.S. Only), [INKR](https:\/\/comics.inkr.com\/title\/233-that-time-i-got-reincarnated-as-a-slime), [Azuki](https:\/\/www.azuki.co\/series\/that-time-i-got-reincarnated-as-a-slime), [Coolmic](https:\/\/coolmic.me\/titles\/587), [Manga Planet](https:\/\/mangaplanet.com\/comic\/618e32db10673)", + "ru": "37-\u043b\u0435\u0442\u043d\u0438\u0439 \u044f\u043f\u043e\u043d\u0435\u0446-\u0445\u043e\u043b\u043e\u0441\u0442\u044f\u043a \u0431\u044b\u043b \u0437\u0430\u0440\u0435\u0437\u0430\u043d \u043d\u0430 \u0443\u043b\u0438\u0446\u0435 \u043a\u0430\u043a\u0438\u043c-\u0442\u043e \u043c\u0435\u0440\u0437\u0430\u0432\u0446\u0435\u043c-\u0433\u0440\u0430\u0431\u0438\u0442\u0435\u043b\u0435\u043c. \u0422\u0443\u0442 \u0431\u044b \u0438 \u0438\u0441\u0442\u043e\u0440\u0438\u0438 \u043a\u043e\u043d\u0435\u0446, \u0434\u0430 \u0432\u0441\u0451 \u043e\u0431\u0435\u0440\u043d\u0443\u043b\u043e\u0441\u044c \u0438\u043d\u0430\u0447\u0435, \u043d\u0435\u043e\u0436\u0438\u0434\u0430\u043d\u043d\u043e \u043e\u043d \u043f\u0435\u0440\u0435\u0440\u043e\u0434\u0438\u043b\u0441\u044f \u0441\u043b\u0438\u0437\u044c\u044e \u0432 \u0444\u044d\u043d\u0442\u0435\u0437\u0438\u0439\u043d\u043e\u043c \u043c\u0438\u0440\u0435. \u041d\u043e \u0447\u0442\u043e \u043c\u043e\u0436\u0435\u0442 \u0441\u0434\u0435\u043b\u0430\u0442\u044c, \u043f\u0443\u0441\u043a\u0430\u0439 \u0438 \u0440\u0430\u0437\u0443\u043c\u043d\u0430\u044f, \u043d\u043e \u0441\u043b\u0438\u0437\u044c? \r\n\r\n\r\n---\r\n\r\n**Links:** \r\n- [Anime Season 1 on ANN](https:\/\/www.animenewsnetwork.com\/encyclopedia\/anime.php?id=20736)", + "es-la": "Un hombre, que al tratar de salvar a su compa\u00f1ero de trabajo y su novia, fue apu\u00f1alado por un ladr\u00f3n que escapaba. Mientras mor\u00eda desangrado escuch\u00f3 una voz extra\u00f1a. Esta voz escuch\u00f3 su lamento de haber muerto virgen y a causa de eso le dio la Habilidad \u00danica \"Gran Sabio\" \u00bfFue esto una burla? Ahora \u00e9l ha reencarnado como un Slime en otro mundo, \u00bfSer\u00e1 este el inicio de una emocionante aventura?", + "pt-br": "Depois de ser morto por um ladr\u00e3o que fugia, um rapaz normal de 37 anos de idade se encontra reencarnado em um outro mundo como um slime cego com habilidades \u00fanicas. Com um novo nome \"Rimuru Tempest\" ele chegou depois de conhecer seu novo amigo, o \"n\u00edvel cat\u00e1strofe\", Drag\u00e3o da Tempestade Verudora, ele come\u00e7a sua vida de slime em outro mundo com seu crescente n\u00famero de seguidores." + }, + "isLocked": true, + "links": { + "al": "86399", + "ap": "that-time-i-got-reincarnated-as-a-slime", + "bw": "series\/56105", + "kt": "35483", + "mu": "119910", + "nu": "tensei-shitara-slime-datta-ken", + "amz": "https:\/\/www.amazon.co.jp\/gp\/product\/B074CFC3N4", + "cdj": "http:\/\/www.cdjapan.co.jp\/product\/NEOBK-1858955", + "ebj": "https:\/\/ebookjapan.yahoo.co.jp\/books\/334900", + "mal": "87609", + "raw": "https:\/\/pocket.shonenmagazine.com\/episode\/10834108156631339284", + "engtl": "https:\/\/kodansha.us\/series\/that-time-i-got-reincarnated-as-a-slime" + }, + "originalLanguage": "ja", + "lastVolume": "", + "lastChapter": "", + "publicationDemographic": "shounen", + "status": "ongoing", + "year": 2015, + "contentRating": "safe", + "tags": [ + { + "id": "0bc90acb-ccc1-44ca-a34a-b9f3a73259d0", + "type": "tag", + "attributes": { + "name": { "en": "Reincarnation" }, + "description": {}, + "group": "theme", + "version": 1 + }, + "relationships": [] + }, + { + "id": "36fd93ea-e8b8-445e-b836-358f02b3d33d", + "type": "tag", + "attributes": { + "name": { "en": "Monsters" }, + "description": {}, + "group": "theme", + "version": 1 + }, + "relationships": [] + }, + { + "id": "391b0423-d847-456f-aff0-8b0cfc03066b", + "type": "tag", + "attributes": { + "name": { "en": "Action" }, + "description": {}, + "group": "genre", + "version": 1 + }, + "relationships": [] + }, + { + "id": "39730448-9a5f-48a2-85b0-a70db87b1233", + "type": "tag", + "attributes": { + "name": { "en": "Demons" }, + "description": {}, + "group": "theme", + "version": 1 + }, + "relationships": [] + }, + { + "id": "4d32cc48-9f00-4cca-9b5a-a839f0764984", + "type": "tag", + "attributes": { + "name": { "en": "Comedy" }, + "description": {}, + "group": "genre", + "version": 1 + }, + "relationships": [] + }, + { + "id": "81183756-1453-4c81-aa9e-f6e1b63be016", + "type": "tag", + "attributes": { + "name": { "en": "Samurai" }, + "description": {}, + "group": "theme", + "version": 1 + }, + "relationships": [] + }, + { + "id": "ace04997-f6bd-436e-b261-779182193d3d", + "type": "tag", + "attributes": { + "name": { "en": "Isekai" }, + "description": {}, + "group": "genre", + "version": 1 + }, + "relationships": [] + }, + { + "id": "cdc58593-87dd-415e-bbc0-2ec27bf404cc", + "type": "tag", + "attributes": { + "name": { "en": "Fantasy" }, + "description": {}, + "group": "genre", + "version": 1 + }, + "relationships": [] + }, + { + "id": "f4122d1c-3b44-44d0-9936-ff7502c39ad3", + "type": "tag", + "attributes": { + "name": { "en": "Adaptation" }, + "description": {}, + "group": "format", + "version": 1 + }, + "relationships": [] + } + ], + "state": "published", + "chapterNumbersResetOnNewVolume": false, + "createdAt": "2018-11-05T01:59:35+00:00", + "updatedAt": "2025-01-23T19:15:00+00:00", + "version": 82, + "availableTranslatedLanguages": [ "fr", "ar", "es-la", "id", "en", "pt-br" ], + "latestUploadedChapter": "3872f6f3-f327-410c-b61b-0b955fc42609" + }, + "relationships": [ + { + "id": "dbf8af05-7173-49f3-bf60-f4ea3f586486", + "type": "author" + }, + { + "id": "560748c6-fbe7-49f5-8258-7b3292942101", + "type": "artist" + }, + { + "id": "1575a7ba-6f3e-477e-9491-74506a21b268", + "type": "cover_art", + "attributes": { + "description": "Volume 28 Cover from Booklive", + "volume": "28", + "fileName": "67de8b2f-c080-4006-91dd-a3b87abdb7fd.jpg", + "locale": "ja", + "createdAt": "2025-01-23T19:13:27+00:00", + "updatedAt": "2025-01-23T19:13:27+00:00", + "version": 1 + } + }, + { + "id": "0d20230b-60de-4f02-b898-b477748ee667", + "type": "manga", + "related": "colored" + }, + { + "id": "0e620699-0033-4b54-beb6-1bd82e6ee02e", + "type": "manga", + "related": "side_story" + }, + { + "id": "1180743d-8e38-4c00-b767-c53169fadc6a", + "type": "manga", + "related": "spin_off" + }, + { + "id": "1f284c6f-73f2-48db-a43b-2c35c40b1021", + "type": "manga", + "related": "spin_off" + }, + { + "id": "40633ae0-794a-4dc7-b318-30774ef9908d", + "type": "manga", + "related": "doujinshi" + }, + { + "id": "4c0f5ac2-37e9-421e-934f-b1351f9ee6b3", + "type": "manga", + "related": "doujinshi" + }, + { + "id": "4fd9e91c-696f-468a-bf0c-a4d26468851c", + "type": "manga", + "related": "side_story" + }, + { + "id": "58703998-d847-42a2-9ff4-9c671d36772f", + "type": "manga", + "related": "doujinshi" + }, + { + "id": "5ede3032-6278-439f-a06b-c3f6d1493554", + "type": "manga", + "related": "spin_off" + }, + { + "id": "615a8f24-4289-437d-b0b7-c32e5b9d09b0", + "type": "manga", + "related": "doujinshi" + }, + { + "id": "61d81be6-2759-4cc4-9815-7952a3449149", + "type": "manga", + "related": "spin_off" + }, + { + "id": "7afb9330-261e-4717-8042-8d41b2b3deba", + "type": "manga", + "related": "doujinshi" + }, + { + "id": "7b650718-55d6-4094-afe6-95f59e8d0c4c", + "type": "manga", + "related": "doujinshi" + }, + { + "id": "7d580248-cf9c-4fb6-925e-343ffb3dcc7e", + "type": "manga", + "related": "doujinshi" + }, + { + "id": "a1343483-8779-4b6f-b919-9025a89d98c3", + "type": "manga", + "related": "spin_off" + }, + { + "id": "b956fd7d-f50a-4e2b-94d7-84bd9aa125e1", + "type": "manga", + "related": "doujinshi" + }, + { + "id": "bd76862b-640c-4448-b721-5a22b6691774", + "type": "manga", + "related": "doujinshi" + }, + { + "id": "c2972668-1107-4c2f-a06b-aaa2252906fb", + "type": "manga", + "related": "spin_off" + }, + { + "id": "c8e83aab-43e8-425c-bd55-e7fa7fd666f7", + "type": "manga", + "related": "spin_off" + }, + { + "id": "cab847c6-2748-4259-b9f4-c62bffd51311", + "type": "manga", + "related": "spin_off" + }, + { + "id": "e2d738e5-340f-4b24-aef3-e624623154a0", + "type": "manga", + "related": "doujinshi" + }, + { + "id": "f0e05005-4ac8-4f5b-aca9-8762ede16daa", + "type": "manga", + "related": "doujinshi" + }, + { + "id": "f1c79d23-d306-40e5-8b47-17cab7408f1a", + "type": "manga", + "related": "spin_off" + } + ] + }, + { + "id": "5e3a710f-0b0d-482b-9e84-d9c91960c625", + "type": "manga", + "attributes": { + "title": { "en": "Yancha Gal no Anjou-san" }, + "altTitles": [ + { "ja": "\u3084\u3093\u3061\u3083\u30ae\u30e3\u30eb\u306e\u5b89\u57ce\u3055\u3093" }, + { "ja-ro": "Yancha Gyaru no Anjou-san" }, + { "en": "Anjo the Mischievous Gal" }, + { "en": "The Mischievous Gal Anjou-san" }, + { "ru": "\u041e\u0437\u043e\u0440\u043d\u0430\u044f \u0413\u044f\u0440\u0443 \u0410\u043d\u0434\u0437\u0451-\u0441\u0430\u043d" }, + { "th": "\u0e04\u0e38\u0e13\u0e2d\u0e31\u0e19\u0e42\u0e08 \u0e2b\u0e22\u0e2d\u0e01\u0e19\u0e31\u0e01\u0e40\u0e1e\u0e23\u0e32\u0e30\u0e23\u0e31\u0e01\u0e19\u0e30" }, + { "zh": "\u64c5\u957f\u6311\u9017\u7684\u5b89\u57ce\u540c\u5b66" }, + { "zh-ro": "Sh\u00e0nch\u00e1ng ti\u01ceod\u00f2u de \u0101nch\u00e9ng t\u00f3ngxu\u00e9" }, + { "zh": "\u6dd8\u6c14\u8fa3\u59b9\u5b89\u57ce" }, + { "zh": "\u987d\u76ae\u8fa3\u59b9\u5b89\u57ce\u540c\u5b66" }, + { "ko": "\uc7a5\ub09c\uce58\ub294\uac38\ub8e8 \uc548\uc8e0 \uc591" }, + { "ko-ro": "Jangnanchineungyalu Anjyo Yang" } + ], + "description": { + "de": "Seto ist ein total normaler und irgendwie langweiliger Sch\u00fcler. Aus einem unerkl\u00e4rlichen Grund jedoch, l\u00e4sst seine Mitsch\u00fclerin Anjou ihn nicht in Ruhe! Der ernste Seto und die energische Anjou bilden ein kontrastreiches Duo, doch Anjou macht das nichts aus, da sie sehr viel Spa\u00df hat, Seto zu necken. Andererseits hat Seto eine schwere Zeit mit Anjous Eskapaden. Wenn er doch nur w\u00fcsste, dass seine Reaktionen der Grund sind, dass Anjou weitermacht. \n \nDoch \u2026 ist ihr flirten wirklich nur gespielt \u2026?", + "en": "Seto is a completely ordinary and somewhat boring high school student. Yet, for whatever reason, his errant gyaru classmate Anjou just won't leave him alone! The serious Seto and energetic Anjou make a contrasting duo, but Anjou doesn't seem to mind, as she has too much fun teasing him. On the other hand, Seto has a hard time dealing with all of her endless antics; little does he realize that his humorous reactions are precisely the reason Anjou enjoys his company.\n\nBut\u2026 just how much of her flirting is merely an act?\n\n**Official English:** [emaqi - USA & Canada only](https:\/\/emaqi.com\/manga\/anjo-the-mischievous-gal)", + "fr": "Seto est un lyc\u00e9en ne souhaitant qu'une chose : passer sa scolarit\u00e9 sans \u00eatre remarqu\u00e9 et \u00eatre en paix. Malheureusement pour lui, il attire l'attention de la pire personne possible dans sa classe, Anjou la gal. A partir de ce jour, le quotidien de Seto va drastiquement changer gr\u00e2ce (ou \u00e0 cause) de sa camarade, cherchant \u00e0 s'amuser au d\u00e9pend de notre h\u00e9ros.", + "ja": "\u771f\u9762\u76ee\u3067\u30af\u30e9\u30b9\u306e\u4e2d\u3067\u3082\u76ee\u7acb\u305f\u306a\u3044\u702c\u6238\u304f\u3093\u306b\u306f\u3001\u306a\u305c\u304b\u3044\u3064\u3082\u30a4\u30b1\u3066\u308b\u30ae\u30e3\u30eb\u306e\u5b89\u57ce\u3055\u3093\u304c\u3044\u3061\u3044\u3061\u30a8\u30ed\u304f\u7d61\u3093\u3067\u304f\u308b\u3002\u3044\u3064\u3082\u30ae\u30ea\u30ae\u30ea\u3067\u30c9\u30ad\u30c9\u30ad\u3059\u308b\u601d\u6625\u671f\u3074\u3061\u3074\u3061\u30e9\u30d6\u30b3\u30e1\u30c7\u30a3\uff01", + "ru": "\u0421\u0435\u0442\u043e - \u0441\u0435\u0440\u044c\u0451\u0437\u043d\u044b\u0439 \u0441\u0442\u0430\u0440\u0448\u0435\u043a\u043b\u0430\u0441\u0441\u043d\u0438\u043a, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0432\u0435\u043b \u0442\u0438\u0445\u0443\u044e \u0448\u043a\u043e\u043b\u044c\u043d\u0443\u044e \u0436\u0438\u0437\u043d\u044c, \u043f\u043e\u043a\u0430 \u043d\u0435 \u0441\u0442\u0430\u043b \u0436\u0435\u0440\u0442\u0432\u043e\u0439 \u0434\u0440\u0430\u0437\u043d\u0438\u043b\u043e\u043a \u0433\u044f\u0440\u0443 \u0410\u043d\u0434\u0437\u0451-\u0441\u0430\u043d.", + "pt-br": "Seto \u00e9 um estudante completamente comum e um tanto chato. Mesmo assim, por alguma raz\u00e3o, sua nada correta colega de classe Gal Anjou simplesmente n\u00e3o o deixa em paz!\n\nO Seto s\u00e9rio e a en\u00e9rgica Anjou formam uma dupla um tanto interessante, mas Anjou n\u00e3o parece se importar, pois se diverte muito provocando-o. Por outro lado, Seto tem dificuldade em lidar com todas as suas travessuras intermin\u00e1veis; mal ele percebe que suas rea\u00e7\u00f5es humor\u00edsticas s\u00e3o precisamente o motivo pelo qual Anjou gosta de sua companhia.\n\nMas\u2026 quanto de seu flerte \u00e9 meramente uma atua\u00e7\u00e3o?" + }, + "isLocked": true, + "links": { + "al": "101315", + "ap": "yancha-gal-no-anjou-san", + "bw": "series\/154016", + "kt": "40927", + "mu": "145904", + "amz": "https:\/\/www.amazon.co.jp\/dp\/B07J2W5N37", + "cdj": "https:\/\/www.cdjapan.co.jp\/product\/NEOBK-2193867", + "ebj": "https:\/\/ebookjapan.yahoo.co.jp\/books\/447131\/", + "mal": "111357", + "raw": "https:\/\/piccoma.com\/web\/product\/30277?etype=episode", + "engtl": "https:\/\/x.com\/emaqi_official\/status\/1838216760945770879" + }, + "originalLanguage": "ja", + "lastVolume": "", + "lastChapter": "", + "publicationDemographic": "seinen", + "status": "ongoing", + "year": 2017, + "contentRating": "suggestive", + "tags": [ + { + "id": "423e2eae-a7a2-4a8b-ac03-a8351462d71d", + "type": "tag", + "attributes": { + "name": { "en": "Romance" }, + "description": {}, + "group": "genre", + "version": 1 + }, + "relationships": [] + }, + { + "id": "4d32cc48-9f00-4cca-9b5a-a839f0764984", + "type": "tag", + "attributes": { + "name": { "en": "Comedy" }, + "description": {}, + "group": "genre", + "version": 1 + }, + "relationships": [] + }, + { + "id": "caaa44eb-cd40-4177-b930-79d3ef2afe87", + "type": "tag", + "attributes": { + "name": { "en": "School Life" }, + "description": {}, + "group": "theme", + "version": 1 + }, + "relationships": [] + }, + { + "id": "e5301a23-ebd9-49dd-a0cb-2add944c7fe9", + "type": "tag", + "attributes": { + "name": { "en": "Slice of Life" }, + "description": {}, + "group": "genre", + "version": 1 + }, + "relationships": [] + }, + { + "id": "fad12b5e-68ba-460e-b933-9ae8318f5b65", + "type": "tag", + "attributes": { + "name": { "en": "Gyaru" }, + "description": {}, + "group": "theme", + "version": 1 + }, + "relationships": [] + } + ], + "state": "published", + "chapterNumbersResetOnNewVolume": false, + "createdAt": "2019-05-21T04:40:11+00:00", + "updatedAt": "2025-01-02T18:55:29+00:00", + "version": 64, + "availableTranslatedLanguages": [ "en", "fr", "pt-br", "ru", "es-la", "pl", "id", "vi", "it", "hu" ], + "latestUploadedChapter": "8bfd2743-73f1-42b5-8a69-c41ac42f4117" + }, + "relationships": [ + { + "id": "02812ab1-327c-443c-ac52-0e602e0cafe4", + "type": "author" + }, + { + "id": "02812ab1-327c-443c-ac52-0e602e0cafe4", + "type": "artist" + }, + { + "id": "205787ba-a981-4966-b02e-e9cd82baacce", + "type": "cover_art", + "attributes": { + "description": "Volume 15 Cover from BookLive", + "volume": "15", + "fileName": "c5111530-7823-4451-bd42-c439a2aaeece.jpg", + "locale": "ja", + "createdAt": "2025-02-18T21:22:27+00:00", + "updatedAt": "2025-02-18T21:22:27+00:00", + "version": 1 + } + }, + { + "id": "1d2e06de-cc7a-480b-a3b9-0d06971bd165", + "type": "manga", + "related": "spin_off" + }, + { + "id": "2fa18b85-7ec3-4c15-a87d-32cc6aed6ca8", + "type": "manga", + "related": "spin_off" + }, + { + "id": "7a2ecc5c-b215-47ab-a7f0-fcdeff941e9f", + "type": "manga", + "related": "side_story" + }, + { + "id": "c400ee54-26cd-48bf-ac88-5cf7cc3ab77c", + "type": "manga", + "related": "colored" + }, + { + "id": "ce068526-df38-45b9-899f-a2a672b4442a", + "type": "manga", + "related": "preserialization" + } + ] + }, + { + "id": "d8323b7b-9a7a-462b-90f0-2759fed52511", + "type": "manga", + "attributes": { + "title": { "en": "Dosanko Gal wa Namaramenkoi" }, + "altTitles": [ + { "ja": "\u9053\u7523\u5b50\u30ae\u30e3\u30eb\u306f\u306a\u307e\u3089\u3081\u3093\u3053\u3044" }, + { "ja-ro": "Dosanko Gyura wa Namaramenkoi" }, + { "en": "Hokkaido Gals are Super Adorable!" }, + { "ko": "\ub3c4\uc0b0\ucf54 \uac38\ub8e8\ub294 \ucc38\ub9d0\ub85c \uadc0\uc5ec\uc6cc" }, + { "pt-br": "Gyarus de Hokkaido s\u00e3o ador\u00e1veis!" }, + { "ru": "\u0414\u043e\u0441\u0430\u043d\u043a\u043e-\u0433\u044f\u0440\u0443 \u0447\u0443\u0434\u043e \u043a\u0430\u043a \u043c\u0438\u043b\u044b" }, + { "es": "Esa gal de Hokkaido es demasiado linda" }, + { "tr": "Hokkaido'nun Gyaru K\u0131zlar\u0131 Acayip G\u00fczel!" }, + { "uk": "\u0414\u043e\u0441\u0430\u043d\u043a\u043e-\u0433\u044f\u0440\u0443 \u0441\u0442\u0440\u0430\u0445 \u044f\u043a\u0456 \u0433\u0430\u0440\u043d\u0435\u043d\u044c\u043a\u0456" } + ], + "description": { + "en": "Shiki Tsubasa has just moved from Tokyo to Hokkaido in the middle of winter. Not quite appreciating how far apart towns are in the country, he gets off the taxi at the next town over from his destination so he can see the sights around his home, but he is shocked when he learns the \"next town\" is a 3-hour walk away. However, he also meets a cute Dosanko (born and raised in Hokkaido) gyaru named Fuyuki Minami who is braving 8 degrees celcius below 0 weather in the standard gyaru outfit of short skirts and bare legs!", + "es": "Tsubasa es un chico que se va vivir desde Tokyo a Hokkaido en pleno invierno porque han trasladado a su padre. En su antiguo instituto no era nada popular y se met\u00edan con \u00e9l siempre que pod\u00edan. Dispuesto a empezar una nueva vida, llega en taxi entre un paisaje nevado y se baja a las primeras de cambio pensando que ya ha llegado.\n\nPero pronto se da cuenta de que en Hokkaido las cosas son diferentes que en la gran ciudad y aqu\u00ed las distancias son mucho mayores, as\u00ed que el sitio al que iba est\u00e1 a tres horas andando: eso significa que se ha quedado tirado en medio de la nieve. El impacto es mayor cuando se encuentra con Minami Fuyuki, una chica vestida como la t\u00edpica gal pese al fr\u00edo que hace.\n\nMinami no s\u00f3lo es guapa, sino que es una chica supermaja y con las maneras de una gal, un crush instant\u00e1neo para Tsubasa, aunque el combo de gal m\u00e1s chica de pueblo lo deja bastante perplejo y m\u00e1s cuando se entera de que van al mismo instituto. Gracias a Minami podr\u00e1 integrarse bastante r\u00e1pido y conocer a otras gals, aunque el choque cultural a varios niveles est\u00e1 servido. \u00a1Sus d\u00edas de gals, fr\u00edo y diversi\u00f3n acaban de empezar!", + "fr": "Natsukawa Tsubasa vient de d\u00e9m\u00e9nager de Tokyo \u00e0 Hokkaido, en plein hiver. Ne se rendant pas compte de la r\u00e9alit\u00e9 des distances \u00e0 la campagne, il se retrouve perdu \u00e0 3 heures de marche de sa destination. Mais il fait \u00e9galement la rencontre d\u2019une Dosanko (\u00ab n\u00e9e et \u00e9lev\u00e9e \u00e0 Hokkaido \u00bb) gyaru, en minijupe par \u2013 8\u00b0C ! \n\n\n---\n\n**Links:** \n- [Author's Twitter](https:\/\/twitter.com\/ikada_kai) | [Author's YouTube channel](https:\/\/www.youtube.com\/channel\/UC-U4OJu-cEF2VlnM61B77bQ) | [Author's Pixiv](https:\/\/www.pixiv.net\/en\/users\/21326958)", + "ja": "\u5317\u6d77\u9053\u5317\u898b\u5e02\u306b\u8ee2\u6821\u3057\u3066\u304d\u305f\u56db\u5b63 \u7ffc\u306f\u3001\u771f\u3063\u767d\u306a\u9280\u4e16\u754c\u30671\u4eba\u306e\u201c\u30ae\u30e3\u30eb\u201d\u3068\u51fa\u4f1a\u3046\u2015\u2015\u3002\u6c37\u70b9\u4e0b\u3067\u3082\u751f\u8db3\u3067\u3001\u8ddd\u96e2\u304c\u8fd1\u304f\u3066\u3001\u65b9\u8a00\u30d0\u30ea\u30d0\u30ea\uff01", + "ru": "\u041d\u0430\u0446\u0443\u043a\u0430\u0432\u0430 \u0426\u0443\u0431\u0430\u0441\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0442\u043e \u043f\u0435\u0440\u0435\u0435\u0445\u0430\u043b \u0438\u0437 \u0422\u043e\u043a\u0438\u043e \u043d\u0430 \u0425\u043e\u043a\u043a\u0430\u0439\u0434\u043e. \u00a0\u041d\u0435 \u0438\u043c\u0435\u044f \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u044f, \u043e \u0442\u043e\u043c \u043a\u0430\u043a\u043e\u0435 \u0431\u043e\u043b\u044c\u0448\u043e\u0435 \u0440\u0430\u0441\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u043c\u0435\u0436\u0434\u0443 \u043d\u0430\u0441\u0435\u043b\u0451\u043d\u043d\u044b\u043c\u0438 \u043f\u0443\u043d\u043a\u0442\u0430\u043c\u0438 \u0432 \u0437\u0430\u0445\u043e\u043b\u0443\u0441\u0442\u044c\u0435 \u043e\u043d \u0440\u0435\u0448\u0438\u043b \u0432\u044b\u0439\u0442\u0438 \u0438\u0437 \u0442\u0430\u043a\u0441\u0438 \u0432 \u0441\u043e\u0441\u0435\u0434\u043d\u0435\u043c \u0433\u043e\u0440\u043e\u0434\u0435, \u043f\u043e\u0441\u0440\u0435\u0434\u0438 \u0437\u0438\u043c\u044b. \u041e\u0434\u043d\u0430\u043a\u043e \u043e\u043d \u0431\u044b\u043b \u0448\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d \u0443\u0437\u043d\u0430\u0432, \u0447\u0442\u043e \u0434\u043e \u0441\u043e\u0441\u0435\u0434\u043d\u0435\u0433\u043e \u0433\u043e\u0440\u043e\u0434\u0430 \u0438\u0434\u0442\u0438 \u0446\u0435\u043b\u044b\u0445 3 \u0447\u0430\u0441\u0430. \u0422\u0443\u0442 \u0436\u0435 \u043e\u043d \u0432\u0441\u0442\u0440\u0435\u0447\u0430\u0435\u0442 \u0414\u043e\u0441\u0430\u043d\u043a\u043e-\u0433\u044f\u0440\u0443 (\u0440\u043e\u0434\u043e\u043c \u0438\u0437 \u0425\u043e\u043a\u043a\u0430\u0439\u0434\u043e) \u043f\u043e \u0438\u043c\u0435\u043d\u0438 \u0424\u0443\u044e\u043a\u0438 \u041c\u0438\u043d\u0430\u043c\u0438, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u043f\u0440\u0438 \u043c\u0438\u043d\u0443\u0441 8 \u0433\u0440\u0430\u0434\u0443\u0441\u0430\u0445 \u0446\u0435\u043b\u044c\u0441\u0438\u044f \u0445\u043e\u0434\u0438\u0442 \u0432 \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u043e\u0439 \u043e\u0434\u0435\u0436\u0434\u0435 \u0433\u044f\u0440\u0443: \u043a\u043e\u0440\u043e\u0442\u043a\u043e\u0439 \u044e\u0431\u043a\u0435 \u0438 \u0441 \u0433\u043e\u043b\u044b\u043c\u0438 \u043d\u043e\u0433\u0430\u043c\u0438. \n\n\n---", + "tr": "Liseli Tsubasa, Hokkaido\u2019daki Kitami \u015fehrine ta\u015f\u0131n\u0131r ve oradaki bir otob\u00fcs dura\u011f\u0131nda bir \"gyaru\" ile tan\u0131\u015f\u0131r. Dondurucu so\u011fu\u011fa ra\u011fmen \u00e7\u0131plak bacaklar\u0131yla beyaz kar manzaras\u0131n\u0131n kar\u015f\u0131s\u0131nda tek ba\u015f\u0131na durdu\u011funu g\u00f6rmek kalbini cezbeder.", + "pt-br": "Shiki Tsubasa acaba de se mudar de T\u00f3quio para Hokkaido no meio do inverno. Sem perceber a dist\u00e2ncia entre as cidades do pa\u00eds, ele desce do t\u00e1xi na cidade mais pr\u00f3xima de seu destino para poder ver os pontos tur\u00edsticos ao redor de sua casa, mas fica chocado quando descobre que a \"pr\u00f3xima cidade\" \u00e9 uma a tr\u00eas horas a p\u00e9. No entanto, ele tamb\u00e9m conhece uma linda Dosanko (nascido e criado em Hokkaido) gyaru chamado Fuyuki Minami, que est\u00e1 enfrentando um clima de 8 graus Celsius abaixo de 0 com a roupa gyaru padr\u00e3o de saias curtas e pernas nuas!" + }, + "isLocked": false, + "links": { + "al": "111403", + "ap": "dosanko-gyaru-wa-namaramenkoi", + "bw": "series\/225993\/list", + "kt": "55322", + "mu": "152497", + "amz": "https:\/\/www.amazon.co.jp\/dp\/B084NT3Q8X", + "cdj": "https:\/\/www.cdjapan.co.jp\/product\/NEOBK-2635186", + "ebj": "https:\/\/ebookjapan.yahoo.co.jp\/books\/568854", + "mal": "121597", + "raw": "https:\/\/shonenjumpplus.com\/episode\/10834108156684177150", + "engtl": "https:\/\/mangaplus.shueisha.co.jp\/titles\/100116" + }, + "originalLanguage": "ja", + "lastVolume": "14", + "lastChapter": "119", + "publicationDemographic": "shounen", + "status": "completed", + "year": 2019, + "contentRating": "suggestive", + "tags": [ + { + "id": "423e2eae-a7a2-4a8b-ac03-a8351462d71d", + "type": "tag", + "attributes": { + "name": { "en": "Romance" }, + "description": {}, + "group": "genre", + "version": 1 + }, + "relationships": [] + }, + { + "id": "4d32cc48-9f00-4cca-9b5a-a839f0764984", + "type": "tag", + "attributes": { + "name": { "en": "Comedy" }, + "description": {}, + "group": "genre", + "version": 1 + }, + "relationships": [] + }, + { + "id": "aafb99c1-7f60-43fa-b75f-fc9502ce29c7", + "type": "tag", + "attributes": { + "name": { "en": "Harem" }, + "description": {}, + "group": "theme", + "version": 1 + }, + "relationships": [] + }, + { + "id": "b9af3a63-f058-46de-a9a0-e0c13906197a", + "type": "tag", + "attributes": { + "name": { "en": "Drama" }, + "description": {}, + "group": "genre", + "version": 1 + }, + "relationships": [] + }, + { + "id": "caaa44eb-cd40-4177-b930-79d3ef2afe87", + "type": "tag", + "attributes": { + "name": { "en": "School Life" }, + "description": {}, + "group": "theme", + "version": 1 + }, + "relationships": [] + }, + { + "id": "e5301a23-ebd9-49dd-a0cb-2add944c7fe9", + "type": "tag", + "attributes": { + "name": { "en": "Slice of Life" }, + "description": {}, + "group": "genre", + "version": 1 + }, + "relationships": [] + }, + { + "id": "fad12b5e-68ba-460e-b933-9ae8318f5b65", + "type": "tag", + "attributes": { + "name": { "en": "Gyaru" }, + "description": {}, + "group": "theme", + "version": 1 + }, + "relationships": [] + } + ], + "state": "published", + "chapterNumbersResetOnNewVolume": false, + "createdAt": "2019-10-12T20:49:23+00:00", + "updatedAt": "2025-02-01T22:40:38+00:00", + "version": 50, + "availableTranslatedLanguages": [ "es", "ru", "en" ], + "latestUploadedChapter": "b0950d10-7e2d-4278-98bd-52f3d92cc494" + }, + "relationships": [ + { + "id": "b77fe548-6f64-4380-8cca-faee8891a7d3", + "type": "author" + }, + { + "id": "b77fe548-6f64-4380-8cca-faee8891a7d3", + "type": "artist" + }, + { + "id": "a78a2332-99cf-42b3-8285-0eed22c41251", + "type": "cover_art", + "attributes": { + "description": "", + "volume": "14", + "fileName": "2f11791d-e3ff-4347-b4a8-b39aafc3b121.jpg", + "locale": "ja", + "createdAt": "2024-10-31T17:30:22+00:00", + "updatedAt": "2024-10-31T17:30:22+00:00", + "version": 1 + } + } + ] + }, + { + "id": "a920060c-7e39-4ac1-980c-f0e605a40ae4", + "type": "manga", + "attributes": { + "title": { "en": "Gal Yome no Himitsu" }, + "altTitles": [ + { "ja": "\u30ae\u30e3\u30eb\u5ac1\u306e\u79d8\u5bc6" }, + { "ja-ro": "Gyaru Yome no Himitsu" }, + { "en": "Secrets of the Gal Wife" }, + { "id": "Rahasia Istri Gal" }, + { "en": "My Gyaru Wife's Secret" } + ], + "description": { + "en": "Fuyuki is a beautiful and cool gal! But there's a secret side of her that she only shows in front of her husband...?", + "ja": "\u51ac\u96ea\uff08\u3075\u3086\u304d\uff09\u306f\u7f8e\u4eba\u3067\u30af\u30fc\u30eb\u306a\u30ae\u30e3\u30eb\uff01\u3067\u3082\u300c\u65e6\u90a3\u300d\u306e\u524d\u3060\u3051\u898b\u305b\u308b\u79d8\u5bc6\u306e\u59ff\u304c\u3042\u3063\u3066\u2026\u2026\uff1fSNS\u7d2f\u8a0837\u4e07\u3044\u3044\u306d\uff01\u306e\u53ef\u611b\u3044\u300c\u30ae\u30e3\u30eb\u5ac1\u300d\u3068\u306e\u30e9\u30d6\u30b3\u30e1\u30c7\u30a3\uff01", + "es-la": "\u00a1Fuyuki es una hermosa y atractiva gal! \u00bfPero hay una parte de ella que solo le muestra a su esposo y mantiene en secreto del resto? Una linda comedia rom\u00e1ntica con una esposa que es una gal.", + "pt-br": "Fuyuki \u00e9 uma linda gyaru! Mas tem um lado secreto que ela s\u00f3 mostra para o seu marido? Uma com\u00e9dia rom\u00e2ntica fofa com uma esposa gyaru!" + }, + "isLocked": false, + "links": { + "al": "169734", + "ap": "gyaru-yome-no-himitsu", + "bw": "series\/495151\/list", + "kt": "69730", + "mu": "sjl40n9", + "amz": "https:\/\/www.amazon.co.jp\/dp\/B0DMT6X4NC", + "cdj": "https:\/\/www.cdjapan.co.jp\/product\/NEOBK-3016982", + "ebj": "https:\/\/ebookjapan.yahoo.co.jp\/books\/857988", + "mal": "163267", + "raw": "https:\/\/ganma.jp\/galyome" + }, + "originalLanguage": "ja", + "lastVolume": "", + "lastChapter": "", + "publicationDemographic": "seinen", + "status": "ongoing", + "year": 2023, + "contentRating": "suggestive", + "tags": [ + { + "id": "423e2eae-a7a2-4a8b-ac03-a8351462d71d", + "type": "tag", + "attributes": { + "name": { "en": "Romance" }, + "description": {}, + "group": "genre", + "version": 1 + }, + "relationships": [] + }, + { + "id": "4d32cc48-9f00-4cca-9b5a-a839f0764984", + "type": "tag", + "attributes": { + "name": { "en": "Comedy" }, + "description": {}, + "group": "genre", + "version": 1 + }, + "relationships": [] + }, + { + "id": "92d6d951-ca5e-429c-ac78-451071cbf064", + "type": "tag", + "attributes": { + "name": { "en": "Office Workers" }, + "description": {}, + "group": "theme", + "version": 1 + }, + "relationships": [] + }, + { + "id": "e5301a23-ebd9-49dd-a0cb-2add944c7fe9", + "type": "tag", + "attributes": { + "name": { "en": "Slice of Life" }, + "description": {}, + "group": "genre", + "version": 1 + }, + "relationships": [] + }, + { + "id": "fad12b5e-68ba-460e-b933-9ae8318f5b65", + "type": "tag", + "attributes": { + "name": { "en": "Gyaru" }, + "description": {}, + "group": "theme", + "version": 1 + }, + "relationships": [] + } + ], + "state": "published", + "chapterNumbersResetOnNewVolume": false, + "createdAt": "2023-09-30T01:44:37+00:00", + "updatedAt": "2025-01-27T20:50:36+00:00", + "version": 24, + "availableTranslatedLanguages": [ "en", "pt-br", "th", "es-la", "id", "it", "vi", "ru" ], + "latestUploadedChapter": "8f5be93c-b5fa-43ee-8e29-41599a5d9bb5" + }, + "relationships": [ + { + "id": "5c66247f-85bb-4fac-8c08-c207f5ec53ce", + "type": "author" + }, + { + "id": "5c66247f-85bb-4fac-8c08-c207f5ec53ce", + "type": "artist" + }, + { + "id": "e8a74e9c-bd63-4424-a1d2-02a9d7286ab5", + "type": "cover_art", + "attributes": { + "description": "", + "volume": "2", + "fileName": "07d02b26-cbd0-4323-8774-9d83579863d5.jpg", + "locale": "ja", + "createdAt": "2025-02-18T09:27:22+00:00", + "updatedAt": "2025-02-18T09:27:22+00:00", + "version": 1 + } + }, + { + "id": "45220025-46fb-44c5-a975-a4754fe512a2", + "type": "creator" + } + ] + }, + { + "id": "cf7b7869-3d9a-4c4d-bd06-249eba113558", + "type": "manga", + "attributes": { + "title": { "en": "Boku to Gal ga Fufu ni Narumade" }, + "altTitles": [ + { "ja": "\u50d5\u3068\u541b\u304c\u592b\u5a66\u306b\u306a\u308b\u307e\u3067" }, + { "ja-ro": "Boku to Gal ga Fuufu ni Naru made" }, + { "en": "Until the Gal and I Become a Married Couple" }, + { "ru": "\u041f\u043e\u043a\u0430 \u043c\u044b \u0441 \u0434\u0435\u0432\u0443\u0448\u043a\u043e\u0439 \u043d\u0435 \u0441\u0442\u0430\u043d\u0435\u043c \u0441\u0443\u043f\u0440\u0443\u0436\u0435\u0441\u043a\u043e\u0439 \u043f\u0430\u0440\u043e\u0439" } + ], + "description": { + "en": "Saku Kanakura was a boy born into a poor household. He studied hard and managed to pass the exam to an excellent high school but, it soon turned out that his parents were in debt! When it was time to pay the debt off, a mysterious gal appeared and offered to pay off their debt. A big burden was almost off Kanakura's head, but the gal made a condition in return which stated that\u2014", + "id": "Saku Kanakura adalah anak laki-laki yang lahir dari keluarga miskin. Dia belajar dengan giat dan berhasil lulus ujian ke sekolah menengah yang bagus, tetapi ternyata orang tuanya terlilit hutang! Ketika tiba waktunya untuk melunasi hutang, seorang gadis misterius muncul dan menawarkan untuk melunasi hutang mereka. Sebuah beban besar hampir terlepas dari kepala Kanakura, tetapi gadis itu membuat syarat sebagai balasannya yang menyatakan bahwa\u2014", + "ja": "\u8ca7\u4e4f\u306a\u5bb6\u5ead\u306b\u80b2\u3063\u305f\u5c11\u5e74\u30fb\u795e\u9577\u5009\u98af\u7a7a\u3002\u731b\u52c9\u5f37\u306e\u7532\u6590\u3042\u3063\u3066\u898b\u4e8b\u5e0c\u671b\u3059\u308b\u9ad8\u6821\u3078\u5408\u683c\u3057\u305f\u5f7c\u3060\u3063\u305f\u304c\u3001\u4e21\u89aa\u306e\u501f\u91d1\u304c\u5224\u660e\uff01 \u501f\u91d1\u3092\u53d6\u308a\u7acb\u3066\u3089\u308c\u3066\u3044\u305f\u3068\u3053\u308d\u3001\u8b0e\u306e\u30ae\u30e3\u30eb\u304c\u73fe\u308c\u3066\u305d\u308c\u3089\u3092\u8fd4\u6e08\u3057\u3066\u304f\u308c\u308b\u3053\u3068\u306b\uff01 \u7aae\u5730\u3092\u8131\u3057\u305f\u795e\u9577\u5009\u3060\u3063\u305f\u304c\u3001\u30ae\u30e3\u30eb\u304c\u501f\u91d1\u3092\u80a9\u4ee3\u308f\u308a\u3059\u308b\u969b\u306b\u5f7c\u306b\u51fa\u3057\u305f\u6761\u4ef6\u306f\u2015\u2015\u3002" + }, + "isLocked": false, + "links": { + "al": "159308", + "ap": "boku-to-gal-ga-fuufu-ni-naru-made", + "bw": "series\/431780\/list", + "kt": "boku-to-gal-ga-fuufu-ni-naru-made", + "mu": "tobfmjc", + "amz": "https:\/\/www.amazon.co.jp\/dp\/B0CHMJMTNY", + "ebj": "https:\/\/ebookjapan.yahoo.co.jp\/books\/781303\/", + "mal": "154486", + "raw": "https:\/\/comic-walker.com\/detail\/KC_001788_S" + }, + "originalLanguage": "ja", + "lastVolume": "", + "lastChapter": "", + "publicationDemographic": "shounen", + "status": "ongoing", + "year": 2022, + "contentRating": "suggestive", + "tags": [ + { + "id": "423e2eae-a7a2-4a8b-ac03-a8351462d71d", + "type": "tag", + "attributes": { + "name": { "en": "Romance" }, + "description": {}, + "group": "genre", + "version": 1 + }, + "relationships": [] + }, + { + "id": "4d32cc48-9f00-4cca-9b5a-a839f0764984", + "type": "tag", + "attributes": { + "name": { "en": "Comedy" }, + "description": {}, + "group": "genre", + "version": 1 + }, + "relationships": [] + }, + { + "id": "caaa44eb-cd40-4177-b930-79d3ef2afe87", + "type": "tag", + "attributes": { + "name": { "en": "School Life" }, + "description": {}, + "group": "theme", + "version": 1 + }, + "relationships": [] + }, + { + "id": "e5301a23-ebd9-49dd-a0cb-2add944c7fe9", + "type": "tag", + "attributes": { + "name": { "en": "Slice of Life" }, + "description": {}, + "group": "genre", + "version": 1 + }, + "relationships": [] + }, + { + "id": "fad12b5e-68ba-460e-b933-9ae8318f5b65", + "type": "tag", + "attributes": { + "name": { "en": "Gyaru" }, + "description": {}, + "group": "theme", + "version": 1 + }, + "relationships": [] + } + ], + "state": "published", + "chapterNumbersResetOnNewVolume": false, + "createdAt": "2022-12-25T17:05:32+00:00", + "updatedAt": "2024-11-02T18:34:30+00:00", + "version": 28, + "availableTranslatedLanguages": [ "ru", "en", "es-la", "pt-br", "id", "tr", "vi" ], + "latestUploadedChapter": "37170d3e-108c-4c5e-b287-ef2394d7a3e8" + }, + "relationships": [ + { + "id": "08ee3ef5-2878-46d4-9040-2aef23fabf74", + "type": "author" + }, + { + "id": "08ee3ef5-2878-46d4-9040-2aef23fabf74", + "type": "artist" + }, + { + "id": "4243e338-6306-4d6a-a6cc-b1fdcb30c7cb", + "type": "cover_art", + "attributes": { + "description": "", + "volume": "2", + "fileName": "5b978b74-b18f-4039-a972-fa51d15e5d12.jpg", + "locale": "ja", + "createdAt": "2024-09-21T10:34:15+00:00", + "updatedAt": "2024-09-21T10:34:15+00:00", + "version": 1 + } + }, + { + "id": "e7dac780-4b88-4e0d-ac14-aa3b3a7f08a6", + "type": "creator" + } + ] + } + ], + "limit": 5, + "offset": 0, + "total": 363 +} \ No newline at end of file diff --git a/MangaReader.Tests/Sources/MangaDex/Api/MangaDexClientTests.cs b/MangaReader.Tests/Sources/MangaDex/Api/MangaDexClientTests.cs index 0beaefa..5e1ac47 100644 --- a/MangaReader.Tests/Sources/MangaDex/Api/MangaDexClientTests.cs +++ b/MangaReader.Tests/Sources/MangaDex/Api/MangaDexClientTests.cs @@ -70,6 +70,68 @@ public class MangaDexClientTests coverArtEntity.Attributes.FileName.ShouldBe("6b3073de-bb65-4723-8113-6068bf8c6eb4.jpg"); } + [Fact] + public async Task Search_Manga_2() + { + string searchResultJson = await ReadJsonResourceAsync("Manga-Search-Response-2.json"); + + IHttpService httpService = Substitute.For(); + + httpService.GetStringAsync(Arg.Any(), CancellationToken.None) + .Returns(Task.FromResult(searchResultJson)); + + MangaDexClient mangaDexClient = new(httpService); + MangaDexResponse? mangaDexResponse = await mangaDexClient.SearchMangaByTitleAsync("Some random text", CancellationToken.None); + + // Testing here + mangaDexResponse.ShouldNotBeNull(); + mangaDexResponse.Response.ShouldBe("collection"); + mangaDexResponse.ShouldBeOfType(); + + MangaDexCollectionResponse mangaDexCollectionResponse = (mangaDexResponse as MangaDexCollectionResponse)!; + mangaDexCollectionResponse.Data.Count.ShouldBe(5); + + mangaDexCollectionResponse.Data[3].ShouldBeOfType(); + MangaEntity mangaEntity = (mangaDexCollectionResponse.Data[3] as MangaEntity)!; + + mangaEntity.Attributes.ShouldNotBeNull(); + + mangaEntity.Attributes.Title.ShouldContainKey("en"); + mangaEntity.Attributes.Title["en"].ShouldBe("Gal Yome no Himitsu"); + + mangaEntity.Attributes.Description.ShouldContainKey("en"); + mangaEntity.Attributes.Description["en"].ShouldBe("Fuyuki is a beautiful and cool gal! But there's a secret side of her that she only shows in front of her husband...?"); + + mangaEntity.Attributes.Tags.Count.ShouldBe(5); + + //mangaEntity.Attributes.Tags[0].Attributes.ShouldNotBeNull(); + //mangaEntity.Attributes.Tags[0].Attributes!.Name.ShouldContainKey("en"); + //mangaEntity.Attributes.Tags[0].Attributes!.Name["en"].ShouldBe("Romance"); + + //mangaEntity.Attributes.Tags[1].Attributes.ShouldNotBeNull(); + //mangaEntity.Attributes.Tags[1].Attributes!.Name.ShouldContainKey("en"); + //mangaEntity.Attributes.Tags[1].Attributes!.Name["en"].ShouldBe("Comedy"); + + //mangaEntity.Attributes.Tags[2].Attributes.ShouldNotBeNull(); + //mangaEntity.Attributes.Tags[2].Attributes!.Name.ShouldContainKey("en"); + //mangaEntity.Attributes.Tags[2].Attributes!.Name["en"].ShouldBe("School Life"); + + //mangaEntity.Attributes.Tags[3].Attributes.ShouldNotBeNull(); + //mangaEntity.Attributes.Tags[3].Attributes!.Name.ShouldContainKey("en"); + //mangaEntity.Attributes.Tags[3].Attributes!.Name["en"].ShouldBe("Slice of Life"); + + //mangaEntity.Attributes.Tags[4].Attributes.ShouldNotBeNull(); + //mangaEntity.Attributes.Tags[4].Attributes!.Name.ShouldContainKey("en"); + //mangaEntity.Attributes.Tags[4].Attributes!.Name["en"].ShouldBe("Gyaru"); + + mangaEntity.Relationships.Count.ShouldBe(4); + mangaEntity.Relationships[2].ShouldBeOfType(); + + CoverArtEntity coverArtEntity = (mangaEntity.Relationships[2] as CoverArtEntity)!; + coverArtEntity.Attributes.ShouldNotBeNull(); + coverArtEntity.Attributes.FileName.ShouldBe("07d02b26-cbd0-4323-8774-9d83579863d5.jpg"); + } + [Fact] public async Task Get_Manga_Metadata() { diff --git a/MangaReader.WinUI/App.xaml b/MangaReader.WinUI/App.xaml new file mode 100644 index 0000000..66a08fe --- /dev/null +++ b/MangaReader.WinUI/App.xaml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + diff --git a/MangaReader.WinUI/App.xaml.cs b/MangaReader.WinUI/App.xaml.cs new file mode 100644 index 0000000..7f2b68f --- /dev/null +++ b/MangaReader.WinUI/App.xaml.cs @@ -0,0 +1,37 @@ +using MangaReader.WinUI.ViewModels; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.UI.Xaml; +using System; + +namespace MangaReader.WinUI; + +public partial class App : Application +{ + private Window? _window; + + public static IServiceProvider ServiceProvider { get; private set; } + + static App() + { + ServiceCollection services = new(); + + services.AddSingleton(); + + services.AddSingleton(); + + services.AddMangaReader(); + + ServiceProvider = services.BuildServiceProvider(); + } + + public App() + { + InitializeComponent(); + } + + protected override void OnLaunched(LaunchActivatedEventArgs args) + { + _window = ServiceProvider.GetRequiredService(); + _window.Activate(); + } +} \ No newline at end of file diff --git a/MangaReader.WinUI/Assets/Fonts/Poppins-Medium.otf b/MangaReader.WinUI/Assets/Fonts/Poppins-Medium.otf new file mode 100644 index 0000000..49e7b6b Binary files /dev/null and b/MangaReader.WinUI/Assets/Fonts/Poppins-Medium.otf differ diff --git a/MangaReader.WinUI/Assets/Fonts/Poppins-Regular.otf b/MangaReader.WinUI/Assets/Fonts/Poppins-Regular.otf new file mode 100644 index 0000000..e5c4eee Binary files /dev/null and b/MangaReader.WinUI/Assets/Fonts/Poppins-Regular.otf differ diff --git a/MangaReader.WinUI/Assets/Fonts/Poppins-SemiBold.otf b/MangaReader.WinUI/Assets/Fonts/Poppins-SemiBold.otf new file mode 100644 index 0000000..fcd0845 Binary files /dev/null and b/MangaReader.WinUI/Assets/Fonts/Poppins-SemiBold.otf differ diff --git a/MangaReader.WinUI/Assets/LockScreenLogo.scale-200.png b/MangaReader.WinUI/Assets/LockScreenLogo.scale-200.png new file mode 100644 index 0000000..7440f0d Binary files /dev/null and b/MangaReader.WinUI/Assets/LockScreenLogo.scale-200.png differ diff --git a/MangaReader.WinUI/Assets/SplashScreen.scale-200.png b/MangaReader.WinUI/Assets/SplashScreen.scale-200.png new file mode 100644 index 0000000..32f486a Binary files /dev/null and b/MangaReader.WinUI/Assets/SplashScreen.scale-200.png differ diff --git a/MangaReader.WinUI/Assets/Square150x150Logo.scale-200.png b/MangaReader.WinUI/Assets/Square150x150Logo.scale-200.png new file mode 100644 index 0000000..53ee377 Binary files /dev/null and b/MangaReader.WinUI/Assets/Square150x150Logo.scale-200.png differ diff --git a/MangaReader.WinUI/Assets/Square44x44Logo.scale-200.png b/MangaReader.WinUI/Assets/Square44x44Logo.scale-200.png new file mode 100644 index 0000000..f713bba Binary files /dev/null and b/MangaReader.WinUI/Assets/Square44x44Logo.scale-200.png differ diff --git a/MangaReader.WinUI/Assets/Square44x44Logo.targetsize-24_altform-unplated.png b/MangaReader.WinUI/Assets/Square44x44Logo.targetsize-24_altform-unplated.png new file mode 100644 index 0000000..dc9f5be Binary files /dev/null and b/MangaReader.WinUI/Assets/Square44x44Logo.targetsize-24_altform-unplated.png differ diff --git a/MangaReader.WinUI/Assets/StoreLogo.png b/MangaReader.WinUI/Assets/StoreLogo.png new file mode 100644 index 0000000..a4586f2 Binary files /dev/null and b/MangaReader.WinUI/Assets/StoreLogo.png differ diff --git a/MangaReader.WinUI/Assets/Wide310x150Logo.scale-200.png b/MangaReader.WinUI/Assets/Wide310x150Logo.scale-200.png new file mode 100644 index 0000000..8b4a5d0 Binary files /dev/null and b/MangaReader.WinUI/Assets/Wide310x150Logo.scale-200.png differ diff --git a/MangaReader.WinUI/MainWindow.xaml b/MangaReader.WinUI/MainWindow.xaml new file mode 100644 index 0000000..7c73399 --- /dev/null +++ b/MangaReader.WinUI/MainWindow.xaml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + diff --git a/MangaReader.WinUI/MainWindow.xaml.cs b/MangaReader.WinUI/MainWindow.xaml.cs new file mode 100644 index 0000000..71210d4 --- /dev/null +++ b/MangaReader.WinUI/MainWindow.xaml.cs @@ -0,0 +1,62 @@ +using MangaReader.Core.Search; +using MangaReader.Core.Sources.MangaDex.Api; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Controls.Primitives; +using Microsoft.UI.Xaml.Data; +using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Navigation; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using System.Threading; +using Windows.Foundation; +using Windows.Foundation.Collections; + +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. + +namespace MangaReader.WinUI +{ + /// + /// An empty window that can be used on its own or navigated to within a Frame. + /// + public sealed partial class MainWindow : Window + { + private readonly IMangaSearchCoordinator _mangaSearchCoordinator; + private readonly IMangaDexClient _mangaDexClient; + + private CancellationTokenSource? _cancellationTokenSource; + + public MainWindow(IMangaSearchCoordinator mangaSearchCoordinator, IMangaDexClient mangaDexClient) + { + InitializeComponent(); + + _mangaSearchCoordinator = mangaSearchCoordinator; + _mangaDexClient = mangaDexClient; + } + + //private async void Button_Click(object sender, RoutedEventArgs e) + //{ + // if (string.IsNullOrWhiteSpace(KeywordTextBox.Text)) + // return; + + // _cancellationTokenSource?.Cancel(); + // _cancellationTokenSource = new(); + + // var result = await _mangaSearchCoordinator.SearchAsync(KeywordTextBox.Text, _cancellationTokenSource.Token); + + // //Guid mangaGuid = new("a920060c-7e39-4ac1-980c-f0e605a40ae4"); + // //var coverArtResult = await _mangaDexClient.GetCoverArtAsync(mangaGuid, _cancellationTokenSource.Token); + + // // if ( (coverArtResult is MangaDexC) + // // { + + // // } + // // if (coverArtResult.) + //} + } +} diff --git a/MangaReader.WinUI/MangaReader.WinUI.csproj b/MangaReader.WinUI/MangaReader.WinUI.csproj new file mode 100644 index 0000000..ee7ff29 --- /dev/null +++ b/MangaReader.WinUI/MangaReader.WinUI.csproj @@ -0,0 +1,88 @@ + + + WinExe + net9.0-windows10.0.19041.0 + 10.0.17763.0 + MangaReader.WinUI + app.manifest + x86;x64;ARM64 + win-x86;win-x64;win-arm64 + win-$(Platform).pubxml + true + true + enable + None + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Designer + + + Designer + + + MSBuild:Compile + + + + + MSBuild:Compile + + + + + + true + + + + + False + True + False + True + + \ No newline at end of file diff --git a/MangaReader.WinUI/Package.appxmanifest b/MangaReader.WinUI/Package.appxmanifest new file mode 100644 index 0000000..3af069e --- /dev/null +++ b/MangaReader.WinUI/Package.appxmanifest @@ -0,0 +1,51 @@ + + + + + + + + + + MangaReader.WinUI + Brian + Assets\StoreLogo.png + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MangaReader.WinUI/Properties/launchSettings.json b/MangaReader.WinUI/Properties/launchSettings.json new file mode 100644 index 0000000..5c43d2a --- /dev/null +++ b/MangaReader.WinUI/Properties/launchSettings.json @@ -0,0 +1,10 @@ +{ + "profiles": { + "MangaReader.WinUI (Package)": { + "commandName": "MsixPackage" + }, + "MangaReader.WinUI (Unpackaged)": { + "commandName": "Project" + } + } +} \ No newline at end of file diff --git a/MangaReader.WinUI/Resources/Fonts.xaml b/MangaReader.WinUI/Resources/Fonts.xaml new file mode 100644 index 0000000..f2e2d19 --- /dev/null +++ b/MangaReader.WinUI/Resources/Fonts.xaml @@ -0,0 +1,11 @@ + + + + ms-appx:///Assets/Fonts/Poppins-Regular.ttf#Poppins Regular + ms-appx:///Assets/Fonts/Poppins-Medium.ttf#Poppins Medium + ms-appx:///Assets/Fonts/Poppins-SemiBold.ttf#Poppins SemiBold + + \ No newline at end of file diff --git a/MangaReader.WinUI/Resources/Styles.xaml b/MangaReader.WinUI/Resources/Styles.xaml new file mode 100644 index 0000000..748dd0c --- /dev/null +++ b/MangaReader.WinUI/Resources/Styles.xaml @@ -0,0 +1,13 @@ + + + + + + + + \ No newline at end of file diff --git a/MangaReader.WinUI/Resources/ViewModels.xaml b/MangaReader.WinUI/Resources/ViewModels.xaml new file mode 100644 index 0000000..ca1d796 --- /dev/null +++ b/MangaReader.WinUI/Resources/ViewModels.xaml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/MangaReader.WinUI/ViewModels/SearchViewModel.cs b/MangaReader.WinUI/ViewModels/SearchViewModel.cs new file mode 100644 index 0000000..d08c58b --- /dev/null +++ b/MangaReader.WinUI/ViewModels/SearchViewModel.cs @@ -0,0 +1,67 @@ +using CommunityToolkit.Mvvm.Input; +using MangaReader.Core.Search; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Input; + +namespace MangaReader.WinUI.ViewModels; + +public partial class SearchViewModel(IMangaSearchCoordinator searchCoordinator) : ViewModelBase +{ + private CancellationTokenSource? _cancellationTokenSource; + + private string? _keyword; + + public string? Keyword + { + get + { + return _keyword; + } + set + { + SetProperty(ref _keyword, value); + } + } + + private ObservableCollection _searchResults = []; + + public ObservableCollection SearchResults + { + get + { + return _searchResults; + } + set + { + SetProperty(ref _searchResults, value); + } + } + + public ICommand SearchCommand => new AsyncRelayCommand(SearchAsync); + + public async Task SearchAsync() + { + if (string.IsNullOrWhiteSpace(Keyword)) + return; + + _cancellationTokenSource?.Cancel(); + _cancellationTokenSource = new(); + + Dictionary result = await searchCoordinator.SearchAsync(Keyword, _cancellationTokenSource.Token); + + List searchResults = []; + + foreach (var item in result) + { + foreach (MangaSearchResult searchResult in item.Value) + { + searchResults.Add(searchResult); + } + } + + SearchResults = new(searchResults); + } +} \ No newline at end of file diff --git a/MangaReader.WinUI/ViewModels/ViewModelBase.cs b/MangaReader.WinUI/ViewModels/ViewModelBase.cs new file mode 100644 index 0000000..879dc81 --- /dev/null +++ b/MangaReader.WinUI/ViewModels/ViewModelBase.cs @@ -0,0 +1,8 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace MangaReader.WinUI.ViewModels; + +public partial class ViewModelBase : ObservableObject +{ + +} \ No newline at end of file diff --git a/MangaReader.WinUI/ViewModels/ViewModelLocator.cs b/MangaReader.WinUI/ViewModels/ViewModelLocator.cs new file mode 100644 index 0000000..c97793c --- /dev/null +++ b/MangaReader.WinUI/ViewModels/ViewModelLocator.cs @@ -0,0 +1,9 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace MangaReader.WinUI.ViewModels; + +public class ViewModelLocator +{ + public static SearchViewModel SearchViewModel + => App.ServiceProvider.GetRequiredService(); +} \ No newline at end of file diff --git a/MangaReader.WinUI/Views/SearchView.xaml b/MangaReader.WinUI/Views/SearchView.xaml new file mode 100644 index 0000000..27f0327 --- /dev/null +++ b/MangaReader.WinUI/Views/SearchView.xaml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MangaReader.WinUI/Views/SearchView.xaml.cs b/MangaReader.WinUI/Views/SearchView.xaml.cs new file mode 100644 index 0000000..fce05e8 --- /dev/null +++ b/MangaReader.WinUI/Views/SearchView.xaml.cs @@ -0,0 +1,12 @@ +using MangaReader.Core.Search; +using Microsoft.UI.Xaml.Controls; + +namespace MangaReader.WinUI.Views; + +public sealed partial class SearchView : UserControl +{ + public SearchView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/MangaReader.WinUI/app.manifest b/MangaReader.WinUI/app.manifest new file mode 100644 index 0000000..8f31fe4 --- /dev/null +++ b/MangaReader.WinUI/app.manifest @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + PerMonitorV2 + + + \ No newline at end of file diff --git a/MangaReader.sln b/MangaReader.sln index 65572d6..53c1c40 100644 --- a/MangaReader.sln +++ b/MangaReader.sln @@ -7,6 +7,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MangaReader.Core", "MangaRe EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MangaReader.Tests", "MangaReader.Tests\MangaReader.Tests.csproj", "{D86F1282-485A-4FF2-A75A-AB8102F3C853}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MangaReader.WinUI", "MangaReader.WinUI\MangaReader.WinUI.csproj", "{9B2AB426-6100-488A-B09E-EEAA3A3E7F06}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -21,6 +23,12 @@ Global {D86F1282-485A-4FF2-A75A-AB8102F3C853}.Debug|Any CPU.Build.0 = Debug|Any CPU {D86F1282-485A-4FF2-A75A-AB8102F3C853}.Release|Any CPU.ActiveCfg = Release|Any CPU {D86F1282-485A-4FF2-A75A-AB8102F3C853}.Release|Any CPU.Build.0 = Release|Any CPU + {9B2AB426-6100-488A-B09E-EEAA3A3E7F06}.Debug|Any CPU.ActiveCfg = Debug|x64 + {9B2AB426-6100-488A-B09E-EEAA3A3E7F06}.Debug|Any CPU.Build.0 = Debug|x64 + {9B2AB426-6100-488A-B09E-EEAA3A3E7F06}.Debug|Any CPU.Deploy.0 = Debug|x64 + {9B2AB426-6100-488A-B09E-EEAA3A3E7F06}.Release|Any CPU.ActiveCfg = Release|x64 + {9B2AB426-6100-488A-B09E-EEAA3A3E7F06}.Release|Any CPU.Build.0 = Release|x64 + {9B2AB426-6100-488A-B09E-EEAA3A3E7F06}.Release|Any CPU.Deploy.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE