diff --git a/API/Controllers/BookController.cs b/API/Controllers/BookController.cs index 2bca8bb2f..521c763d4 100644 --- a/API/Controllers/BookController.cs +++ b/API/Controllers/BookController.cs @@ -77,11 +77,16 @@ namespace API.Controllers return File(content, contentType, $"{chapterId}-{file}"); } + /// + /// 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 + /// + /// This is essentially building the table of contents + /// + /// [HttpGet("{chapterId}/chapters")] public async Task>> 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); using var book = await EpubReader.OpenBookAsync(chapter.Files.ElementAt(0).FilePath); 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")); 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 content = await book.Content.Html[tocPage].ReadContentAsync(); doc.LoadHtml(content); @@ -252,7 +257,7 @@ namespace API.Controllers 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); foreach (var error in doc.ParseErrors) diff --git a/API/Controllers/ServerController.cs b/API/Controllers/ServerController.cs index 0f2124d06..8b5cc80c8 100644 --- a/API/Controllers/ServerController.cs +++ b/API/Controllers/ServerController.cs @@ -25,22 +25,20 @@ namespace API.Controllers private readonly IConfiguration _config; private readonly IBackupService _backupService; private readonly IArchiveService _archiveService; - private readonly ICacheService _cacheService; private readonly IVersionUpdaterService _versionUpdaterService; private readonly IStatsService _statsService; private readonly ICleanupService _cleanupService; private readonly IEmailService _emailService; public ServerController(IHostApplicationLifetime applicationLifetime, ILogger logger, IConfiguration config, - IBackupService backupService, IArchiveService archiveService, ICacheService cacheService, - IVersionUpdaterService versionUpdaterService, IStatsService statsService, ICleanupService cleanupService, IEmailService emailService) + IBackupService backupService, IArchiveService archiveService, IVersionUpdaterService versionUpdaterService, IStatsService statsService, + ICleanupService cleanupService, IEmailService emailService) { _applicationLifetime = applicationLifetime; _logger = logger; _config = config; _backupService = backupService; _archiveService = archiveService; - _cacheService = cacheService; _versionUpdaterService = versionUpdaterService; _statsService = statsService; _cleanupService = cleanupService; @@ -111,6 +109,9 @@ namespace API.Controllers } } + /// + /// Checks for updates, if no updates that are > current version installed, returns null + /// [HttpGet("check-update")] public async Task> CheckForUpdates() { diff --git a/API/Data/Repositories/SeriesRepository.cs b/API/Data/Repositories/SeriesRepository.cs index 4fa0690aa..1aad6b349 100644 --- a/API/Data/Repositories/SeriesRepository.cs +++ b/API/Data/Repositories/SeriesRepository.cs @@ -240,11 +240,6 @@ public class SeriesRepository : ISeriesRepository { var query = await CreateFilteredSearchQueryable(userId, libraryId, filter); - if (filter.SortOptions == null) - { - query = query.OrderBy(s => s.SortName); - } - var retSeries = query .ProjectTo(_mapper.ConfigurationProvider) .AsSplitQuery() @@ -314,6 +309,7 @@ public class SeriesRepository : ISeriesRepository .Where(sm => seriesIds.Contains(sm.SeriesId)) .SelectMany(sm => sm.People.Where(t => EF.Functions.Like(t.Name, $"%{searchQuery}%"))) .AsSplitQuery() + .Distinct() .ProjectTo(_mapper.ConfigurationProvider) .ToListAsync(); diff --git a/API/Services/BookService.cs b/API/Services/BookService.cs index 5d40437d3..0b59e109d 100644 --- a/API/Services/BookService.cs +++ b/API/Services/BookService.cs @@ -458,6 +458,11 @@ namespace API.Services return content; } + /// + /// Removes the leading ../ + /// + /// + /// public static string CleanContentKeys(string key) { return key.Replace("../", string.Empty); diff --git a/API/Services/Tasks/VersionUpdaterService.cs b/API/Services/Tasks/VersionUpdaterService.cs index 4b341ba36..255d0b105 100644 --- a/API/Services/Tasks/VersionUpdaterService.cs +++ b/API/Services/Tasks/VersionUpdaterService.cs @@ -57,8 +57,8 @@ public class VersionUpdaterService : IVersionUpdaterService private readonly IPresenceTracker _tracker; private readonly Markdown _markdown = new MarkdownDeep.Markdown(); #pragma warning disable S1075 - private static readonly 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 GithubLatestReleasesUrl = "https://api.github.com/repos/Kareadita/Kavita/releases/latest"; + private const string GithubAllReleasesUrl = "https://api.github.com/repos/Kareadita/Kavita/releases"; #pragma warning restore S1075 public VersionUpdaterService(ILogger logger, IHubContext messageHub, IPresenceTracker tracker) @@ -76,10 +76,12 @@ public class VersionUpdaterService : IVersionUpdaterService /// /// Fetches the latest release from Github /// - public async Task CheckForUpdate() + /// Latest update or null if current version is greater than latest update + public async Task CheckForUpdate() { 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> GetAllReleases() diff --git a/API/Services/TokenService.cs b/API/Services/TokenService.cs index ffd3c9429..4b734e8b9 100644 --- a/API/Services/TokenService.cs +++ b/API/Services/TokenService.cs @@ -50,7 +50,7 @@ public class TokenService : ITokenService var tokenDescriptor = new SecurityTokenDescriptor() { Subject = new ClaimsIdentity(claims), - Expires = DateTime.Now.AddDays(7), + Expires = DateTime.Now.AddDays(14), SigningCredentials = creds }; diff --git a/Logo/hosting-sponsor.png b/Logo/hosting-sponsor.png new file mode 100644 index 000000000..81c0b3d78 Binary files /dev/null and b/Logo/hosting-sponsor.png differ diff --git a/README.md b/README.md index d32b48c26..55570e192 100644 --- a/README.md +++ b/README.md @@ -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] Dark and Light themes - [ ] 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 - [ ] Ability to sync ratings and reviews to external services - [x] Fully Accessible with active accessibility audits @@ -116,8 +116,12 @@ Thank you to [ JetBrains](http: * [ Rider](http://www.jetbrains.com/rider/) * [ dotTrace](http://www.jetbrains.com/dottrace/) +## Palace-Designs +We would like to extend a big thank you to [](https://www.palace-designs.com/) who hosts our infrastructure pro-bono. + + ### License * [GNU GPL v3](http://www.gnu.org/licenses/gpl.html) -* Copyright 2020-2021 +* Copyright 2020-2022 diff --git a/UI/Web/src/app/_services/metadata.service.ts b/UI/Web/src/app/_services/metadata.service.ts index 4d64a7920..97d39624b 100644 --- a/UI/Web/src/app/_services/metadata.service.ts +++ b/UI/Web/src/app/_services/metadata.service.ts @@ -42,7 +42,7 @@ export class MetadataService { if (libraries != undefined && libraries.length > 0) { method += '?libraryIds=' + libraries.join(','); } - return this.httpClient.get>(this.baseUrl + method);; + return this.httpClient.get>(this.baseUrl + method); } getAllPublicationStatus(libraries?: Array) { @@ -50,7 +50,7 @@ export class MetadataService { if (libraries != undefined && libraries.length > 0) { method += '?libraryIds=' + libraries.join(','); } - return this.httpClient.get>(this.baseUrl + method);; + return this.httpClient.get>(this.baseUrl + method); } getAllTags(libraries?: Array) { @@ -58,7 +58,7 @@ export class MetadataService { if (libraries != undefined && libraries.length > 0) { method += '?libraryIds=' + libraries.join(','); } - return this.httpClient.get>(this.baseUrl + method);; + return this.httpClient.get>(this.baseUrl + method); } getAllGenres(libraries?: Array) { @@ -66,7 +66,7 @@ export class MetadataService { if (libraries != undefined && libraries.length > 0) { method += '?libraryIds=' + libraries.join(','); } - return this.httpClient.get(this.baseUrl + method); + return this.httpClient.get>(this.baseUrl + method); } getAllLanguages(libraries?: Array) { @@ -74,7 +74,7 @@ export class MetadataService { if (libraries != undefined && libraries.length > 0) { method += '?libraryIds=' + libraries.join(','); } - return this.httpClient.get(this.baseUrl + method); + return this.httpClient.get>(this.baseUrl + method); } getAllPeople(libraries?: Array) { @@ -82,6 +82,6 @@ export class MetadataService { if (libraries != undefined && libraries.length > 0) { method += '?libraryIds=' + libraries.join(','); } - return this.httpClient.get(this.baseUrl + method); + return this.httpClient.get>(this.baseUrl + method); } } diff --git a/UI/Web/src/app/book-reader/book-reader/book-reader.component.ts b/UI/Web/src/app/book-reader/book-reader/book-reader.component.ts index be34ac510..0e474968a 100644 --- a/UI/Web/src/app/book-reader/book-reader/book-reader.component.ts +++ b/UI/Web/src/app/book-reader/book-reader/book-reader.component.ts @@ -615,7 +615,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { let margin = '15%'; if (windowWidth <= 700) { - margin = '0%'; + margin = '5%'; } if (this.user) { if (windowWidth > 700) { diff --git a/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.html b/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.html index ae5d6c414..19626e296 100644 --- a/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.html +++ b/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.html @@ -5,7 +5,6 @@   {{header}}  - {{pagination.totalItems}} diff --git a/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.ts b/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.ts index ab3ec7df6..79e9be68e 100644 --- a/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.ts +++ b/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.ts @@ -367,7 +367,7 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy { }; this.collectionSettings.compareFn = (options: CollectionTag[], filter: string) => { 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) { diff --git a/UI/Web/src/app/grouped-typeahead/grouped-typeahead.component.html b/UI/Web/src/app/grouped-typeahead/grouped-typeahead.component.html index f9871106d..264d7e732 100644 --- a/UI/Web/src/app/grouped-typeahead/grouped-typeahead.component.html +++ b/UI/Web/src/app/grouped-typeahead/grouped-typeahead.component.html @@ -35,6 +35,16 @@ + +
  • Genres
  • +
      +
    • + +
    • +
    +
    +
  • Tags
    • @@ -46,7 +56,7 @@ -
    • Tags
    • +
    • People
      • diff --git a/UI/Web/src/app/grouped-typeahead/grouped-typeahead.component.scss b/UI/Web/src/app/grouped-typeahead/grouped-typeahead.component.scss index b3e7d3021..dec2c2f3b 100644 --- a/UI/Web/src/app/grouped-typeahead/grouped-typeahead.component.scss +++ b/UI/Web/src/app/grouped-typeahead/grouped-typeahead.component.scss @@ -15,8 +15,10 @@ input { width: 100% !important; } + .typeahead-input { - border: 1px solid #ccc; + border: 1px solid transparent; + border-radius: 4px; padding: 0px 6px; display: inline-block; overflow: hidden; @@ -24,7 +26,6 @@ input { z-index: 1; box-sizing: border-box; box-shadow: none; - border-radius: 4px; cursor: text; background-color: #fff; min-height: 38px; @@ -32,6 +33,7 @@ input { transition-duration: 0.3s; display: block; + .close { cursor: pointer; position: absolute; @@ -67,6 +69,7 @@ input { .typeahead-input.focused { width: 100%; + border-color: #ccc; } /* small devices (phones, 650px and down) */ @@ -117,7 +120,6 @@ input { flex: auto; max-height: calc(100vh - 58px); height: fit-content; - //background-color: colors.$dark-bg-color; } .list-group.results { @@ -149,13 +151,34 @@ ul ul { cursor: pointer; } -.section-header { - background: colors.$dark-item-accent-bg; - cursor: default; +::ng-deep .bg-dark { + & .section-header { + + background: colors.$dark-item-accent-bg; + cursor: default; + } + + & .section-header:hover { + background-color: colors.$dark-item-accent-bg !important; + } } -.section-header:hover { - background-color: colors.$dark-item-accent-bg !important; +::ng-deep .bg-light { + & .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 { diff --git a/UI/Web/src/app/grouped-typeahead/grouped-typeahead.component.ts b/UI/Web/src/app/grouped-typeahead/grouped-typeahead.component.ts index 385c0d87e..030a6e720 100644 --- a/UI/Web/src/app/grouped-typeahead/grouped-typeahead.component.ts +++ b/UI/Web/src/app/grouped-typeahead/grouped-typeahead.component.ts @@ -56,6 +56,7 @@ export class GroupedTypeaheadComponent implements OnInit, OnDestroy { @ContentChild('collectionTemplate') collectionTemplate: TemplateRef | undefined; @ContentChild('tagTemplate') tagTemplate: TemplateRef | undefined; @ContentChild('personTemplate') personTemplate: TemplateRef | undefined; + @ContentChild('genreTemplate') genreTemplate!: TemplateRef; @ContentChild('noResultsTemplate') noResultsTemplate!: TemplateRef; @@ -147,6 +148,7 @@ export class GroupedTypeaheadComponent implements OnInit, OnDestroy { } resetField() { + this.prevSearchTerm = ''; this.typeaheadForm.get('typeahead')?.setValue(this.initialValue); this.clearField.emit(); } @@ -159,6 +161,9 @@ export class GroupedTypeaheadComponent implements OnInit, OnDestroy { return; } } + if (this.searchTerm === '') { + this.resetField(); + } this.hasFocus = false; this.focusChanged.emit(this.hasFocus); } @@ -169,7 +174,8 @@ export class GroupedTypeaheadComponent implements OnInit, OnDestroy { } public clear() { - this.resetField(); + this.prevSearchTerm = ''; + this.typeaheadForm.get('typeahead')?.setValue(this.initialValue); } } diff --git a/UI/Web/src/app/nav-header/nav-header.component.html b/UI/Web/src/app/nav-header/nav-header.component.html index 9258dff24..56c5d2518 100644 --- a/UI/Web/src/app/nav-header/nav-header.component.html +++ b/UI/Web/src/app/nav-header/nav-header.component.html @@ -23,7 +23,7 @@ > -
        +
        @@ -39,7 +39,7 @@ -
        +
        @@ -50,7 +50,7 @@ -
        +
        @@ -58,7 +58,7 @@ -
        +
        @@ -67,6 +67,14 @@
        + +
        +
        +
        +
        +
        +
        + No results found diff --git a/UI/Web/src/app/nav-header/nav-header.component.ts b/UI/Web/src/app/nav-header/nav-header.component.ts index 4a8e459e1..ebf7318a9 100644 --- a/UI/Web/src/app/nav-header/nav-header.component.ts +++ b/UI/Web/src/app/nav-header/nav-header.component.ts @@ -3,8 +3,8 @@ import { Component, HostListener, Inject, OnDestroy, OnInit, ViewChild } from '@ import { Router } from '@angular/router'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; -import { isTemplateSpan } from 'typescript'; import { ScrollService } from '../scroll.service'; +import { CollectionTag } from '../_models/collection-tag'; import { PersonRole } from '../_models/person'; import { SearchResult } from '../_models/search-result'; import { SearchResultGroup } from '../_models/search/search-result-group'; @@ -104,11 +104,13 @@ export class NavHeaderComponent implements OnInit, OnDestroy { let params: any = {}; params[queryParamName] = filter; params['page'] = 1; + this.clearSearch(); this.router.navigate(['all-series'], {queryParams: params}); } goToPerson(role: PersonRole, filter: any) { // TODO: Move this to utility service + this.clearSearch(); switch(role) { case PersonRole.Artist: this.goTo('artist', filter); @@ -147,19 +149,24 @@ export class NavHeaderComponent implements OnInit, OnDestroy { } clearSearch() { + this.searchViewRef.clear(); + this.searchTerm = ''; this.searchResults = new SearchResultGroup(); } - clickSearchResult(item: SearchResult) { - console.log('Click occured'); + clickSeriesSearchResult(item: SearchResult) { + this.clearSearch(); const libraryId = item.libraryId; const seriesId = item.seriesId; - this.searchViewRef.clear(); - this.searchResults.reset(); - this.searchTerm = ''; this.router.navigate(['library', libraryId, 'series', seriesId]); } + clickCollectionSearchResult(item: CollectionTag) { + this.clearSearch(); + this.router.navigate(['collections', item.id]); + } + + scrollToTop() { window.scroll({ top: 0, @@ -168,7 +175,6 @@ export class NavHeaderComponent implements OnInit, OnDestroy { } focusUpdate(searchFocused: boolean) { - console.log('search has focus', searchFocused); this.searchFocused = searchFocused return searchFocused; } diff --git a/UI/Web/src/app/person-role.pipe.ts b/UI/Web/src/app/person-role.pipe.ts index 69190fcf6..9559e4ad6 100644 --- a/UI/Web/src/app/person-role.pipe.ts +++ b/UI/Web/src/app/person-role.pipe.ts @@ -11,7 +11,7 @@ export class PersonRolePipe implements PipeTransform { case PersonRole.Artist: return 'Artist'; case PersonRole.Character: return 'Character'; case PersonRole.Colorist: return 'Colorist'; - case PersonRole.CoverArtist: return 'CoverArtist'; + case PersonRole.CoverArtist: return 'Cover Artist'; case PersonRole.Editor: return 'Editor'; case PersonRole.Inker: return 'Inker'; case PersonRole.Letterer: return 'Letterer'; diff --git a/UI/Web/src/app/typeahead/typeahead-settings.ts b/UI/Web/src/app/typeahead/typeahead-settings.ts index 9fc3b28e8..7e531bf84 100644 --- a/UI/Web/src/app/typeahead/typeahead-settings.ts +++ b/UI/Web/src/app/typeahead/typeahead-settings.ts @@ -22,7 +22,7 @@ export class TypeaheadSettings { */ 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[]); /** diff --git a/UI/Web/src/app/user-settings/series-bookmarks/series-bookmarks.component.html b/UI/Web/src/app/user-settings/series-bookmarks/series-bookmarks.component.html index 334110032..047b14381 100644 --- a/UI/Web/src/app/user-settings/series-bookmarks/series-bookmarks.component.html +++ b/UI/Web/src/app/user-settings/series-bookmarks/series-bookmarks.component.html @@ -5,7 +5,7 @@
      • - {{series.name | titlecase}} + {{series.name | titlecase}}  {{getBookmarkPages(series.id)}}
        -

        diff --git a/UI/Web/src/assets/themes/dark.scss b/UI/Web/src/assets/themes/dark.scss index 8a5545fa8..7f49264ca 100644 --- a/UI/Web/src/assets/themes/dark.scss +++ b/UI/Web/src/assets/themes/dark.scss @@ -29,6 +29,8 @@ color: $dark-primary-color; } + + .accent { background-color: $dark-form-background !important; @@ -175,6 +177,10 @@ background-color: $dark-card-color; color: $dark-text-color; border-color: $dark-form-border; + + div[role="tabpanel"] { + background-color: rgba(52, 60, 70, 0.5); // This is a good accent color + } } .section-title { diff --git a/UI/Web/src/theme/_colors.scss b/UI/Web/src/theme/_colors.scss index 8500c1cce..31fbdede7 100644 --- a/UI/Web/src/theme/_colors.scss +++ b/UI/Web/src/theme/_colors.scss @@ -16,6 +16,9 @@ $dark-form-readonly: #434648; $dark-item-accent-bg: #292d32; +$white-item-accent-bg: rgba(247, 247, 247, 1); + + //========================= // Ratings //========================= @@ -29,6 +32,7 @@ $rating-empty: #b0c4de; // --drawer-background-color: #FFF; // } + $theme-colors: ( "primary": $primary-color, "danger": $error-color