mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-05-30 19:54:14 -04:00
Misc Fixes (#1031)
* Changed the default margin for mobile in book reader to 5% * Fixed a bug where checking for update did no current version validation before sending the update to the UI. * Added some documentation to the book code * Changed token expiry to 2 weeks. * Search bar will by default not have a border outline * Cleaned up some styles for white mode hovering on search * Added missing genre search group, reworked some clearing code, fixed click handlers * Fixed genre property * Changed the series title to show bookmarks and the edit button will now take you to series * Fixed up accordion tabpanel color in dark mode * Fixed a typo of CoverArtist instead of "Cover artist" * Added some documentation changes * Fixed a bug where sort options on All-Series wasn't working * Added a thanks to Palace-Designs who hosts our infrastructure to the readme. * Fixed a bug where duplicate people for the same role would be returned * Fixed a bug where when user cleared out input manually, search would retain old search results
This commit is contained in:
parent
dc2d5b505f
commit
19e17c85fa
@ -77,11 +77,16 @@ namespace API.Controllers
|
|||||||
return File(content, contentType, $"{chapterId}-{file}");
|
return File(content, contentType, $"{chapterId}-{file}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This will return a list of mappings from ID -> page num. ID will be the xhtml key and page num will be the reading order
|
||||||
|
/// this is used to rewrite anchors in the book text so that we always load properly in FE
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>This is essentially building the table of contents</remarks>
|
||||||
|
/// <param name="chapterId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpGet("{chapterId}/chapters")]
|
[HttpGet("{chapterId}/chapters")]
|
||||||
public async Task<ActionResult<ICollection<BookChapterItem>>> GetBookChapters(int chapterId)
|
public async Task<ActionResult<ICollection<BookChapterItem>>> GetBookChapters(int chapterId)
|
||||||
{
|
{
|
||||||
// This will return a list of mappings from ID -> pagenum. ID will be the xhtml key and pagenum will be the reading order
|
|
||||||
// this is used to rewrite anchors in the book text so that we always load properly in FE
|
|
||||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
|
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
|
||||||
using var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath);
|
using var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath);
|
||||||
var mappings = await _bookService.CreateKeyToPageMappingAsync(book);
|
var mappings = await _bookService.CreateKeyToPageMappingAsync(book);
|
||||||
@ -126,7 +131,7 @@ namespace API.Controllers
|
|||||||
var tocPage = book.Content.Html.Keys.FirstOrDefault(k => k.ToUpper().Contains("TOC"));
|
var tocPage = book.Content.Html.Keys.FirstOrDefault(k => k.ToUpper().Contains("TOC"));
|
||||||
if (tocPage == null) return Ok(chaptersList);
|
if (tocPage == null) return Ok(chaptersList);
|
||||||
|
|
||||||
// Find all anchor tags, for each anchor we get inner text, to lower then titlecase on UI. Get href and generate page content
|
// Find all anchor tags, for each anchor we get inner text, to lower then title case on UI. Get href and generate page content
|
||||||
var doc = new HtmlDocument();
|
var doc = new HtmlDocument();
|
||||||
var content = await book.Content.Html[tocPage].ReadContentAsync();
|
var content = await book.Content.Html[tocPage].ReadContentAsync();
|
||||||
doc.LoadHtml(content);
|
doc.LoadHtml(content);
|
||||||
@ -252,7 +257,7 @@ namespace API.Controllers
|
|||||||
return BadRequest("Could not find the appropriate html for that page");
|
return BadRequest("Could not find the appropriate html for that page");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LogBookErrors(EpubBookRef book, EpubTextContentFileRef contentFileRef, HtmlDocument doc)
|
private void LogBookErrors(EpubBookRef book, EpubContentFileRef contentFileRef, HtmlDocument doc)
|
||||||
{
|
{
|
||||||
_logger.LogError("{FilePath} has an invalid html file (Page {PageName})", book.FilePath, contentFileRef.FileName);
|
_logger.LogError("{FilePath} has an invalid html file (Page {PageName})", book.FilePath, contentFileRef.FileName);
|
||||||
foreach (var error in doc.ParseErrors)
|
foreach (var error in doc.ParseErrors)
|
||||||
|
@ -25,22 +25,20 @@ namespace API.Controllers
|
|||||||
private readonly IConfiguration _config;
|
private readonly IConfiguration _config;
|
||||||
private readonly IBackupService _backupService;
|
private readonly IBackupService _backupService;
|
||||||
private readonly IArchiveService _archiveService;
|
private readonly IArchiveService _archiveService;
|
||||||
private readonly ICacheService _cacheService;
|
|
||||||
private readonly IVersionUpdaterService _versionUpdaterService;
|
private readonly IVersionUpdaterService _versionUpdaterService;
|
||||||
private readonly IStatsService _statsService;
|
private readonly IStatsService _statsService;
|
||||||
private readonly ICleanupService _cleanupService;
|
private readonly ICleanupService _cleanupService;
|
||||||
private readonly IEmailService _emailService;
|
private readonly IEmailService _emailService;
|
||||||
|
|
||||||
public ServerController(IHostApplicationLifetime applicationLifetime, ILogger<ServerController> logger, IConfiguration config,
|
public ServerController(IHostApplicationLifetime applicationLifetime, ILogger<ServerController> logger, IConfiguration config,
|
||||||
IBackupService backupService, IArchiveService archiveService, ICacheService cacheService,
|
IBackupService backupService, IArchiveService archiveService, IVersionUpdaterService versionUpdaterService, IStatsService statsService,
|
||||||
IVersionUpdaterService versionUpdaterService, IStatsService statsService, ICleanupService cleanupService, IEmailService emailService)
|
ICleanupService cleanupService, IEmailService emailService)
|
||||||
{
|
{
|
||||||
_applicationLifetime = applicationLifetime;
|
_applicationLifetime = applicationLifetime;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_config = config;
|
_config = config;
|
||||||
_backupService = backupService;
|
_backupService = backupService;
|
||||||
_archiveService = archiveService;
|
_archiveService = archiveService;
|
||||||
_cacheService = cacheService;
|
|
||||||
_versionUpdaterService = versionUpdaterService;
|
_versionUpdaterService = versionUpdaterService;
|
||||||
_statsService = statsService;
|
_statsService = statsService;
|
||||||
_cleanupService = cleanupService;
|
_cleanupService = cleanupService;
|
||||||
@ -111,6 +109,9 @@ namespace API.Controllers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks for updates, if no updates that are > current version installed, returns null
|
||||||
|
/// </summary>
|
||||||
[HttpGet("check-update")]
|
[HttpGet("check-update")]
|
||||||
public async Task<ActionResult<UpdateNotificationDto>> CheckForUpdates()
|
public async Task<ActionResult<UpdateNotificationDto>> CheckForUpdates()
|
||||||
{
|
{
|
||||||
|
@ -240,11 +240,6 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
{
|
{
|
||||||
var query = await CreateFilteredSearchQueryable(userId, libraryId, filter);
|
var query = await CreateFilteredSearchQueryable(userId, libraryId, filter);
|
||||||
|
|
||||||
if (filter.SortOptions == null)
|
|
||||||
{
|
|
||||||
query = query.OrderBy(s => s.SortName);
|
|
||||||
}
|
|
||||||
|
|
||||||
var retSeries = query
|
var retSeries = query
|
||||||
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
||||||
.AsSplitQuery()
|
.AsSplitQuery()
|
||||||
@ -314,6 +309,7 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
.Where(sm => seriesIds.Contains(sm.SeriesId))
|
.Where(sm => seriesIds.Contains(sm.SeriesId))
|
||||||
.SelectMany(sm => sm.People.Where(t => EF.Functions.Like(t.Name, $"%{searchQuery}%")))
|
.SelectMany(sm => sm.People.Where(t => EF.Functions.Like(t.Name, $"%{searchQuery}%")))
|
||||||
.AsSplitQuery()
|
.AsSplitQuery()
|
||||||
|
.Distinct()
|
||||||
.ProjectTo<PersonDto>(_mapper.ConfigurationProvider)
|
.ProjectTo<PersonDto>(_mapper.ConfigurationProvider)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
|
@ -458,6 +458,11 @@ namespace API.Services
|
|||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes the leading ../
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key"></param>
|
||||||
|
/// <returns></returns>
|
||||||
public static string CleanContentKeys(string key)
|
public static string CleanContentKeys(string key)
|
||||||
{
|
{
|
||||||
return key.Replace("../", string.Empty);
|
return key.Replace("../", string.Empty);
|
||||||
|
@ -57,8 +57,8 @@ public class VersionUpdaterService : IVersionUpdaterService
|
|||||||
private readonly IPresenceTracker _tracker;
|
private readonly IPresenceTracker _tracker;
|
||||||
private readonly Markdown _markdown = new MarkdownDeep.Markdown();
|
private readonly Markdown _markdown = new MarkdownDeep.Markdown();
|
||||||
#pragma warning disable S1075
|
#pragma warning disable S1075
|
||||||
private static readonly string GithubLatestReleasesUrl = "https://api.github.com/repos/Kareadita/Kavita/releases/latest";
|
private const string GithubLatestReleasesUrl = "https://api.github.com/repos/Kareadita/Kavita/releases/latest";
|
||||||
private static readonly string GithubAllReleasesUrl = "https://api.github.com/repos/Kareadita/Kavita/releases";
|
private const string GithubAllReleasesUrl = "https://api.github.com/repos/Kareadita/Kavita/releases";
|
||||||
#pragma warning restore S1075
|
#pragma warning restore S1075
|
||||||
|
|
||||||
public VersionUpdaterService(ILogger<VersionUpdaterService> logger, IHubContext<MessageHub> messageHub, IPresenceTracker tracker)
|
public VersionUpdaterService(ILogger<VersionUpdaterService> logger, IHubContext<MessageHub> messageHub, IPresenceTracker tracker)
|
||||||
@ -76,10 +76,12 @@ public class VersionUpdaterService : IVersionUpdaterService
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fetches the latest release from Github
|
/// Fetches the latest release from Github
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<UpdateNotificationDto> CheckForUpdate()
|
/// <returns>Latest update or null if current version is greater than latest update</returns>
|
||||||
|
public async Task<UpdateNotificationDto?> CheckForUpdate()
|
||||||
{
|
{
|
||||||
var update = await GetGithubRelease();
|
var update = await GetGithubRelease();
|
||||||
return CreateDto(update);
|
var dto = CreateDto(update);
|
||||||
|
return new Version(dto.UpdateVersion) <= new Version(dto.CurrentVersion) ? null : dto;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<UpdateNotificationDto>> GetAllReleases()
|
public async Task<IEnumerable<UpdateNotificationDto>> GetAllReleases()
|
||||||
|
@ -50,7 +50,7 @@ public class TokenService : ITokenService
|
|||||||
var tokenDescriptor = new SecurityTokenDescriptor()
|
var tokenDescriptor = new SecurityTokenDescriptor()
|
||||||
{
|
{
|
||||||
Subject = new ClaimsIdentity(claims),
|
Subject = new ClaimsIdentity(claims),
|
||||||
Expires = DateTime.Now.AddDays(7),
|
Expires = DateTime.Now.AddDays(14),
|
||||||
SigningCredentials = creds
|
SigningCredentials = creds
|
||||||
};
|
};
|
||||||
|
|
||||||
|
BIN
Logo/hosting-sponsor.png
Normal file
BIN
Logo/hosting-sponsor.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
@ -22,7 +22,7 @@ your reading collection with your friends and family!
|
|||||||
- [x] First class responsive readers that work great on any device (phone, tablet, desktop)
|
- [x] First class responsive readers that work great on any device (phone, tablet, desktop)
|
||||||
- [x] Dark and Light themes
|
- [x] Dark and Light themes
|
||||||
- [ ] Provide hooks into metadata providers to fetch metadata for Comics, Manga, and Books
|
- [ ] Provide hooks into metadata providers to fetch metadata for Comics, Manga, and Books
|
||||||
- [ ] Metadata should allow for collections, want to read integration from 3rd party services, genres.
|
- [x] Metadata should allow for collections, want to read integration from 3rd party services, genres.
|
||||||
- [x] Ability to manage users, access, and ratings
|
- [x] Ability to manage users, access, and ratings
|
||||||
- [ ] Ability to sync ratings and reviews to external services
|
- [ ] Ability to sync ratings and reviews to external services
|
||||||
- [x] Fully Accessible with active accessibility audits
|
- [x] Fully Accessible with active accessibility audits
|
||||||
@ -116,8 +116,12 @@ Thank you to [<img src="/Logo/jetbrains.svg" alt="" width="32"> JetBrains](http:
|
|||||||
* [<img src="/Logo/rider.svg" alt="" width="32"> Rider](http://www.jetbrains.com/rider/)
|
* [<img src="/Logo/rider.svg" alt="" width="32"> Rider](http://www.jetbrains.com/rider/)
|
||||||
* [<img src="/Logo/dottrace.svg" alt="" width="32"> dotTrace](http://www.jetbrains.com/dottrace/)
|
* [<img src="/Logo/dottrace.svg" alt="" width="32"> dotTrace](http://www.jetbrains.com/dottrace/)
|
||||||
|
|
||||||
|
## Palace-Designs
|
||||||
|
We would like to extend a big thank you to [<img src="/Logo/hosting-sponsor.png" alt="" width="128">](https://www.palace-designs.com/) who hosts our infrastructure pro-bono.
|
||||||
|
|
||||||
|
|
||||||
### License
|
### License
|
||||||
|
|
||||||
* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
|
* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
|
||||||
* Copyright 2020-2021
|
* Copyright 2020-2022
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ export class MetadataService {
|
|||||||
if (libraries != undefined && libraries.length > 0) {
|
if (libraries != undefined && libraries.length > 0) {
|
||||||
method += '?libraryIds=' + libraries.join(',');
|
method += '?libraryIds=' + libraries.join(',');
|
||||||
}
|
}
|
||||||
return this.httpClient.get<Array<AgeRatingDto>>(this.baseUrl + method);;
|
return this.httpClient.get<Array<AgeRatingDto>>(this.baseUrl + method);
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllPublicationStatus(libraries?: Array<number>) {
|
getAllPublicationStatus(libraries?: Array<number>) {
|
||||||
@ -50,7 +50,7 @@ export class MetadataService {
|
|||||||
if (libraries != undefined && libraries.length > 0) {
|
if (libraries != undefined && libraries.length > 0) {
|
||||||
method += '?libraryIds=' + libraries.join(',');
|
method += '?libraryIds=' + libraries.join(',');
|
||||||
}
|
}
|
||||||
return this.httpClient.get<Array<PublicationStatusDto>>(this.baseUrl + method);;
|
return this.httpClient.get<Array<PublicationStatusDto>>(this.baseUrl + method);
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllTags(libraries?: Array<number>) {
|
getAllTags(libraries?: Array<number>) {
|
||||||
@ -58,7 +58,7 @@ export class MetadataService {
|
|||||||
if (libraries != undefined && libraries.length > 0) {
|
if (libraries != undefined && libraries.length > 0) {
|
||||||
method += '?libraryIds=' + libraries.join(',');
|
method += '?libraryIds=' + libraries.join(',');
|
||||||
}
|
}
|
||||||
return this.httpClient.get<Array<Tag>>(this.baseUrl + method);;
|
return this.httpClient.get<Array<Tag>>(this.baseUrl + method);
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllGenres(libraries?: Array<number>) {
|
getAllGenres(libraries?: Array<number>) {
|
||||||
@ -66,7 +66,7 @@ export class MetadataService {
|
|||||||
if (libraries != undefined && libraries.length > 0) {
|
if (libraries != undefined && libraries.length > 0) {
|
||||||
method += '?libraryIds=' + libraries.join(',');
|
method += '?libraryIds=' + libraries.join(',');
|
||||||
}
|
}
|
||||||
return this.httpClient.get<Genre[]>(this.baseUrl + method);
|
return this.httpClient.get<Array<Genre>>(this.baseUrl + method);
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllLanguages(libraries?: Array<number>) {
|
getAllLanguages(libraries?: Array<number>) {
|
||||||
@ -74,7 +74,7 @@ export class MetadataService {
|
|||||||
if (libraries != undefined && libraries.length > 0) {
|
if (libraries != undefined && libraries.length > 0) {
|
||||||
method += '?libraryIds=' + libraries.join(',');
|
method += '?libraryIds=' + libraries.join(',');
|
||||||
}
|
}
|
||||||
return this.httpClient.get<Language[]>(this.baseUrl + method);
|
return this.httpClient.get<Array<Language>>(this.baseUrl + method);
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllPeople(libraries?: Array<number>) {
|
getAllPeople(libraries?: Array<number>) {
|
||||||
@ -82,6 +82,6 @@ export class MetadataService {
|
|||||||
if (libraries != undefined && libraries.length > 0) {
|
if (libraries != undefined && libraries.length > 0) {
|
||||||
method += '?libraryIds=' + libraries.join(',');
|
method += '?libraryIds=' + libraries.join(',');
|
||||||
}
|
}
|
||||||
return this.httpClient.get<Person[]>(this.baseUrl + method);
|
return this.httpClient.get<Array<Person>>(this.baseUrl + method);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -615,7 +615,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
|
|
||||||
let margin = '15%';
|
let margin = '15%';
|
||||||
if (windowWidth <= 700) {
|
if (windowWidth <= 700) {
|
||||||
margin = '0%';
|
margin = '5%';
|
||||||
}
|
}
|
||||||
if (this.user) {
|
if (this.user) {
|
||||||
if (windowWidth > 700) {
|
if (windowWidth > 700) {
|
||||||
|
@ -5,7 +5,6 @@
|
|||||||
<span *ngIf="actions.length > 0" class="">
|
<span *ngIf="actions.length > 0" class="">
|
||||||
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="header"></app-card-actionables>
|
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="header"></app-card-actionables>
|
||||||
</span>{{header}}
|
</span>{{header}}
|
||||||
<!-- NOTE: On mobile the pill can eat up a lot of space, we can hide it and move to the filter section if user is interested -->
|
|
||||||
<span class="badge badge-primary badge-pill" attr.aria-label="{{pagination.totalItems}} total items" *ngIf="pagination != undefined">{{pagination.totalItems}}</span>
|
<span class="badge badge-primary badge-pill" attr.aria-label="{{pagination.totalItems}} total items" *ngIf="pagination != undefined">{{pagination.totalItems}}</span>
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
@ -367,7 +367,7 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy {
|
|||||||
};
|
};
|
||||||
this.collectionSettings.compareFn = (options: CollectionTag[], filter: string) => {
|
this.collectionSettings.compareFn = (options: CollectionTag[], filter: string) => {
|
||||||
const f = filter.toLowerCase();
|
const f = filter.toLowerCase();
|
||||||
return options.filter(m => m.title.toLowerCase() === f);
|
return options.filter(m => m.title.toLowerCase() === f && this.utilityService.filter(m.title, filter));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.filterSettings.presets?.collectionTags && this.filterSettings.presets?.collectionTags.length > 0) {
|
if (this.filterSettings.presets?.collectionTags && this.filterSettings.presets?.collectionTags.length > 0) {
|
||||||
|
@ -35,6 +35,16 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngIf="genreTemplate !== undefined && grouppedData.genres.length > 0">
|
||||||
|
<li class="list-group-item section-header"><h5>Genres</h5></li>
|
||||||
|
<ul class="list-group results">
|
||||||
|
<li *ngFor="let option of grouppedData.genres; let index = index;" (click)="handleResultlick(option)" tabindex="0"
|
||||||
|
class="list-group-item" role="option">
|
||||||
|
<ng-container [ngTemplateOutlet]="genreTemplate" [ngTemplateOutletContext]="{ $implicit: option, idx: index }"></ng-container>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
<ng-container *ngIf="tagTemplate !== undefined && grouppedData.tags.length > 0">
|
<ng-container *ngIf="tagTemplate !== undefined && grouppedData.tags.length > 0">
|
||||||
<li class="list-group-item section-header"><h5>Tags</h5></li>
|
<li class="list-group-item section-header"><h5>Tags</h5></li>
|
||||||
<ul class="list-group results">
|
<ul class="list-group results">
|
||||||
@ -46,7 +56,7 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container *ngIf="personTemplate !== undefined && grouppedData.persons.length > 0">
|
<ng-container *ngIf="personTemplate !== undefined && grouppedData.persons.length > 0">
|
||||||
<li class="list-group-item section-header"><h5>Tags</h5></li>
|
<li class="list-group-item section-header"><h5>People</h5></li>
|
||||||
<ul class="list-group results">
|
<ul class="list-group results">
|
||||||
<li *ngFor="let option of grouppedData.persons; let index = index;" (click)="handleResultlick(option)" tabindex="0"
|
<li *ngFor="let option of grouppedData.persons; let index = index;" (click)="handleResultlick(option)" tabindex="0"
|
||||||
class="list-group-item" role="option">
|
class="list-group-item" role="option">
|
||||||
|
@ -15,8 +15,10 @@ input {
|
|||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.typeahead-input {
|
.typeahead-input {
|
||||||
border: 1px solid #ccc;
|
border: 1px solid transparent;
|
||||||
|
border-radius: 4px;
|
||||||
padding: 0px 6px;
|
padding: 0px 6px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@ -24,7 +26,6 @@ input {
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
border-radius: 4px;
|
|
||||||
cursor: text;
|
cursor: text;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
min-height: 38px;
|
min-height: 38px;
|
||||||
@ -32,6 +33,7 @@ input {
|
|||||||
transition-duration: 0.3s;
|
transition-duration: 0.3s;
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
|
|
||||||
.close {
|
.close {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -67,6 +69,7 @@ input {
|
|||||||
|
|
||||||
.typeahead-input.focused {
|
.typeahead-input.focused {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
border-color: #ccc;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* small devices (phones, 650px and down) */
|
/* small devices (phones, 650px and down) */
|
||||||
@ -117,7 +120,6 @@ input {
|
|||||||
flex: auto;
|
flex: auto;
|
||||||
max-height: calc(100vh - 58px);
|
max-height: calc(100vh - 58px);
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
//background-color: colors.$dark-bg-color;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-group.results {
|
.list-group.results {
|
||||||
@ -149,13 +151,34 @@ ul ul {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-header {
|
::ng-deep .bg-dark {
|
||||||
background: colors.$dark-item-accent-bg;
|
& .section-header {
|
||||||
cursor: default;
|
|
||||||
|
background: colors.$dark-item-accent-bg;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .section-header:hover {
|
||||||
|
background-color: colors.$dark-item-accent-bg !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-header:hover {
|
::ng-deep .bg-light {
|
||||||
background-color: colors.$dark-item-accent-bg !important;
|
& .section-header {
|
||||||
|
|
||||||
|
background: colors.$white-item-accent-bg;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .section-header:hover, .list-group-item.section-header:hover {
|
||||||
|
background: colors.$white-item-accent-bg !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .list-group-item:hover {
|
||||||
|
background-color: colors.$primary-color !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.spinner-border {
|
.spinner-border {
|
||||||
|
@ -56,6 +56,7 @@ export class GroupedTypeaheadComponent implements OnInit, OnDestroy {
|
|||||||
@ContentChild('collectionTemplate') collectionTemplate: TemplateRef<any> | undefined;
|
@ContentChild('collectionTemplate') collectionTemplate: TemplateRef<any> | undefined;
|
||||||
@ContentChild('tagTemplate') tagTemplate: TemplateRef<any> | undefined;
|
@ContentChild('tagTemplate') tagTemplate: TemplateRef<any> | undefined;
|
||||||
@ContentChild('personTemplate') personTemplate: TemplateRef<any> | undefined;
|
@ContentChild('personTemplate') personTemplate: TemplateRef<any> | undefined;
|
||||||
|
@ContentChild('genreTemplate') genreTemplate!: TemplateRef<any>;
|
||||||
@ContentChild('noResultsTemplate') noResultsTemplate!: TemplateRef<any>;
|
@ContentChild('noResultsTemplate') noResultsTemplate!: TemplateRef<any>;
|
||||||
|
|
||||||
|
|
||||||
@ -147,6 +148,7 @@ export class GroupedTypeaheadComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resetField() {
|
resetField() {
|
||||||
|
this.prevSearchTerm = '';
|
||||||
this.typeaheadForm.get('typeahead')?.setValue(this.initialValue);
|
this.typeaheadForm.get('typeahead')?.setValue(this.initialValue);
|
||||||
this.clearField.emit();
|
this.clearField.emit();
|
||||||
}
|
}
|
||||||
@ -159,6 +161,9 @@ export class GroupedTypeaheadComponent implements OnInit, OnDestroy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (this.searchTerm === '') {
|
||||||
|
this.resetField();
|
||||||
|
}
|
||||||
this.hasFocus = false;
|
this.hasFocus = false;
|
||||||
this.focusChanged.emit(this.hasFocus);
|
this.focusChanged.emit(this.hasFocus);
|
||||||
}
|
}
|
||||||
@ -169,7 +174,8 @@ export class GroupedTypeaheadComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public clear() {
|
public clear() {
|
||||||
this.resetField();
|
this.prevSearchTerm = '';
|
||||||
|
this.typeaheadForm.get('typeahead')?.setValue(this.initialValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
>
|
>
|
||||||
|
|
||||||
<ng-template #seriesTemplate let-item>
|
<ng-template #seriesTemplate let-item>
|
||||||
<div style="display: flex;padding: 5px;" (click)="clickSearchResult(item)">
|
<div style="display: flex;padding: 5px;" (click)="clickSeriesSearchResult(item)">
|
||||||
<div style="width: 24px" class="mr-1">
|
<div style="width: 24px" class="mr-1">
|
||||||
<app-image class="mr-3 search-result" width="24px" [imageUrl]="imageService.getSeriesCoverImage(item.seriesId)"></app-image>
|
<app-image class="mr-3 search-result" width="24px" [imageUrl]="imageService.getSeriesCoverImage(item.seriesId)"></app-image>
|
||||||
</div>
|
</div>
|
||||||
@ -39,7 +39,7 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template #collectionTemplate let-item>
|
<ng-template #collectionTemplate let-item>
|
||||||
<div style="display: flex;padding: 5px;" (click)="goToPerson(item.role, item.id)">
|
<div style="display: flex;padding: 5px;" (click)="clickCollectionSearchResult(item)">
|
||||||
<div style="width: 24px" class="mr-1">
|
<div style="width: 24px" class="mr-1">
|
||||||
<app-image class="mr-3 search-result" width="24px" [imageUrl]="imageService.getCollectionCoverImage(item.id)"></app-image>
|
<app-image class="mr-3 search-result" width="24px" [imageUrl]="imageService.getCollectionCoverImage(item.id)"></app-image>
|
||||||
</div>
|
</div>
|
||||||
@ -50,7 +50,7 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template #tagTemplate let-item>
|
<ng-template #tagTemplate let-item>
|
||||||
<div style="display: flex;padding: 5px;" (click)="goTo('tag', item.id)">
|
<div style="display: flex;padding: 5px;" (click)="goTo('tags', item.id)">
|
||||||
<div class="ml-1">
|
<div class="ml-1">
|
||||||
<span *ngIf="item.title.toLowerCase().trim().indexOf(searchTerm) >= 0" [innerHTML]="item.title"></span>
|
<span *ngIf="item.title.toLowerCase().trim().indexOf(searchTerm) >= 0" [innerHTML]="item.title"></span>
|
||||||
</div>
|
</div>
|
||||||
@ -58,7 +58,7 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template #personTemplate let-item>
|
<ng-template #personTemplate let-item>
|
||||||
<div style="display: flex;padding: 5px;" class="clickable" (click)="goTo('genres', item.id)">
|
<div style="display: flex;padding: 5px;" class="clickable" (click)="goToPerson(item.role, item.id)">
|
||||||
<div class="ml-1">
|
<div class="ml-1">
|
||||||
|
|
||||||
<div [innerHTML]="item.name"></div>
|
<div [innerHTML]="item.name"></div>
|
||||||
@ -67,6 +67,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template #genreTemplate let-item>
|
||||||
|
<div style="display: flex;padding: 5px;" class="clickable" (click)="goTo('genres', item.id)">
|
||||||
|
<div class="ml-1">
|
||||||
|
<div [innerHTML]="item.title"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
<ng-template #noResultsTemplate let-notFound>
|
<ng-template #noResultsTemplate let-notFound>
|
||||||
No results found
|
No results found
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
@ -3,8 +3,8 @@ import { Component, HostListener, Inject, OnDestroy, OnInit, ViewChild } from '@
|
|||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { takeUntil } from 'rxjs/operators';
|
import { takeUntil } from 'rxjs/operators';
|
||||||
import { isTemplateSpan } from 'typescript';
|
|
||||||
import { ScrollService } from '../scroll.service';
|
import { ScrollService } from '../scroll.service';
|
||||||
|
import { CollectionTag } from '../_models/collection-tag';
|
||||||
import { PersonRole } from '../_models/person';
|
import { PersonRole } from '../_models/person';
|
||||||
import { SearchResult } from '../_models/search-result';
|
import { SearchResult } from '../_models/search-result';
|
||||||
import { SearchResultGroup } from '../_models/search/search-result-group';
|
import { SearchResultGroup } from '../_models/search/search-result-group';
|
||||||
@ -104,11 +104,13 @@ export class NavHeaderComponent implements OnInit, OnDestroy {
|
|||||||
let params: any = {};
|
let params: any = {};
|
||||||
params[queryParamName] = filter;
|
params[queryParamName] = filter;
|
||||||
params['page'] = 1;
|
params['page'] = 1;
|
||||||
|
this.clearSearch();
|
||||||
this.router.navigate(['all-series'], {queryParams: params});
|
this.router.navigate(['all-series'], {queryParams: params});
|
||||||
}
|
}
|
||||||
|
|
||||||
goToPerson(role: PersonRole, filter: any) {
|
goToPerson(role: PersonRole, filter: any) {
|
||||||
// TODO: Move this to utility service
|
// TODO: Move this to utility service
|
||||||
|
this.clearSearch();
|
||||||
switch(role) {
|
switch(role) {
|
||||||
case PersonRole.Artist:
|
case PersonRole.Artist:
|
||||||
this.goTo('artist', filter);
|
this.goTo('artist', filter);
|
||||||
@ -147,19 +149,24 @@ export class NavHeaderComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
clearSearch() {
|
clearSearch() {
|
||||||
|
this.searchViewRef.clear();
|
||||||
|
this.searchTerm = '';
|
||||||
this.searchResults = new SearchResultGroup();
|
this.searchResults = new SearchResultGroup();
|
||||||
}
|
}
|
||||||
|
|
||||||
clickSearchResult(item: SearchResult) {
|
clickSeriesSearchResult(item: SearchResult) {
|
||||||
console.log('Click occured');
|
this.clearSearch();
|
||||||
const libraryId = item.libraryId;
|
const libraryId = item.libraryId;
|
||||||
const seriesId = item.seriesId;
|
const seriesId = item.seriesId;
|
||||||
this.searchViewRef.clear();
|
|
||||||
this.searchResults.reset();
|
|
||||||
this.searchTerm = '';
|
|
||||||
this.router.navigate(['library', libraryId, 'series', seriesId]);
|
this.router.navigate(['library', libraryId, 'series', seriesId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clickCollectionSearchResult(item: CollectionTag) {
|
||||||
|
this.clearSearch();
|
||||||
|
this.router.navigate(['collections', item.id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
scrollToTop() {
|
scrollToTop() {
|
||||||
window.scroll({
|
window.scroll({
|
||||||
top: 0,
|
top: 0,
|
||||||
@ -168,7 +175,6 @@ export class NavHeaderComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
focusUpdate(searchFocused: boolean) {
|
focusUpdate(searchFocused: boolean) {
|
||||||
console.log('search has focus', searchFocused);
|
|
||||||
this.searchFocused = searchFocused
|
this.searchFocused = searchFocused
|
||||||
return searchFocused;
|
return searchFocused;
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ export class PersonRolePipe implements PipeTransform {
|
|||||||
case PersonRole.Artist: return 'Artist';
|
case PersonRole.Artist: return 'Artist';
|
||||||
case PersonRole.Character: return 'Character';
|
case PersonRole.Character: return 'Character';
|
||||||
case PersonRole.Colorist: return 'Colorist';
|
case PersonRole.Colorist: return 'Colorist';
|
||||||
case PersonRole.CoverArtist: return 'CoverArtist';
|
case PersonRole.CoverArtist: return 'Cover Artist';
|
||||||
case PersonRole.Editor: return 'Editor';
|
case PersonRole.Editor: return 'Editor';
|
||||||
case PersonRole.Inker: return 'Inker';
|
case PersonRole.Inker: return 'Inker';
|
||||||
case PersonRole.Letterer: return 'Letterer';
|
case PersonRole.Letterer: return 'Letterer';
|
||||||
|
@ -22,7 +22,7 @@ export class TypeaheadSettings<T> {
|
|||||||
*/
|
*/
|
||||||
savedData!: T[] | T;
|
savedData!: T[] | T;
|
||||||
/**
|
/**
|
||||||
* Function to compare the elements. Should return all elements that fit the matching criteria.
|
* Function to compare the elements. Should return all elements that fit the matching criteria. This is only used with non-Observable based fetchFn, but must be defined for all uses of typeahead (TODO)
|
||||||
*/
|
*/
|
||||||
compareFn!: ((optionList: T[], filter: string) => T[]);
|
compareFn!: ((optionList: T[], filter: string) => T[]);
|
||||||
/**
|
/**
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
<li *ngFor="let series of series" class="list-group-item">
|
<li *ngFor="let series of series" class="list-group-item">
|
||||||
<div>
|
<div>
|
||||||
<h4>
|
<h4>
|
||||||
<a id="series--{{series.name}}" href="/library/{{series.libraryId}}/series/{{series.id}}" >{{series.name | titlecase}}</a>
|
<a id="series--{{series.name}}" href="javascript:void(0);" (click)="viewBookmarks(series)">{{series.name | titlecase}}</a>
|
||||||
<span class="badge badge-secondary badge-pill">{{getBookmarkPages(series.id)}}</span>
|
<span class="badge badge-secondary badge-pill">{{getBookmarkPages(series.id)}}</span>
|
||||||
<div class="float-right">
|
<div class="float-right">
|
||||||
<button attr.aria-labelledby="series--{{series.name}}" class="btn btn-danger mr-2 btn-sm" (click)="clearBookmarks(series)" [disabled]="clearingSeries[series.id]" placement="top" ngbTooltip="Clear Bookmarks" attr.aria-label="Clear Bookmarks">
|
<button attr.aria-labelledby="series--{{series.name}}" class="btn btn-danger mr-2 btn-sm" (click)="clearBookmarks(series)" [disabled]="clearingSeries[series.id]" placement="top" ngbTooltip="Clear Bookmarks" attr.aria-label="Clear Bookmarks">
|
||||||
@ -26,8 +26,8 @@
|
|||||||
<i class="fa fa-arrow-alt-circle-down" aria-hidden="true"></i>
|
<i class="fa fa-arrow-alt-circle-down" aria-hidden="true"></i>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</button>
|
</button>
|
||||||
<button attr.aria-labelledby="series--{{series.name}}" class="btn btn-primary mr-2 btn-sm" (click)="viewBookmarks(series)" placement="top" ngbTooltip="View Bookmarks" attr.aria-label="View Bookmarks">
|
<button attr.aria-labelledby="series--{{series.name}}" class="btn btn-primary mr-2 btn-sm" routerLink="/library/{{series.libraryId}}/series/{{series.id}}" placement="top" ngbTooltip="Open Series" attr.aria-label="Open Series">
|
||||||
<i class="fa fa-pen" title="View Bookmarks"></i>
|
<i class="fa fa-eye" title="Open Series"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</h4>
|
</h4>
|
||||||
|
@ -29,6 +29,8 @@
|
|||||||
color: $dark-primary-color;
|
color: $dark-primary-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.accent {
|
.accent {
|
||||||
background-color: $dark-form-background !important;
|
background-color: $dark-form-background !important;
|
||||||
@ -175,6 +177,10 @@
|
|||||||
background-color: $dark-card-color;
|
background-color: $dark-card-color;
|
||||||
color: $dark-text-color;
|
color: $dark-text-color;
|
||||||
border-color: $dark-form-border;
|
border-color: $dark-form-border;
|
||||||
|
|
||||||
|
div[role="tabpanel"] {
|
||||||
|
background-color: rgba(52, 60, 70, 0.5); // This is a good accent color
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
|
@ -16,6 +16,9 @@ $dark-form-readonly: #434648;
|
|||||||
$dark-item-accent-bg: #292d32;
|
$dark-item-accent-bg: #292d32;
|
||||||
|
|
||||||
|
|
||||||
|
$white-item-accent-bg: rgba(247, 247, 247, 1);
|
||||||
|
|
||||||
|
|
||||||
//=========================
|
//=========================
|
||||||
// Ratings
|
// Ratings
|
||||||
//=========================
|
//=========================
|
||||||
@ -29,6 +32,7 @@ $rating-empty: #b0c4de;
|
|||||||
// --drawer-background-color: #FFF;
|
// --drawer-background-color: #FFF;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
|
||||||
$theme-colors: (
|
$theme-colors: (
|
||||||
"primary": $primary-color,
|
"primary": $primary-color,
|
||||||
"danger": $error-color
|
"danger": $error-color
|
||||||
|
Loading…
x
Reference in New Issue
Block a user