Added the ability to browse different genres (still needs polish).
Fixed a rare bug with crypto.UUID by backfilling it.
This commit is contained in:
parent
d400938610
commit
2e80316057
19 changed files with 288 additions and 33 deletions
|
|
@ -6,6 +6,7 @@ using System.Threading.Tasks;
|
||||||
using API.Constants;
|
using API.Constants;
|
||||||
using API.Data;
|
using API.Data;
|
||||||
using API.Data.Repositories;
|
using API.Data.Repositories;
|
||||||
|
using API.DTOs;
|
||||||
using API.DTOs.Filtering;
|
using API.DTOs.Filtering;
|
||||||
using API.DTOs.Metadata;
|
using API.DTOs.Metadata;
|
||||||
using API.DTOs.Person;
|
using API.DTOs.Person;
|
||||||
|
|
@ -46,6 +47,22 @@ public class MetadataController(IUnitOfWork unitOfWork, ILocalizationService loc
|
||||||
return Ok(await unitOfWork.GenreRepository.GetAllGenreDtosForLibrariesAsync(User.GetUserId(), ids, context));
|
return Ok(await unitOfWork.GenreRepository.GetAllGenreDtosForLibrariesAsync(User.GetUserId(), ids, context));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a list of Genres with counts for counts when Genre is on Series/Chapter
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpPost("genres-with-counts")]
|
||||||
|
[ResponseCache(CacheProfileName = ResponseCacheProfiles.FiveMinute)]
|
||||||
|
public async Task<ActionResult<PagedList<BrowseGenreDto>>> GetBrowseGenres(UserParams? userParams = null)
|
||||||
|
{
|
||||||
|
userParams ??= UserParams.Default;
|
||||||
|
|
||||||
|
var list = await unitOfWork.GenreRepository.GetBrowseableGenre(User.GetUserId(), userParams);
|
||||||
|
Response.AddPaginationHeader(list.CurrentPage, list.PageSize, list.TotalCount, list.TotalPages);
|
||||||
|
|
||||||
|
return Ok(list);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fetches people from the instance by role
|
/// Fetches people from the instance by role
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
13
API/DTOs/Metadata/BrowseGenreDto.cs
Normal file
13
API/DTOs/Metadata/BrowseGenreDto.cs
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
namespace API.DTOs.Metadata;
|
||||||
|
|
||||||
|
public sealed record BrowseGenreDto : GenreTagDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Number of Series this Entity is on
|
||||||
|
/// </summary>
|
||||||
|
public int SeriesCount { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Number of Issues this Entity is on
|
||||||
|
/// </summary>
|
||||||
|
public int IssueCount { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
namespace API.DTOs.Metadata;
|
namespace API.DTOs.Metadata;
|
||||||
|
|
||||||
public sealed record GenreTagDto
|
public record GenreTagDto
|
||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
public required string Title { get; set; }
|
public required string Title { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ public class BrowsePersonDto : PersonDto
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int SeriesCount { get; set; }
|
public int SeriesCount { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Number or Issues this Person is the Writer for
|
/// Number of Issues this Person is the Writer for
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int IssueCount { get; set; }
|
public int IssueCount { get; set; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ using API.DTOs.Metadata;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using API.Extensions.QueryExtensions;
|
using API.Extensions.QueryExtensions;
|
||||||
|
using API.Helpers;
|
||||||
using API.Services.Tasks.Scanner.Parser;
|
using API.Services.Tasks.Scanner.Parser;
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using AutoMapper.QueryableExtensions;
|
using AutoMapper.QueryableExtensions;
|
||||||
|
|
@ -27,6 +28,7 @@ public interface IGenreRepository
|
||||||
Task<GenreTagDto> GetRandomGenre();
|
Task<GenreTagDto> GetRandomGenre();
|
||||||
Task<GenreTagDto> GetGenreById(int id);
|
Task<GenreTagDto> GetGenreById(int id);
|
||||||
Task<List<string>> GetAllGenresNotInListAsync(ICollection<string> genreNames);
|
Task<List<string>> GetAllGenresNotInListAsync(ICollection<string> genreNames);
|
||||||
|
Task<PagedList<BrowseGenreDto>> GetBrowseableGenre(int userId, UserParams userParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class GenreRepository : IGenreRepository
|
public class GenreRepository : IGenreRepository
|
||||||
|
|
@ -165,4 +167,28 @@ public class GenreRepository : IGenreRepository
|
||||||
// Return the original non-normalized genres for the missing ones
|
// Return the original non-normalized genres for the missing ones
|
||||||
return missingGenres.Select(normalizedName => normalizedToOriginalMap[normalizedName]).ToList();
|
return missingGenres.Select(normalizedName => normalizedToOriginalMap[normalizedName]).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<PagedList<BrowseGenreDto>> GetBrowseableGenre(int userId, UserParams userParams)
|
||||||
|
{
|
||||||
|
var ageRating = await _context.AppUser.GetUserAgeRestriction(userId);
|
||||||
|
|
||||||
|
var query = _context.Genre
|
||||||
|
.RestrictAgainstAgeRestriction(ageRating)
|
||||||
|
.Select(g => new BrowseGenreDto
|
||||||
|
{
|
||||||
|
Id = g.Id,
|
||||||
|
Title = g.Title,
|
||||||
|
SeriesCount = g.SeriesMetadatas
|
||||||
|
.Select(sm => sm.Id)
|
||||||
|
.Distinct()
|
||||||
|
.Count(),
|
||||||
|
IssueCount = g.Chapters
|
||||||
|
.Select(ch => ch.Id)
|
||||||
|
.Distinct()
|
||||||
|
.Count()
|
||||||
|
})
|
||||||
|
.OrderBy(g => g.Title);
|
||||||
|
|
||||||
|
return await PagedList<BrowseGenreDto>.CreateAsync(query, userParams.PageNumber, userParams.PageSize);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using API.Data.Misc;
|
using API.Data.Misc;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
|
using API.Entities.Metadata;
|
||||||
|
|
||||||
namespace API.Extensions;
|
namespace API.Extensions;
|
||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
@ -42,4 +43,16 @@ public static class EnumerableExtensions
|
||||||
|
|
||||||
return q;
|
return q;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static IEnumerable<SeriesMetadata> RestrictAgainstAgeRestriction(this IEnumerable<SeriesMetadata> items, AgeRestriction restriction)
|
||||||
|
{
|
||||||
|
if (restriction.AgeRating == AgeRating.NotApplicable) return items;
|
||||||
|
var q = items.Where(s => s.AgeRating <= restriction.AgeRating);
|
||||||
|
if (!restriction.IncludeUnknowns)
|
||||||
|
{
|
||||||
|
return q.Where(s => s.AgeRating != AgeRating.Unknown);
|
||||||
|
}
|
||||||
|
|
||||||
|
return q;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ using System.Linq;
|
||||||
using API.Data.Misc;
|
using API.Data.Misc;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
|
using API.Entities.Metadata;
|
||||||
using API.Entities.Person;
|
using API.Entities.Person;
|
||||||
|
|
||||||
namespace API.Extensions.QueryExtensions;
|
namespace API.Extensions.QueryExtensions;
|
||||||
|
|
@ -26,6 +27,19 @@ public static class RestrictByAgeExtensions
|
||||||
return q;
|
return q;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static IQueryable<SeriesMetadata> RestrictAgainstAgeRestriction(this IQueryable<SeriesMetadata> queryable, AgeRestriction restriction)
|
||||||
|
{
|
||||||
|
if (restriction.AgeRating == AgeRating.NotApplicable) return queryable;
|
||||||
|
var q = queryable.Where(s => s.AgeRating <= restriction.AgeRating);
|
||||||
|
|
||||||
|
if (!restriction.IncludeUnknowns)
|
||||||
|
{
|
||||||
|
return q.Where(s => s.AgeRating != AgeRating.Unknown);
|
||||||
|
}
|
||||||
|
|
||||||
|
return q;
|
||||||
|
}
|
||||||
|
|
||||||
public static IQueryable<Chapter> RestrictAgainstAgeRestriction(this IQueryable<Chapter> queryable, AgeRestriction restriction)
|
public static IQueryable<Chapter> RestrictAgainstAgeRestriction(this IQueryable<Chapter> queryable, AgeRestriction restriction)
|
||||||
{
|
{
|
||||||
if (restriction.AgeRating == AgeRating.NotApplicable) return queryable;
|
if (restriction.AgeRating == AgeRating.NotApplicable) return queryable;
|
||||||
|
|
|
||||||
6
UI/Web/src/app/_models/metadata/browse-genre.ts
Normal file
6
UI/Web/src/app/_models/metadata/browse-genre.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
import {Genre} from "./genre";
|
||||||
|
|
||||||
|
export interface BrowseGenre extends Genre {
|
||||||
|
seriesCount: number;
|
||||||
|
issueCount: number;
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { Routes } from "@angular/router";
|
import {Routes} from "@angular/router";
|
||||||
import { AllSeriesComponent } from "../all-series/_components/all-series/all-series.component";
|
|
||||||
import {BrowseAuthorsComponent} from "../browse-people/browse-authors.component";
|
import {BrowseAuthorsComponent} from "../browse-people/browse-authors.component";
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
7
UI/Web/src/app/_routes/browse-genres-routing.module.ts
Normal file
7
UI/Web/src/app/_routes/browse-genres-routing.module.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import {Routes} from "@angular/router";
|
||||||
|
import {BrowseGenresComponent} from "../all-genres/browse-genres.component";
|
||||||
|
|
||||||
|
|
||||||
|
export const routes: Routes = [
|
||||||
|
{path: '', component: BrowseGenresComponent, pathMatch: 'full'},
|
||||||
|
];
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Injectable } from '@angular/core';
|
import {Injectable} from '@angular/core';
|
||||||
import { JumpKey } from '../_models/jumpbar/jump-key';
|
import {JumpKey} from '../_models/jumpbar/jump-key';
|
||||||
|
|
||||||
const keySize = 25; // Height of the JumpBar button
|
const keySize = 25; // Height of the JumpBar button
|
||||||
|
|
||||||
|
|
@ -105,14 +105,18 @@ export class JumpbarService {
|
||||||
getJumpKeys(data :Array<any>, keySelector: (data: any) => string) {
|
getJumpKeys(data :Array<any>, keySelector: (data: any) => string) {
|
||||||
const keys: {[key: string]: number} = {};
|
const keys: {[key: string]: number} = {};
|
||||||
data.forEach(obj => {
|
data.forEach(obj => {
|
||||||
let ch = keySelector(obj).charAt(0).toUpperCase();
|
try {
|
||||||
if (/\d|\#|!|%|@|\(|\)|\^|\.|_|\*/g.test(ch)) {
|
let ch = keySelector(obj).charAt(0).toUpperCase();
|
||||||
ch = '#';
|
if (/\d|\#|!|%|@|\(|\)|\^|\.|_|\*/g.test(ch)) {
|
||||||
|
ch = '#';
|
||||||
|
}
|
||||||
|
if (!keys.hasOwnProperty(ch)) {
|
||||||
|
keys[ch] = 0;
|
||||||
|
}
|
||||||
|
keys[ch] += 1;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to calculate jump key for ', obj, e);
|
||||||
}
|
}
|
||||||
if (!keys.hasOwnProperty(ch)) {
|
|
||||||
keys[ch] = 0;
|
|
||||||
}
|
|
||||||
keys[ch] += 1;
|
|
||||||
});
|
});
|
||||||
return Object.keys(keys).map(k => {
|
return Object.keys(keys).map(k => {
|
||||||
k = k.toUpperCase();
|
k = k.toUpperCase();
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import {HttpClient} from '@angular/common/http';
|
import {HttpClient, HttpParams} from '@angular/common/http';
|
||||||
import {inject, Injectable} from '@angular/core';
|
import {inject, Injectable} from '@angular/core';
|
||||||
import {tap} from 'rxjs/operators';
|
import {tap} from 'rxjs/operators';
|
||||||
import {map, of} from 'rxjs';
|
import {map, of} from 'rxjs';
|
||||||
|
|
@ -25,6 +25,9 @@ import {MangaFormatPipe} from "../_pipes/manga-format.pipe";
|
||||||
import {TranslocoService} from "@jsverse/transloco";
|
import {TranslocoService} from "@jsverse/transloco";
|
||||||
import {LibraryService} from './library.service';
|
import {LibraryService} from './library.service';
|
||||||
import {CollectionTagService} from "./collection-tag.service";
|
import {CollectionTagService} from "./collection-tag.service";
|
||||||
|
import {PaginatedResult} from "../_models/pagination";
|
||||||
|
import {UtilityService} from "../shared/_services/utility.service";
|
||||||
|
import {BrowseGenre} from "../_models/metadata/browse-genre";
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
|
|
@ -34,6 +37,7 @@ export class MetadataService {
|
||||||
private readonly translocoService = inject(TranslocoService);
|
private readonly translocoService = inject(TranslocoService);
|
||||||
private readonly libraryService = inject(LibraryService);
|
private readonly libraryService = inject(LibraryService);
|
||||||
private readonly collectionTagService = inject(CollectionTagService);
|
private readonly collectionTagService = inject(CollectionTagService);
|
||||||
|
private readonly utilityService = inject(UtilityService);
|
||||||
|
|
||||||
baseUrl = environment.apiUrl;
|
baseUrl = environment.apiUrl;
|
||||||
private validLanguages: Array<Language> = [];
|
private validLanguages: Array<Language> = [];
|
||||||
|
|
@ -85,6 +89,17 @@ export class MetadataService {
|
||||||
return this.httpClient.get<Array<Genre>>(this.baseUrl + method);
|
return this.httpClient.get<Array<Genre>>(this.baseUrl + method);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getGenreWithCounts(pageNum?: number, itemsPerPage?: number) {
|
||||||
|
let params = new HttpParams();
|
||||||
|
params = this.utilityService.addPaginationIfExists(params, pageNum, itemsPerPage);
|
||||||
|
|
||||||
|
return this.httpClient.post<PaginatedResult<BrowseGenre[]>>(this.baseUrl + 'metadata/genres-with-counts', {}, {observe: 'response', params}).pipe(
|
||||||
|
map((response: any) => {
|
||||||
|
return this.utilityService.createPaginatedResult(response) as PaginatedResult<BrowseGenre[]>;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
getAllLanguages(libraries?: Array<number>) {
|
getAllLanguages(libraries?: Array<number>) {
|
||||||
let method = 'metadata/languages'
|
let method = 'metadata/languages'
|
||||||
if (libraries != undefined && libraries.length > 0) {
|
if (libraries != undefined && libraries.length > 0) {
|
||||||
|
|
|
||||||
34
UI/Web/src/app/all-genres/browse-genres.component.html
Normal file
34
UI/Web/src/app/all-genres/browse-genres.component.html
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
<div class="main-container container-fluid">
|
||||||
|
<ng-container *transloco="let t; read:'browse-genres'" >
|
||||||
|
<app-side-nav-companion-bar [hasFilter]="false">
|
||||||
|
<h2 title>
|
||||||
|
<span>{{t('title')}}</span>
|
||||||
|
</h2>
|
||||||
|
<h6 subtitle>{{t('genre-count', {num: pagination.totalItems | number})}} </h6>
|
||||||
|
|
||||||
|
</app-side-nav-companion-bar>
|
||||||
|
|
||||||
|
<app-card-detail-layout
|
||||||
|
[isLoading]="isLoading"
|
||||||
|
[items]="genres"
|
||||||
|
[pagination]="pagination"
|
||||||
|
[trackByIdentity]="trackByIdentity"
|
||||||
|
[jumpBarKeys]="jumpKeys"
|
||||||
|
[filteringDisabled]="true"
|
||||||
|
[refresh]="refresh"
|
||||||
|
>
|
||||||
|
<ng-template #cardItem let-item let-position="idx">
|
||||||
|
|
||||||
|
<div class="genre-card" (click)="openFilter(FilterField.Genres, item.id)">
|
||||||
|
<div class="genre-name">{{ item.title }}</div>
|
||||||
|
<div class="genre-meta">
|
||||||
|
<span>{{ item.seriesCount }} Series</span>
|
||||||
|
<span>{{ item.issueCount }} Issues</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</ng-template>
|
||||||
|
</app-card-detail-layout>
|
||||||
|
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
26
UI/Web/src/app/all-genres/browse-genres.component.scss
Normal file
26
UI/Web/src/app/all-genres/browse-genres.component.scss
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
.genre-card {
|
||||||
|
background-color: var(--bs-card-color, #2c2c2c);
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
||||||
|
transition: transform 0.2s ease, background 0.3s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.genre-card:hover {
|
||||||
|
background-color: #3a3a3a;
|
||||||
|
transform: translateY(-3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.genre-name {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.genre-meta {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
color: var(--text-muted-color, #bbb);
|
||||||
|
}
|
||||||
72
UI/Web/src/app/all-genres/browse-genres.component.ts
Normal file
72
UI/Web/src/app/all-genres/browse-genres.component.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
ChangeDetectorRef,
|
||||||
|
Component,
|
||||||
|
DestroyRef,
|
||||||
|
EventEmitter,
|
||||||
|
inject,
|
||||||
|
OnInit
|
||||||
|
} from '@angular/core';
|
||||||
|
import {CardDetailLayoutComponent} from "../cards/card-detail-layout/card-detail-layout.component";
|
||||||
|
import {DecimalPipe} from "@angular/common";
|
||||||
|
import {
|
||||||
|
SideNavCompanionBarComponent
|
||||||
|
} from "../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component";
|
||||||
|
import {TranslocoDirective} from "@jsverse/transloco";
|
||||||
|
import {JumpbarService} from "../_services/jumpbar.service";
|
||||||
|
import {BrowsePerson} from "../_models/person/browse-person";
|
||||||
|
import {Pagination} from "../_models/pagination";
|
||||||
|
import {JumpKey} from "../_models/jumpbar/jump-key";
|
||||||
|
import {MetadataService} from "../_services/metadata.service";
|
||||||
|
import {BrowseGenre} from "../_models/metadata/browse-genre";
|
||||||
|
import {FilterField} from "../_models/metadata/v2/filter-field";
|
||||||
|
import {FilterComparison} from "../_models/metadata/v2/filter-comparison";
|
||||||
|
import {FilterUtilitiesService} from "../shared/_services/filter-utilities.service";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-all-genres',
|
||||||
|
imports: [
|
||||||
|
CardDetailLayoutComponent,
|
||||||
|
DecimalPipe,
|
||||||
|
SideNavCompanionBarComponent,
|
||||||
|
TranslocoDirective
|
||||||
|
],
|
||||||
|
templateUrl: './browse-genres.component.html',
|
||||||
|
styleUrl: './browse-genres.component.scss',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class BrowseGenresComponent implements OnInit {
|
||||||
|
|
||||||
|
protected readonly FilterField = FilterField;
|
||||||
|
|
||||||
|
private readonly destroyRef = inject(DestroyRef);
|
||||||
|
private readonly cdRef = inject(ChangeDetectorRef);
|
||||||
|
private readonly metadataService = inject(MetadataService);
|
||||||
|
private readonly jumpbarService = inject(JumpbarService);
|
||||||
|
protected readonly filterUtilityService = inject(FilterUtilitiesService);
|
||||||
|
|
||||||
|
isLoading = false;
|
||||||
|
genres: Array<BrowseGenre> = [];
|
||||||
|
pagination: Pagination = {currentPage: 0, totalPages: 0, totalItems: 0, itemsPerPage: 0};
|
||||||
|
refresh: EventEmitter<void> = new EventEmitter();
|
||||||
|
jumpKeys: Array<JumpKey> = [];
|
||||||
|
trackByIdentity = (index: number, item: BrowsePerson) => `${item.id}`;
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.isLoading = true;
|
||||||
|
this.cdRef.markForCheck();
|
||||||
|
this.metadataService.getGenreWithCounts(undefined, undefined).subscribe(d => {
|
||||||
|
this.genres = d.result;
|
||||||
|
this.pagination = d.pagination;
|
||||||
|
this.jumpKeys = this.jumpbarService.getJumpKeys(this.genres, (d: BrowseGenre) => d.title);
|
||||||
|
this.isLoading = false;
|
||||||
|
this.cdRef.markForCheck();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
openFilter(field: FilterField, value: string | number) {
|
||||||
|
this.filterUtilityService.applyFilter(['all-series'], field, FilterComparison.Equal, `${value}`).subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import { NgModule } from '@angular/core';
|
import {NgModule} from '@angular/core';
|
||||||
import { Routes, RouterModule, PreloadAllModules } from '@angular/router';
|
import {PreloadAllModules, RouterModule, Routes} from '@angular/router';
|
||||||
import { AuthGuard } from './_guards/auth.guard';
|
import {AuthGuard} from './_guards/auth.guard';
|
||||||
import { LibraryAccessGuard } from './_guards/library-access.guard';
|
import {LibraryAccessGuard} from './_guards/library-access.guard';
|
||||||
import { AdminGuard } from './_guards/admin.guard';
|
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
|
|
@ -54,6 +53,10 @@ const routes: Routes = [
|
||||||
path: 'browse/authors',
|
path: 'browse/authors',
|
||||||
loadChildren: () => import('./_routes/browse-authors-routing.module').then(m => m.routes)
|
loadChildren: () => import('./_routes/browse-authors-routing.module').then(m => m.routes)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'browse/genres',
|
||||||
|
loadChildren: () => import('./_routes/browse-genres-routing.module').then(m => m.routes)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'library',
|
path: 'library',
|
||||||
runGuardsAndResolvers: 'always',
|
runGuardsAndResolvers: 'always',
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,7 @@ import {DecimalPipe} from "@angular/common";
|
||||||
import {Series} from "../_models/series";
|
import {Series} from "../_models/series";
|
||||||
import {Pagination} from "../_models/pagination";
|
import {Pagination} from "../_models/pagination";
|
||||||
import {JumpKey} from "../_models/jumpbar/jump-key";
|
import {JumpKey} from "../_models/jumpbar/jump-key";
|
||||||
import {ActivatedRoute, Router} from "@angular/router";
|
import {Router} from "@angular/router";
|
||||||
import {Title} from "@angular/platform-browser";
|
|
||||||
import {ActionFactoryService} from "../_services/action-factory.service";
|
|
||||||
import {ActionService} from "../_services/action.service";
|
|
||||||
import {MessageHubService} from "../_services/message-hub.service";
|
|
||||||
import {UtilityService} from "../shared/_services/utility.service";
|
|
||||||
import {PersonService} from "../_services/person.service";
|
import {PersonService} from "../_services/person.service";
|
||||||
import {BrowsePerson} from "../_models/person/browse-person";
|
import {BrowsePerson} from "../_models/person/browse-person";
|
||||||
import {JumpbarService} from "../_services/jumpbar.service";
|
import {JumpbarService} from "../_services/jumpbar.service";
|
||||||
|
|
@ -48,13 +43,7 @@ export class BrowseAuthorsComponent implements OnInit {
|
||||||
|
|
||||||
private readonly destroyRef = inject(DestroyRef);
|
private readonly destroyRef = inject(DestroyRef);
|
||||||
private readonly cdRef = inject(ChangeDetectorRef);
|
private readonly cdRef = inject(ChangeDetectorRef);
|
||||||
private readonly route = inject(ActivatedRoute);
|
|
||||||
private readonly router = inject(Router);
|
private readonly router = inject(Router);
|
||||||
private readonly titleService = inject(Title);
|
|
||||||
private readonly actionFactoryService = inject(ActionFactoryService);
|
|
||||||
private readonly actionService = inject(ActionService);
|
|
||||||
private readonly hubService = inject(MessageHubService);
|
|
||||||
private readonly utilityService = inject(UtilityService);
|
|
||||||
private readonly personService = inject(PersonService);
|
private readonly personService = inject(PersonService);
|
||||||
private readonly jumpbarService = inject(JumpbarService);
|
private readonly jumpbarService = inject(JumpbarService);
|
||||||
protected readonly imageService = inject(ImageService);
|
protected readonly imageService = inject(ImageService);
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ export class SettingSwitchComponent implements AfterContentInit {
|
||||||
const inputElement = element.querySelector('input');
|
const inputElement = element.querySelector('input');
|
||||||
|
|
||||||
// If no id, generate a random id and assign it to the input
|
// If no id, generate a random id and assign it to the input
|
||||||
inputElement.id = crypto.randomUUID();
|
inputElement.id = this.generateId();
|
||||||
|
|
||||||
if (inputElement && inputElement.id) {
|
if (inputElement && inputElement.id) {
|
||||||
this.labelId = inputElement.id;
|
this.labelId = inputElement.id;
|
||||||
|
|
@ -62,4 +62,14 @@ export class SettingSwitchComponent implements AfterContentInit {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private generateId(): string {
|
||||||
|
if (crypto && crypto.randomUUID) {
|
||||||
|
return crypto.randomUUID();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback for browsers without crypto.randomUUID (which has happened multiple times in my user base)
|
||||||
|
return 'id-' + Math.random().toString(36).substr(2, 9) + '-' + Date.now().toString(36);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1111,6 +1111,13 @@
|
||||||
"series-count": "{{common.series-count}}"
|
"series-count": "{{common.series-count}}"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"browse-genres": {
|
||||||
|
"title": "Browse Genres",
|
||||||
|
"genre-count": "{{num}} Genres",
|
||||||
|
"issue-count": "{{common.issue-count}}",
|
||||||
|
"series-count": "{{common.series-count}}"
|
||||||
|
},
|
||||||
|
|
||||||
"person-detail": {
|
"person-detail": {
|
||||||
"aka-title": "Also known as ",
|
"aka-title": "Also known as ",
|
||||||
"known-for-title": "Known For",
|
"known-for-title": "Known For",
|
||||||
|
|
@ -2595,7 +2602,7 @@
|
||||||
"browse-title-pipe": {
|
"browse-title-pipe": {
|
||||||
"publication-status": "{{value}} works",
|
"publication-status": "{{value}} works",
|
||||||
"age-rating": "Rated {{value}}",
|
"age-rating": "Rated {{value}}",
|
||||||
"user-rating": "Rated {{value}} stars",
|
"user-rating": "{{value}} star rating",
|
||||||
"tag": "Has Tag {{value}}",
|
"tag": "Has Tag {{value}}",
|
||||||
"translator": "Translated by {{value}}",
|
"translator": "Translated by {{value}}",
|
||||||
"character": "Has character {{value}}",
|
"character": "Has character {{value}}",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue