Front update

main
Ogoun 4 months ago
parent 112daccec9
commit f836c4963b

@ -6,17 +6,6 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<None Remove="web\css\fonts\Raleway-Italic-VariableFont_wght.ttf" />
<None Remove="web\css\fonts\Raleway-VariableFont_wght.ttf" />
<None Remove="web\css\fonts\Roboto-300.ttf" />
<None Remove="web\css\fonts\Roboto-300.woff" />
<None Remove="web\css\fonts\Roboto-300.woff2" />
<None Remove="web\css\fonts\Roboto-400.ttf" />
<None Remove="web\css\fonts\Roboto-400.woff" />
<None Remove="web\css\fonts\Roboto-400.woff2" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<Page Include="web\css\fonts\Raleway-Italic-VariableFont_wght.ttf" /> <Page Include="web\css\fonts\Raleway-Italic-VariableFont_wght.ttf" />
<Page Include="web\css\fonts\Raleway-VariableFont_wght.ttf" /> <Page Include="web\css\fonts\Raleway-VariableFont_wght.ttf" />
@ -49,37 +38,40 @@
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
<None Update="web\assets\cover.jpg"> <None Update="web\assets\cover.jpg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
<None Update="web\assets\heart.svg"> <None Update="web\assets\heart.svg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
<None Update="web\assets\heart_full.svg"> <None Update="web\assets\heart_full.svg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="web\assets\pencil.svg">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
<None Update="web\assets\search.svg"> <None Update="web\assets\search.svg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
<None Update="web\css\buki-common.css"> <None Update="web\css\buki-common.css">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
<None Update="web\css\common.css"> <None Update="web\css\common.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
<None Update="web\css\login.css"> <None Update="web\css\login.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
<None Update="web\css\logo.css"> <None Update="web\css\logo.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
<None Update="web\css\main.css"> <None Update="web\css\main.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
<None Update="web\css\normalize.css"> <None Update="web\css\normalize.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
<None Update="web\css\scroll.css"> <None Update="web\css\scroll.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
<None Update="web\fonts\Roboto-Black.ttf"> <None Update="web\fonts\Roboto-Black.ttf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
@ -133,25 +125,34 @@
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
<None Update="web\js\common\jquery.js"> <None Update="web\js\common\jquery.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="web\js\common\jquery.min.js">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="web\js\components\tags\index.js">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
<None Update="web\js\constants\index.js"> <None Update="web\js\constants\index.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
<None Update="web\js\jquery.js"> <None Update="web\js\jquery.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None> </None>
<None Update="web\js\login\index.js"> <None Update="web\js\login\index.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
<None Update="web\js\main\index.js"> <None Update="web\js\main\index.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
<None Update="web\js\requests\index.js"> <None Update="web\js\requests\index.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
<None Update="web\js\scroll\index.js"> <None Update="web\js\scroll\index.js">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="web\js\utils\index.js">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
<None Update="web\login.html"> <None Update="web\login.html">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>

@ -33,11 +33,39 @@ namespace BukiVedi.App.Controllers
/// Добавление тега для книги /// Добавление тега для книги
/// </summary> /// </summary>
/// <returns>Ok</returns> /// <returns>Ok</returns>
[HttpPost] /*[HttpPost]
public async Task<IActionResult> AppendTag([FromBody] AppendTagRequest request) public async Task<IActionResult> AppendTag([FromBody] AppendTagRequest request)
{ {
await _handler.AppendTag(request?.BookId!, request?.Name!, OperationContext); await _handler.AppendTag(request?.BookId!, request?.Name!, OperationContext);
return Ok(); return Ok();
}*/
/// <summary>
/// Добавление тегов для книги
/// </summary>
/// <returns>Ok</returns>
[HttpPost]
public async Task<ActionResult<IEnumerable<TagInfo>>> AppendTags([FromBody] AppendTagsRequest request)
{
var existsTags = (await _handler.GetBookTags(request.BookId, this.OperationContext))?.Select(t => t.Id)?.ToList();
var tags = new List<TagInfo>();
foreach (var name in request.Names)
{
var tag = await _handler.AppendTag(request.BookId, name, OperationContext);
if (tag != null && existsTags != null)
{
existsTags.Remove(tag.Id);
tags.Add(tag);
}
}
if (existsTags != null)
{
foreach (var id in existsTags)
{
await _handler.RemoveTag(id, OperationContext);
}
}
return Ok(tags);
} }
/// <summary> /// <summary>

@ -5,4 +5,10 @@
public string BookId { get; set; } public string BookId { get; set; }
public string Name { get; set; } public string Name { get; set; }
} }
public class AppendTagsRequest
{
public string BookId { get; set; }
public string[] Names { get; set; }
}
} }

@ -14,7 +14,7 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("BukiVedi.App")] [assembly: System.Reflection.AssemblyCompanyAttribute("BukiVedi.App")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+431f2503a837ccf39eac97076a865db39f86a8dd")] [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+112daccec93b21f74faf3a3b85b2f9a47b2ae81f")]
[assembly: System.Reflection.AssemblyProductAttribute("BukiVedi.App")] [assembly: System.Reflection.AssemblyProductAttribute("BukiVedi.App")]
[assembly: System.Reflection.AssemblyTitleAttribute("BukiVedi.App")] [assembly: System.Reflection.AssemblyTitleAttribute("BukiVedi.App")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]

@ -1 +1 @@
7f90fe300e225561cd7cc7726bf5d2f940d1cc60e07ec84b3e5f3a0ec108d1d7 a7e97588f3d72640975deb9345bc56ed8105e4a411d7365c01f74b565d536185

@ -82,3 +82,14 @@ G:\Documents\GitHub\BukiVedi\src\BukiVedi.App\bin\Debug\net8.0\web\fonts\Roboto-
G:\Documents\GitHub\BukiVedi\src\BukiVedi.App\bin\Debug\net8.0\web\fonts\Roboto-ThinItalic.ttf G:\Documents\GitHub\BukiVedi\src\BukiVedi.App\bin\Debug\net8.0\web\fonts\Roboto-ThinItalic.ttf
G:\Documents\GitHub\BukiVedi\src\BukiVedi.App\bin\Debug\net8.0\LemmaSharpPrebuilt.pdb G:\Documents\GitHub\BukiVedi\src\BukiVedi.App\bin\Debug\net8.0\LemmaSharpPrebuilt.pdb
G:\Documents\GitHub\BukiVedi\src\BukiVedi.App\bin\Debug\net8.0\LemmaSharpPrebuilt.dll.config G:\Documents\GitHub\BukiVedi\src\BukiVedi.App\bin\Debug\net8.0\LemmaSharpPrebuilt.dll.config
G:\Documents\GitHub\BukiVedi\src\BukiVedi.App\bin\Debug\net8.0\web\js\common\jquery.js
G:\Documents\GitHub\BukiVedi\src\BukiVedi.App\bin\Debug\net8.0\web\js\constants\index.js
G:\Documents\GitHub\BukiVedi\src\BukiVedi.App\bin\Debug\net8.0\web\js\login\index.js
G:\Documents\GitHub\BukiVedi\src\BukiVedi.App\bin\Debug\net8.0\web\js\main\index.js
G:\Documents\GitHub\BukiVedi\src\BukiVedi.App\bin\Debug\net8.0\web\js\requests\index.js
G:\Documents\GitHub\BukiVedi\src\BukiVedi.App\bin\Debug\net8.0\web\js\scroll\index.js
G:\Documents\GitHub\BukiVedi\src\BukiVedi.App\bin\Debug\net8.0\web\script.js
G:\Documents\GitHub\BukiVedi\src\BukiVedi.App\bin\Debug\net8.0\web\assets\pencil.svg
G:\Documents\GitHub\BukiVedi\src\BukiVedi.App\bin\Debug\net8.0\web\js\common\jquery.min.js
G:\Documents\GitHub\BukiVedi\src\BukiVedi.App\bin\Debug\net8.0\web\js\components\tags\index.js
G:\Documents\GitHub\BukiVedi\src\BukiVedi.App\bin\Debug\net8.0\web\js\utils\index.js

