diff --git a/API.Tests/Services/SeriesServiceTests.cs b/API.Tests/Services/SeriesServiceTests.cs index 199b303a9..352b7ca9d 100644 --- a/API.Tests/Services/SeriesServiceTests.cs +++ b/API.Tests/Services/SeriesServiceTests.cs @@ -50,10 +50,7 @@ public class SeriesServiceTests : AbstractDbTest public SeriesServiceTests() : base() { - var ds = new DirectoryService(Substitute.For>(), new FileSystem() - { - - }); + var ds = new DirectoryService(Substitute.For>(), new FileSystem()); var locService = new LocalizationService(ds, new MockHostingEnvironment(), @@ -461,7 +458,7 @@ public class SeriesServiceTests : AbstractDbTest JobStorage.Current = new InMemoryStorage(); var ratings = (await _unitOfWork.UserRepository.GetUserByUsernameAsync("majora2007", - AppUserIncludes.Ratings)) + AppUserIncludes.Ratings)!) .Ratings; Assert.NotEmpty(ratings); Assert.Equal(5, ratings.First().Rating); @@ -526,6 +523,7 @@ public class SeriesServiceTests : AbstractDbTest Assert.True(success); var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1); + Assert.NotNull(series); Assert.NotNull(series.Metadata); Assert.Contains("New Genre".SentenceCase(), series.Metadata.Genres.Select(g => g.Title)); @@ -805,7 +803,7 @@ public class SeriesServiceTests : AbstractDbTest [Fact] public void GetFirstChapterForMetadata_BookWithOnlyVolumeNumbers_Test() { - var file = new MangaFileBuilder("Test.cbz", MangaFormat.Archive, 1).Build(); + var file = new MangaFileBuilder("Test.cbz", MangaFormat.Epub, 1).Build(); var series = new SeriesBuilder("Test") .WithVolume(new VolumeBuilder("1") @@ -819,6 +817,7 @@ public class SeriesServiceTests : AbstractDbTest series.Library = new LibraryBuilder("Test LIb", LibraryType.Book).Build(); var firstChapter = SeriesService.GetFirstChapterForMetadata(series); + Assert.NotNull(firstChapter); Assert.Equal(1, firstChapter.Pages); } @@ -828,6 +827,7 @@ public class SeriesServiceTests : AbstractDbTest var series = CreateSeriesMock(); var firstChapter = SeriesService.GetFirstChapterForMetadata(series); + Assert.NotNull(firstChapter); Assert.Same("1", firstChapter.Range); } diff --git a/API/Services/Tasks/Scanner/Parser/Parser.cs b/API/Services/Tasks/Scanner/Parser/Parser.cs index 80461c12e..d4c7b9bf2 100644 --- a/API/Services/Tasks/Scanner/Parser/Parser.cs +++ b/API/Services/Tasks/Scanner/Parser/Parser.cs @@ -15,7 +15,7 @@ public static class Parser public const string DefaultVolume = "0"; public static readonly TimeSpan RegexTimeout = TimeSpan.FromMilliseconds(500); - public const string ImageFileExtensions = @"^(\.png|\.jpeg|\.jpg|\.webp|\.gif|\.avif)"; + public const string ImageFileExtensions = @"^(\.png|\.jpeg|\.jpg|\.webp|\.gif|\.avif)"; // Don't forget to update CoverChooser public const string ArchiveFileExtensions = @"\.cbz|\.zip|\.rar|\.cbr|\.tar.gz|\.7zip|\.7z|\.cb7|\.cbt"; private const string BookFileExtensions = @"\.epub|\.pdf"; private const string XmlRegexExtensions = @"\.xml"; diff --git a/API/Services/Tasks/Scanner/ProcessSeries.cs b/API/Services/Tasks/Scanner/ProcessSeries.cs index 8def30e8b..00246cfea 100644 --- a/API/Services/Tasks/Scanner/ProcessSeries.cs +++ b/API/Services/Tasks/Scanner/ProcessSeries.cs @@ -309,7 +309,7 @@ public class ProcessSeries : IProcessSeries if (series.Metadata.MaxCount == series.Metadata.TotalCount && series.Metadata.TotalCount > 0) { series.Metadata.PublicationStatus = PublicationStatus.Completed; - } else if (series.Metadata.TotalCount > 0) + } else if (series.Metadata.TotalCount > 0 && series.Metadata.MaxCount > 0) { series.Metadata.PublicationStatus = PublicationStatus.Ended; } diff --git a/UI/Web/README.md b/UI/Web/README.md index f088c87cf..74919b78b 100644 --- a/UI/Web/README.md +++ b/UI/Web/README.md @@ -29,3 +29,7 @@ Run `npx playwright test --reporter=line` or `npx playwright test` to run e2e te ng serve --host 0.0.0.0 and update environment.ts to your local ip. + +## Notes: +- injected services should be at the top of the file +- all components must be standalone diff --git a/UI/Web/package-lock.json b/UI/Web/package-lock.json index 8cb067001..d670fec12 100644 --- a/UI/Web/package-lock.json +++ b/UI/Web/package-lock.json @@ -33,12 +33,8 @@ "@popperjs/core": "^2.11.7", "@swimlane/ngx-charts": "^20.1.2", "@tweenjs/tween.js": "^21.0.0", - "@types/file-saver": "^2.0.6", - "angular-animations": "^0.11.0", "bootstrap": "^5.3.1", "charts.css": "^1.1.0", - "eventsource": "^2.0.2", - "file-saver": "^2.0.5", "luxon": "^3.4.3", "ng-circle-progress": "^1.7.1", "ng-lazyload-image": "^9.1.3", @@ -4785,11 +4781,6 @@ "@types/send": "*" } }, - "node_modules/@types/file-saver": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.6.tgz", - "integrity": "sha512-Mw671DVqoMHbjw0w4v2iiOro01dlT/WhWp5uwecBa0Wg8c+bcZOjgF1ndBnlaxhtvFCgTRBtsGivSVhrK/vnag==" - }, "node_modules/@types/geojson": { "version": "7946.0.10", "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz", @@ -5894,17 +5885,6 @@ "ajv": "^8.8.2" } }, - "node_modules/angular-animations": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/angular-animations/-/angular-animations-0.11.0.tgz", - "integrity": "sha512-P2RuOe+T97bhgGDLtOYK9V45QA5y+kFUxoJfRAua8Ymo0bI5lWyw8oiVmBoEIZUU+nooYoJvQXgVKuZJA7/z3g==", - "dependencies": { - "tslib": "^2.0.0" - }, - "peerDependencies": { - "@angular/animations": ">=6.0.0" - } - }, "node_modules/ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", @@ -8560,11 +8540,6 @@ "node": "^10.12.0 || >=12.0.0" } }, - "node_modules/file-saver": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", - "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==" - }, "node_modules/filelist": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", diff --git a/UI/Web/package.json b/UI/Web/package.json index f384f90b2..0beadfc29 100644 --- a/UI/Web/package.json +++ b/UI/Web/package.json @@ -38,12 +38,8 @@ "@popperjs/core": "^2.11.7", "@swimlane/ngx-charts": "^20.1.2", "@tweenjs/tween.js": "^21.0.0", - "@types/file-saver": "^2.0.6", - "angular-animations": "^0.11.0", "bootstrap": "^5.3.1", "charts.css": "^1.1.0", - "eventsource": "^2.0.2", - "file-saver": "^2.0.5", "luxon": "^3.4.3", "ng-circle-progress": "^1.7.1", "ng-lazyload-image": "^9.1.3", diff --git a/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.html b/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.html index 9be72cd37..7852c8331 100644 --- a/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.html +++ b/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.html @@ -99,13 +99,9 @@
  • {{t(tabs[TabID.Cover].title)}} - - +
  • diff --git a/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.ts b/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.ts index 53bafcced..302de4c33 100644 --- a/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.ts +++ b/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.ts @@ -206,6 +206,11 @@ export class CardDetailDrawerComponent implements OnInit { this.uploadService.updateChapterCoverImage(this.chapter.id, coverUrl).subscribe(() => {}); } + updateCoverImageIndex(selectedIndex: number) { + if (selectedIndex <= 0) return; + this.applyCoverImage(this.imageUrls[selectedIndex]); + } + resetCoverImage() { this.uploadService.resetChapterCoverLock(this.chapter.id).subscribe(() => { this.toastr.info(translate('toasts.regen-cover')); diff --git a/UI/Web/src/app/cards/cover-image-chooser/cover-image-chooser.component.html b/UI/Web/src/app/cards/cover-image-chooser/cover-image-chooser.component.html index b4ea2f4bb..9dad9ea6a 100644 --- a/UI/Web/src/app/cards/cover-image-chooser/cover-image-chooser.component.html +++ b/UI/Web/src/app/cards/cover-image-chooser/cover-image-chooser.component.html @@ -31,7 +31,7 @@
    -
    diff --git a/UI/Web/src/app/cards/cover-image-chooser/cover-image-chooser.component.ts b/UI/Web/src/app/cards/cover-image-chooser/cover-image-chooser.component.ts index 75b531b07..d0a20798d 100644 --- a/UI/Web/src/app/cards/cover-image-chooser/cover-image-chooser.component.ts +++ b/UI/Web/src/app/cards/cover-image-chooser/cover-image-chooser.component.ts @@ -1,7 +1,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, - Component, DestroyRef, + Component, EventEmitter, inject, Inject, @@ -11,7 +11,7 @@ import { } from '@angular/core'; import {FormBuilder, FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms'; import {NgxFileDropEntry, FileSystemFileEntry, NgxFileDropModule} from 'ngx-file-drop'; -import { fromEvent, Subject } from 'rxjs'; +import { fromEvent } from 'rxjs'; import { takeWhile } from 'rxjs/operators'; import { ToastrService } from 'ngx-toastr'; import { ImageService } from 'src/app/_services/image.service'; @@ -37,7 +37,6 @@ import {translate, TranslocoModule} from "@ngneat/transloco"; }) export class CoverImageChooserComponent implements OnInit { - private readonly destroyRef = inject(DestroyRef); private readonly cdRef = inject(ChangeDetectorRef); public readonly imageService = inject(ImageService); public readonly fb = inject(FormBuilder); @@ -84,8 +83,7 @@ export class CoverImageChooserComponent implements OnInit { appliedIndex: number = 0; form!: FormGroup; files: NgxFileDropEntry[] = []; - acceptableExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.webp'].join(','); - + acceptableExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.avif'].join(','); mode: 'file' | 'url' | 'all' = 'all'; constructor(@Inject(DOCUMENT) private document: Document) { } @@ -182,6 +180,25 @@ export class CoverImageChooserComponent implements OnInit { }); } + loadImageFromUrl(url?: string) { + url = url || this.form.get('coverImageUrl')?.value.trim(); + if (!url || url === '') return; + + this.uploadService.uploadByUrl(url).subscribe(filename => { + const img = new Image(); + img.crossOrigin = 'Anonymous'; + img.src = this.imageService.getCoverUploadImage(filename); + img.onload = (e) => this.handleUrlImageAdd(img); + img.onerror = (e) => { + this.toastr.error(translate('errors.rejected-cover-upload')); + this.form.get('coverImageUrl')?.setValue(''); + this.cdRef.markForCheck(); + }; + this.form.get('coverImageUrl')?.setValue(''); + this.cdRef.markForCheck(); + }); + } + changeMode(mode: 'url') { @@ -239,12 +256,6 @@ export class CoverImageChooserComponent implements OnInit { }); } - public fileOver(event: any){ - } - - public fileLeave(event: any){ - } - reset() { this.resetClicked.emit(); this.selectedIndex = -1; @@ -275,4 +286,6 @@ export class CoverImageChooserComponent implements OnInit { }); } + protected fileOver(event: any){} + protected fileLeave(event: any){} } diff --git a/UI/Web/src/app/manga-reader/_components/infinite-scroller/infinite-scroller.component.ts b/UI/Web/src/app/manga-reader/_components/infinite-scroller/infinite-scroller.component.ts index e742e893a..5329fee8b 100644 --- a/UI/Web/src/app/manga-reader/_components/infinite-scroller/infinite-scroller.component.ts +++ b/UI/Web/src/app/manga-reader/_components/infinite-scroller/infinite-scroller.component.ts @@ -157,7 +157,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy { /** * Debug mode. Will show extra information. Use bitwise (|) operators between different modes to enable different output */ - debugMode: DEBUG_MODES = DEBUG_MODES.Outline; + debugMode: DEBUG_MODES = DEBUG_MODES.None; /** * Debug mode. Will filter out any messages in here so they don't hit the log */ diff --git a/UI/Web/src/app/series-detail/_components/metadata-detail/metadata-detail.component.html b/UI/Web/src/app/series-detail/_components/metadata-detail/metadata-detail.component.html index 5b96509af..4bcf433fa 100644 --- a/UI/Web/src/app/series-detail/_components/metadata-detail/metadata-detail.component.html +++ b/UI/Web/src/app/series-detail/_components/metadata-detail/metadata-detail.component.html @@ -3,7 +3,7 @@
    {{heading}}
    - + diff --git a/UI/Web/src/app/series-detail/_components/metadata-detail/metadata-detail.component.ts b/UI/Web/src/app/series-detail/_components/metadata-detail/metadata-detail.component.ts index b006775dc..4523e8db1 100644 --- a/UI/Web/src/app/series-detail/_components/metadata-detail/metadata-detail.component.ts +++ b/UI/Web/src/app/series-detail/_components/metadata-detail/metadata-detail.component.ts @@ -6,6 +6,7 @@ import {TagBadgeComponent, TagBadgeCursor} from "../../../shared/tag-badge/tag-b import {FilterUtilitiesService} from "../../../shared/_services/filter-utilities.service"; import {FilterComparison} from "../../../_models/metadata/v2/filter-comparison"; import {FilterField} from "../../../_models/metadata/v2/filter-field"; +import {Breakpoint, UtilityService} from "../../../shared/_services/utility.service"; @Component({ selector: 'app-metadata-detail', @@ -17,6 +18,11 @@ import {FilterField} from "../../../_models/metadata/v2/filter-field"; }) export class MetadataDetailComponent { + private readonly filterUtilityService = inject(FilterUtilitiesService); + public readonly utilityService = inject(UtilityService); + protected readonly TagBadgeCursor = TagBadgeCursor; + protected readonly Breakpoint = Breakpoint; + @Input({required: true}) tags: Array = []; @Input({required: true}) libraryId!: number; @Input({required: true}) heading!: string; @@ -24,12 +30,11 @@ export class MetadataDetailComponent { @ContentChild('titleTemplate') titleTemplate!: TemplateRef; @ContentChild('itemTemplate') itemTemplate?: TemplateRef; - private readonly filterUtilityService = inject(FilterUtilitiesService); - protected readonly TagBadgeCursor = TagBadgeCursor; - goTo(queryParamName: FilterField, filter: any) { if (queryParamName === FilterField.None) return; this.filterUtilityService.applyFilter(['library', this.libraryId], queryParamName, FilterComparison.Equal, filter).subscribe(); } + + } diff --git a/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.html b/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.html index 221d96ca4..91b9672b8 100644 --- a/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.html +++ b/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.html @@ -1,6 +1,6 @@
    - +
    diff --git a/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.ts b/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.ts index b23a3d0df..e186f622c 100644 --- a/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.ts +++ b/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.ts @@ -121,4 +121,6 @@ export class SeriesMetadataDetailComponent implements OnChanges { navigate(basePage: string, id: number) { this.router.navigate([basePage, id]); } + + protected readonly Breakpoint = Breakpoint; } diff --git a/UI/Web/src/app/shared/_providers/saver.provider.ts b/UI/Web/src/app/shared/_providers/saver.provider.ts deleted file mode 100644 index bd3d35ec9..000000000 --- a/UI/Web/src/app/shared/_providers/saver.provider.ts +++ /dev/null @@ -1,10 +0,0 @@ -import {InjectionToken} from '@angular/core' -import { saveAs } from 'file-saver'; - -export type Saver = (blob: Blob, filename?: string) => void - -export const SAVER = new InjectionToken('saver') - -export function getSaver(): Saver { - return saveAs; -} \ No newline at end of file diff --git a/UI/Web/src/app/shared/_services/download.service.ts b/UI/Web/src/app/shared/_services/download.service.ts index 5820d1b2e..f4ecf664d 100644 --- a/UI/Web/src/app/shared/_services/download.service.ts +++ b/UI/Web/src/app/shared/_services/download.service.ts @@ -14,7 +14,6 @@ import { of, filter, } from 'rxjs'; -import { SAVER, Saver } from '../_providers/saver.provider'; import { download, Download } from '../_models/download'; import { PageBookmark } from 'src/app/_models/readers/page-bookmark'; import {switchMap, take, takeWhile, throttleTime} from 'rxjs/operators'; @@ -69,7 +68,7 @@ export class DownloadService { private readonly destroyRef = inject(DestroyRef); constructor(private httpClient: HttpClient, private confirmService: ConfirmService, - @Inject(SAVER) private save: Saver, private accountService: AccountService) { } + private accountService: AccountService) { } /** @@ -270,4 +269,15 @@ export class DownloadService { finalize(() => this.finalizeDownloadState(downloadType, subtitle)) ); } + + private save(blob: Blob, filename: string) { + const saveLink = document.createElement( 'a' ); + if (saveLink.href) { + URL.revokeObjectURL(saveLink.href); + } + saveLink.href = URL.createObjectURL(blob); + saveLink.download = filename; + saveLink.dispatchEvent( new MouseEvent( 'click' ) ); + } + } diff --git a/UI/Web/src/app/shared/badge-expander/badge-expander.component.ts b/UI/Web/src/app/shared/badge-expander/badge-expander.component.ts index b46892fb4..db56231f2 100644 --- a/UI/Web/src/app/shared/badge-expander/badge-expander.component.ts +++ b/UI/Web/src/app/shared/badge-expander/badge-expander.component.ts @@ -1,4 +1,13 @@ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, Input, OnInit, TemplateRef } from '@angular/core'; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ContentChild, + inject, + Input, + OnInit, + TemplateRef +} from '@angular/core'; import {CommonModule} from "@angular/common"; import {TranslocoDirective} from "@ngneat/transloco"; @@ -12,6 +21,8 @@ import {TranslocoDirective} from "@ngneat/transloco"; }) export class BadgeExpanderComponent implements OnInit { + private readonly cdRef = inject(ChangeDetectorRef); + @Input() items: Array = []; @Input() itemsTillExpander: number = 4; @ContentChild('badgeExpanderItem') itemTemplate!: TemplateRef; @@ -24,8 +35,6 @@ export class BadgeExpanderComponent implements OnInit { return Math.max(this.items.length - this.itemsTillExpander, 0); } - constructor(private readonly cdRef: ChangeDetectorRef) { } - ngOnInit(): void { this.visibleItems = this.items.slice(0, this.itemsTillExpander); this.cdRef.markForCheck(); diff --git a/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.html b/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.html index 52b7a5ad8..8434381e4 100644 --- a/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.html +++ b/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.html @@ -79,13 +79,9 @@

    {{t('cover-description-extra')}}

    - - +
    diff --git a/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.ts b/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.ts index 7ba0930e7..e555da793 100644 --- a/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.ts +++ b/UI/Web/src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component.ts @@ -218,6 +218,11 @@ export class LibrarySettingsModalComponent implements OnInit { this.uploadService.updateLibraryCoverImage(this.library.id, coverUrl).subscribe(() => {}); } + updateCoverImageIndex(selectedIndex: number) { + if (selectedIndex <= 0) return; + this.applyCoverImage(this.imageUrls[selectedIndex]); + } + resetCoverImage() { this.uploadService.updateLibraryCoverImage(this.library.id, '').subscribe(() => {}); } diff --git a/UI/Web/src/main.ts b/UI/Web/src/main.ts index 236ba514d..2a2bf490e 100644 --- a/UI/Web/src/main.ts +++ b/UI/Web/src/main.ts @@ -9,7 +9,6 @@ import { NgCircleProgressModule } from 'ng-circle-progress'; import { ToastrModule } from 'ngx-toastr'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { AppRoutingModule } from './app/app-routing.module'; -import { SAVER, getSaver } from './app/shared/_providers/saver.provider'; import { Title, BrowserModule, bootstrapApplication } from '@angular/platform-browser'; import { JwtInterceptor } from './app/_interceptors/jwt.interceptor'; import { ErrorInterceptor } from './app/_interceptors/error.interceptor'; @@ -147,7 +146,6 @@ bootstrapApplication(AppComponent, { { provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true }, preLoad, Title, - { provide: SAVER, useFactory: getSaver }, provideHttpClient(withInterceptorsFromDi()) ] } as ApplicationConfig) diff --git a/openapi.json b/openapi.json index 1de90ea06..6b9c230c3 100644 --- a/openapi.json +++ b/openapi.json @@ -7,7 +7,7 @@ "name": "GPL-3.0", "url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE" }, - "version": "0.7.10.5" + "version": "0.7.10.6" }, "servers": [ {