diff --git a/API/Controllers/CollectionController.cs b/API/Controllers/CollectionController.cs index 6f17e5ada..8bf10c09a 100644 --- a/API/Controllers/CollectionController.cs +++ b/API/Controllers/CollectionController.cs @@ -116,6 +116,8 @@ namespace API.Controllers _unitOfWork.CollectionTagRepository.Update(tag); } + tag.CoverImageLocked = updateSeriesForTagDto.Tag.CoverImageLocked; + if (!updateSeriesForTagDto.Tag.CoverImageLocked) { tag.CoverImageLocked = false; diff --git a/API/Controllers/SeriesController.cs b/API/Controllers/SeriesController.cs index d99e0f767..e00d99f40 100644 --- a/API/Controllers/SeriesController.cs +++ b/API/Controllers/SeriesController.cs @@ -242,6 +242,7 @@ namespace API.Controllers { var seriesId = updateSeriesMetadataDto.SeriesMetadata.SeriesId; var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId); + var allTags = (await _unitOfWork.CollectionTagRepository.GetAllTagsAsync()).ToList(); if (series.Metadata == null) { series.Metadata = DbFactory.SeriesMetadata(updateSeriesMetadataDto.Tags @@ -266,13 +267,13 @@ namespace API.Controllers // At this point, all tags that aren't in dto have been removed. foreach (var tag in updateSeriesMetadataDto.Tags) { - var existingTag = series.Metadata.CollectionTags.SingleOrDefault(t => t.Title == tag.Title); + var existingTag = allTags.SingleOrDefault(t => t.Title == tag.Title); if (existingTag != null) { - // Update existingTag - existingTag.Promoted = tag.Promoted; - existingTag.Title = tag.Title; - existingTag.NormalizedTitle = Parser.Parser.Normalize(tag.Title).ToUpper(); + if (!series.Metadata.CollectionTags.Any(t => t.Title == tag.Title)) + { + newTags.Add(existingTag); + } } else { diff --git a/API/Data/CollectionTagRepository.cs b/API/Data/CollectionTagRepository.cs index 7530ffdb2..b694b0bb8 100644 --- a/API/Data/CollectionTagRepository.cs +++ b/API/Data/CollectionTagRepository.cs @@ -45,6 +45,14 @@ namespace API.Data return await _context.SaveChangesAsync(); } + public async Task> GetAllTagsAsync() + { + return await _context.CollectionTag + .Select(c => c) + .OrderBy(c => c.NormalizedTitle) + .ToListAsync(); + } + public async Task> GetAllTagDtosAsync() { return await _context.CollectionTag diff --git a/API/Interfaces/ICollectionTagRepository.cs b/API/Interfaces/ICollectionTagRepository.cs index 234670e70..62f813c9d 100644 --- a/API/Interfaces/ICollectionTagRepository.cs +++ b/API/Interfaces/ICollectionTagRepository.cs @@ -16,5 +16,6 @@ namespace API.Interfaces Task GetFullTagAsync(int tagId); void Update(CollectionTag tag); Task RemoveTagsWithoutSeries(); + Task> GetAllTagsAsync(); } } diff --git a/API/Services/Tasks/StatsService.cs b/API/Services/Tasks/StatsService.cs index fc5e7b256..691d13f33 100644 --- a/API/Services/Tasks/StatsService.cs +++ b/API/Services/Tasks/StatsService.cs @@ -41,7 +41,7 @@ namespace API.Services.Tasks public async Task PathData(ClientInfoDto clientInfoDto) { - _logger.LogInformation("Pathing client data to the file"); + _logger.LogDebug("Pathing client data to the file"); var statisticsDto = await GetData(); @@ -52,12 +52,12 @@ namespace API.Services.Tasks public async Task CollectRelevantData() { - _logger.LogInformation("Collecting data from the server and database"); + _logger.LogDebug("Collecting data from the server and database"); - _logger.LogInformation("Collecting usage info"); + _logger.LogDebug("Collecting usage info"); var usageInfo = await GetUsageInfo(); - _logger.LogInformation("Collecting server info"); + _logger.LogDebug("Collecting server info"); var serverInfo = GetServerInfo(); await PathData(serverInfo, usageInfo); @@ -67,14 +67,14 @@ namespace API.Services.Tasks { try { - _logger.LogInformation("Finalizing Stats collection flow"); + _logger.LogDebug("Finalizing Stats collection flow"); var data = await GetExistingData(); - _logger.LogInformation("Sending data to the Stats server"); + _logger.LogDebug("Sending data to the Stats server"); await _client.SendDataToStatsServer(data); - _logger.LogInformation("Deleting the file from disk"); + _logger.LogDebug("Deleting the file from disk"); if (FileExists) File.Delete(FinalPath); } catch (Exception ex) @@ -92,7 +92,7 @@ namespace API.Services.Tasks private async Task PathData(ServerInfoDto serverInfoDto, UsageInfoDto usageInfoDto) { - _logger.LogInformation("Pathing server and usage info to the file"); + _logger.LogDebug("Pathing server and usage info to the file"); var data = await GetData(); @@ -169,19 +169,19 @@ namespace API.Services.Tasks private async Task SaveFile(UsageStatisticsDto statisticsDto) { - _logger.LogInformation("Saving file"); + _logger.LogDebug("Saving file"); var finalDirectory = FinalPath.Replace(TempFileName, string.Empty); if (!Directory.Exists(finalDirectory)) { - _logger.LogInformation("Creating tmp directory"); + _logger.LogDebug("Creating tmp directory"); Directory.CreateDirectory(finalDirectory); } - _logger.LogInformation("Serializing data to write"); + _logger.LogDebug("Serializing data to write"); var dataJson = JsonSerializer.Serialize(statisticsDto); - _logger.LogInformation("Writing file to the disk"); + _logger.LogDebug("Writing file to the disk"); await File.WriteAllTextAsync(FinalPath, dataJson); } } diff --git a/UI/Web/src/app/_services/action-factory.service.ts b/UI/Web/src/app/_services/action-factory.service.ts index 8ed3385cd..178e72400 100644 --- a/UI/Web/src/app/_services/action-factory.service.ts +++ b/UI/Web/src/app/_services/action-factory.service.ts @@ -2,6 +2,7 @@ import { Injectable } from '@angular/core'; import { Chapter } from '../_models/chapter'; import { CollectionTag } from '../_models/collection-tag'; import { Library } from '../_models/library'; +import { MangaFormat } from '../_models/manga-format'; import { Series } from '../_models/series'; import { Volume } from '../_models/volume'; import { AccountService } from './account.service'; @@ -156,6 +157,11 @@ export class ActionFactoryService { return this.collectionTagActions; } + filterBookmarksForFormat(action: ActionItem, series: Series) { + if (action.action === Action.Bookmarks && series?.format === MangaFormat.EPUB) return false; + return true; + } + dummyCallback(action: Action, data: any) {} _resetActions() { 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 209ff8388..0b6e35163 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 @@ -21,6 +21,7 @@ import { Stack } from 'src/app/shared/data-structures/stack'; import { Preferences } from 'src/app/_models/preferences/preferences'; import { MemberService } from 'src/app/_services/member.service'; import { ReadingDirection } from 'src/app/_models/preferences/reading-direction'; +import { ScrollService } from 'src/app/scroll.service'; interface PageStyle { @@ -149,7 +150,8 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { constructor(private route: ActivatedRoute, private router: Router, private accountService: AccountService, private seriesService: SeriesService, private readerService: ReaderService, private location: Location, private renderer: Renderer2, private navService: NavService, private toastr: ToastrService, - private domSanitizer: DomSanitizer, private bookService: BookService, private memberService: MemberService) { + private domSanitizer: DomSanitizer, private bookService: BookService, private memberService: MemberService, + private scrollService: ScrollService) { this.navService.hideNavBar(); this.darkModeStyleElem = this.renderer.createElement('style'); @@ -207,9 +209,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { if (this.isLoading) return; if (Object.keys(this.pageAnchors).length !== 0) { // get the height of the document so we can capture markers that are halfway on the document viewport - const verticalOffset = (window.pageYOffset - || document.documentElement.scrollTop - || document.body.scrollTop || 0) + (document.body.offsetHeight / 2); + const verticalOffset = this.scrollService.scrollPosition + (document.body.offsetHeight / 2); const alreadyReached = Object.values(this.pageAnchors).filter((i: number) => i <= verticalOffset); if (alreadyReached.length > 0) { @@ -518,15 +518,9 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { if (part !== undefined && part !== '') { this.scrollTo(part); } else if (scrollTop !== undefined && scrollTop !== 0) { - window.scroll({ - top: scrollTop, - behavior: 'smooth' - }); + this.scrollService.scrollTo(scrollTop); } else { - window.scroll({ - top: 0, - behavior: 'smooth' - }); + this.scrollService.scrollTo(0); } } @@ -754,10 +748,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { if (element === null) return; - window.scroll({ - top: element.getBoundingClientRect().top + window.pageYOffset + TOP_OFFSET, - behavior: 'smooth' - }); + this.scrollService.scrollTo(element.getBoundingClientRect().top + window.pageYOffset + TOP_OFFSET); } toggleClickToPaginate() { diff --git a/UI/Web/src/app/cards/series-card/series-card.component.ts b/UI/Web/src/app/cards/series-card/series-card.component.ts index 94f3b7be1..42c1ccfde 100644 --- a/UI/Web/src/app/cards/series-card/series-card.component.ts +++ b/UI/Web/src/app/cards/series-card/series-card.component.ts @@ -18,7 +18,7 @@ import { EditSeriesModalComponent } from '../_modals/edit-series-modal/edit-seri styleUrls: ['./series-card.component.scss'] }) export class SeriesCardComponent implements OnInit, OnChanges { - @Input() data: Series | undefined; + @Input() data!: Series; @Input() libraryId = 0; @Input() suppressLibraryLink = false; @Output() clicked = new EventEmitter(); @@ -50,7 +50,7 @@ export class SeriesCardComponent implements OnInit, OnChanges { ngOnChanges(changes: any) { if (this.data) { - this.actions = this.actionFactoryService.getSeriesActions((action: Action, series: Series) => this.handleSeriesActionCallback(action, series)); + this.actions = this.actionFactoryService.getSeriesActions((action: Action, series: Series) => this.handleSeriesActionCallback(action, series)).filter(action => this.actionFactoryService.filterBookmarksForFormat(action, this.data)); this.imageUrl = this.imageService.randomize(this.imageService.getSeriesCoverImage(this.data.id)); } } diff --git a/UI/Web/src/app/library/library.component.html b/UI/Web/src/app/library/library.component.html index e8e5ba1b9..c80c3223f 100644 --- a/UI/Web/src/app/library/library.component.html +++ b/UI/Web/src/app/library/library.component.html @@ -20,7 +20,7 @@ - + diff --git a/UI/Web/src/app/library/library.component.ts b/UI/Web/src/app/library/library.component.ts index e1d3a3982..f31e98445 100644 --- a/UI/Web/src/app/library/library.component.ts +++ b/UI/Web/src/app/library/library.component.ts @@ -5,6 +5,7 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { Subject } from 'rxjs'; import { take, takeUntil } from 'rxjs/operators'; import { EditCollectionTagsComponent } from '../cards/_modals/edit-collection-tags/edit-collection-tags.component'; +import { ScrollService } from '../scroll.service'; import { CollectionTag } from '../_models/collection-tag'; import { InProgressChapter } from '../_models/in-progress-chapter'; import { Library } from '../_models/library'; @@ -42,7 +43,8 @@ export class LibraryComponent implements OnInit, OnDestroy { constructor(public accountService: AccountService, private libraryService: LibraryService, private seriesService: SeriesService, private actionFactoryService: ActionFactoryService, private collectionService: CollectionTagService, private router: Router, - private modalService: NgbModal, private titleService: Title, public imageService: ImageService) { } + private modalService: NgbModal, private titleService: Title, public imageService: ImageService, + private scrollService: ScrollService) { } ngOnInit(): void { this.titleService.setTitle('Kavita - Dashboard'); @@ -67,13 +69,9 @@ export class LibraryComponent implements OnInit, OnDestroy { } reloadSeries() { - this.seriesService.getRecentlyAdded(0, 0, 20).pipe(takeUntil(this.onDestroy)).subscribe(updatedSeries => { - this.recentlyAdded = updatedSeries.result; - }); + this.loadRecentlyAdded(); - this.seriesService.getInProgress().pipe(takeUntil(this.onDestroy)).subscribe((updatedSeries) => { - this.inProgress = updatedSeries.result; - }); + this.loadInProgress(); this.reloadTags(); } @@ -88,11 +86,20 @@ export class LibraryComponent implements OnInit, OnDestroy { return; } + this.loadInProgress(); + this.reloadTags(); + } + + loadInProgress() { this.seriesService.getInProgress().pipe(takeUntil(this.onDestroy)).subscribe((updatedSeries) => { this.inProgress = updatedSeries.result; }); - - this.reloadTags(); + } + + loadRecentlyAdded() { + this.seriesService.getRecentlyAdded(0, 0, 20).pipe(takeUntil(this.onDestroy)).subscribe(updatedSeries => { + this.recentlyAdded = updatedSeries.result; + }); } reloadTags() { 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 4a42df8ef..8d03f56bb 100644 --- a/UI/Web/src/app/nav-header/nav-header.component.ts +++ b/UI/Web/src/app/nav-header/nav-header.component.ts @@ -3,6 +3,7 @@ import { Component, HostListener, Inject, OnDestroy, OnInit, ViewChild } from '@ import { Router } from '@angular/router'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; +import { ScrollService } from '../scroll.service'; import { UtilityService } from '../shared/_services/utility.service'; import { SearchResult } from '../_models/search-result'; import { AccountService } from '../_services/account.service'; @@ -30,7 +31,7 @@ export class NavHeaderComponent implements OnInit, OnDestroy { private readonly onDestroy = new Subject(); constructor(public accountService: AccountService, private router: Router, public navService: NavService, - private libraryService: LibraryService, public imageService: ImageService, @Inject(DOCUMENT) private document: Document) { } + private libraryService: LibraryService, public imageService: ImageService, @Inject(DOCUMENT) private document: Document, private scrollService: ScrollService) { } ngOnInit(): void { this.navService.darkMode$.pipe(takeUntil(this.onDestroy)).subscribe(res => { @@ -46,7 +47,7 @@ export class NavHeaderComponent implements OnInit, OnDestroy { @HostListener("window:scroll", []) checkBackToTopNeeded() { - const offset = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop; + const offset = this.scrollService.scrollPosition; if (offset > 100) { this.backToTopNeeded = true; } else if (offset < 40) { diff --git a/UI/Web/src/app/scroll.service.ts b/UI/Web/src/app/scroll.service.ts new file mode 100644 index 000000000..b49de4584 --- /dev/null +++ b/UI/Web/src/app/scroll.service.ts @@ -0,0 +1,22 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class ScrollService { + + constructor() { } + + get scrollPosition() { + return (window.pageYOffset + || document.documentElement.scrollTop + || document.body.scrollTop || 0); + } + + scrollTo(top: number) { + window.scroll({ + top: top, + behavior: 'smooth' + }); + } +} diff --git a/UI/Web/src/app/series-detail/series-detail.component.ts b/UI/Web/src/app/series-detail/series-detail.component.ts index d51e407fb..8010c710b 100644 --- a/UI/Web/src/app/series-detail/series-detail.component.ts +++ b/UI/Web/src/app/series-detail/series-detail.component.ts @@ -116,11 +116,6 @@ export class SeriesDetailComponent implements OnInit { return; } - this.seriesActions = this.actionFactoryService.getSeriesActions(this.handleSeriesActionCallback.bind(this)).filter(action => action.action !== Action.Edit); - this.volumeActions = this.actionFactoryService.getVolumeActions(this.handleVolumeActionCallback.bind(this)); - this.chapterActions = this.actionFactoryService.getChapterActions(this.handleChapterActionCallback.bind(this)); - - const seriesId = parseInt(routeId, 10); this.libraryId = parseInt(libraryId, 10); this.seriesImage = this.imageService.getSeriesCoverImage(seriesId); @@ -243,6 +238,12 @@ export class SeriesDetailComponent implements OnInit { this.createHTML(); this.titleService.setTitle('Kavita - ' + this.series.name + ' Details'); + + this.seriesActions = this.actionFactoryService.getSeriesActions(this.handleSeriesActionCallback.bind(this)) + .filter(action => action.action !== Action.Edit) + .filter(action => this.actionFactoryService.filterBookmarksForFormat(action, this.series)); + this.volumeActions = this.actionFactoryService.getVolumeActions(this.handleVolumeActionCallback.bind(this)); + this.chapterActions = this.actionFactoryService.getChapterActions(this.handleChapterActionCallback.bind(this)); this.seriesService.getVolumes(this.series.id).subscribe(volumes => {