@ -0,0 +1,11 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg fill="#1d1c1e" height="15px" width="15px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 306.637 306.637" xml:space="preserve" stroke="#fb977e">
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
<g id="SVGRepo_iconCarrier"> <g> <g> <path d="M12.809,238.52L0,306.637l68.118-12.809l184.277-184.277l-55.309-55.309L12.809,238.52z M60.79,279.943l-41.992,7.896 l7.896-41.992L197.086,75.455l34.096,34.096L60.79,279.943z"/> <path d="M251.329,0l-41.507,41.507l55.308,55.308l41.507-41.507L251.329,0z M231.035,41.507l20.294-20.294l34.095,34.095 L265.13,75.602L231.035,41.507z"/> </g> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> <g> </g> </g> </g>
</svg>

After

Width:  |  Height:  |  Size: 977 B

@ -1,53 +1,53 @@
@font-face { @font-face {
font-family: Vedi; font-family: Vedi;
src: url(fonts/Raleway-VariableFont_wght.ttf) format(woff2); src: url(fonts/Raleway-VariableFont_wght.ttf) format(woff2);
font-weight: 400; font-weight: 400;
font-style: normal; font-style: normal;
font-stretch: normal; font-stretch: normal;
font-variant: normal; font-variant: normal;
} }
@font-face { @font-face {
font-family: VediItalic; font-family: VediItalic;
src: url(fonts/Raleway-Italic-VariableFont_wght.ttf) format(woff2); src: url(fonts/Raleway-Italic-VariableFont_wght.ttf) format(woff2);
font-weight: 400; font-weight: 400;
font-style: italic; font-style: italic;
font-variant: normal; font-variant: normal;
} }
body { body {
font-family: Vedi, Verdana, Tahoma; font-family: Vedi, Verdana, Tahoma;
font-size: 16px; font-size: 16px;
line-height: 21px; line-height: 21px;
margin: 0px; margin: 0px;
padding: 0px; padding: 0px;
width: 100%; width: 100%;
min-height: 100%; min-height: 100%;
height: 100%; height: 100%;
background-color: var(--main-bg-color); background-color: var(--main-bg-color);
} }
html, html,
body { body {
height: 100%; height: 100%;
} }
* { * {
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
input { input {
outline: none; outline: none;
} }
:root { :root {
--main-bg-color: #F9F9F9; --main-bg-color: #f9f9f9;
--main-font-color: #1D1C1E; --main-font-color: #1d1c1e;
--second-font-color: #bfbfbf; --second-font-color: #bfbfbf;
--active-color: #2173e0; --active-color: #2173e0;
--matte-white: #f2f3f4; --matte-white: #f2f3f4;
--accent-color: #FF748F; --accent-color: #ff748f;
} }
/*** /***
@ -63,116 +63,135 @@ input {
Other margins are not welcomed Other margins are not welcomed
**/ **/
.mt-s { .mt-s {
margin-top: 12px; margin-top: 12px;
}
.mv-xs {
margin-top: 6px;
margin-bottom: 6px;
}
.mv-s {
margin-top: 12px;
margin-bottom: 12px;
} }
.mt-m { .mt-m {
margin-top: 20px; margin-top: 20px;
} }
.mb-m { .mb-m {
margin-bottom: 20px; margin-bottom: 20px;
} }
.mt-l { .mt-l {
margin-top: 40px; margin-top: 40px;
} }
.mh-auto { .mh-auto {
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
} }
.ml-s { .ml-s {
margin-left: 12px !important; margin-left: 12px !important;
} }
.ml-m { .ml-m {
margin-left: 20px !important; margin-left: 20px !important;
} }
.mr-m { .mr-m {
margin-right: 20px !important; margin-right: 20px !important;
} }
.pl-l { .pl-l {
padding-left: 40px; padding-left: 40px;
} }
.pl-m { .pl-m {
padding-left: 20px; padding-left: 20px;
} }
.pl-s { .pl-s {
padding-left: 12px; padding-left: 12px;
} }
.pl-xs { .pl-xs {
padding-left: 6px; padding-left: 6px;
} }
.pr-s { .pr-s {
padding-right: 12px; padding-right: 12px;
} }
.ph-s { .ph-s {
padding-left: 12px; padding-left: 12px;
padding-right: 12px; padding-right: 12px;
} }
/**
* display forms
**/
/****/
.d-fl { .d-fl {
display: flex; display: flex;
} }
.d-bl { .d-bl {
display: block; display: block;
}
.w-100 {
width: 100%;
}
.hidden {
visibility: hidden;
} }
/** /**
headings and texts headings and texts
*/ */
.h-2 { .h-2 {
font-size: 25px; font-size: 25px;
line-height: 35px; line-height: 35px;
} }
.text-c { .text-c {
text-align: center; text-align: center;
} }
.second-text { .second-text {
font-size: 14px; font-size: 14px;
line-height: 16px; line-height: 16px;
} }
.no-wrap { .no-wrap {
white-space: nowrap; white-space: nowrap;
} }
/** spinner*/ /** spinner*/
@keyframes spinner { @keyframes spinner {
to { to {
transform: rotate(360deg); transform: rotate(360deg);
} }
} }
.spinner:before { .spinner:before {
content: ''; content: "";
box-sizing: border-box; box-sizing: border-box;
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 50%; left: 50%;
width: 50px; width: 50px;
height: 50px; height: 50px;
margin-top: -15px; margin-top: -15px;
margin-left: -15px; margin-left: -15px;
border-radius: 50%; border-radius: 50%;
border: 2px solid #ccc; border: 2px solid #ccc;
border-top-color: var(--active-color); border-top-color: var(--active-color);
animation: spinner .6s linear infinite; animation: spinner 0.6s linear infinite;
z-index: 1; z-index: 1;
} }

@ -137,6 +137,11 @@ header {
padding: 20px 40px; padding: 20px 40px;
} }
.main-book__card.card_closed {
max-height: 450px;
overflow-y: hidden;
}
.no-book__card { .no-book__card {
position: relative; position: relative;
border-radius: 14px; border-radius: 14px;
@ -160,15 +165,15 @@ header {
} }
.main-book__description { .main-book__description {
width: 100%;
max-width: 615px; max-width: 615px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.main-book__body { .main-book__body {
width: 100%;
max-width: 600px; max-width: 600px;
max-height: 300px;
overflow-y: auto;
flex: 1 0 auto; flex: 1 0 auto;
padding-right: 17px; padding-right: 17px;
text-align: justify; text-align: justify;
@ -176,6 +181,35 @@ header {
scrollbar-width: thin; scrollbar-width: thin;
} }
.main-book__body.body_closed {
max-height: 300px;
overflow: hidden;
}
.main-book__body:not(.body_closed) {
animation: 0.8s slideDown1;
}
@keyframes slideDown1 {
0% {
transform: translateY(-2%);
}
100% {
transform: translateY(0%);
}
}
@-webkit-keyframes slideDown1 {
0% {
-webkit-transform: translateY(-2%);
}
100% {
-webkit-transform: translateY(0%);
}
}
.main-book__body::-webkit-scrollbar-track { .main-book__body::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.1); -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.1);
background-color: #fff; background-color: #fff;
@ -197,6 +231,10 @@ header {
padding-right: 65px; padding-right: 65px;
} }
.main-card__author {
font-size: 13px;
}
.main-book__menu { .main-book__menu {
position: absolute; position: absolute;
top: 20px; top: 20px;
@ -240,7 +278,6 @@ header {
box-shadow: 0px 10px 20px 2px rgba(0, 0, 0, 0.25); box-shadow: 0px 10px 20px 2px rgba(0, 0, 0, 0.25);
} }
.main-book__menu_closed { .main-book__menu_closed {
width: 50px; width: 50px;
} }
@ -255,11 +292,11 @@ header {
} }
.main-book__menu_like { .main-book__menu_like {
background-image: url(../web/assets/heart.svg); background-image: url(../assets/heart.svg);
background-repeat: no-repeat; background-repeat: no-repeat;
position: absolute; position: absolute;
width: 20px; width: 20px;
height: 100%; height: 20px;
right: 105px; right: 105px;
top: 20px; top: 20px;
cursor: pointer; cursor: pointer;
@ -270,7 +307,7 @@ header {
} }
.main-book__menu_like.liked { .main-book__menu_like.liked {
background-image: url(../web/assets/heart_full.svg); background-image: url(../assets/heart_full.svg);
} }
.main-book__action { .main-book__action {
@ -297,17 +334,49 @@ header {
background: var(--main-bg-color); background: var(--main-bg-color);
} }
.main-book__footer { .main-book__center {
width: 100%; width: 100%;
flex-shrink: 0; flex-shrink: 0;
flex-wrap: wrap; flex-wrap: wrap;
} }
.main-book__tags_title {
position: relative;
width: 55px;
font-family: Vedi, Verdana, Tahoma;
font-size: 14px;
line-height: 24px;
font-weight: normal;
}
.main-book__tags_title:hover,
.main-book__tags_title:active {
cursor: pointer;
color: var(--active-color);
}
.pencil {
position: absolute;
top: 4px;
left: 40px;
background-image: url(../assets/pencil.svg);
background-repeat: no-repeat;
width: 20px;
height: 100%;
}
.main-book__tags_title:hover,
.main-book__tags_title:active .pencil {
filter: brightness(0) saturate(100%) invert(43%) sepia(34%) saturate(7336%)
hue-rotate(202deg) brightness(82%) contrast(97%);
}
.main-book__tag { .main-book__tag {
color: var(--active-color); color: var(--active-color);
margin-left: 6px; margin-left: 6px;
cursor: pointer; cursor: pointer;
font-size: 14px;
} }
.main-book__tag:hover { .main-book__tag:hover {
text-decoration: underline; text-decoration: underline;
opacity: 0.8; opacity: 0.8;
@ -321,3 +390,112 @@ header {
color: var(--active-color); color: var(--active-color);
opacity: 0.8; opacity: 0.8;
} }
.main-book__arrow_wrapper {
height: 20px;
}
.main-book__arrow_wrapper:before {
content: "";
display: block;
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 120px;
background-image: linear-gradient(
to bottom,
rgba(255, 255, 255, 0),
rgba(255, 2555, 255, 1)
);
}
.main-book__arrow {
cursor: pointer;
height: 50px;
left: 50%;
position: absolute;
z-index: 1;
bottom: 0;
transform: translateX(-50%) translateY(-50%);
transition: transform 0s;
width: 100px;
background-color: rgba(255, 255, 255, 0.4);
}
.main-book__arrow:hover,
.main-book__arrow:active {
.main-book__arrow-top,
.main-book__arrow-bottom {
background-color: var(--active-color);
}
}
.main-book__arrow-top,
.main-book__arrow-bottom {
background-color: var(--second-font-color);
height: 1px;
left: -5px;
position: absolute;
width: 50px;
}
.main-book__arrow-top:after,
.main-book__arrow-bottom:after {
background-color: #fff;
content: "";
height: 100%;
position: absolute;
top: 0;
transition: all 0.15s;
}
.main-book__arrow-top {
transform: rotate(20deg);
transform-origin: bottom right;
bottom: 5px;
}
.main-book__arrow-bottom {
transform: rotate(160deg);
transform-origin: top right;
bottom: 4px;
}
.main-book__arrow_wrapper .opened {
transform: rotate(180deg);
transform-origin: 25% 50%;
}
.main-book__arrow_wrapper.opened:before {
background-image: linear-gradient(
to bottom,
rgba(255, 255, 255, 0),
rgba(255, 2555, 255, 0)
);
bottom: 0;
}
.main-book__tags_wrapper {
width: 100%;
}
.main-book__textarea {
width: 100%;
height: 50px;
padding: 12px 20px;
box-sizing: border-box;
border: 1px solid #ccc;
border-radius: 4px;
background-color: #FFF;
color: var(--main-font-color);
font-size: 14px;
font-family: Vedi, Verdana, Tahoma;
resize: none;
outline: none;
}
.main-book__textarea:focus,
.main-book__textarea:active {
outline: none !important;
border: 1px solid var(--active-color);
}

@ -29,6 +29,7 @@
autofocus /> autofocus />
</div> </div>
<button id="searchButton" class="main-search-btn mt-m">Найти</button> <button id="searchButton" class="main-search-btn mt-m">Найти</button>
</div>
</header> </header>
<main> <main>
<div id="content" class="main-content mt-l"> <div id="content" class="main-content mt-l">

@ -0,0 +1,106 @@
import { waitForElement, changeTags } from "../../requests/index.js";
export default class TagsComponent {
element;
constructor({ tags = [], bookid = null, article }) {
this.tags = tags;
this.article = article;
this.bookid = bookid;
this.isTextareaMode = false;
this.render();
}
get template() {
return `
<h6 data-edit-tag class="main-book__tags_title mt-m">
Теги
<div class="pencil"></div>
</h6>
<div class="main-book__center main-book__tags d-fl mv-s">${this.getTags(
this.tags
)}</div>
`;
}
getTags(tags) {
return (tags || [])
.map(({ id, name }) => {
return `<a class="main-book__tag main-book__link" data-tag=${id}>#${name}</a>`;
})
.join("");
}
getTextarea(tags) {
return `
<textarea class="main-book__textarea" data-area>${(tags || [])
.map(({ name }) => name)
.join(" ")}</textarea>
`;
}
initialize() {
this.initEventListeners();
}
initEventListeners() {
const tagsEditorListener = this.article.querySelector(
".main-book__tags_title"
);
tagsEditorListener.addEventListener("click", () => {
const tagsWrapper = this.article.querySelector(".main-book__tags");
if (!this.isTextareaMode) {
tagsWrapper.innerHTML = this.getTextarea(this.tags);
this.article.querySelector(".main-book__textarea").focus();
this.isTextareaMode = true;
}
});
this.article.addEventListener("keypress", (event) => {
if (event.key === "Enter") {
if (this.isTextareaMode) {
this.closeTextarea();
}
}
});
document.addEventListener("outside-click", async () => {
if (this.isTextareaMode) {
this.closeTextarea();
}
});
}
async closeTextarea() {
const tagsWrapper = this.article.querySelector(".main-book__tags");
const area = this.article.querySelector(".main-book__textarea");
const tags = area.value
.split(" ")
.map((item) => item.trim())
.filter((i) => i);
if (!!(tags || []).length > 0) {
await this.setTags({ bookid: this.bookid, tags });
}
tagsWrapper.innerHTML = this.getTags(this.tags);
this.isTextareaMode = false;
}
async waitRendered() {
await waitForElement(".main-book__tags");
this.initialize();
}
async render() {
this.element = document.createElement("div");
this.element.innerHTML = this.template;
await this.waitRendered();
}
async setTags({ bookid, tags }) {
document.body.classList.add("blur", "spinner");
const newTags = await changeTags({ bookid, tags });
this.tags = newTags;
document.body.classList.remove("spinner");
}
}

@ -2,11 +2,6 @@ import * as requests from '../requests/index.js';
export const SUCCESS = "success"; export const SUCCESS = "success";
export const menuEnum = [ export const menuEnum = [
// {
// id: 0,
// label: "В избранное",
// action: requests.addBookToFavourites,
// },
{ {
id: 1, id: 1,
label: "Авторов в избранное", label: "Авторов в избранное",
@ -25,11 +20,7 @@ export const menuEnum = [
{ {
id: 4, id: 4,
label: "Игнорировать автора", label: "Игнорировать автора",
action: requests.ignoreAuthor, action: requests.ignoreAuthors,
},
{
id: 5,
label: "Теги",
}, },
{ {
id: 6, id: 6,
@ -40,3 +31,21 @@ export const menuEnum = [
label: "Поделиться", label: "Поделиться",
}, },
]; ];
export const DEFAULT_AUTHOR = {
id: 0,
name: 'Неизвестно'
};
export const likeEnum = [
{
id: 0,
label: "Нравится",
action: requests.addBookToFavourites,
},
{
id: 1,
label: "Не нравится",
action: requests.removeBookToFavourites,
}
]

@ -1,13 +1,22 @@
import { menuEnum } from "../constants/index.js"; import { menuEnum, likeEnum, DEFAULT_AUTHOR } from "../constants/index.js";
import { fetchData, downloadBook, searchByAuthor } from "../requests/index.js"; import {
fetchData,
downloadBook,
searchByAuthor,
searchByTag,
waitForElement,
} from "../requests/index.js";
import { stopPropagation } from "../utils/index.js";
import TagsComponent from "../components/tags/index.js";
export default class BookSection { export default class BookSection {
subElements = []; subElements = [];
element; element;
constructor({ url = "", label = "" } = {}) { constructor({ url = "" } = {}) {
this.url = url; this.url = url;
this.label = label; this.data = [];
this.render(); this.render();
} }
@ -29,11 +38,15 @@ export default class BookSection {
booksListener.addEventListener("click", (event) => { booksListener.addEventListener("click", (event) => {
let id, title; let id, title;
const isLike = event.target.closest("[data-like]"); const isLike = event.target.closest("[data-like]");
const isMenu = event.target.closest("[data-menu]"); const isMenu = event.target.closest("[data-menu]");
const isAction = event.target.closest("[data-action]"); const isAction = event.target.closest("[data-action]");
const isLink = event.target.closest("[data-link]"); const isLink = event.target.closest("[data-link]");
const isAuthor = event.target.closest("[data-author]"); const isAuthor = event.target.closest("[data-author]");
const isMoreDetails = event.target.closest("[data-details]");
const isTagWrapper = event.target.closest("[data-tags-wrapper]");
const isTag = event.target.closest("[data-tag]");
stopPropagation(event);
switch (true) { switch (true) {
case !!isAction: case !!isAction:
@ -62,6 +75,21 @@ export default class BookSection {
id = isAuthor.dataset.author; id = isAuthor.dataset.author;
this.update({ isByAuthor: true, id }); this.update({ isByAuthor: true, id });
break;
case !!isMoreDetails:
this.toggleDetails(isMoreDetails);
break;
case !isTagWrapper:
const myEvent = new CustomEvent("outside-click");
document.dispatchEvent(myEvent);
this.closeMenu();
break;
case !!isTag:
id = isTag.dataset.tag;
this.update({ isByTag: true, id });
default: default:
this.closeMenu(); this.closeMenu();
@ -75,30 +103,33 @@ export default class BookSection {
getBookBody(data) { getBookBody(data) {
return (data || []) return (data || [])
.map( .map(
({ (
id, {
authors, id,
description, authors,
format, description,
genres, format,
imageUrl, genres,
title, imageUrl,
series, title,
subseries, series,
year, subseries,
tags, year,
isFavorite, isFavorite,
}) => { },
const { name, id: authorid } = authors[0] || "Неизвестно"; index
) => {
const isLast = !!(index === data?.length - 1);
return ` return `
<article data-element="card" class="main-book__card d-fl mb-m mh-auto"> <article data-card data-bookid=${id} class="main-book__card card_closed d-fl mb-m mh-auto ${
isLast ? "last" : ""
}">
<div class="main-book__menu_wrapper d-fl"> <div class="main-book__menu_wrapper d-fl">
<div class="main-book__menu_like ${ <div class="main-book__menu_like ${
isFavorite ? "liked" : "not-liked" isFavorite ? "liked" : "not-liked"
}" data-like data-bookid=${id}></div> }" data-like=${isFavorite} data-bookid=${id}></div>
<nav class="main-book__menu main-book__menu_closed" <nav class="main-book__menu main-book__menu_closed"
data-authorid=${authorid}
data-bookid=${id} data-bookid=${id}
data-menu></nav> data-menu></nav>
</div> </div>
@ -118,8 +149,12 @@ export default class BookSection {
<img class="d-bl" src=${ <img class="d-bl" src=${
imageUrl ? imageUrl : "../web/assets/cover.jpg" imageUrl ? imageUrl : "../web/assets/cover.jpg"
} alt="cover" style="width:80px;height:128px;"> } alt="cover" style="width:80px;height:128px;">
<a class="main-card__author main-book__link second-text mt-s" data-author=${authorid}> <p class="second-text no-wrap mv-xs">
${name || ""}</a> ${series || "Серия "}
</p>
<p class="second-text no-wrap mv-xs">
${subseries || "Подсерия"}
</p>
<div class="second-text mt-s"> <div class="second-text mt-s">
${year || "Год неизвестен"}</div> ${year || "Год неизвестен"}</div>
</div> </div>
@ -128,24 +163,27 @@ export default class BookSection {
</div> </div>
<div class="main-book__description "> <div class="main-book__description ">
<div class="main-book__header"> <div class="main-book__header">
<span class="second-text mt-s no-wrap "><strong> <h3 class="second-text mt-s"><strong>
${title || "Название неизвестно"}</strong></span> ${title || "Название неизвестно"}</strong></h3>
<span class="second-text pl-xs no-wrap"> <p>
${series || ""}</span> ${this.getAuthors(authors)}
<span class="second-text pl-xs no-wrap "> </p>
${subseries || ""}</span>
</div> </div>
<div class="main-book__body mt-s"> <div class="main-book__body body_closed mb-m mt-s">
<div data-tags-wrapper class="main-book__tags_wrapper"></div>
<div> <div>
<span class="second-text"> <span class="second-text">
${description || "Нет описания"}</span> ${description || "Нет описания"}</span>
</div> </div>
</div> </div>
<div class="main-book__footer d-fl mt-m">${this.getTags(
tags
)}</div>
</div> </div>
<div class="main-book__arrow_wrapper w-100 hidden closed">
<div data-details class="main-book__arrow">
<div class="main-book__arrow-top"></div>
<div class="main-book__arrow-bottom"></div>
</div>
</div>
</article>`; </article>`;
} }
) )
@ -170,10 +208,11 @@ export default class BookSection {
.join(""); .join("");
} }
getTags(tags) { getAuthors(authors) {
return (tags || []) return (authors || [DEFAULT_AUTHOR])
.map(({ id, name }) => { .map(({ id, name }) => {
return `<a class="main-book__tag main-book__link" data-id=${id}>#${name}</a>`; return `<a class="main-card__author main-book__link second-text mt-s" data-author=${id}>
${name || ""}&nbsp;&nbsp;</a>`;
}) })
.join(""); .join("");
} }
@ -201,42 +240,105 @@ export default class BookSection {
} }
} }
async makeAction({ id, authorid, bookid }) { toggleDetails(element) {
const isOpened = element.classList.contains("opened");
const article = element.parentNode.parentNode;
const article_body = article.querySelector(".main-book__body");
if (isOpened) {
element.classList.remove("opened");
article.classList.add("card_closed");
article_body.classList.remove("body_opened");
article_body.classList.add("body_closed");
} else {
element.classList.add("opened");
article.classList.remove("card_closed");
article_body.classList.remove("body_closed");
article_body.classList.add("body_opened");
}
}
async countHeight() {
await waitForElement(".last");
const articles = document.querySelectorAll("[data-card]");
articles.forEach((article, i) => {
const tagWrapper = article.querySelector(".main-book__tags_wrapper");
const bookid = article.dataset.bookid;
const { tags } = this.data[i] || [];
const tagsComponent = new TagsComponent({ tags, article, bookid });
tagWrapper.append(tagsComponent.element);
if (article.scrollHeight > 502) {
const arrow = article.querySelector(".main-book__arrow_wrapper");
arrow.classList.remove("hidden");
}
});
}
async makeAction({ id, bookid }) {
this.element.classList.add("blur", "spinner"); this.element.classList.add("blur", "spinner");
if (menuEnum[id].action) { if (menuEnum[id].action) {
const data = await menuEnum[id].action({ id, authorid, bookid }); const data = await menuEnum[id].action({ id, bookid });
} else { } else {
alert("Напиши меня"); alert("Напиши меня");
} }
this.element.classList.remove("spinner"); this.element.classList.remove("spinner");
this.closeMenu() this.closeMenu();
} }
makeLike(element) { async makeLike(element) {
const { bookid: likedBookId } = element.dataset || {}; this.element.classList.add("blur", "spinner");
this.makeAction({ id: 0, authorid: "", bookid: likedBookId });
element.classList.add("liked"); const { bookid: likedBookId, like } = element.dataset || {};
const isLiked = like === "true";
if (!isLiked) {
await likeEnum[0].action({ bookid: likedBookId });
element.classList.remove("not-liked");
element.classList.add("liked");
} else {
await likeEnum[1].action({ bookid: likedBookId });
element.classList.remove("liked");
element.classList.add("not-liked");
}
element.dataset.like = !isLiked;
this.data = this.data.map(({ isFavorite, id, ...rest }) =>
likedBookId === id
? { id, isFavorite: !isFavorite, ...rest }
: {
id,
isFavorite,
...rest,
}
);
console.log("new data", this.data);
this.element.classList.remove("spinner");
} }
async update(params) { async update(params) {
this.element.classList.add("blur", "spinner"); this.element.classList.add("blur", "spinner");
let data = [];
if (params?.isByAuthor) { if (params?.isByAuthor) {
data = await searchByAuthor({ id: params.id }); this.data = await searchByAuthor({ id: params.id });
} else if (params?.isByTag) {
this.data = await searchByTag({ id: params.id });
} else { } else {
const query = params; const query = params;
data = await fetchData({ query, url: this.url }); this.data = await fetchData({ query, url: this.url });
} }
console.log("data", data); console.log("data", this.data);
if (data && Object.values(data).length) {
this.subElements.body.innerHTML = this.getBookBody(data); if (this.data && Object.values(this.data).length) {
this.subElements.body.innerHTML = this.getBookBody(this.data);
this.initialize(); this.initialize();
} else { } else {
this.subElements.body.innerHTML = this.getEmptyBody(); this.subElements.body.innerHTML = this.getEmptyBody();
} }
await this.countHeight();
this.element.classList.remove("spinner"); this.element.classList.remove("spinner");
} }
@ -246,7 +348,6 @@ export default class BookSection {
for (const subElement of elements) { for (const subElement of elements) {
const name = subElement.dataset.element; const name = subElement.dataset.element;
result[name] = subElement; result[name] = subElement;
} }
return result; return result;
@ -273,3 +374,49 @@ export default class BookSection {
this.element = null; this.element = null;
} }
} }
// this.data = [
// {
// id: "2",
// authors: [{ id: "1", name: "123" }],
// description:
// "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.",
// format: "123",
// genres: [],
// imageUrl: "",
// title: "What is Lorem Ipsum?",
// series: [],
// subseries: [],
// year: 1992,
// tags: [
// { id: 1, name: "Интересно" },
// { id: 22, name: "Почитать" },
// { id: 155, name: "Рекомендовали" },
// { id: 166, name: "завтра" },
// { id: 11, name: "наверное" },
// { id: 221, name: "Почитданетать" },
// { id: 1515, name: "раздватри" },
// { id: 1661, name: "возможно" },
// ],
// isFavorite: true,
// },
// {
// id: "22",
// authors: [{ id: "1", name: "123" }],
// description:
// "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.",
// format: "123",
// genres: [],
// imageUrl: "",
// title: "dsfwqedwqefrwef",
// series: [],
// subseries: [],
// year: 1992,
// tags: [
// { id: 1, name: "Интересно" },
// { id: 22, name: "Почитать" },
// { id: 155, name: "Рекомендовали" },
// ],
// isFavorite: true,
// },
// ];

@ -1,7 +1,7 @@
import { SUCCESS } from "../constants/index.js"; import { SUCCESS } from "../constants/index.js";
export const fetchData = ({ query, url }) => { export const fetchData = ({ query, url }) => {
console.log('query, url', query, url); console.log("query, url", query, url);
return $.ajax({ return $.ajax({
contentType: "application/json; charset=utf-8", contentType: "application/json; charset=utf-8",
dataType: "json", dataType: "json",
@ -35,6 +35,21 @@ export const addBookToFavourites = ({ bookid }) => {
}); });
}; };
export const removeBookToFavourites = ({ bookid }) => {
return $.ajax({
type: "DELETE",
url: `../api/books/${bookid}/favorite`,
success: function (data, textStatus, jqXHR) {
if (textStatus === SUCCESS) {
return data;
}
},
error: function (jqXHR, textStatus, errorThrown) {
console.log(jqXHR.statusText);
},
});
};
export const addAuthorToFavourites = ({ bookid }) => { export const addAuthorToFavourites = ({ bookid }) => {
return $.ajax({ return $.ajax({
type: "POST", type: "POST",
@ -65,6 +80,21 @@ export const readLater = ({ bookid }) => {
}); });
}; };
export const removeFromReadLater = ({ bookid }) => {
return $.ajax({
type: "DELETE",
url: `../api/books/${bookid}/read`,
success: function (data, textStatus, jqXHR) {
if (textStatus === SUCCESS) {
return data;
}
},
error: function (jqXHR, textStatus, errorThrown) {
console.log(jqXHR.statusText);
},
});
};
export const ignoreBook = ({ bookid }) => { export const ignoreBook = ({ bookid }) => {
return $.ajax({ return $.ajax({
type: "POST", type: "POST",
@ -80,7 +110,22 @@ export const ignoreBook = ({ bookid }) => {
}); });
}; };
export const ignoreAuthor = ({ bookid }) => { export const stopIgnoreBook = ({ bookid }) => {
return $.ajax({
type: "DELETE",
url: `../api/books/${bookid}/block`,
success: function (data, textStatus, jqXHR) {
if (textStatus === SUCCESS) {
return data;
}
},
error: function (jqXHR, textStatus, errorThrown) {
console.log(jqXHR.statusText);
},
});
};
export const ignoreAuthors = ({ bookid }) => {
return $.ajax({ return $.ajax({
type: "POST", type: "POST",
url: `../api/books/${bookid}/author/block`, url: `../api/books/${bookid}/author/block`,
@ -99,28 +144,28 @@ export const downloadBook = ({ id, title, format }) => {
$.ajax({ $.ajax({
type: "GET", type: "GET",
url: `../api/books/download/${id}`, url: `../api/books/download/${id}`,
responseType: 'blob', responseType: "blob",
success: function (data, textStatus, jqXHR) { success: function (data, textStatus, jqXHR) {
if (textStatus === SUCCESS) { if (textStatus === SUCCESS) {
const name = title.replace(' ', '_') const name = title.replace(" ", "_");
const url = window.URL.createObjectURL(new Blob([data])); const url = window.URL.createObjectURL(new Blob([data]));
const link = document.createElement('a'); const link = document.createElement("a");
link.href = url; link.href = url;
link.setAttribute('download', `${name}.${format}`); link.setAttribute("download", `${name}.${format}`);
document.body.appendChild(link); document.body.appendChild(link);
link.click(); link.click();
} }
}, },
error: function (jqXHR, textStatus, errorThrown) { error: function (jqXHR, textStatus, errorThrown) {
console.log(jqXHR.statusText); console.log(jqXHR.statusText);
}, },
}); });
} };
export const searchByAuthor = ({ id }) => { export const searchByAuthor = ({ id }) => {
return $.ajax({ return $.ajax({
@ -139,3 +184,63 @@ export const searchByAuthor = ({ id }) => {
}, },
}); });
}; };
export const waitForElement = (selector) => {
return new Promise((resolve) => {
if (document.querySelector(selector)) {
return resolve(document.querySelector(selector));
}
const observer = new MutationObserver((mutations) => {
if (document.querySelector(selector)) {
observer.disconnect();
resolve(document.querySelector(selector));
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
});
};
export const changeTags = ({ bookid, tags }) => {
return $.ajax({
contentType: "application/json; charset=utf-8",
dataType: "json",
type: "POST",
url: "/api/tags",
data: JSON.stringify({ bookid, names: tags }),
success: function (data, textStatus, jqXHR) {
if (textStatus === SUCCESS) {
return data;
}
},
error: function (jqXHR, textStatus, errorThrown) {
console.log(jqXHR.statusText);
},
});
// return [
// {id: "6611598e1468849d1b00570d", name: "шиза"}
// ];
};
export const searchByTag = ({ id }) => {
return $.ajax({
contentType: "application/json; charset=utf-8",
dataType: "json",
type: "POST",
url: `../api/tags/${id}/books`,
success: function (data, textStatus, jqXHR) {
if (textStatus === SUCCESS) {
return data;
}
return [];
},
error: function (jqXHR, textStatus, errorThrown) {
console.log(jqXHR.statusText);
},
});
};

@ -0,0 +1,4 @@
export const stopPropagation = event => {
event.stopImmediatePropagation();
event.stopPropagation();
}

@ -23,21 +23,21 @@
<div class="login-form"> <div class="login-form">
<div class="login-input-wrapper"> <div class="login-input-wrapper">
<input type="text" id="login" class="login-input pl-s pr-s" name="username" placeholder="Username" <input type="text" id="login" class="login-input pl-s pr-s" name="username" placeholder="Username"
required> required>
</div> </div>
<div class="login-input-wrapper"> <div class="login-input-wrapper">
<input type="password" id="password" class="login-input pl-s pr-s mt-m" name="password" <input type="password" id="password" class="login-input pl-s pr-s mt-m" name="password"
placeholder="Password" required> placeholder="Password" required>
</div> </div>
<button id="loginButton" class="login-input-btn mt-l">Войти</button> <button id="loginButton" class="login-input-btn mt-l">Войти</button>
</div> </div>
</div> </div>
</main> </main>
<script type="module"> <script type="module">
import Auth from '/web/js/login/index.js'; import Auth from './login/index.js';
const auth = new Auth() const auth = new Auth()
const enter = () => { const enter = () => {

@ -6,22 +6,22 @@ const input = document.getElementById("search");
const books = document.getElementById("books"); const books = document.getElementById("books");
const content = document.getElementById("content"); const content = document.getElementById("content");
const booksSection = new BookSection({ const booksSection = new BookSection({
url: "../api/books/search", url: "../api/books/search",
label: "books", label: "books",
}); });
const click = () => { const click = () => {
const query = document.getElementById("search").value; const query = document.getElementById("search").value;
if (query) { if (query) {
booksSection.update({ query }); booksSection.update({ query });
} }
}; };
searchButton.addEventListener("click", click); searchButton.addEventListener("click", click);
input.addEventListener("keypress", function (event) { input.addEventListener("keypress", function (event) {
if (event.key === "Enter") { if (event.key === "Enter") {
click(); click();
} }
}); });
books.append(booksSection.element); books.append(booksSection.element);

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
</Project>

@ -0,0 +1,79 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Text.Json.Serialization;
namespace BukiVedi.GenreBooksSelector
{
internal class Program
{
private static bool IsValidJson(string strInput)
{
if (string.IsNullOrWhiteSpace(strInput)) { return false; }
strInput = strInput.Trim();
if ((strInput.StartsWith("{") && strInput.EndsWith("}")) || //For object
(strInput.StartsWith("[") && strInput.EndsWith("]"))) //For array
{
try
{
var obj = JToken.Parse(strInput);
return true;
}
catch (JsonReaderException jex)
{
//Exception in parsing json
Console.WriteLine(jex.Message);
return false;
}
catch (Exception ex) //some other exception
{
Console.WriteLine(ex.ToString());
return false;
}
}
else
{
return false;
}
}
static string JsonCorrection(string file)
{
var json = File.ReadAllText(file);
if (json.StartsWith("{")) return json;
json = "{\r\n" + json + "}";
if (IsValidJson(json))
{
File.WriteAllText(file, json);
}
else
{
Console.WriteLine($"Not valid file '{file}'");
}
return json;
}
static void SearchBooks(string booksFile)
{
dynamic data = JsonConvert.DeserializeObject(File.ReadAllText(booksFile));
foreach (dynamic book in data.books)
{
Console.WriteLine(book.title);
}
}
static void Main(string[] args)
{
var genre_folder = @"F:\Documents\Python Scripts\bookgenres";
foreach(var genre in Directory.GetDirectories(genre_folder))
{
var genre_books = Path.Combine(genre, "books.txt");
SearchBooks(genre_books);
foreach (var subgenre in Directory.GetDirectories(genre))
{
var subgenre_books = Path.Combine(subgenre, "books.txt");
SearchBooks(subgenre_books);
}
}
}
}
}

@ -4,9 +4,10 @@ namespace BukiVedi.Shared.Apps
{ {
public interface ITagsHandler public interface ITagsHandler
{ {
Task AppendTag(string bookId, string name, OperationContext context); Task<TagInfo> AppendTag(string bookId, string name, OperationContext context);
Task<bool> RemoveTag(string id, OperationContext context); Task<bool> RemoveTag(string id, OperationContext context);
Task<IEnumerable<string>> GetUserTags(OperationContext context); Task<IEnumerable<string>> GetUserTags(OperationContext context);
Task<IEnumerable<TagInfo>> GetBookTags(string bookId, OperationContext context);
Task<IEnumerable<BookInfo>> Search(string id, OperationContext context); Task<IEnumerable<BookInfo>> Search(string id, OperationContext context);
} }
} }

@ -16,17 +16,36 @@ namespace BukiVedi.Shared.Apps
_library = library; _library = library;
} }
public async Task AppendTag(string bookId, string name, OperationContext context) public async Task<TagInfo> AppendTag(string bookId, string name, OperationContext context)
{ {
if (string.IsNullOrWhiteSpace(name) == false && await Tables.Books.ExistById(bookId)) if (string.IsNullOrWhiteSpace(name) == false && await Tables.Books.ExistById(bookId))
{ {
name = name.Trim().ToLowerInvariant(); name = name.Trim().ToLowerInvariant();
var tagFilter = Builders<UserTag>.Filter.And(Builders<UserTag>.Filter.Eq(t => t.BookId, bookId), Builders<UserTag>.Filter.Eq(t => t.Name, name)); var tagFilter = Builders<UserTag>.Filter.And(Builders<UserTag>.Filter.And(Builders<UserTag>.Filter.Eq(t => t.BookId, bookId), Builders<UserTag>.Filter.Eq(t => t.Name, name)), Builders<UserTag>.Filter.Eq(t => t.UserId, context.OperationInitiator.Id));
if (false == await Tables.UserTag.Exists(tagFilter)) var existTag = await Tables.UserTag.Get(tagFilter);
if (existTag != null && existTag.Length > 0)
{ {
await Tables.UserTag.Write(new UserTag { BookId = bookId, Name = name, UserId = context.OperationInitiator.Id }); return new TagInfo { Id = existTag[0].Id, Name = existTag[0].Name };
}
else
{
var userTag = await Tables.UserTag.Write(new UserTag { BookId = bookId, Name = name, UserId = context.OperationInitiator.Id });
return new TagInfo { Id = userTag.Id, Name = name };
} }
} }
return null!;
}
public async Task<IEnumerable<TagInfo>> GetBookTags(string bookId, OperationContext context)
{
var userFilter = Builders<UserTag>.Filter.Eq(t => t.UserId, context.OperationInitiator.Id);
var bookFilter = Builders<UserTag>.Filter.Eq(t => t.BookId, bookId);
var filter = Builders<UserTag>.Filter.And(userFilter, bookFilter);
return (await Tables.UserTag.Get(filter))?.Select(t => new TagInfo
{
Id = t.Id,
Name = t.Name,
})!;
} }
public async Task<IEnumerable<string>> GetUserTags(OperationContext context) public async Task<IEnumerable<string>> GetUserTags(OperationContext context)

@ -67,11 +67,6 @@
}, },
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\8.0.204\\RuntimeIdentifierGraph.json" "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\8.0.204\\RuntimeIdentifierGraph.json"
} }
},
"runtimes": {
"linux-x64": {
"#import": []
}
} }
}, },
"G:\\Documents\\GitHub\\BukiVedi\\src\\BukiVedi.Shared\\BukiVedi.Shared.csproj": { "G:\\Documents\\GitHub\\BukiVedi\\src\\BukiVedi.Shared\\BukiVedi.Shared.csproj": {
@ -163,11 +158,6 @@
}, },
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\8.0.204/PortableRuntimeIdentifierGraph.json" "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\8.0.204/PortableRuntimeIdentifierGraph.json"
} }
},
"runtimes": {
"linux-x64": {
"#import": []
}
} }
}, },
"G:\\Documents\\GitHub\\BukiVedi\\src\\Vendors\\LemmaGen_v3.0_PrebuiltFull\\LemmaSharpPrebuilt\\LemmaSharpPrebuilt.csproj": { "G:\\Documents\\GitHub\\BukiVedi\\src\\Vendors\\LemmaGen_v3.0_PrebuiltFull\\LemmaSharpPrebuilt\\LemmaSharpPrebuilt.csproj": {
@ -293,11 +283,6 @@
}, },
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\8.0.204/PortableRuntimeIdentifierGraph.json" "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\8.0.204/PortableRuntimeIdentifierGraph.json"
} }
},
"runtimes": {
"linux-x64": {
"#import": []
}
} }
} }
} }

@ -14,7 +14,7 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("BukiVedi.Shared")] [assembly: System.Reflection.AssemblyCompanyAttribute("BukiVedi.Shared")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+431f2503a837ccf39eac97076a865db39f86a8dd")] [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+112daccec93b21f74faf3a3b85b2f9a47b2ae81f")]
[assembly: System.Reflection.AssemblyProductAttribute("BukiVedi.Shared")] [assembly: System.Reflection.AssemblyProductAttribute("BukiVedi.Shared")]
[assembly: System.Reflection.AssemblyTitleAttribute("BukiVedi.Shared")] [assembly: System.Reflection.AssemblyTitleAttribute("BukiVedi.Shared")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]

@ -1 +1 @@
22258ed3f3bd48ae74b9c3607cf0cf82ec9b3322b631691b32743438f798c7cc 509bc785dfee6d2836ba0599e079958f32ff95621449af032e3881340993551e

@ -359,332 +359,6 @@
"bin/placeholder/ZeroLevel.dll": {} "bin/placeholder/ZeroLevel.dll": {}
} }
} }
},
"net8.0/linux-x64": {
"AWSSDK.Core/3.7.100.14": {
"type": "package",
"compile": {
"lib/netcoreapp3.1/AWSSDK.Core.dll": {
"related": ".pdb;.xml"
}
},
"runtime": {
"lib/netcoreapp3.1/AWSSDK.Core.dll": {
"related": ".pdb;.xml"
}
}
},
"AWSSDK.SecurityToken/3.7.100.14": {
"type": "package",
"dependencies": {
"AWSSDK.Core": "[3.7.100.14, 4.0.0)"
},
"compile": {
"lib/netcoreapp3.1/AWSSDK.SecurityToken.dll": {
"related": ".pdb;.xml"
}
},
"runtime": {
"lib/netcoreapp3.1/AWSSDK.SecurityToken.dll": {
"related": ".pdb;.xml"
}
}
},
"DnsClient/1.6.1": {
"type": "package",
"dependencies": {
"Microsoft.Win32.Registry": "5.0.0"
},
"compile": {
"lib/net5.0/DnsClient.dll": {
"related": ".xml"
}
},
"runtime": {
"lib/net5.0/DnsClient.dll": {
"related": ".xml"
}
}
},
"FB2Library/1.3.3": {
"type": "package",
"compile": {
"lib/net6.0/FB2Library.dll": {}
},
"runtime": {
"lib/net6.0/FB2Library.dll": {}
}
},
"Microsoft.Extensions.Logging.Abstractions/2.0.0": {
"type": "package",
"compile": {
"lib/netstandard2.0/Microsoft.Extensions.Logging.Abstractions.dll": {
"related": ".xml"
}
},
"runtime": {
"lib/netstandard2.0/Microsoft.Extensions.Logging.Abstractions.dll": {
"related": ".xml"
}
}
},
"Microsoft.NETCore.Platforms/5.0.0": {
"type": "package",
"compile": {
"lib/netstandard1.0/_._": {}
},
"runtime": {
"lib/netstandard1.0/_._": {}
}
},
"Microsoft.Win32.Registry/5.0.0": {
"type": "package",
"dependencies": {
"System.Security.AccessControl": "5.0.0",
"System.Security.Principal.Windows": "5.0.0"
},
"compile": {
"ref/netstandard2.0/Microsoft.Win32.Registry.dll": {
"related": ".xml"
}
},
"runtime": {
"lib/netstandard2.0/Microsoft.Win32.Registry.dll": {
"related": ".xml"
}
}
},
"MongoDB.Bson/2.24.0": {
"type": "package",
"dependencies": {
"System.Memory": "4.5.5",
"System.Runtime.CompilerServices.Unsafe": "5.0.0"
},
"compile": {
"lib/netstandard2.1/MongoDB.Bson.dll": {
"related": ".xml"
}
},
"runtime": {
"lib/netstandard2.1/MongoDB.Bson.dll": {
"related": ".xml"
}
}
},
"MongoDB.Driver/2.24.0": {
"type": "package",
"dependencies": {
"Microsoft.Extensions.Logging.Abstractions": "2.0.0",
"MongoDB.Bson": "2.24.0",
"MongoDB.Driver.Core": "2.24.0",
"MongoDB.Libmongocrypt": "1.8.2"
},
"compile": {
"lib/netstandard2.1/MongoDB.Driver.dll": {
"related": ".xml"
}
},
"runtime": {
"lib/netstandard2.1/MongoDB.Driver.dll": {
"related": ".xml"
}
}
},
"MongoDB.Driver.Core/2.24.0": {
"type": "package",
"dependencies": {
"AWSSDK.SecurityToken": "3.7.100.14",
"DnsClient": "1.6.1",
"Microsoft.Extensions.Logging.Abstractions": "2.0.0",
"MongoDB.Bson": "2.24.0",
"MongoDB.Libmongocrypt": "1.8.2",
"SharpCompress": "0.30.1",
"Snappier": "1.0.0",
"System.Buffers": "4.5.1",
"ZstdSharp.Port": "0.7.3"
},
"compile": {
"lib/netstandard2.1/MongoDB.Driver.Core.dll": {
"related": ".xml"
}
},
"runtime": {
"lib/netstandard2.1/MongoDB.Driver.Core.dll": {
"related": ".xml"
}
}
},
"MongoDB.Libmongocrypt/1.8.2": {
"type": "package",
"compile": {
"lib/netstandard2.1/MongoDB.Libmongocrypt.dll": {}
},
"runtime": {
"lib/netstandard2.1/MongoDB.Libmongocrypt.dll": {}
},
"native": {
"runtimes/linux/native/libmongocrypt.so": {}
},
"contentFiles": {
"contentFiles/any/any/_._": {
"buildAction": "None",
"codeLanguage": "any",
"copyToOutput": false
}
},
"build": {
"build/_._": {}
}
},
"SharpCompress/0.30.1": {
"type": "package",
"compile": {
"lib/net5.0/SharpCompress.dll": {}
},
"runtime": {
"lib/net5.0/SharpCompress.dll": {}
}
},
"Snappier/1.0.0": {
"type": "package",
"compile": {
"lib/net5.0/Snappier.dll": {
"related": ".xml"
}
},
"runtime": {
"lib/net5.0/Snappier.dll": {
"related": ".xml"
}
}
},
"System.Buffers/4.5.1": {
"type": "package",
"compile": {
"ref/netcoreapp2.0/_._": {}
},
"runtime": {
"lib/netcoreapp2.0/_._": {}
}
},
"System.Memory/4.5.5": {
"type": "package",
"compile": {
"ref/netcoreapp2.1/_._": {}
},
"runtime": {
"lib/netcoreapp2.1/_._": {}
}
},
"System.Runtime.CompilerServices.Unsafe/5.0.0": {
"type": "package",
"compile": {
"ref/netstandard2.1/System.Runtime.CompilerServices.Unsafe.dll": {
"related": ".xml"
}
},
"runtime": {
"lib/netcoreapp2.0/System.Runtime.CompilerServices.Unsafe.dll": {
"related": ".xml"
}
}
},
"System.Security.AccessControl/5.0.0": {
"type": "package",
"dependencies": {
"Microsoft.NETCore.Platforms": "5.0.0",
"System.Security.Principal.Windows": "5.0.0"
},
"compile": {
"ref/netstandard2.0/System.Security.AccessControl.dll": {
"related": ".xml"
}
},
"runtime": {
"lib/netstandard2.0/System.Security.AccessControl.dll": {
"related": ".xml"
}
}
},
"System.Security.Principal.Windows/5.0.0": {
"type": "package",
"compile": {
"ref/netcoreapp3.0/System.Security.Principal.Windows.dll": {
"related": ".xml"
}
},
"runtime": {
"runtimes/unix/lib/netcoreapp2.1/System.Security.Principal.Windows.dll": {
"related": ".xml"
}
}
},
"ZstdSharp.Port/0.7.3": {
"type": "package",
"compile": {
"lib/net7.0/ZstdSharp.dll": {}
},
"runtime": {
"lib/net7.0/ZstdSharp.dll": {}
}
},
"LemmaSharp/1.0.0": {
"type": "project",
"compile": {
"bin/placeholder/LemmaSharp.dll": {}
},
"runtime": {
"bin/placeholder/LemmaSharp.dll": {}
}
},
"LemmaSharpPrebuilt/1.0.0": {
"type": "project",
"dependencies": {
"LemmaSharp": "1.0.0"
},
"compile": {
"bin/placeholder/LemmaSharpPrebuilt.dll": {}
},
"runtime": {
"bin/placeholder/LemmaSharpPrebuilt.dll": {}
}
},
"LemmaSharpPrebuiltFull/1.0.0": {
"type": "project",
"dependencies": {
"LemmaSharp": "1.0.0",
"LemmaSharpPrebuilt": "1.0.0"
},
"compile": {
"bin/placeholder/LemmaSharpPrebuiltFull.dll": {}
},
"runtime": {
"bin/placeholder/LemmaSharpPrebuiltFull.dll": {}
}
},
"Sleopok.Engine/1.0.0": {
"type": "project",
"framework": ".NETCoreApp,Version=v8.0",
"dependencies": {
"ZeroLevel": "4.0.0"
},
"compile": {
"bin/placeholder/Sleopok.Engine.dll": {}
},
"runtime": {
"bin/placeholder/Sleopok.Engine.dll": {}
}
},
"ZeroLevel/4.0.0": {
"type": "project",
"framework": ".NETStandard,Version=v2.1",
"compile": {
"bin/placeholder/ZeroLevel.dll": {}
},
"runtime": {
"bin/placeholder/ZeroLevel.dll": {}
}
}
} }
}, },
"libraries": { "libraries": {
@ -1313,11 +987,6 @@
}, },
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\8.0.204/PortableRuntimeIdentifierGraph.json" "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\8.0.204/PortableRuntimeIdentifierGraph.json"
} }
},
"runtimes": {
"linux-x64": {
"#import": []
}
} }
} }
} }

