From bfbcb4b741eaa23b52a587972840b6263a6b37f2 Mon Sep 17 00:00:00 2001 From: Joe Milazzo Date: Mon, 9 Dec 2024 13:06:28 -0600 Subject: [PATCH] PR Flush (#3446) Co-authored-by: Hippari Co-authored-by: Gavin Mogan --- .github/workflows/develop-workflow.yml | 20 +- .github/workflows/release-workflow.yml | 33 +- .../Extensions/QueryableExtensionsTests.cs | 8 +- API/Controllers/AccountController.cs | 20 +- API/DTOs/Account/UpdateUserDto.cs | 4 + .../RestrictByAgeExtensions.cs | 11 +- API/I18N/en.json | 1 + API/Services/AccountService.cs | 8 +- API/Services/Tasks/StatsService.cs | 19 +- .../app/_directives/dbl-click.directive.ts | 27 ++ UI/Web/src/app/_services/person.service.ts | 2 +- .../age-rating-image.component.html | 0 .../age-rating-image.component.scss | 0 .../age-rating-image.component.ts | 0 .../card-actionables.component.html | 4 +- .../publisher-flipper.component.html | 0 .../publisher-flipper.component.scss | 0 .../publisher-flipper.component.ts | 0 .../related-tab/related-tab.component.html | 0 .../related-tab/related-tab.component.scss | 0 .../related-tab/related-tab.component.ts | 0 .../review-card/review-card.component.html | 2 +- .../admin/edit-user/edit-user.component.html | 158 +++---- .../admin/edit-user/edit-user.component.ts | 1 - .../manage-settings.component.html | 2 +- UI/Web/src/app/app.component.scss | 2 +- .../chapter-detail.component.html | 2 +- .../chapter-detail.component.ts | 2 +- .../collection-detail.component.html | 2 +- .../manga-reader/manga-reader.component.html | 407 +++++++++--------- .../manga-reader/manga-reader.component.ts | 9 +- .../person-detail.component.html | 23 +- .../person-detail.component.scss | 15 + .../person-detail/person-detail.component.ts | 15 +- .../reading-list-detail.component.html | 4 +- .../metadata-detail-row.component.ts | 4 +- .../series-detail.component.html | 2 +- .../series-detail/series-detail.component.ts | 2 +- .../src/app/shared/image/image.component.html | 1 + .../src/app/shared/image/image.component.ts | 4 + .../person-badge/person-badge.component.html | 38 +- .../person-badge/person-badge.component.scss | 21 +- .../shared/read-more/read-more.component.scss | 7 + .../volume-detail.component.html | 2 +- .../volume-detail/volume-detail.component.ts | 2 +- UI/Web/src/theme/components/_sidenav.scss | 31 +- 46 files changed, 551 insertions(+), 364 deletions(-) create mode 100644 UI/Web/src/app/_directives/dbl-click.directive.ts rename UI/Web/src/app/{_single-modules => _single-module}/age-rating-image/age-rating-image.component.html (100%) rename UI/Web/src/app/{_single-modules => _single-module}/age-rating-image/age-rating-image.component.scss (100%) rename UI/Web/src/app/{_single-modules => _single-module}/age-rating-image/age-rating-image.component.ts (100%) rename UI/Web/src/app/{_single-modules => _single-module}/publisher-flipper/publisher-flipper.component.html (100%) rename UI/Web/src/app/{_single-modules => _single-module}/publisher-flipper/publisher-flipper.component.scss (100%) rename UI/Web/src/app/{_single-modules => _single-module}/publisher-flipper/publisher-flipper.component.ts (100%) rename UI/Web/src/app/{_single-modules => _single-module}/related-tab/related-tab.component.html (100%) rename UI/Web/src/app/{_single-modules => _single-module}/related-tab/related-tab.component.scss (100%) rename UI/Web/src/app/{_single-modules => _single-module}/related-tab/related-tab.component.ts (100%) diff --git a/.github/workflows/develop-workflow.yml b/.github/workflows/develop-workflow.yml index b7f229b14..939cda4e5 100644 --- a/.github/workflows/develop-workflow.yml +++ b/.github/workflows/develop-workflow.yml @@ -128,7 +128,7 @@ jobs: - name: Compile dotnet app uses: actions/setup-dotnet@v4 with: - dotnet-version: 8.0.x + dotnet-version: 8.0.x - name: Install Swashbuckle CLI run: dotnet tool install -g Swashbuckle.AspNetCore.Cli @@ -137,6 +137,7 @@ jobs: - name: Login to Docker Hub uses: docker/login-action@v3 + if: ${{ github.repository_owner == 'Kareadita' }} with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} @@ -155,20 +156,33 @@ jobs: id: buildx uses: docker/setup-buildx-action@v3 + - name: Extract metadata (tags, labels) for Docker + id: docker_meta_nightly + uses: docker/metadata-action@v5 + with: + tags: | + type=raw,value=nightly + type=raw,value=nightly-${{ steps.parse-version.outputs.VERSION }} + images: | + name=jvmilazz0/kavita,enable=${{ github.repository_owner == 'Kareadita' }} + name=ghcr.io/${{ github.repository }} + - name: Build and push id: docker_build - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 with: context: . platforms: linux/amd64,linux/arm/v7,linux/arm64 push: true - tags: jvmilazz0/kavita:nightly, jvmilazz0/kavita:nightly-${{ steps.parse-version.outputs.VERSION }}, ghcr.io/kareadita/kavita:nightly, ghcr.io/kareadita/kavita:nightly-${{ steps.parse-version.outputs.VERSION }} + tags: ${{ steps.docker_meta_nightly.outputs.tags }} + labels: ${{ steps.docker_meta_nightly.outputs.labels }} - name: Image digest run: echo ${{ steps.docker_build.outputs.digest }} - name: Notify Discord uses: rjstone/discord-webhook-notify@v1 + if: ${{ github.repository_owner == 'Kareadita' }} with: severity: info description: v${{steps.get-version.outputs.assembly-version}} - ${{ steps.findPr.outputs.title }} diff --git a/.github/workflows/release-workflow.yml b/.github/workflows/release-workflow.yml index 251683b6c..95e4dc7e3 100644 --- a/.github/workflows/release-workflow.yml +++ b/.github/workflows/release-workflow.yml @@ -114,6 +114,7 @@ jobs: - name: Login to Docker Hub uses: docker/login-action@v3 + if: ${{ github.repository_owner == 'Kareadita' }} with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} @@ -132,23 +133,47 @@ jobs: id: buildx uses: docker/setup-buildx-action@v3 + - name: Extract metadata (tags, labels) for Docker + id: docker_meta_stable + uses: docker/metadata-action@v5 + with: + tags: | + type=raw,value=latest + type=raw,value=${{ steps.parse-version.outputs.VERSION }} + images: | + name=jvmilazz0/kavita,enable=${{ github.repository_owner == 'Kareadita' }} + name=ghcr.io/${{ github.repository }} + - name: Build and push stable id: docker_build_stable - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 with: context: . platforms: linux/amd64,linux/arm/v7,linux/arm64 push: true - tags: jvmilazz0/kavita:latest, jvmilazz0/kavita:${{ steps.parse-version.outputs.VERSION }}, ghcr.io/kareadita/kavita:latest, ghcr.io/kareadita/kavita:${{ steps.parse-version.outputs.VERSION }} + tags: ${{ steps.docker_meta_stable.outputs.tags }} + labels: ${{ steps.docker_meta_stable.outputs.labels }} + + - name: Extract metadata (tags, labels) for Docker + id: docker_meta_nightly + uses: docker/metadata-action@v5 + with: + tags: | + type=raw,value=nightly + type=raw,value=nightly-${{ steps.parse-version.outputs.VERSION }} + images: | + name=jvmilazz0/kavita,enable=${{ github.repository_owner == 'Kareadita' }} + name=ghcr.io/${{ github.repository }} - name: Build and push nightly id: docker_build_nightly - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 with: context: . platforms: linux/amd64,linux/arm/v7,linux/arm64 push: true - tags: jvmilazz0/kavita:nightly, jvmilazz0/kavita:nightly-${{ steps.parse-version.outputs.VERSION }}, ghcr.io/kareadita/kavita:nightly, ghcr.io/kareadita/kavita:nightly-${{ steps.parse-version.outputs.VERSION }} + tags: ${{ steps.docker_meta_nightly.outputs.tags }} + labels: ${{ steps.docker_meta_nightly.outputs.labels }} - name: Image digest run: echo ${{ steps.docker_build_stable.outputs.digest }} diff --git a/API.Tests/Extensions/QueryableExtensionsTests.cs b/API.Tests/Extensions/QueryableExtensionsTests.cs index d902ae353..4ea9a5a4b 100644 --- a/API.Tests/Extensions/QueryableExtensionsTests.cs +++ b/API.Tests/Extensions/QueryableExtensionsTests.cs @@ -123,14 +123,14 @@ public class QueryableExtensionsTests [Theory] [InlineData(true, 2)] - [InlineData(false, 1)] - public void RestrictAgainstAgeRestriction_Person_ShouldRestrictEverythingAboveTeen(bool includeUnknowns, int expectedCount) + [InlineData(false, 2)] + public void RestrictAgainstAgeRestriction_Person_ShouldRestrictEverythingAboveTeen(bool includeUnknowns, int expectedPeopleCount) { // Arrange var items = new List { CreatePersonWithSeriesMetadata("Test1", AgeRating.Teen), - CreatePersonWithSeriesMetadata("Test2", AgeRating.Unknown, AgeRating.Teen), + CreatePersonWithSeriesMetadata("Test2", AgeRating.Unknown, AgeRating.Teen), // 2 series on this person, restrict will still allow access CreatePersonWithSeriesMetadata("Test3", AgeRating.X18Plus) }; @@ -144,7 +144,7 @@ public class QueryableExtensionsTests var filtered = items.AsQueryable().RestrictAgainstAgeRestriction(ageRestriction); // Assert - Assert.Equal(expectedCount, filtered.Count()); + Assert.Equal(expectedPeopleCount, filtered.Count()); } private static Person CreatePersonWithSeriesMetadata(string name, params AgeRating[] ageRatings) diff --git a/API/Controllers/AccountController.cs b/API/Controllers/AccountController.cs index 888be5eff..0b47aa526 100644 --- a/API/Controllers/AccountController.cs +++ b/API/Controllers/AccountController.cs @@ -509,6 +509,21 @@ public class AccountController : BaseApiController _unitOfWork.UserRepository.Update(user); } + // Check if email is changing for a non-admin user + var isUpdatingAnotherAccount = user.Id != adminUser.Id; + if (isUpdatingAnotherAccount && !string.IsNullOrEmpty(dto.Email) && user.Email != dto.Email) + { + // Validate username change + var errors = await _accountService.ValidateEmail(dto.Email); + if (errors.Any()) return BadRequest(await _localizationService.Translate(User.GetUserId(), "email-taken")); + + user.Email = dto.Email; + user.EmailConfirmed = true; // When an admin performs the flow, we assume the email address is able to receive data + + await _userManager.UpdateNormalizedEmailAsync(user); + _unitOfWork.UserRepository.Update(user); + } + // Update roles var existingRoles = await _userManager.GetRolesAsync(user); var hasAdminRole = dto.Roles.Contains(PolicyConstants.AdminRole); @@ -612,8 +627,7 @@ public class AccountController : BaseApiController if (adminUser == null) return Unauthorized(await _localizationService.Translate(userId, "permission-denied")); dto.Email = dto.Email.Trim(); - if (string.IsNullOrEmpty(dto.Email)) - return BadRequest(await _localizationService.Translate(userId, "invalid-payload")); + if (string.IsNullOrEmpty(dto.Email)) return BadRequest(await _localizationService.Translate(userId, "invalid-payload")); _logger.LogInformation("{User} is inviting {Email} to the server", adminUser.UserName, dto.Email); @@ -623,7 +637,7 @@ public class AccountController : BaseApiController { var invitedUser = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email); if (await _userManager.IsEmailConfirmedAsync(invitedUser!)) - return BadRequest(await _localizationService.Translate(User.GetUserId(), "user-already-registered", invitedUser.UserName)); + return BadRequest(await _localizationService.Translate(User.GetUserId(), "user-already-registered", invitedUser!.UserName)); return BadRequest(await _localizationService.Translate(User.GetUserId(), "user-already-invited")); } diff --git a/API/DTOs/Account/UpdateUserDto.cs b/API/DTOs/Account/UpdateUserDto.cs index bda664bdb..ef19973f5 100644 --- a/API/DTOs/Account/UpdateUserDto.cs +++ b/API/DTOs/Account/UpdateUserDto.cs @@ -18,4 +18,8 @@ public record UpdateUserDto /// An Age Rating which will limit the account to seeing everything equal to or below said rating. /// public AgeRestrictionDto AgeRestriction { get; init; } = default!; + /// + /// Email of the user + /// + public string? Email { get; set; } = default!; } diff --git a/API/Extensions/QueryExtensions/RestrictByAgeExtensions.cs b/API/Extensions/QueryExtensions/RestrictByAgeExtensions.cs index b8def7377..ebc233056 100644 --- a/API/Extensions/QueryExtensions/RestrictByAgeExtensions.cs +++ b/API/Extensions/QueryExtensions/RestrictByAgeExtensions.cs @@ -101,12 +101,15 @@ public static class RestrictByAgeExtensions if (restriction.IncludeUnknowns) { - return queryable.Where(c => c.SeriesMetadataPeople.All(sm => - sm.SeriesMetadata.AgeRating <= restriction.AgeRating)); + return queryable.Where(c => + c.SeriesMetadataPeople.Any(sm => sm.SeriesMetadata.AgeRating <= restriction.AgeRating) || + c.ChapterPeople.Any(cp => cp.Chapter.AgeRating <= restriction.AgeRating)); } - return queryable.Where(c => c.SeriesMetadataPeople.All(sm => - sm.SeriesMetadata.AgeRating <= restriction.AgeRating && sm.SeriesMetadata.AgeRating > AgeRating.Unknown)); + return queryable.Where(c => + c.SeriesMetadataPeople.Any(sm => sm.SeriesMetadata.AgeRating <= restriction.AgeRating && sm.SeriesMetadata.AgeRating != AgeRating.Unknown) || + c.ChapterPeople.Any(cp => cp.Chapter.AgeRating <= restriction.AgeRating && cp.Chapter.AgeRating != AgeRating.Unknown) + ); } public static IQueryable RestrictAgainstAgeRestriction(this IQueryable queryable, AgeRestriction restriction) diff --git a/API/I18N/en.json b/API/I18N/en.json index 8781a8603..418427111 100644 --- a/API/I18N/en.json +++ b/API/I18N/en.json @@ -18,6 +18,7 @@ "age-restriction-update": "There was an error updating the age restriction", "no-user": "User does not exist", "username-taken": "Username already taken", + "email-taken": "Email already in use", "user-already-confirmed": "User is already confirmed", "generic-user-update": "There was an exception when updating the user", "manual-setup-fail": "Manual setup is unable to be completed. Please cancel and recreate the invite", diff --git a/API/Services/AccountService.cs b/API/Services/AccountService.cs index 71dc0f3b6..241198811 100644 --- a/API/Services/AccountService.cs +++ b/API/Services/AccountService.cs @@ -95,12 +95,12 @@ public class AccountService : IAccountService public async Task> ValidateEmail(string email) { var user = await _unitOfWork.UserRepository.GetUserByEmailAsync(email); - if (user == null) return Array.Empty(); + if (user == null) return []; - return new List() - { + return + [ new ApiException(400, "Email is already registered") - }; + ]; } /// diff --git a/API/Services/Tasks/StatsService.cs b/API/Services/Tasks/StatsService.cs index 33ad72719..4e6fcfb60 100644 --- a/API/Services/Tasks/StatsService.cs +++ b/API/Services/Tasks/StatsService.cs @@ -14,6 +14,7 @@ using API.DTOs.Stats.V3; using API.Entities; using API.Entities.Enums; using API.Services.Plus; +using API.Services.Tasks.Scanner.Parser; using Flurl.Http; using Kavita.Common.EnvironmentInfo; using Kavita.Common.Helpers; @@ -231,11 +232,12 @@ public class StatsService : IStatsService { // If first time flow, just return 0 if (!await _context.Chapter.AnyAsync()) return 0; + return await _context.Series .AsNoTracking() .AsSplitQuery() .MaxAsync(s => s.Volumes! - .Where(v => v.MinNumber == 0) + .Where(v => v.MinNumber == Parser.LooseLeafVolumeNumber) .SelectMany(v => v.Chapters!) .Count()); } @@ -262,13 +264,13 @@ public class StatsService : IStatsService dto.MaxSeriesInALibrary = await MaxSeriesInAnyLibrary(); dto.MaxVolumesInASeries = await MaxVolumesInASeries(); dto.MaxChaptersInASeries = await MaxChaptersInASeries(); - dto.TotalFiles = await _unitOfWork.LibraryRepository.GetTotalFiles(); - dto.TotalGenres = await _unitOfWork.GenreRepository.GetCountAsync(); - dto.TotalPeople = await _unitOfWork.PersonRepository.GetCountAsync(); - dto.TotalSeries = await _unitOfWork.SeriesRepository.GetCountAsync(); - dto.TotalLibraries = (await _unitOfWork.LibraryRepository.GetLibrariesAsync()).Count(); - dto.NumberOfCollections = (await _unitOfWork.CollectionTagRepository.GetAllCollectionsAsync()).Count(); - dto.NumberOfReadingLists = await _unitOfWork.ReadingListRepository.Count(); + dto.TotalFiles = await _context.MangaFile.CountAsync(); + dto.TotalGenres = await _context.Genre.CountAsync(); + dto.TotalPeople = await _context.Person.CountAsync(); + dto.TotalSeries = await _context.Series.CountAsync(); + dto.TotalLibraries = await _context.Library.CountAsync(); + dto.NumberOfCollections = await _context.AppUserCollection.CountAsync(); + dto.NumberOfReadingLists = await _context.ReadingList.CountAsync(); try { @@ -314,6 +316,7 @@ public class StatsService : IStatsService libDto.UsingFolderWatching = library.FolderWatching; libDto.CreateCollectionsFromMetadata = library.ManageCollections; libDto.CreateReadingListsFromMetadata = library.ManageReadingLists; + libDto.LibraryType = library.Type; dto.Libraries.Add(libDto); } diff --git a/UI/Web/src/app/_directives/dbl-click.directive.ts b/UI/Web/src/app/_directives/dbl-click.directive.ts new file mode 100644 index 000000000..98fabf843 --- /dev/null +++ b/UI/Web/src/app/_directives/dbl-click.directive.ts @@ -0,0 +1,27 @@ +import {Directive, EventEmitter, HostListener, Output} from '@angular/core'; + +@Directive({ + selector: '[appDblClick]', + standalone: true +}) +export class DblClickDirective { + + @Output() doubleClick = new EventEmitter(); + + private lastTapTime = 0; + private tapTimeout = 300; // Time threshold for a double tap (in milliseconds) + + @HostListener('click', ['$event']) + handleClick(event: Event): void { + event.stopPropagation(); + event.preventDefault(); + + const currentTime = new Date().getTime(); + if (currentTime - this.lastTapTime < this.tapTimeout) { + // Detected a double click/tap + this.doubleClick.emit(event); + } + this.lastTapTime = currentTime; + } + +} diff --git a/UI/Web/src/app/_services/person.service.ts b/UI/Web/src/app/_services/person.service.ts index f37ba2d65..676aa6e71 100644 --- a/UI/Web/src/app/_services/person.service.ts +++ b/UI/Web/src/app/_services/person.service.ts @@ -26,7 +26,7 @@ export class PersonService { } get(name: string) { - return this.httpClient.get(this.baseUrl + `person?name=${name}`); + return this.httpClient.get(this.baseUrl + `person?name=${name}`); } getRolesForPerson(personId: number) { diff --git a/UI/Web/src/app/_single-modules/age-rating-image/age-rating-image.component.html b/UI/Web/src/app/_single-module/age-rating-image/age-rating-image.component.html similarity index 100% rename from UI/Web/src/app/_single-modules/age-rating-image/age-rating-image.component.html rename to UI/Web/src/app/_single-module/age-rating-image/age-rating-image.component.html diff --git a/UI/Web/src/app/_single-modules/age-rating-image/age-rating-image.component.scss b/UI/Web/src/app/_single-module/age-rating-image/age-rating-image.component.scss similarity index 100% rename from UI/Web/src/app/_single-modules/age-rating-image/age-rating-image.component.scss rename to UI/Web/src/app/_single-module/age-rating-image/age-rating-image.component.scss diff --git a/UI/Web/src/app/_single-modules/age-rating-image/age-rating-image.component.ts b/UI/Web/src/app/_single-module/age-rating-image/age-rating-image.component.ts similarity index 100% rename from UI/Web/src/app/_single-modules/age-rating-image/age-rating-image.component.ts rename to UI/Web/src/app/_single-module/age-rating-image/age-rating-image.component.ts diff --git a/UI/Web/src/app/_single-module/card-actionables/card-actionables.component.html b/UI/Web/src/app/_single-module/card-actionables/card-actionables.component.html index 9e1b96ac9..2543a7106 100644 --- a/UI/Web/src/app/_single-module/card-actionables/card-actionables.component.html +++ b/UI/Web/src/app/_single-module/card-actionables/card-actionables.component.html @@ -1,14 +1,14 @@ @if (actions.length > 0) { @if ((utilityService.activeBreakpoint$ | async)! <= Breakpoint.Tablet) { - } @else {
-
diff --git a/UI/Web/src/app/admin/edit-user/edit-user.component.html b/UI/Web/src/app/admin/edit-user/edit-user.component.html index 1d9db290c..ef0e608cb 100644 --- a/UI/Web/src/app/admin/edit-user/edit-user.component.html +++ b/UI/Web/src/app/admin/edit-user/edit-user.component.html @@ -1,76 +1,90 @@ - + + + + + diff --git a/UI/Web/src/app/admin/edit-user/edit-user.component.ts b/UI/Web/src/app/admin/edit-user/edit-user.component.ts index b460e82f8..4de2e5205 100644 --- a/UI/Web/src/app/admin/edit-user/edit-user.component.ts +++ b/UI/Web/src/app/admin/edit-user/edit-user.component.ts @@ -50,7 +50,6 @@ export class EditUserComponent implements OnInit { this.userForm.addControl('email', new FormControl(this.member.email, [Validators.required, Validators.email])); this.userForm.addControl('username', new FormControl(this.member.username, [Validators.required, Validators.pattern(AllowedUsernameCharacters)])); - this.userForm.get('email')?.disable(); this.selectedRestriction = this.member.ageRestriction; this.cdRef.markForCheck(); } diff --git a/UI/Web/src/app/admin/manage-settings/manage-settings.component.html b/UI/Web/src/app/admin/manage-settings/manage-settings.component.html index 3f5c71d61..e4468ccc5 100644 --- a/UI/Web/src/app/admin/manage-settings/manage-settings.component.html +++ b/UI/Web/src/app/admin/manage-settings/manage-settings.component.html @@ -290,7 +290,7 @@
@if (settingsForm.get('onDeckUpdateDays'); as formControl) { - + {{formControl.value}} diff --git a/UI/Web/src/app/app.component.scss b/UI/Web/src/app/app.component.scss index bc68e8372..e5061412b 100644 --- a/UI/Web/src/app/app.component.scss +++ b/UI/Web/src/app/app.component.scss @@ -7,7 +7,7 @@ .companion-bar { transition: all var(--side-nav-companion-bar-transistion); - margin-left: 60px; + margin-left: 45px; overflow-y: hidden; overflow-x: hidden; height: calc(var(--vh)* 100 - var(--nav-mobile-offset)); diff --git a/UI/Web/src/app/chapter-detail/chapter-detail.component.html b/UI/Web/src/app/chapter-detail/chapter-detail.component.html index 38baf9a61..b342849aa 100644 --- a/UI/Web/src/app/chapter-detail/chapter-detail.component.html +++ b/UI/Web/src/app/chapter-detail/chapter-detail.component.html @@ -85,7 +85,7 @@
- +
diff --git a/UI/Web/src/app/chapter-detail/chapter-detail.component.ts b/UI/Web/src/app/chapter-detail/chapter-detail.component.ts index 72eb9d975..eea06f2fb 100644 --- a/UI/Web/src/app/chapter-detail/chapter-detail.component.ts +++ b/UI/Web/src/app/chapter-detail/chapter-detail.component.ts @@ -48,7 +48,7 @@ import {FilterUtilitiesService} from "../shared/_services/filter-utilities.servi import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; import {ReadingList} from "../_models/reading-list"; import {ReadingListService} from "../_services/reading-list.service"; -import {RelatedTabComponent} from "../_single-modules/related-tab/related-tab.component"; +import {RelatedTabComponent} from "../_single-module/related-tab/related-tab.component"; import {BadgeExpanderComponent} from "../shared/badge-expander/badge-expander.component"; import { MetadataDetailRowComponent diff --git a/UI/Web/src/app/collections/_components/collection-detail/collection-detail.component.html b/UI/Web/src/app/collections/_components/collection-detail/collection-detail.component.html index bfe7aaba7..4fc6fdbad 100644 --- a/UI/Web/src/app/collections/_components/collection-detail/collection-detail.component.html +++ b/UI/Web/src/app/collections/_components/collection-detail/collection-detail.component.html @@ -41,7 +41,7 @@
@if (summary.length > 0) {
- +
@if (collectionTag.source !== ScrobbleProvider.Kavita) { diff --git a/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.html b/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.html index 21b711822..8fe1f6833 100644 --- a/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.html +++ b/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.html @@ -3,47 +3,58 @@ @if(debugMode) {
@for(img of cachedImages; track img.src) { - + @if (this.readerService.imageUrlToPageNum(img.src); as imageNum) { {{this.readerService.imageUrlToPageNum(img.src)}} - + } }
} -
-
- + @if (menuOpen) { +
+
+ -
-
{{title}} ({{t('incognito-title')}})
-
- {{subtitle}} {{t('series-progress', {percentage: (Math.min(1, ((totalSeriesPagesRead + pageNum) / totalSeriesPages)) | percent)}) }} +
+
{{title}} + @if (incognitoMode) { + ({{t('incognito-title')}}) + } +
+
+ {{subtitle}} + @if (totalSeriesPages > 0) { + {{t('series-progress', {percentage: (Math.min(1, ((totalSeriesPagesRead + pageNum) / totalSeriesPages)) | percent)}) }} + } +
+
+ +
+ + @if (!bookmarkMode && hasBookmarkRights) { + + }
- -
- - @if (!bookmarkMode && hasBookmarkRights) { - - } -
-
+ } + +
- + @if (readerMode !== ReaderMode.Webtoon) {
-
- -
+ @if (showClickOverlay) { +
+ +
+ }
-
- -
+ @if (showClickOverlay) { +
+ +
+ }
-
+
- - - - -
- - -
-
- - + } @else { + @if (!isLoading && !inSetup) { +
+ + +
+ } + }
-
- -
- -
- - -
- -
- -
- + @if (menuOpen) { +
+ @if (pageOptions !== undefined && pageOptions.ceil !== undefined) { +
+ +
+ + + @if (pageOptions.ceil > 0) { +
+ +
+ } @else { +
+ +
+ } + +
- - - +
+ } +
+
+ +
+
+ +
+
+ +
+
+ +
-
-
-
- -
-
- -
-
- -
-
- -
-
-
-
-
-
-   -
-
+ @if (settingsOpen && generalSettingsForm) { +
+ +
+
+   +
+
+
+ +
+ +
+   + +
- -
-
-   - -
-
- -
-
-   - - -
+
+
+   + + +
-
-
- -
+
+
+ +
- + -
-
- -
+
+
+ +
- + -
-
-
- -
-
-
-
-
- - -
+
+ + +
-
+
+
+
+
+ + +
+
+
-
-
-
- - +
+
+
+ + +
+
+
+
+
+
+
+
+ + +
+
-
-
-
-
-
- - -
+
+
+ + {{generalSettingsForm.get('darkness')?.value + '%'}} + +
+ +
+ + +
+ + +
+
-
+
-
-
- - {{generalSettingsForm.get('darkness')?.value + '%'}} - -
- -
- - -
- - -
- -
-
- + }
-
+ } +
diff --git a/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.ts b/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.ts index 0095c5052..f796012ea 100644 --- a/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.ts +++ b/UI/Web/src/app/manga-reader/_components/manga-reader/manga-reader.component.ts @@ -13,7 +13,7 @@ import { OnInit, ViewChild } from '@angular/core'; -import {AsyncPipe, DOCUMENT, NgClass, NgFor, NgIf, NgStyle, NgSwitch, NgSwitchCase, PercentPipe} from '@angular/common'; +import {AsyncPipe, DOCUMENT, NgClass, NgFor, NgStyle, NgSwitch, NgSwitchCase, PercentPipe} from '@angular/common'; import {ActivatedRoute, Router} from '@angular/router'; import { BehaviorSubject, @@ -70,6 +70,7 @@ import {SwipeDirective} from '../../../ng-swipe/ng-swipe.directive'; import {LoadingComponent} from '../../../shared/loading/loading.component'; import {translate, TranslocoDirective} from "@jsverse/transloco"; import {shareReplay} from "rxjs/operators"; +import {DblClickDirective} from "../../../_directives/dbl-click.directive"; const PREFETCH_PAGES = 10; @@ -123,10 +124,10 @@ enum KeyDirection { ]) ], standalone: true, - imports: [NgStyle, NgIf, LoadingComponent, SwipeDirective, CanvasRendererComponent, SingleRendererComponent, + imports: [NgStyle, LoadingComponent, SwipeDirective, CanvasRendererComponent, SingleRendererComponent, DoubleRendererComponent, DoubleReverseRendererComponent, DoubleNoCoverRendererComponent, InfiniteScrollerComponent, NgxSliderModule, ReactiveFormsModule, NgFor, NgSwitch, NgSwitchCase, FittingIconPipe, ReaderModeIconPipe, - FullscreenIconPipe, TranslocoDirective, NgbProgressbar, PercentPipe, NgClass, AsyncPipe] + FullscreenIconPipe, TranslocoDirective, PercentPipe, NgClass, AsyncPipe, DblClickDirective] }) export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { @@ -1656,7 +1657,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { /** * Bookmarks the current page for the chapter */ - bookmarkPage(event: MouseEvent | undefined = undefined) { + bookmarkPage(event: Event | undefined = undefined) { if (event) { event.stopPropagation(); event.preventDefault(); diff --git a/UI/Web/src/app/person-detail/person-detail.component.html b/UI/Web/src/app/person-detail/person-detail.component.html index 219bf32f2..eb2701b90 100644 --- a/UI/Web/src/app/person-detail/person-detail.component.html +++ b/UI/Web/src/app/person-detail/person-detail.component.html @@ -14,11 +14,24 @@
-
- -
+ @if (HasCoverImage) { +
+
+ +
+
+ } @else { +
+
+ +
+
+ }
diff --git a/UI/Web/src/app/person-detail/person-detail.component.scss b/UI/Web/src/app/person-detail/person-detail.component.scss index dc52bc49c..c33cafd4b 100644 --- a/UI/Web/src/app/person-detail/person-detail.component.scss +++ b/UI/Web/src/app/person-detail/person-detail.component.scss @@ -1,4 +1,19 @@ .main-container { margin-top: 10px; padding: 0 0 0 10px; + + .person-badge { + background: var(--card-bg-color); + max-height: 200px; + height: 200px; + width: 200px; + border-radius: 50%; + overflow: hidden; + + i { + max-height: 128px; + height: 128px; + font-size: 8rem; + } + } } diff --git a/UI/Web/src/app/person-detail/person-detail.component.ts b/UI/Web/src/app/person-detail/person-detail.component.ts index 3ebe918f1..e8294aaa4 100644 --- a/UI/Web/src/app/person-detail/person-detail.component.ts +++ b/UI/Web/src/app/person-detail/person-detail.component.ts @@ -39,6 +39,7 @@ import {translate, TranslocoDirective} from "@jsverse/transloco"; import {ChapterCardComponent} from "../cards/chapter-card/chapter-card.component"; import {ThemeService} from "../_services/theme.service"; import {DefaultModalOptions} from "../_models/default-modal-options"; +import {ToastrService} from "ngx-toastr"; @Component({ selector: 'app-person-detail', @@ -74,6 +75,7 @@ export class PersonDetailComponent { protected readonly imageService = inject(ImageService); protected readonly accountService = inject(AccountService); private readonly themeService = inject(ThemeService); + private readonly toastr = inject(ToastrService); protected readonly TagBadgeCursor = TagBadgeCursor; @@ -92,6 +94,10 @@ export class PersonDetailComponent { private readonly personSubject = new BehaviorSubject(null); protected readonly person$ = this.personSubject.asObservable(); + get HasCoverImage() { + return (this.person as Person).coverImage; + } + constructor() { this.route.paramMap.pipe( switchMap(params => { @@ -104,7 +110,14 @@ export class PersonDetailComponent { this.personName = personName; return this.personService.get(personName); }), - tap(person => { + tap((person) => { + + if (person == null) { + this.toastr.error(translate('toasts.unauthorized-1')); + this.router.navigateByUrl('/home'); + return; + } + this.person = person; this.personSubject.next(person); // emit the person data for subscribers this.themeService.setColorScape(person.primaryColor || '', person.secondaryColor); diff --git a/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.html b/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.html index 90f8f1de8..55ffee870 100644 --- a/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.html +++ b/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.html @@ -124,8 +124,8 @@ -
- +
+
@if (characters$ | async; as characters) { diff --git a/UI/Web/src/app/series-detail/_components/metadata-detail-row/metadata-detail-row.component.ts b/UI/Web/src/app/series-detail/_components/metadata-detail-row/metadata-detail-row.component.ts index 952916d08..0647ae192 100644 --- a/UI/Web/src/app/series-detail/_components/metadata-detail-row/metadata-detail-row.component.ts +++ b/UI/Web/src/app/series-detail/_components/metadata-detail-row/metadata-detail-row.component.ts @@ -1,5 +1,5 @@ import {ChangeDetectionStrategy, Component, inject, Input} from '@angular/core'; -import {AgeRatingImageComponent} from "../../../_single-modules/age-rating-image/age-rating-image.component"; +import {AgeRatingImageComponent} from "../../../_single-module/age-rating-image/age-rating-image.component"; import {CompactNumberPipe} from "../../../_pipes/compact-number.pipe"; import {ReadTimeLeftPipe} from "../../../_pipes/read-time-left.pipe"; import {ReadTimePipe} from "../../../_pipes/read-time.pipe"; @@ -17,7 +17,7 @@ import {FilterComparison} from "../../../_models/metadata/v2/filter-comparison"; import {FilterField} from "../../../_models/metadata/v2/filter-field"; import {MangaFormat} from "../../../_models/manga-format"; import {SeriesFormatComponent} from "../../../shared/series-format/series-format.component"; -import {PublisherFlipperComponent} from "../../../_single-modules/publisher-flipper/publisher-flipper.component"; +import {PublisherFlipperComponent} from "../../../_single-module/publisher-flipper/publisher-flipper.component"; @Component({ selector: 'app-metadata-detail-row', diff --git a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.html b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.html index 950b5a8eb..3895f84f3 100644 --- a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.html +++ b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.html @@ -109,7 +109,7 @@
- +
diff --git a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts index c3d92fc47..250fbc0a6 100644 --- a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts +++ b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts @@ -115,7 +115,7 @@ import {DownloadButtonComponent} from "../download-button/download-button.compon import {hasAnyCast} from "../../../_models/common/i-has-cast"; import {EditVolumeModalComponent} from "../../../_single-module/edit-volume-modal/edit-volume-modal.component"; import {CoverUpdateEvent} from "../../../_models/events/cover-update-event"; -import {RelatedSeriesPair, RelatedTabComponent} from "../../../_single-modules/related-tab/related-tab.component"; +import {RelatedSeriesPair, RelatedTabComponent} from "../../../_single-module/related-tab/related-tab.component"; import {CollectionTagService} from "../../../_services/collection-tag.service"; import {UserCollection} from "../../../_models/collection-tag"; import {CoverImageComponent} from "../../../_single-module/cover-image/cover-image.component"; diff --git a/UI/Web/src/app/shared/image/image.component.html b/UI/Web/src/app/shared/image/image.component.html index aef9819b9..323dad697 100644 --- a/UI/Web/src/app/shared/image/image.component.html +++ b/UI/Web/src/app/shared/image/image.component.html @@ -1,4 +1,5 @@ ; diff --git a/UI/Web/src/app/shared/person-badge/person-badge.component.html b/UI/Web/src/app/shared/person-badge/person-badge.component.html index 9a7b6f104..6f95ef85f 100644 --- a/UI/Web/src/app/shared/person-badge/person-badge.component.html +++ b/UI/Web/src/app/shared/person-badge/person-badge.component.html @@ -1,30 +1,26 @@ @if (person !== undefined) { -
-
- @if (HasCoverImage) { -
- - + +
+
+ - } @else { -
- -
- } - -
- @if (isStaff) { +
{{person.name}} - } @else { - {{person.name}} - } +
-
+ } diff --git a/UI/Web/src/app/shared/person-badge/person-badge.component.scss b/UI/Web/src/app/shared/person-badge/person-badge.component.scss index 4679209bf..44683a9e2 100644 --- a/UI/Web/src/app/shared/person-badge/person-badge.component.scss +++ b/UI/Web/src/app/shared/person-badge/person-badge.component.scss @@ -1,18 +1,31 @@ +@import '../../../theme/_variables.scss'; + .tagbadge { background-color: var(--tagbadge-bg-color); transition: all .3s ease-out; - margin: 3px 5px 3px 0px; - padding: 2px 10px; + margin: 3px 10px 3px 0px; border-radius: 6px; font-size: .8rem; display: inline-block; cursor: pointer; - width: 120px; + width: 96px; word-break: break-word; i { + max-height: 48px; + height: 48px; + width: 48px; font-size: 2.96rem; font-weight: bold; cursor: pointer; } -} + + .image-container { + background: var(--card-bg-color); + max-height: 96px; + height: 96px; + width: 96px; + border-radius: 50%; + overflow: hidden; + } +} \ No newline at end of file diff --git a/UI/Web/src/app/shared/read-more/read-more.component.scss b/UI/Web/src/app/shared/read-more/read-more.component.scss index fe87f8f84..1680ce481 100644 --- a/UI/Web/src/app/shared/read-more/read-more.component.scss +++ b/UI/Web/src/app/shared/read-more/read-more.component.scss @@ -1,3 +1,5 @@ +@import "../../../theme/variables"; + .blur-text { color: transparent; text-shadow: 0 0 5px var(--body-text-color); @@ -8,5 +10,10 @@ div { word-break: break-word; + max-width: 75ch; + + @media (max-width: $grid-breakpoints-sm) { + max-width: 50ch; + } } } diff --git a/UI/Web/src/app/volume-detail/volume-detail.component.html b/UI/Web/src/app/volume-detail/volume-detail.component.html index d1f64e20f..75f196e1e 100644 --- a/UI/Web/src/app/volume-detail/volume-detail.component.html +++ b/UI/Web/src/app/volume-detail/volume-detail.component.html @@ -89,7 +89,7 @@
- +
diff --git a/UI/Web/src/app/volume-detail/volume-detail.component.ts b/UI/Web/src/app/volume-detail/volume-detail.component.ts index 0797fd7de..8baecad29 100644 --- a/UI/Web/src/app/volume-detail/volume-detail.component.ts +++ b/UI/Web/src/app/volume-detail/volume-detail.component.ts @@ -59,7 +59,7 @@ import { } from "../_single-module/edit-volume-modal/edit-volume-modal.component"; import {Genre} from "../_models/metadata/genre"; import {Tag} from "../_models/tag"; -import {RelatedTabComponent} from "../_single-modules/related-tab/related-tab.component"; +import {RelatedTabComponent} from "../_single-module/related-tab/related-tab.component"; import {ReadingList} from "../_models/reading-list"; import {ReadingListService} from "../_services/reading-list.service"; import {BadgeExpanderComponent} from "../shared/badge-expander/badge-expander.component"; diff --git a/UI/Web/src/theme/components/_sidenav.scss b/UI/Web/src/theme/components/_sidenav.scss index 5f8f567e9..7d4d7a140 100644 --- a/UI/Web/src/theme/components/_sidenav.scss +++ b/UI/Web/src/theme/components/_sidenav.scss @@ -12,6 +12,7 @@ border-radius: var(--side-nav-border-radius); transition: width var(--side-nav-openclose-transition), background-color var(--side-nav-bg-color-transition), border-color var(--side-nav-border-transition); border: var(--side-nav-border); + overflow: hidden; &::-webkit-scrollbar { visibility: hidden; @@ -27,7 +28,7 @@ } //START closed state of the sidebar &.closed { - width: 4.0625rem; + width: 2.825rem; overflow-x: hidden; overflow-y: hidden; background-color: var(--side-nav-closed-bg-color); @@ -49,11 +50,6 @@ opacity: 0; } - .side-nav-text { - opacity: 0; - display: none; - } - .card-actions { opacity: 0; display: none; @@ -64,7 +60,7 @@ //END closed state of the sidebar //START sidebar .side-nav { - overflow-y: hidden; + overflow: hidden; height: 100%; scrollbar-gutter: stable; scrollbar-width: thin; @@ -73,7 +69,7 @@ position: relative; align-items: center; display: flex; - justify-content: space-between; + justify-content: start; width: 100%; height: auto; min-height: 2.6rem; @@ -89,6 +85,7 @@ &:first-of-type { text-align: center; width: 2.5rem; + min-width: 2.5rem; margin-left: 0.3rem; } @@ -101,7 +98,7 @@ align-items: center; height: 100%; justify-content: inherit; - width: 100%; + padding: 0 0.625rem; i { font-size: var(--side-nav-icon-size); @@ -291,10 +288,6 @@ .side-nav-item { width: 100%; padding: 0; - display: block; - line-height: 2.5rem; - text-align: center; - min-height: unset; color: var(--side-nav-item-closed-color); &:hover { @@ -317,11 +310,13 @@ margin: 0; left: 0; top: 0; - transition: width var(--side-nav-openclose-transition); + transition: width var(--side-nav-openclose-transition), visibility var(--side-nav-openclose-transition); z-index: 1050; + overflow-x: hidden; overflow-y: auto; border: var(--side-nav-mobile-border); border-radius: 0rem; + visibility: visible; &.preference { background-color: unset; @@ -349,8 +344,10 @@ &.closed { width: 0; + background-color: var(--side-nav-mobile-bg-color); overflow: hidden; box-shadow: none; + visibility: hidden; } .side-nav { @@ -383,9 +380,13 @@ left: 0; top: 0; z-index: 1041; + visibility: visible; + opacity: 1; + transition: visibility var(--side-nav-openclose-transition), opacity var(--side-nav-openclose-transition); &.closed { - display: none; + visibility: hidden; + opacity: 0; } } }