Colorscape Love (#3326)

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
This commit is contained in:
Joe Milazzo 2024-10-31 18:44:03 -05:00 committed by GitHub
parent b44f89d1e8
commit a847468a6c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 1009 additions and 429 deletions

View file

@ -1,6 +1,8 @@
import { Injectable, Inject } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { BehaviorSubject } from 'rxjs';
import {BehaviorSubject, filter, take, tap, timer} from 'rxjs';
import {NavigationEnd, Router} from "@angular/router";
import {debounceTime} from "rxjs/operators";
interface ColorSpace {
primary: string;
@ -39,13 +41,41 @@ const colorScapeSelector = 'colorscape';
})
export class ColorscapeService {
private colorSubject = new BehaviorSubject<ColorSpaceRGBA | null>(null);
private colorSeedSubject = new BehaviorSubject<{primary: string, complementary: string | null} | null>(null);
public readonly colors$ = this.colorSubject.asObservable();
private minDuration = 1000; // minimum duration
private maxDuration = 4000; // maximum duration
private defaultColorspaceDuration = 300; // duration to wait before defaulting back to default colorspace
constructor(@Inject(DOCUMENT) private document: Document) {
constructor(@Inject(DOCUMENT) private document: Document, private readonly router: Router) {
this.router.events.pipe(
filter(event => event instanceof NavigationEnd),
tap(() => this.checkAndResetColorscapeAfterDelay())
).subscribe();
}
/**
* Due to changing ColorScape on route end, we might go from one space to another, but the router events resets to default
* This delays it to see if the colors changed or not in 500ms and if not, then we will reset to default.
* @private
*/
private checkAndResetColorscapeAfterDelay() {
// Capture the current colors at the start of NavigationEnd
const initialColors = this.colorSubject.getValue();
// Wait for X ms, then check if colors have changed
timer(this.defaultColorspaceDuration).pipe(
take(1), // Complete after the timer emits
tap(() => {
const currentColors = this.colorSubject.getValue();
if (initialColors != null && currentColors != null && this.areColorSpacesVisuallyEqual(initialColors, currentColors)) {
this.setColorScape(''); // Reset to default if colors haven't changed
}
})
).subscribe();
}
/**
@ -64,6 +94,15 @@ export class ColorscapeService {
return;
}
// Check the old seed colors and check if they are similar, then avoid a change. In case you scan a series and this re-generates
const previousColors = this.colorSeedSubject.getValue();
if (previousColors != null && primaryColor == previousColors.primary) {
this.colorSeedSubject.next({primary: primaryColor, complementary: complementaryColor});
return;
}
this.colorSeedSubject.next({primary: primaryColor, complementary: complementaryColor});
const newColors: ColorSpace = primaryColor ?
this.generateBackgroundColors(primaryColor, complementaryColor, this.isDarkTheme()) :
this.defaultColors();
@ -72,7 +111,6 @@ export class ColorscapeService {
const oldColors = this.colorSubject.getValue() || this.convertColorsToRGBA(this.defaultColors());
const duration = this.calculateTransitionDuration(oldColors, newColorsRGBA);
// Check if the colors we are transitioning to are visually equal
if (this.areColorSpacesVisuallyEqual(oldColors, newColorsRGBA)) {
return;
@ -156,7 +194,10 @@ export class ColorscapeService {
const normalizedDistance = Math.min(totalDistance / (255 * 3 * 4), 1); // Max possible distance is 255*3*4
const duration = this.minDuration + normalizedDistance * (this.maxDuration - this.minDuration);
return Math.round(duration);
// Add random variance to the duration
const durationVariance = this.getRandomInRange(-500, 500);
return Math.round(duration + durationVariance);
}
private rgbaToRgb(rgba: RGBAColor): RGB {
@ -244,12 +285,19 @@ export class ColorscapeService {
const primaryHSL = this.rgbToHsl(primary);
const secondaryHSL = this.rgbToHsl(secondary);
if (isDarkTheme) {
return this.calculateDarkThemeColors(secondaryHSL, primaryHSL, primary);
} else {
// NOTE: Light themes look bad in general with this system.
return this.calculateLightThemeDarkColors(primaryHSL, primary);
}
return isDarkTheme
? this.calculateDarkThemeColors(secondaryHSL, primaryHSL, primary)
: this.calculateLightThemeDarkColors(primaryHSL, primary); // NOTE: Light themes look bad in general with this system.
}
private adjustColorWithVariance(color: string): string {
const rgb = this.hexToRgb(color);
const randomVariance = () => this.getRandomInRange(-10, 10); // Random variance for each color channel
return this.rgbToHex({
r: Math.min(255, Math.max(0, rgb.r + randomVariance())),
g: Math.min(255, Math.max(0, rgb.g + randomVariance())),
b: Math.min(255, Math.max(0, rgb.b + randomVariance()))
});
}
private calculateLightThemeDarkColors(primaryHSL: { h: number; s: number; l: number }, primary: RGB) {
@ -289,14 +337,62 @@ export class ColorscapeService {
complementaryHSL.s = Math.min(complementaryHSL.s + 0.1, 1);
complementaryHSL.l = Math.max(complementaryHSL.l - 0.2, 0.2);
// Array of colors to shuffle
const colors = [
this.rgbToHex(primary),
this.rgbToHex(this.hslToRgb(lighterHSL)),
this.rgbToHex(this.hslToRgb(darkerHSL)),
this.rgbToHex(this.hslToRgb(complementaryHSL))
];
// Shuffle colors array
this.shuffleArray(colors);
// Set a brightness threshold (you can adjust this value as needed)
const brightnessThreshold = 100; // Adjust based on your needs (0-255)
// Ensure the 'lighter' color is not too bright
if (this.getBrightness(colors[1]) > brightnessThreshold) {
// If it is too bright, find a suitable swap
for (let i = 0; i < colors.length; i++) {
if (this.getBrightness(colors[i]) <= brightnessThreshold) {
// Swap colors[1] (lighter) with a less bright color
[colors[1], colors[i]] = [colors[i], colors[1]];
break;
}
}
}
// Ensure no color is repeating and variance is maintained
const uniqueColors = new Set(colors);
if (uniqueColors.size < colors.length) {
// If there are duplicates, re-shuffle the array
this.shuffleArray(colors);
}
return {
primary: this.rgbToHex(primary),
lighter: this.rgbToHex(this.hslToRgb(lighterHSL)),
darker: this.rgbToHex(this.hslToRgb(darkerHSL)),
complementary: this.rgbToHex(this.hslToRgb(complementaryHSL))
primary: colors[0],
lighter: colors[1],
darker: colors[2],
complementary: colors[3]
};
}
// Calculate brightness of a color (RGB)
private getBrightness(color: string) {
const rgb = this.hexToRgb(color); // Convert hex to RGB
// Using the luminance formula for brightness
return (0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b);
}
// Fisher-Yates shuffle algorithm
private shuffleArray(array: string[]) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
}
private hexToRgb(hex: string): RGB {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
@ -404,7 +500,7 @@ export class ColorscapeService {
styleElement.textContent = styles;
}
private unsetPageColorOverrides() {
Array.from(this.document.head.children).filter(el => el.tagName === 'STYLE' && el.id.toLowerCase() === colorScapeSelector).forEach(c => this.document.head.removeChild(c));
private getRandomInRange(min: number, max: number): number {
return Math.random() * (max - min) + min;
}
}

View file

@ -10,6 +10,7 @@ import {UtilityService} from "../shared/_services/utility.service";
import {BrowsePerson} from "../_models/person/browse-person";
import {Chapter} from "../_models/chapter";
import {StandaloneChapter} from "../_models/standalone-chapter";
import {TextResonse} from "../_types/text-response";
@Injectable({
providedIn: 'root'
@ -50,4 +51,8 @@ export class PersonService {
})
);
}
downloadCover(personId: number) {
return this.httpClient.post<string>(this.baseUrl + 'person/fetch-cover?personId=' + personId, {}, TextResonse);
}
}

View file

@ -26,6 +26,7 @@ import {SiteThemeUpdatedEvent} from "../_models/events/site-theme-updated-event"
import {NavigationEnd, Router} from "@angular/router";
import {ColorscapeService} from "./colorscape.service";
import {ColorScape} from "../_models/theme/colorscape";
import {debounceTime} from "rxjs/operators";
@Injectable({
providedIn: 'root'
@ -58,12 +59,6 @@ export class ThemeService {
private router: Router) {
this.renderer = rendererFactory.createRenderer(null, null);
this.router.events.pipe(
filter(event => event instanceof NavigationEnd)
).subscribe(() => {
this.setColorScape('');
});
messageHub.messages$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(message => {
if (message.event === EVENTS.NotificationProgress) {

View file

@ -262,7 +262,7 @@ export class EditChapterModalComponent implements OnInit {
const selectedIndex = this.editForm.get('coverImageIndex')?.value || 0;
this.chapter.releaseDate = model.releaseDate;
this.chapter.ageRating = model.ageRating as AgeRating;
this.chapter.ageRating = parseInt(model.ageRating + '', 10) as AgeRating;
this.chapter.genres = model.genres;
this.chapter.tags = model.tags;
this.chapter.sortOrder = model.sortOrder;

View file

@ -26,7 +26,7 @@
[class.is-invalid]="settingsForm.get('taskScanCustom')?.invalid && settingsForm.get('taskScanCustom')?.touched"
aria-describedby="task-scan-validations">
@if (settingsForm.dirty || settingsForm.touched) {
@if (settingsForm.dirty || !settingsForm.untouched) {
<div id="task-scan-validations" class="invalid-feedback">
@if(settingsForm.get('taskScanCustom')?.errors?.required) {
<div>{{t('required')}}</div>
@ -65,7 +65,7 @@
[class.is-invalid]="settingsForm.get('taskBackupCustom')?.invalid && settingsForm.get('taskBackupCustom')?.touched"
aria-describedby="task-scan-validations">
@if (settingsForm.dirty || settingsForm.touched) {
@if (settingsForm.dirty || !settingsForm.untouched) {
<div id="task-backup-validations" class="invalid-feedback">
@if(settingsForm.get('taskBackupCustom')?.errors?.required) {
<div>{{t('required')}}</div>
@ -105,7 +105,7 @@
[class.is-invalid]="settingsForm.get('taskCleanupCustom')?.invalid && settingsForm.get('taskCleanupCustom')?.touched"
aria-describedby="task-scan-validations">
@if (settingsForm.dirty || settingsForm.touched) {
@if (settingsForm.dirty || !settingsForm.untouched) {
<div id="task-cleanup-validations" class="invalid-feedback">
@if(settingsForm.get('taskCleanupCustom')?.errors?.required) {
<div>{{t('required')}}</div>
@ -151,7 +151,7 @@
</tr>
</thead>
<tbody>
@for(task of recurringTasks$ | async; track task.lastExecutionUtc + task.cron; let idx = $index) {
@for(task of recurringTasks$ | async; track task; let idx = $index) {
<tr>
<td>
{{task.title | titlecase}}

View file

@ -1,94 +1,3 @@
<ng-container *transloco="let t; read: 'entity-title'">
{{renderText | defaultValue}}
<!-- @switch (libraryType) {-->
<!-- @case (LibraryType.Comic) {-->
<!-- @if (titleName !== '' && prioritizeTitleName) {-->
<!-- @if (isChapter && includeChapter) {-->
<!-- {{t('issue-num') + ' ' + number + ' - ' }}-->
<!-- }-->
<!-- {{titleName}}-->
<!-- } @else {-->
<!-- @if (includeVolume && volumeTitle !== '') {-->
<!-- {{number !== LooseLeafOrSpecial ? (isChapter && includeVolume ? volumeTitle : '') : ''}}-->
<!-- }-->
<!-- {{number !== LooseLeafOrSpecial ? (isChapter ? t('issue-num') + number : volumeTitle) : t('special')}}-->
<!-- }-->
<!-- }-->
<!-- @case (LibraryType.ComicVine) {-->
<!-- @if (titleName !== '' && prioritizeTitleName) {-->
<!-- @if (isChapter && includeChapter) {-->
<!-- {{t('issue-num') + ' ' + number + ' - ' }}-->
<!-- }-->
<!-- {{titleName}}-->
<!-- } @else {-->
<!-- @if (includeVolume && volumeTitle !== '') {-->
<!-- {{number !== LooseLeafOrSpecial ? (isChapter && includeVolume ? volumeTitle : '') : ''}}-->
<!-- }-->
<!-- @if (number !== LooseLeafOrSpecial) {-->
<!-- {{isChapter ? t('issue-num') + number : volumeTitle}}-->
<!-- } @else {-->
<!-- {{t('special')}}-->
<!-- }-->
<!-- }-->
<!-- }-->
<!-- @case (LibraryType.Manga) {-->
<!-- @if (titleName !== '' && prioritizeTitleName) {-->
<!-- @if (isChapter && includeChapter) {-->
<!-- @if (number === LooseLeafOrSpecial) {-->
<!-- {{t('chapter') + ' - ' }}-->
<!-- } @else {-->
<!-- {{t('chapter') + ' ' + number + ' - ' }}-->
<!-- }-->
<!-- }-->
<!-- {{titleName}}-->
<!-- } @else {-->
<!-- @if (includeVolume && volumeTitle !== '') {-->
<!-- @if (number !== LooseLeafOrSpecial && isChapter && includeVolume) {-->
<!-- {{volumeTitle}}-->
<!-- }-->
<!-- }-->
<!-- @if (number !== LooseLeafOrSpecial) {-->
<!-- @if (isChapter) {-->
<!-- {{t('chapter') + ' ' + number}}-->
<!-- } @else {-->
<!-- {{volumeTitle}}-->
<!-- }-->
<!-- } @else if (fallbackToVolume && isChapter && volumeTitle) {-->
<!-- {{t('vol-num', {num: volumeTitle})}}-->
<!-- } @else {-->
<!-- {{t('special')}}-->
<!-- }-->
<!-- }-->
<!-- }-->
<!-- @case (LibraryType.Book) {-->
<!-- @if (titleName !== '' && prioritizeTitleName) {-->
<!-- {{titleName}}-->
<!-- } @else if (number === LooseLeafOrSpecial) {-->
<!-- {{null | defaultValue}}-->
<!-- } @else {-->
<!-- {{t('book-num', {num: volumeTitle})}}-->
<!-- }-->
<!-- }-->
<!-- @case (LibraryType.LightNovel) {-->
<!-- @if (titleName !== '' && prioritizeTitleName) {-->
<!-- {{titleName}}-->
<!-- } @else if (number === LooseLeafOrSpecial) {-->
<!-- {{null | defaultValue}}-->
<!-- } @else {-->
<!-- {{t('book-num', {num: (isChapter ? number : volumeTitle)})}}-->
<!-- }-->
<!-- }-->
<!-- @case (LibraryType.Images) {-->
<!-- {{number !== LooseLeafOrSpecial ? (isChapter ? (t('chapter') + ' ') + number : volumeTitle) : t('special')}}-->
<!-- }-->
<!-- }-->
</ng-container>

View file

@ -187,12 +187,26 @@ export class EntityTitleComponent implements OnInit {
private calculateComicRenderText() {
let renderText = '';
if (this.titleName !== '' && this.prioritizeTitleName) {
// If titleName is provided and prioritized
if (this.titleName && this.prioritizeTitleName) {
if (this.isChapter && this.includeChapter) {
renderText = translate('entity-title.issue-num') + ' ' + this.number + ' - ';
}
renderText += this.titleName;
} else {
// Otherwise, check volume and number logic
if (this.includeVolume && this.volumeTitle) {
if (this.number !== this.LooseLeafOrSpecial) {
renderText = this.isChapter ? this.volumeTitle : '';
}
}
// Render either issue number or volume title, or "special" if applicable
renderText += this.number !== this.LooseLeafOrSpecial
? (this.isChapter ? translate('entity-title.issue-num') + ' ' + this.number : this.volumeTitle)
: translate('entity-title.special');
}
return renderText;
}
}

View file

@ -18,8 +18,8 @@
@if (editForm.get('name'); as formControl) {
<app-setting-item [title]="t('name-label')" [toggleOnViewClick]="false" [showEdit]="false">
<ng-template #view>
<input id="name" class="form-control" formControlName="name" type="text" readonly
[class.is-invalid]="formControl.invalid && formControl.touched">
<input id="name" class="form-control" formControlName="name" type="text"
[class.is-invalid]="formControl.invalid && !formControl.untouched">
@if (formControl.errors; as errors) {
@if (errors.required) {
<div class="invalid-feedback">{{t('required-field')}}</div>
@ -34,10 +34,10 @@
<div class="row g-0">
<div class="mb-3 col-md-6 col-xs-12 pe-2">
@if (editForm.get('malId'); as formControl) {
<app-setting-item [title]="t('mal-id-label')" [toggleOnViewClick]="false" [showEdit]="false">
<app-setting-item [title]="t('mal-id-label')" [toggleOnViewClick]="false" [showEdit]="false" [subtitle]="t('mal-tooltip')">
<ng-template #view>
<input id="mal-id" class="form-control" formControlName="malId" type="number"
[class.is-invalid]="formControl.invalid && formControl.touched">
[class.is-invalid]="formControl.invalid && !formControl.untouched">
</ng-template>
</app-setting-item>
}
@ -45,10 +45,10 @@
<div class="mb-3 col-md-6 col-xs-12">
@if (editForm.get('aniListId'); as formControl) {
<app-setting-item [title]="t('anilist-id-label')" [toggleOnViewClick]="false" [showEdit]="false">
<app-setting-item [title]="t('anilist-id-label')" [toggleOnViewClick]="false" [showEdit]="false" [subtitle]="t('anilist-tooltip')">
<ng-template #view>
<input id="anilist-id" class="form-control" formControlName="aniListId" type="number"
[class.is-invalid]="formControl.invalid && formControl.touched">
[class.is-invalid]="formControl.invalid && !formControl.untouched">
</ng-template>
</app-setting-item>
}
@ -58,10 +58,10 @@
<div class="row g-0">
<div class="mb-3 col-md-6 col-xs-12 pe-2">
@if (editForm.get('hardcoverId'); as formControl) {
<app-setting-item [title]="t('hardcover-id-label')" [toggleOnViewClick]="false" [showEdit]="false">
<app-setting-item [title]="t('hardcover-id-label')" [toggleOnViewClick]="false" [showEdit]="false" [subtitle]="t('hardcover-tooltip')">
<ng-template #view>
<input id="hardcover-id" class="form-control" formControlName="hardcoverId" type="text"
[class.is-invalid]="formControl.invalid && formControl.touched">
[class.is-invalid]="formControl.invalid && !formControl.untouched">
</ng-template>
</app-setting-item>
}
@ -69,10 +69,12 @@
<div class="mb-3 col-md-6 col-xs-12">
@if (editForm.get('asin'); as formControl) {
<app-setting-item [title]="t('asin-label')" [toggleOnViewClick]="false" [showEdit]="false">
<app-setting-item [title]="t('asin-label')" [toggleOnViewClick]="false" [showEdit]="false" [subtitle]="t('asin-tooltip')">
<ng-template #view>
<input id="asin" class="form-control" formControlName="asin" type="text"
[class.is-invalid]="formControl.invalid && formControl.touched">
[class.is-invalid]="formControl.invalid && !formControl.untouched">
</ng-template>
</app-setting-item>
}
@ -100,7 +102,11 @@
<ng-template ngbNavContent>
<p class="alert alert-primary" role="alert">
{{t('cover-image-description')}}
<!-- {{t('cover-image-description-extra')}}-->
</p>
<!-- <button class="btn btn-primary mb-2 w-100" (click)="downloadCover()" [disabled]="fetchDisabled">{{t('download-coversdb')}}</button>-->
<app-cover-image-chooser [(imageUrls)]="imageUrls"
(imageUrlsChange)="handleUploadByUrl($event)"
(imageSelected)="updateSelectedIndex($event)"

View file

@ -13,7 +13,7 @@ import {
NgbNavOutlet
} from "@ng-bootstrap/ng-bootstrap";
import {PersonService} from "../../../_services/person.service";
import { TranslocoDirective } from '@jsverse/transloco';
import {translate, TranslocoDirective} from '@jsverse/transloco';
import {CoverImageChooserComponent} from "../../../cards/cover-image-chooser/cover-image-chooser.component";
import {forkJoin} from "rxjs";
import {UploadService} from "../../../_services/upload.service";
@ -21,6 +21,7 @@ import {CompactNumberPipe} from "../../../_pipes/compact-number.pipe";
import {SettingItemComponent} from "../../../settings/_components/setting-item/setting-item.component";
import {AccountService} from "../../../_services/account.service";
import {User} from "../../../_models/user";
import {ToastrService} from "ngx-toastr";
enum TabID {
General = 'general-tab',
@ -57,6 +58,7 @@ export class EditPersonModalComponent implements OnInit {
private readonly personService = inject(PersonService);
private readonly uploadService = inject(UploadService);
protected readonly accountService = inject(AccountService);
protected readonly toastr = inject(ToastrService);
protected readonly Breakpoint = Breakpoint;
protected readonly TabID = TabID;
@ -77,6 +79,7 @@ export class EditPersonModalComponent implements OnInit {
selectedCover: string = '';
coverImageReset = false;
touchedCoverImage = false;
fetchDisabled: boolean = false;
ngOnInit() {
if (this.person) {
@ -91,6 +94,8 @@ export class EditPersonModalComponent implements OnInit {
this.editForm.addControl('coverImageLocked', new FormControl(this.person.coverImageLocked, []));
this.cdRef.markForCheck();
} else {
alert('no person')
}
}
@ -156,4 +161,15 @@ export class EditPersonModalComponent implements OnInit {
this.cdRef.markForCheck();
}
downloadCover() {
this.personService.downloadCover(this.person.id).subscribe(imgUrl => {
if (imgUrl) {
this.toastr.success(translate('toasts.person-image-downloaded'));
this.fetchDisabled = true;
this.imageUrls.push(imgUrl);
this.cdRef.markForCheck();
}
});
}
}

View file

@ -4,14 +4,14 @@ import {
Component, DestroyRef,
ElementRef,
Inject,
inject,
inject, OnInit,
ViewChild
} from '@angular/core';
import {ActivatedRoute, Router} from "@angular/router";
import {PersonService} from "../_services/person.service";
import {Observable, switchMap, tap} from "rxjs";
import {BehaviorSubject, EMPTY, Observable, switchMap, tap} from "rxjs";
import {Person, PersonRole} from "../_models/metadata/person";
import {AsyncPipe, DOCUMENT, NgStyle} from "@angular/common";
import {AsyncPipe, NgStyle} from "@angular/common";
import {ImageComponent} from "../shared/image/image.component";
import {ImageService} from "../_services/image.service";
import {
@ -80,7 +80,6 @@ export class PersonDetailComponent {
@ViewChild('companionBar') companionBar: ElementRef<HTMLDivElement> | undefined;
personName!: string;
person$: Observable<Person> | null = null;
person: Person | null = null;
roles$: Observable<PersonRole[]> | null = null;
roles: PersonRole[] | null = null;
@ -89,42 +88,50 @@ export class PersonDetailComponent {
filter: SeriesFilterV2 | null = null;
personActions: Array<ActionItem<Person>> = this.actionService.getPersonActions(this.handleAction.bind(this));
chaptersByRole: any = {};
private readonly personSubject = new BehaviorSubject<Person | null>(null);
protected readonly person$ = this.personSubject.asObservable();
constructor(@Inject(DOCUMENT) private document: Document) {
this.route.paramMap.subscribe(_ => {
const personName = this.route.snapshot.paramMap.get('name');
if (personName === null || undefined) {
this.router.navigateByUrl('/home');
return;
}
constructor() {
this.route.paramMap.pipe(
switchMap(params => {
const personName = params.get('name');
if (!personName) {
this.router.navigateByUrl('/home');
return EMPTY;
}
this.personName = personName;
this.personName = personName;
return this.personService.get(personName);
}),
tap(person => {
this.person = person;
this.personSubject.next(person); // emit the person data for subscribers
this.themeService.setColorScape(person.primaryColor || '', person.secondaryColor);
// Fetch roles and process them
this.roles$ = this.personService.getRolesForPerson(this.personName).pipe(
tap(roles => {
this.roles = roles;
this.filter = this.createFilter(roles);
this.chaptersByRole = {}; // Reset chaptersByRole for each person
this.person$ = this.personService.get(this.personName).pipe(tap(p => {
this.person = p;
this.themeService.setColorScape(this.person.primaryColor || '', this.person.secondaryColor);
this.roles$ = this.personService.getRolesForPerson(this.personName).pipe(tap(roles => {
this.roles = roles;
this.filter = this.createFilter(roles);
for(let role of roles) {
this.chaptersByRole[role] = this.personService.getChaptersByRole(this.person!.id, role).pipe(takeUntilDestroyed(this.destroyRef));
}
this.cdRef.markForCheck();
}), takeUntilDestroyed(this.destroyRef));
this.works$ = this.personService.getSeriesMostKnownFor(this.person.id).pipe(
// Populate chapters by role
roles.forEach(role => {
this.chaptersByRole[role] = this.personService.getChaptersByRole(person.id, role)
.pipe(takeUntilDestroyed(this.destroyRef));
});
this.cdRef.markForCheck();
}),
takeUntilDestroyed(this.destroyRef)
);
this.cdRef.markForCheck();
}), takeUntilDestroyed(this.destroyRef));
});
// Fetch series known for this person
this.works$ = this.personService.getSeriesMostKnownFor(person.id).pipe(
takeUntilDestroyed(this.destroyRef)
);
}),
takeUntilDestroyed(this.destroyRef)
).subscribe();
}
createFilter(roles: PersonRole[]) {
@ -154,13 +161,7 @@ export class PersonDetailComponent {
};
if (this.person) {
loadPage(this.person).subscribe();
} else {
this.person$?.pipe(switchMap((p: Person) => {
return loadPage(p);
})).subscribe();
}
loadPage(this.person!).subscribe();
}
loadFilterByRole(role: PersonRole) {
@ -191,7 +192,18 @@ export class PersonDetailComponent {
ref.closed.subscribe(r => {
if (r.success) {
const nameChanged = this.personName !== r.person.name;
this.person = {...r.person};
this.personName = this.person!.name;
this.personSubject.next(this.person);
// Update the url to reflect the new name change
if (nameChanged) {
const baseUrl = window.location.href.split('/').slice(0, -1).join('/');
window.history.replaceState({}, '', `${baseUrl}/${encodeURIComponent(this.personName)}`);
}
this.cdRef.markForCheck();
}
});

View file

@ -191,21 +191,19 @@
</li>
}
@if (showDetailsTab) {
<li [ngbNavItem]="TabID.Details" id="details-tab">
<a ngbNavLink>{{t('details-tab')}}</a>
<ng-template ngbNavContent>
@defer (when activeTabId === TabID.Details; prefetch on idle) {
<app-details-tab [metadata]="volumeCast"
[genres]="genres"
[tags]="tags"
[readingTime]="volume"
[language]="volume.chapters[0].language"
[format]="series.format"></app-details-tab>
}
</ng-template>
</li>
}
<li [ngbNavItem]="TabID.Details" id="details-tab">
<a ngbNavLink>{{t('details-tab')}}</a>
<ng-template ngbNavContent>
@defer (when activeTabId === TabID.Details; prefetch on idle) {
<app-details-tab [metadata]="volumeCast"
[genres]="genres"
[tags]="tags"
[readingTime]="volume"
[language]="volume.chapters[0].language"
[format]="series.format"></app-details-tab>
}
</ng-template>
</li>
</ul>
</div>
<!-- Min height helps with scroll jerking when switching from chapter -> related/details -->

View file

@ -262,7 +262,6 @@ export class VolumeDetailComponent implements OnInit {
* This is the download we get from download service.
*/
download$: Observable<DownloadEvent | null> | null = null;
showDetailsTab: boolean = true;
currentlyReadingChapter: Chapter | undefined = undefined;
maxAgeRating: AgeRating = AgeRating.Unknown;
@ -506,7 +505,6 @@ export class VolumeDetailComponent implements OnInit {
this.setContinuePoint();
this.showDetailsTab = hasAnyCast(this.volumeCast) || (this.genres || []).length > 0 || (this.tags || []).length > 0;
this.isLoading = false;
this.cdRef.markForCheck();
});

View file

@ -2024,13 +2024,19 @@
"name-label": "{{edit-series-modal.name-label}}",
"role-label": "Role",
"mal-id-label": "MAL Id",
"mal-tooltip": "https://myanimelist.net/people/{MalId}/",
"anilist-id-label": "AniList Id",
"anilist-tooltip": "https://anilist.co/staff/{AniListId}/",
"hardcover-id-label": "Hardcover Id",
"hardcover-tooltip": "https://hardcover.app/authors/{HardcoverId}",
"asin-label": "ASIN",
"asin-tooltip": "https://www.amazon.com/stores/J.K.-Rowling/author/{ASIN}",
"description-label": "Description",
"required-field": "{{validations.required-field}}",
"cover-image-description": "{{edit-series-modal.cover-image-description}}",
"save": "{{common.save}}"
"cover-image-description-extra": "Alternatively you can download a cover from CoversDB if available.",
"save": "{{common.save}}",
"download-coversdb": "Download from CoversDB"
},
"day-breakdown": {
@ -2424,7 +2430,8 @@
"confirm-reset-server-settings": "This will reset your settings to first install values. Are you sure you want to continue?",
"must-select-library": "At least one library must be selected",
"bulk-scan": "Scanning multiple libraries will be done linearly. This may take a long time and not complete depending on library size.",
"bulk-covers": "Refreshing covers on multiple libraries is intensive and can take a long time. Are you sure you want to continue?"
"bulk-covers": "Refreshing covers on multiple libraries is intensive and can take a long time. Are you sure you want to continue?",
"person-image-downloaded": "Person cover was downloaded and applied."
},
"read-time-pipe": {

View file

@ -99,7 +99,7 @@ const languageCodes = [
'syr', 'syr_SY', 'ta', 'ta_IN', 'te', 'te_IN', 'th', 'th_TH', 'tl', 'tl_PH', 'tn',
'tn_ZA', 'tr', 'tr_TR', 'tt', 'tt_RU', 'ts', 'uk', 'uk_UA', 'ur', 'ur_PK', 'uz',
'uz_UZ', 'uz_UZ', 'vi', 'vi_VN', 'xh', 'xh_ZA', 'zh', 'zh_CN', 'zh_HK', 'zh_MO',
'zh_SG', 'zh_TW', 'zu', 'zu_ZA', 'zh_Hans', 'zh_Hant', 'nb_NO'
'zh_SG', 'zh_TW', 'zu', 'zu_ZA', 'zh_Hans', 'zh_Hant', 'nb_NO', 'ga'
];
const translocoOptions = {