@ -1,6 +1,6 @@
{ {
"version": 2, "version": 2,
"dgSpecHash": "I5NF3rZJZUmDLwIKUi7T7NRpUaNrPoHKlXnBGtdzl5MdYPzg4P1cn162b2nsZJbsHNBvnds14OIqZJxmohNxNw==", "dgSpecHash": "9b5dNjgn+wv+pfjNvt/5b1bT0J1w9OQimtBKgbK1yNIjkPu1p362ahWugnoJioABQfO2QlX4qHlK5bZKLwvjlQ==",
"success": true, "success": true,
"projectFilePath": "G:\\Documents\\GitHub\\BukiVedi\\src\\BukiVedi.Shared\\BukiVedi.Shared.csproj", "projectFilePath": "G:\\Documents\\GitHub\\BukiVedi\\src\\BukiVedi.Shared\\BukiVedi.Shared.csproj",
"expectedPackageFiles": [ "expectedPackageFiles": [

@ -23,6 +23,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IndexExportTest", "IndexExp
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ZeroLevel", "F:\Documents\GitHub\Zero\ZeroLevel\ZeroLevel.csproj", "{4A5BEE51-4869-49E7-9619-2D1674776A64}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ZeroLevel", "F:\Documents\GitHub\Zero\ZeroLevel\ZeroLevel.csproj", "{4A5BEE51-4869-49E7-9619-2D1674776A64}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BukiVedi.GenreBooksSelector", "BukiVedi.GenreBooksSelector\BukiVedi.GenreBooksSelector.csproj", "{75CD7F83-1706-49CD-945C-7E4B1DFAD501}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -198,6 +200,24 @@ Global
{4A5BEE51-4869-49E7-9619-2D1674776A64}.WPFDevelop|x64.Build.0 = Debug|Any CPU {4A5BEE51-4869-49E7-9619-2D1674776A64}.WPFDevelop|x64.Build.0 = Debug|Any CPU
{4A5BEE51-4869-49E7-9619-2D1674776A64}.WPFDevelop|x86.ActiveCfg = Debug|Any CPU {4A5BEE51-4869-49E7-9619-2D1674776A64}.WPFDevelop|x86.ActiveCfg = Debug|Any CPU
{4A5BEE51-4869-49E7-9619-2D1674776A64}.WPFDevelop|x86.Build.0 = Debug|Any CPU {4A5BEE51-4869-49E7-9619-2D1674776A64}.WPFDevelop|x86.Build.0 = Debug|Any CPU
{75CD7F83-1706-49CD-945C-7E4B1DFAD501}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{75CD7F83-1706-49CD-945C-7E4B1DFAD501}.Debug|Any CPU.Build.0 = Debug|Any CPU
{75CD7F83-1706-49CD-945C-7E4B1DFAD501}.Debug|x64.ActiveCfg = Debug|Any CPU
{75CD7F83-1706-49CD-945C-7E4B1DFAD501}.Debug|x64.Build.0 = Debug|Any CPU
{75CD7F83-1706-49CD-945C-7E4B1DFAD501}.Debug|x86.ActiveCfg = Debug|Any CPU
{75CD7F83-1706-49CD-945C-7E4B1DFAD501}.Debug|x86.Build.0 = Debug|Any CPU
{75CD7F83-1706-49CD-945C-7E4B1DFAD501}.Release|Any CPU.ActiveCfg = Release|Any CPU
{75CD7F83-1706-49CD-945C-7E4B1DFAD501}.Release|Any CPU.Build.0 = Release|Any CPU
{75CD7F83-1706-49CD-945C-7E4B1DFAD501}.Release|x64.ActiveCfg = Release|Any CPU
{75CD7F83-1706-49CD-945C-7E4B1DFAD501}.Release|x64.Build.0 = Release|Any CPU
{75CD7F83-1706-49CD-945C-7E4B1DFAD501}.Release|x86.ActiveCfg = Release|Any CPU
{75CD7F83-1706-49CD-945C-7E4B1DFAD501}.Release|x86.Build.0 = Release|Any CPU
{75CD7F83-1706-49CD-945C-7E4B1DFAD501}.WPFDevelop|Any CPU.ActiveCfg = Debug|Any CPU
{75CD7F83-1706-49CD-945C-7E4B1DFAD501}.WPFDevelop|Any CPU.Build.0 = Debug|Any CPU
{75CD7F83-1706-49CD-945C-7E4B1DFAD501}.WPFDevelop|x64.ActiveCfg = Debug|Any CPU
{75CD7F83-1706-49CD-945C-7E4B1DFAD501}.WPFDevelop|x64.Build.0 = Debug|Any CPU
{75CD7F83-1706-49CD-945C-7E4B1DFAD501}.WPFDevelop|x86.ActiveCfg = Debug|Any CPU
{75CD7F83-1706-49CD-945C-7E4B1DFAD501}.WPFDevelop|x86.Build.0 = Debug|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

@ -14,7 +14,7 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("TitleReader")] [assembly: System.Reflection.AssemblyCompanyAttribute("TitleReader")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+431f2503a837ccf39eac97076a865db39f86a8dd")] [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+112daccec93b21f74faf3a3b85b2f9a47b2ae81f")]
[assembly: System.Reflection.AssemblyProductAttribute("TitleReader")] [assembly: System.Reflection.AssemblyProductAttribute("TitleReader")]
[assembly: System.Reflection.AssemblyTitleAttribute("TitleReader")] [assembly: System.Reflection.AssemblyTitleAttribute("TitleReader")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]

@ -1 +1 @@
d7606117e667c055ed565f5f0d94c916cbdbb0bbb171442e9a9d4b51ab356451 153deb8a7a296fdeedff24e4e5322961f1ebc6ec896447287c432b0746206083

Loading…
Cancel
Save

Powered by TurnKey Linux.