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:
parent
b44f89d1e8
commit
a847468a6c
42 changed files with 1009 additions and 429 deletions
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)"
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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 -->
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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 = {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue