Added UI app.

This commit is contained in:
2025-06-01 22:29:14 -04:00
parent 1348684144
commit 7dbcdc6169
36 changed files with 1645 additions and 25 deletions

View File

@@ -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<IMangaSearchProvider, NatoMangaSearchProvider>();
// Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:139.0) Gecko/20100101 Firefox/139.0
services.AddHttpClient<IHttpService, HttpService>(client =>
{
client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:139.0) Gecko/20100101 Firefox/139.0");
});
services.AddScoped<IHttpService, HttpService>();
//services.AddScoped<INatoMangaClient, NatoMangaClient>();
services.AddScoped<IMangaDexClient, MangaDexClient>();
//services.AddScoped<IMangaSearchProvider, NatoMangaSearchProvider>();
services.AddScoped<IMangaSearchProvider, MangaDexSearchProvider>();
services.AddScoped<IMangaSearchCoordinator, MangaSearchCoordinator>();
//services.AddScoped<IMangaMetadataProvider, NatoMangaWebCrawler>();
services.AddScoped<IMangaMetadataProvider, MangaDexMetadataProvider>();
return services;
}
}

View File

@@ -1,7 +1,16 @@
namespace MangaReader.Core.HttpService;
public class HttpService(HttpClient httpClient) : IHttpService
public class HttpService : IHttpService
{
public Task<string> GetStringAsync(string url, CancellationToken cancellationToken)
=> httpClient.GetStringAsync(url, cancellationToken);
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<string> GetStringAsync(string url, CancellationToken cancellationToken)
=> _httpClient.GetStringAsync(url, cancellationToken);
}

View File

@@ -9,6 +9,7 @@
<ItemGroup>
<PackageReference Include="HtmlAgilityPack" Version="1.12.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.5" />
</ItemGroup>
<ItemGroup>

View File

@@ -9,4 +9,5 @@ public interface IMangaDexClient
Task<MangaDexResponse?> GetFeedAsync(Guid mangaGuid, CancellationToken cancellationToken);
Task<MangaDexChapterResponse?> GetChapterAsync(Guid chapterGuid, CancellationToken cancellationToken);
Task<MangaDexResponse?> GetCoverArtAsync(Guid mangaGuid, CancellationToken cancellationToken);
Task<MangaDexResponse?> GetCoverArtAsync(Guid[] mangaGuid, CancellationToken cancellationToken);
}

View File

@@ -20,17 +20,28 @@ namespace MangaReader.Core.Sources.MangaDex.Api
}
private async Task<MangaDexResponse?> GetAsync(string url, CancellationToken cancellationToken)
{
//string response = await httpService.GetStringAsync(url, cancellationToken);
//return JsonSerializer.Deserialize<MangaDexResponse>(response, _jsonSerializerOptions);
return await GetAsync<MangaDexResponse>(url, cancellationToken);
}
private async Task<T?> GetAsync<T>(string url, CancellationToken cancellationToken)
{
string response = await httpService.GetStringAsync(url, cancellationToken);
return JsonSerializer.Deserialize<MangaDexResponse>(response, _jsonSerializerOptions);
return JsonSerializer.Deserialize<T?>(response, _jsonSerializerOptions);
}
public async Task<MangaDexResponse?> 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<MangaDexResponse?> SearchMangaByAuthorAsync(string author, CancellationToken cancellationToken)
@@ -64,15 +75,24 @@ namespace MangaReader.Core.Sources.MangaDex.Api
public async Task<MangaDexChapterResponse?> 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<MangaDexChapterResponse>(response, _jsonSerializerOptions);
//return JsonSerializer.Deserialize<MangaDexChapterResponse>(response, _jsonSerializerOptions);
return await GetAsync<MangaDexChapterResponse>($"https://api.mangadex.org/at-home/server/{chapterGuid}?forcePort443=false", cancellationToken);
}
public async Task<MangaDexResponse?> 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<MangaDexResponse?> 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);
}
}
}

View File

@@ -21,43 +21,83 @@ public partial class MangaDexSearchProvider(IMangaDexClient mangaDexClient) : IM
if (response == null || (response is not MangaDexCollectionResponse collectionResponse))
return [];
List<MangaSearchResult> mangaSearchResults = [];
MangaEntity[] mangaEntities = [.. collectionResponse.Data.Where(entity => entity is MangaEntity).Cast<MangaEntity>()];
foreach (MangaDexEntity entity in collectionResponse.Data)
if (mangaEntities.Length == 0)
return [];
Dictionary<Guid, List<CoverArtEntity>> mangaCoverArtMap = await GetCoverArtFileNamesAsync(mangaEntities, cancellationToken);
List<MangaSearchResult> mangaSearchResults = [];
Dictionary<Guid, MangaSearchResult> 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<Dictionary<Guid, List<CoverArtEntity>>> GetCoverArtFileNamesAsync(MangaEntity[] mangaEntities, CancellationToken cancellationToken)
{
Guid[] mangaGuids = [.. mangaEntities.Select(entity => entity.Id)];
return await GetCoverArtFileNamesAsync(mangaGuids, cancellationToken);
}
private async Task<Dictionary<Guid, List<CoverArtEntity>>> GetCoverArtFileNamesAsync(Guid[] mangaGuids, CancellationToken cancellationToken)
{
Dictionary<Guid, List<CoverArtEntity>> 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<CoverArtEntity>()];
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;
}
}

View File

@@ -12,6 +12,7 @@
<ItemGroup>
<None Remove="Sources\MangaDex\Api\Manga-Chapter-Response.json" />
<None Remove="Sources\MangaDex\Api\Manga-Cover-Art-Response.json" />
<None Remove="Sources\MangaDex\Api\Manga-Search-Response-2.json" />
<None Remove="WebCrawlers\Samples\MangaNato - Please Go Home, Akutsu-San!.htm" />
</ItemGroup>
@@ -27,6 +28,7 @@
<ItemGroup>
<EmbeddedResource Include="Sources\MangaDex\Api\Manga-Chapter-Response.json" />
<EmbeddedResource Include="Sources\MangaDex\Api\Manga-Cover-Art-Response.json" />
<EmbeddedResource Include="Sources\MangaDex\Api\Manga-Search-Response-2.json" />
<EmbeddedResource Include="Sources\MangaDex\Api\Manga-Search-Response.json" />
<EmbeddedResource Include="Sources\NatoManga\Api\Manga-Search-Response.json" />
<EmbeddedResource Include="Sources\MangaDex\Api\Manga-Feed-Response.json" />

View File

@@ -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
}

View File

@@ -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<IHttpService>();
httpService.GetStringAsync(Arg.Any<string>(), 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 mangaDexCollectionResponse = (mangaDexResponse as MangaDexCollectionResponse)!;
mangaDexCollectionResponse.Data.Count.ShouldBe(5);
mangaDexCollectionResponse.Data[3].ShouldBeOfType<MangaEntity>();
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 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()
{

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<Application
x:Class="MangaReader.WinUI.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:MangaReader.WinUI">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
<!-- Other merged dictionaries here -->
<ResourceDictionary Source="/Resources/Fonts.xaml"/>
<ResourceDictionary Source="/Resources/Styles.xaml"/>
<ResourceDictionary Source="/Resources/ViewModels.xaml"/>
</ResourceDictionary.MergedDictionaries>
<!-- Other app resources here -->
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@@ -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<MainWindow>();
services.AddSingleton<SearchViewModel>();
services.AddMangaReader();
ServiceProvider = services.BuildServiceProvider();
}
public App()
{
InitializeComponent();
}
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
_window = ServiceProvider.GetRequiredService<MainWindow>();
_window.Activate();
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<Window
x:Class="MangaReader.WinUI.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:MangaReader.WinUI"
xmlns:views="using:MangaReader.WinUI.Views"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Title="MangaReader.WinUI">
<Window.SystemBackdrop>
<MicaBackdrop />
</Window.SystemBackdrop>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<views:SearchView></views:SearchView>
<!--<StackPanel Orientation="Vertical">
<StackPanel Orientation="Horizontal" Spacing="10">
<TextBox Name="KeywordTextBox" Width="300"></TextBox>
<Button Content="Search" Click="Button_Click"></Button>
</StackPanel>
<Image Name="CoverImage" Width="256" Height="364" Source="https://mangadex.org/covers/a920060c-7e39-4ac1-980c-f0e605a40ae4/07d02b26-cbd0-4323-8774-9d83579863d5.jpg.256.jpg"></Image>
</StackPanel>-->
</Grid>
</Window>

View File

@@ -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
{
/// <summary>
/// An empty window that can be used on its own or navigated to within a Frame.
/// </summary>
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.)
//}
}
}

View File

@@ -0,0 +1,88 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net9.0-windows10.0.19041.0</TargetFramework>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<RootNamespace>MangaReader.WinUI</RootNamespace>
<ApplicationManifest>app.manifest</ApplicationManifest>
<Platforms>x86;x64;ARM64</Platforms>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<PublishProfile>win-$(Platform).pubxml</PublishProfile>
<UseWinUI>true</UseWinUI>
<EnableMsixTooling>true</EnableMsixTooling>
<Nullable>enable</Nullable>
<WindowsPackageType>None</WindowsPackageType>
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
</PropertyGroup>
<ItemGroup>
<None Remove="Assets\Fonts\Poppins-Medium.otf" />
<None Remove="Assets\Fonts\Poppins-Regular.otf" />
<None Remove="Assets\Fonts\Poppins-SemiBold.otf" />
<None Remove="Resources\ViewModels.xaml" />
</ItemGroup>
<ItemGroup>
<Content Include="Assets\SplashScreen.scale-200.png" />
<Content Include="Assets\LockScreenLogo.scale-200.png" />
<Content Include="Assets\Square150x150Logo.scale-200.png" />
<Content Include="Assets\Square44x44Logo.scale-200.png" />
<Content Include="Assets\Square44x44Logo.targetsize-24_altform-unplated.png" />
<Content Include="Assets\StoreLogo.png" />
<Content Include="Assets\Wide310x150Logo.scale-200.png" />
</ItemGroup>
<ItemGroup>
<Manifest Include="$(ApplicationManifest)" />
</ItemGroup>
<!--
Defining the "Msix" ProjectCapability here allows the Single-project MSIX Packaging
Tools extension to be activated for this project even if the Windows App SDK Nuget
package has not yet been restored.
-->
<ItemGroup Condition="'$(DisableMsixProjectCapabilityAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
<ProjectCapability Include="Msix" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="CommunityToolkit.WinUI.Media" Version="8.2.250402" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.4188" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.7.250513003" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MangaReader.Core\MangaReader.Core.csproj" />
</ItemGroup>
<ItemGroup>
<Page Update="Resources\Styles.xaml">
<SubType>Designer</SubType>
</Page>
<Page Update="Resources\Fonts.xaml">
<SubType>Designer</SubType>
</Page>
<Page Update="Views\SearchView.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Resources\ViewModels.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<!--
Defining the "HasPackageAndPublishMenuAddedByProject" property here allows the Solution
Explorer "Package and Publish" context menu entry to be enabled for this project even if
the Windows App SDK Nuget package has not yet been restored.
-->
<PropertyGroup Condition="'$(DisableHasPackageAndPublishMenuAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
<HasPackageAndPublishMenu>true</HasPackageAndPublishMenu>
</PropertyGroup>
<!-- Publish Properties -->
<PropertyGroup>
<PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun>
<PublishReadyToRun Condition="'$(Configuration)' != 'Debug'">True</PublishReadyToRun>
<PublishTrimmed Condition="'$(Configuration)' == 'Debug'">False</PublishTrimmed>
<PublishTrimmed Condition="'$(Configuration)' != 'Debug'">True</PublishTrimmed>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
IgnorableNamespaces="uap rescap">
<Identity
Name="cdba7326-1a2b-45f0-a233-463a21c91c91"
Publisher="CN=Brian"
Version="1.0.0.0" />
<mp:PhoneIdentity PhoneProductId="cdba7326-1a2b-45f0-a233-463a21c91c91" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
<Properties>
<DisplayName>MangaReader.WinUI</DisplayName>
<PublisherDisplayName>Brian</PublisherDisplayName>
<Logo>Assets\StoreLogo.png</Logo>
</Properties>
<Dependencies>
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
</Dependencies>
<Resources>
<Resource Language="x-generate"/>
</Resources>
<Applications>
<Application Id="App"
Executable="$targetnametoken$.exe"
EntryPoint="$targetentrypoint$">
<uap:VisualElements
DisplayName="MangaReader.WinUI"
Description="MangaReader.WinUI"
BackgroundColor="transparent"
Square150x150Logo="Assets\Square150x150Logo.png"
Square44x44Logo="Assets\Square44x44Logo.png">
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" />
<uap:SplashScreen Image="Assets\SplashScreen.png" />
</uap:VisualElements>
</Application>
</Applications>
<Capabilities>
<rescap:Capability Name="runFullTrust" />
</Capabilities>
</Package>

View File

@@ -0,0 +1,10 @@
{
"profiles": {
"MangaReader.WinUI (Package)": {
"commandName": "MsixPackage"
},
"MangaReader.WinUI (Unpackaged)": {
"commandName": "Project"
}
}
}

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:MangaReader.WinUI.ViewModels">
<FontFamily x:Key="PoppinsRegular">ms-appx:///Assets/Fonts/Poppins-Regular.ttf#Poppins Regular</FontFamily>
<FontFamily x:Key="PoppinsMedium">ms-appx:///Assets/Fonts/Poppins-Medium.ttf#Poppins Medium</FontFamily>
<FontFamily x:Key="PoppinsSemiBold">ms-appx:///Assets/Fonts/Poppins-SemiBold.ttf#Poppins SemiBold</FontFamily>
</ResourceDictionary>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Media="using:CommunityToolkit.WinUI.Media">
<Style TargetType="TextBlock">
<Setter Property="FontFamily" Value="{StaticResource PoppinsRegular}" />
</Style>
<Media:AttachedCardShadow x:Key="CommonShadow" Offset="5" BlurRadius="10" Opacity=".4" />
</ResourceDictionary>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:MangaReader.WinUI.ViewModels">
<vm:ViewModelLocator x:Key="Locator" />
</ResourceDictionary>

View File

@@ -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<MangaSearchResult> _searchResults = [];
public ObservableCollection<MangaSearchResult> 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<string, MangaSearchResult[]> result = await searchCoordinator.SearchAsync(Keyword, _cancellationTokenSource.Token);
List<MangaSearchResult> searchResults = [];
foreach (var item in result)
{
foreach (MangaSearchResult searchResult in item.Value)
{
searchResults.Add(searchResult);
}
}
SearchResults = new(searchResults);
}
}

View File

@@ -0,0 +1,8 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace MangaReader.WinUI.ViewModels;
public partial class ViewModelBase : ObservableObject
{
}

View File

@@ -0,0 +1,9 @@
using Microsoft.Extensions.DependencyInjection;
namespace MangaReader.WinUI.ViewModels;
public class ViewModelLocator
{
public static SearchViewModel SearchViewModel
=> App.ServiceProvider.GetRequiredService<SearchViewModel>();
}

View File

@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<UserControl
x:Class="MangaReader.WinUI.Views.SearchView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:MangaReader.WinUI.Views"
xmlns:vm="using:MangaReader.WinUI.ViewModels"
xmlns:search="using:MangaReader.Core.Search"
xmlns:UI="using:CommunityToolkit.WinUI"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
DataContext="{Binding Source={StaticResource Locator}, Path=SearchViewModel}"
d:DataContext="{d:DesignInstance Type=vm:SearchViewModel, IsDesignTimeCreatable=True}"
mc:Ignorable="d">
<UserControl.Resources>
<DataTemplate x:Key="MangaSearchResultTemplate" x:DataType="search:MangaSearchResult">
<Grid Padding="20" ColumnSpacing="20" MaxHeight="600" VerticalAlignment="Top" Background="{StaticResource CardBackgroundFillColorDefault}" CornerRadius="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Width="300">
<Grid VerticalAlignment="Top" HorizontalAlignment="Left" CornerRadius="8">
<Image Source="{x:Bind Thumbnail, Mode=OneWay}" MaxWidth="300" UI:Effects.Shadow="{StaticResource CommonShadow}"></Image>
<Canvas Background="#19000000"></Canvas>
</Grid>
</Border>
<Grid Grid.Column="1" RowSpacing="20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="{x:Bind Title}" FontSize="24" FontFamily="{StaticResource PoppinsSemiBold}" TextWrapping="Wrap"></TextBlock>
<ScrollViewer Grid.Row="1">
<TextBlock Text="{x:Bind Description}" Foreground="{StaticResource TextFillColorSecondaryBrush}" FontSize="16" TextWrapping="Wrap" LineStackingStrategy="BlockLineHeight" LineHeight="22"></TextBlock>
</ScrollViewer>
</Grid>
</Grid>
</DataTemplate>
</UserControl.Resources>
<Grid Padding="0" RowSpacing="20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Spacing="10" HorizontalAlignment="Center" Padding="20">
<TextBox Text="{Binding Keyword, Mode=TwoWay}" Width="300"></TextBox>
<Button Content="Search" Command="{Binding SearchCommand}"></Button>
</StackPanel>
<ScrollViewer Grid.Row="1" RenderTransformOrigin=".5,.5" Padding="50">
<ScrollViewer.RenderTransform>
<ScaleTransform ScaleX="1" ScaleY="1" />
</ScrollViewer.RenderTransform>
<ItemsRepeater ItemsSource="{Binding SearchResults, Mode=OneWay}" ItemTemplate="{StaticResource MangaSearchResultTemplate}">
<ItemsRepeater.Layout>
<UniformGridLayout MinRowSpacing="50" MinColumnSpacing="50" ItemsStretch="Fill" MinItemWidth="800"></UniformGridLayout>
</ItemsRepeater.Layout>
</ItemsRepeater>
</ScrollViewer>
</Grid>
</UserControl>

View File

@@ -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();
}
}

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="MangaReader.WinUI.app"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- The ID below informs the system that this application is compatible with OS features first introduced in Windows 10.
It is necessary to support features in unpackaged applications, for example the custom titlebar implementation.
For more info see https://docs.microsoft.com/windows/apps/windows-app-sdk/use-windows-app-sdk-run-time#declare-os-compatibility-in-your-application-manifest -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
</assembly>

View File

@@ -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