Stats Fix & Library Bulk Actions (#3209)

Co-authored-by: Fesaa <77553571+Fesaa@users.noreply.github.com>
Co-authored-by: Weblate (bot) <hosted@weblate.org>
Co-authored-by: Gregory.Open <gregory.open@proton.me>
Co-authored-by: Mateusz <mateuszvx8.96@gmail.com>
Co-authored-by: majora2007 <kavitareader@gmail.com>
Co-authored-by: 無情天 <kofzhanganguo@126.com>
This commit is contained in:
Joe Milazzo 2024-09-23 08:07:37 -05:00 committed by GitHub
parent 894b49bb76
commit 857e419e4e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
77 changed files with 72523 additions and 30914 deletions

View file

@ -108,6 +108,10 @@ export enum Action {
* Invoke a refresh covers as false to generate colorscapes
*/
GenerateColorScape = 26,
/**
* Copy settings from one entity to another
*/
CopySettings = 27
}
/**
@ -254,6 +258,43 @@ export class ActionFactoryService {
return tasks.filter(t => !blacklist.includes(t.action));
}
getBulkLibraryActions(callback: ActionCallback<Library>) {
// Scan is currently not supported due to the backend not being able to handle it yet
const actions = this.flattenActions<Library>(this.libraryActions).filter(a => {
return [Action.Delete, Action.GenerateColorScape, Action.AnalyzeFiles, Action.RefreshMetadata, Action.CopySettings].includes(a.action);
});
actions.push({
_extra: undefined,
class: undefined,
description: '',
dynamicList: undefined,
action: Action.CopySettings,
callback: this.dummyCallback,
children: [],
requiresAdmin: true,
title: 'copy-settings'
})
return this.applyCallbackToList(actions, callback);
}
flattenActions<T>(actions: Array<ActionItem<T>>): Array<ActionItem<T>> {
return actions.reduce<Array<ActionItem<T>>>((flatArray, action) => {
if (action.action !== Action.Submenu) {
flatArray.push(action);
}
// Recursively flatten the children, if any
if (action.children && action.children.length > 0) {
flatArray.push(...this.flattenActions<T>(action.children));
}
return flatArray;
}, [] as Array<ActionItem<T>>); // Explicitly defining the type of flatArray
}
private _resetActions() {
this.libraryActions = [
{

View file

@ -93,6 +93,10 @@ export class LibraryService {
return this.httpClient.post(this.baseUrl + 'library/scan?libraryId=' + libraryId + '&force=' + force, {});
}
scanMultipleLibraries(libraryIds: Array<number>, force = false) {
return this.httpClient.post(this.baseUrl + 'library/scan-multiple', {ids: libraryIds, force: force});
}
analyze(libraryId: number) {
return this.httpClient.post(this.baseUrl + 'library/analyze?libraryId=' + libraryId, {});
}
@ -101,6 +105,18 @@ export class LibraryService {
return this.httpClient.post(this.baseUrl + `library/refresh-metadata?libraryId=${libraryId}&force=${forceUpdate}&forceColorscape=${forceColorscape}`, {});
}
refreshMetadataMultipleLibraries(libraryIds: Array<number>, force = false, forceColorscape = false) {
return this.httpClient.post(this.baseUrl + 'library/refresh-metadata-multiple?forceColorscape=' + forceColorscape, {ids: libraryIds, force: force});
}
analyzeFilesMultipleLibraries(libraryIds: Array<number>) {
return this.httpClient.post(this.baseUrl + 'library/analyze-multiple', {ids: libraryIds, force: false});
}
copySettingsFromLibrary(sourceLibraryId: number, targetLibraryIds: Array<number>, includeType: boolean) {
return this.httpClient.post(this.baseUrl + 'library/copy-settings-from', {sourceLibraryId, targetLibraryIds, includeType});
}
create(model: {name: string, type: number, folders: string[]}) {
return this.httpClient.post(this.baseUrl + 'library/create', model);
}

View file

@ -2,11 +2,17 @@
@if (actions.length > 0) {
@if ((utilityService.activeBreakpoint$ | async)! <= Breakpoint.Tablet) {
<button [disabled]="disabled" class="btn {{btnClass}}" id="actions-{{labelBy}}"
(click)="openMobileActionableMenu($event)"><i class="fa {{iconClass}}" aria-hidden="true"></i></button>
(click)="openMobileActionableMenu($event)">
{{label}}
<i class="fa {{iconClass}}" aria-hidden="true"></i>
</button>
} @else {
<div ngbDropdown container="body" class="d-inline-block">
<button [disabled]="disabled" class="btn {{btnClass}}" id="actions-{{labelBy}}" ngbDropdownToggle
(click)="preventEvent($event)"><i class="fa {{iconClass}}" aria-hidden="true"></i></button>
(click)="preventEvent($event)">
{{label}}
<i class="fa {{iconClass}}" aria-hidden="true"></i>
</button>
<div ngbDropdownMenu attr.aria-labelledby="actions-{{labelBy}}">
<ng-container *ngTemplateOutlet="submenu; context: { list: actions }"></ng-container>
</div>

View file

@ -16,7 +16,6 @@ import {TranslocoDirective} from "@jsverse/transloco";
import {DynamicListPipe} from "./_pipes/dynamic-list.pipe";
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import {Breakpoint, UtilityService} from "../../shared/_services/utility.service";
import {NavLinkModalComponent} from "../../nav/_components/nav-link-modal/nav-link-modal.component";
import {ActionableModalComponent} from "../actionable-modal/actionable-modal.component";
@Component({
@ -41,6 +40,10 @@ export class CardActionablesComponent implements OnInit {
@Input() btnClass = '';
@Input() actions: ActionItem<any>[] = [];
@Input() labelBy = 'card';
/**
* Text to display as if actionable was a button
*/
@Input() label = '';
@Input() disabled: boolean = false;
@Output() actionHandler = new EventEmitter<ActionItem<any>>();

View file

@ -0,0 +1,23 @@
<ng-container *transloco="let t; read:'copy-settings-from-library-modal'">
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{t('title')}}</h4>
<button type="button" class="btn-close" [attr.aria-label]="t('close')" (click)="modal.close(null)"></button>
</div>
<div class="modal-body">
<p>{{t('description')}}</p>
<form [formGroup]="libForm">
<select class="form-select" formControlName="library">
<option [value]="null">{{t('select-option')}}</option>
@for (lib of libraries; track lib.id) {
<option [value]="lib.id">{{lib.name}}</option>
}
</select>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" (click)="modal.close(null)">{{t('close')}}</button>
<button type="button" class="btn btn-primary" (click)="save()">{{t('select')}}</button>
</div>
</ng-container>

View file

@ -0,0 +1,30 @@
import {ChangeDetectionStrategy, Component, inject, Input} from '@angular/core';
import {Library} from "../../../_models/library/library";
import {NgbActiveModal} from "@ng-bootstrap/ng-bootstrap";
import {TranslocoDirective} from "@jsverse/transloco";
import {FormControl, FormGroup, ReactiveFormsModule} from "@angular/forms";
@Component({
selector: 'app-copy-settings-from-library-modal',
standalone: true,
imports: [
TranslocoDirective,
ReactiveFormsModule,
],
templateUrl: './copy-settings-from-library-modal.component.html',
styleUrl: './copy-settings-from-library-modal.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CopySettingsFromLibraryModalComponent {
protected readonly modal = inject(NgbActiveModal);
@Input() libraries: Array<Library> = [];
libForm = new FormGroup({
'library': new FormControl(null),
});
save() {
this.modal.close(parseInt(this.libForm.get('library')?.value + '', 10));
}
}

View file

@ -18,19 +18,20 @@ import {SelectionModel} from "../../../typeahead/_models/selection-model";
})
export class LibraryAccessModalComponent implements OnInit {
protected readonly modal = inject(NgbActiveModal);
private readonly cdRef = inject(ChangeDetectorRef);
private readonly libraryService = inject(LibraryService);
@Input() member: Member | undefined;
allLibraries: Library[] = [];
selectedLibraries: Array<{selected: boolean, data: Library}> = [];
selections!: SelectionModel<Library>;
selectAll: boolean = false;
cdRef = inject(ChangeDetectorRef);
get hasSomeSelected() {
return this.selections != null && this.selections.hasSomeSelected();
}
constructor(public modal: NgbActiveModal, private libraryService: LibraryService) { }
ngOnInit(): void {
this.libraryService.getLibraries().subscribe(libs => {

View file

@ -1,15 +1,45 @@
<ng-container *transloco="let t; read: 'manage-library'">
<div class="position-relative">
<div class="position-absolute custom-position-2">
<app-card-actionables [actions]="bulkActions" btnClass="btn-primary-outline ms-1" [label]="t('bulk-action-label')" [disabled]="bulkMode" (actionHandler)="handleBulkAction($event, null)">
</app-card-actionables>
</div>
<button class="btn btn-primary-outline position-absolute custom-position" (click)="addLibrary()" [title]="t('add-library')">
<i class="fa fa-plus" aria-hidden="true"></i><span class="phone-hidden ms-1">{{t('add-library')}}</span>
</button>
</div>
<!-- TODO: We need a filter bar when there is more than 10 libraries -->
@if (bulkMode && bulkAction === Action.CopySettings && sourceCopyToLibrary) {
<div class="alert alert-warning">
{{t('bulk-copy-to', {libraryName: sourceCopyToLibrary.name})}}
<form [formGroup]="bulkForm">
<div class="form-check form-switch">
<input id="bulk-action-type" type="checkbox" class="form-check-input" formControlName="includeType" aria-describedby="include-type-help">
<label class="form-check-label" for="bulk-action-type">{{t('include-type-label')}}</label>
<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="left" [ngbTooltip]="includeTypeTooltip" role="button" tabindex="0"></i>
<ng-template #includeTypeTooltip>{{t('include-type-tooltip')}}</ng-template>
<span class="visually-hidden" id="include-type-help"><ng-container [ngTemplateOutlet]="includeTypeTooltip"></ng-container></span>
</div>
</form>
<div class="mt-2">
<button class="btn btn-secondary" (click)="resetBulkMode()">{{t('cancel')}}</button>
<button class="btn btn-primary ms-1" (click)="applyBulkAction()" [disabled]="!hasSomeSelected">{{t('apply')}}</button>
</div>
</div>
}
<table class="table table-striped">
<thead>
<tr>
<th scope="col">
<div class="form-check">
<input id="select-all" type="checkbox" class="form-check-input"
[ngModel]="selectAll" (change)="toggleAll()" [indeterminate]="hasSomeSelected">
<label for="select-all" class="form-check-label d-md-block d-none">{{t('select-all')}}</label>
</div>
</th>
<th scope="col">{{t('name-header')}}</th>
<th scope="col">{{t('type-title')}}</th>
<th scope="col">{{t('shared-folders-title')}}</th>
@ -20,7 +50,14 @@
<tbody>
@for(library of libraries; track library.name + library.type + library.folders.length + library.lastScanned; let idx = $index) {
<tr>
<td id="username--{{idx}}">
<td>
<div class="form-check">
<input id="select-library-{{idx}}" type="checkbox" class="form-check-input"
[ngModel]="selections.isSelected(library)" (change)="handleSelection(library, idx)">
<label for="select-library-{{idx}}" class="form-check-label visually-hidden">{{library.name}}</label>
</div>
</td>
<td id="library--{{idx}}">
<a [routerLink]="'/library/' + library.id">{{library.name}}</a>
</td>
<td>
@ -33,6 +70,7 @@
{{library.lastScanned | timeAgo | defaultDate}}
</td>
<td>
<!-- On Mobile we want to use ... for each row -->
@if (useActionables$ | async) {
<app-card-actionables [actions]="actions" (actionHandler)="performAction($event, library)"></app-card-actionables>
} @else {

View file

@ -5,6 +5,19 @@
top: -42px;
}
.custom-position-2 {
right: 160px;
top: -42px;
}
@media(max-width: 576px) {
.custom-position-2 {
right: 65px;
}
}
.member-name {
word-break: keep-all;
margin: 0;

View file

@ -2,28 +2,31 @@ import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
DestroyRef, HostListener,
DestroyRef,
HostListener,
inject,
OnInit
} from '@angular/core';
import { NgbModal, NgbTooltip } from '@ng-bootstrap/ng-bootstrap';
import { ToastrService } from 'ngx-toastr';
import { distinctUntilChanged, filter, take } from 'rxjs/operators';
import { ConfirmService } from 'src/app/shared/confirm.service';
import { LibrarySettingsModalComponent } from 'src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component';
import { NotificationProgressEvent } from 'src/app/_models/events/notification-progress-event';
import { ScanSeriesEvent } from 'src/app/_models/events/scan-series-event';
import { Library } from 'src/app/_models/library/library';
import { LibraryService } from 'src/app/_services/library.service';
import { EVENTS, Message, MessageHubService } from 'src/app/_services/message-hub.service';
import {NgbModal, NgbTooltip} from '@ng-bootstrap/ng-bootstrap';
import {ToastrService} from 'ngx-toastr';
import {distinctUntilChanged, filter, take} from 'rxjs/operators';
import {ConfirmService} from 'src/app/shared/confirm.service';
import {
LibrarySettingsModalComponent
} from 'src/app/sidenav/_modals/library-settings-modal/library-settings-modal.component';
import {NotificationProgressEvent} from 'src/app/_models/events/notification-progress-event';
import {ScanSeriesEvent} from 'src/app/_models/events/scan-series-event';
import {Library} from 'src/app/_models/library/library';
import {LibraryService} from 'src/app/_services/library.service';
import {EVENTS, Message, MessageHubService} from 'src/app/_services/message-hub.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import { SentenceCasePipe } from '../../_pipes/sentence-case.pipe';
import { TimeAgoPipe } from '../../_pipes/time-ago.pipe';
import { LibraryTypePipe } from '../../_pipes/library-type.pipe';
import { RouterLink } from '@angular/router';
import {SentenceCasePipe} from '../../_pipes/sentence-case.pipe';
import {TimeAgoPipe} from '../../_pipes/time-ago.pipe';
import {LibraryTypePipe} from '../../_pipes/library-type.pipe';
import {RouterLink} from '@angular/router';
import {translate, TranslocoModule} from "@jsverse/transloco";
import {DefaultDatePipe} from "../../_pipes/default-date.pipe";
import {AsyncPipe, TitleCasePipe} from "@angular/common";
import {AsyncPipe, NgTemplateOutlet, TitleCasePipe} from "@angular/common";
import {DefaultValuePipe} from "../../_pipes/default-value.pipe";
import {LoadingComponent} from "../../shared/loading/loading.component";
import {TagBadgeComponent} from "../../shared/tag-badge/tag-badge.component";
@ -33,6 +36,12 @@ import {Action, ActionFactoryService, ActionItem} from "../../_services/action-f
import {ActionService} from "../../_services/action.service";
import {CardActionablesComponent} from "../../_single-module/card-actionables/card-actionables.component";
import {BehaviorSubject, Observable} from "rxjs";
import {Select2Module} from "ng-select2-component";
import {SelectionModel} from "../../typeahead/_models/selection-model";
import {
CopySettingsFromLibraryModalComponent
} from "../_modals/copy-settings-from-library-modal/copy-settings-from-library-modal.component";
import {FormControl, FormGroup} from "@angular/forms";
@Component({
selector: 'app-manage-library',
@ -40,7 +49,9 @@ import {BehaviorSubject, Observable} from "rxjs";
styleUrls: ['./manage-library.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [RouterLink, NgbTooltip, LibraryTypePipe, TimeAgoPipe, SentenceCasePipe, TranslocoModule, DefaultDatePipe, AsyncPipe, DefaultValuePipe, LoadingComponent, TagBadgeComponent, TitleCasePipe, UtcToLocalTimePipe, CardActionablesComponent]
imports: [RouterLink, NgbTooltip, LibraryTypePipe, TimeAgoPipe, SentenceCasePipe, TranslocoModule, DefaultDatePipe,
AsyncPipe, DefaultValuePipe, LoadingComponent, TagBadgeComponent, TitleCasePipe, UtcToLocalTimePipe,
CardActionablesComponent, Select2Module, NgTemplateOutlet]
})
export class ManageLibraryComponent implements OnInit {
@ -56,8 +67,10 @@ export class ManageLibraryComponent implements OnInit {
private readonly actionService = inject(ActionService);
protected readonly Breakpoint = Breakpoint;
protected readonly Action = Action;
actions = this.actionFactoryService.getLibraryActions(this.handleAction.bind(this));
bulkActions = this.actionFactoryService.getBulkLibraryActions(this.handleBulkAction.bind(this));
libraries: Library[] = [];
loading = false;
/**
@ -66,6 +79,25 @@ export class ManageLibraryComponent implements OnInit {
deletionInProgress: boolean = false;
useActionableSource = new BehaviorSubject<boolean>(this.utilityService.getActiveBreakpoint() <= Breakpoint.Tablet);
useActionables$: Observable<boolean> = this.useActionableSource.asObservable();
selections!: SelectionModel<Library>;
selectAll: boolean = false;
bulkMode = false;
bulkAction: Action | null = null;
sourceCopyToLibrary: Library | null = null;
bulkForm = new FormGroup({'includeType': new FormControl(false)});
isShiftDown: boolean = false;
lastSelectedIndex: number | null = null;
@HostListener('document:keydown.shift', ['$event'])
handleKeypress(event: KeyboardEvent) {
this.isShiftDown = true;
}
@HostListener('document:keyup.shift', ['$event'])
handleKeyUp(event: KeyboardEvent) {
this.isShiftDown = false;
}
@HostListener('window:resize', ['$event'])
@HostListener('window:orientationchange', ['$event'])
@ -73,6 +105,10 @@ export class ManageLibraryComponent implements OnInit {
this.useActionableSource.next(this.utilityService.getActiveBreakpoint() <= Breakpoint.Tablet);
}
get hasSomeSelected() {
return this.selections != null && this.selections.hasSomeSelected();
}
ngOnInit(): void {
this.getLibraries();
@ -118,6 +154,8 @@ export class ManageLibraryComponent implements OnInit {
this.cdRef.markForCheck();
this.libraryService.getLibraries().pipe(take(1), takeUntilDestroyed(this.destroyRef)).subscribe(libraries => {
this.libraries = [...libraries];
this.setupSelections();
this.resetBulkMode();
this.loading = false;
this.cdRef.markForCheck();
});
@ -158,6 +196,80 @@ export class ManageLibraryComponent implements OnInit {
await this.actionService.scanLibrary(library);
}
async applyBulkAction() {
if (!this.bulkMode) {
this.resetBulkMode();
}
// Get Selected libraries
let selected = this.selections.selected();
// Remove the source library id from selected (if applicable)
if (this.bulkAction === Action.CopySettings) {
selected = selected.filter(l => l.id !== this.sourceCopyToLibrary!.id);
}
if (selected.length === 0) {
await this.confirmService.alert(translate('toasts.must-select-library'));
return;
}
switch(this.bulkAction) {
case (Action.Scan):
await this.confirmService.alert(translate('toasts.bulk-scan'));
this.libraryService.scanMultipleLibraries(selected.map(l => l.id)).subscribe();
break;
case Action.RefreshMetadata:
if (!await this.confirmService.confirm(translate('toasts.bulk-covers'))) return;
this.libraryService.refreshMetadataMultipleLibraries(selected.map(l => l.id), true, false).subscribe(() => this.getLibraries());
break
case Action.AnalyzeFiles:
this.libraryService.analyzeFilesMultipleLibraries(selected.map(l => l.id)).subscribe(() => this.getLibraries());
break;
case Action.GenerateColorScape:
this.libraryService.refreshMetadataMultipleLibraries(selected.map(l => l.id), false, true).subscribe(() => this.getLibraries());
break;
case Action.CopySettings:
// Remove the source library from the list
if (selected.length === 1 && selected[0].id === this.sourceCopyToLibrary!.id) {
return;
}
const includeType = this.bulkForm.get('includeType')!.value + '' == 'true';
this.libraryService.copySettingsFromLibrary(this.sourceCopyToLibrary!.id, selected.map(l => l.id), includeType).subscribe(() => this.getLibraries());
break;
}
}
async handleBulkAction(action: ActionItem<Library>, library : Library | null) {
this.bulkMode = true;
this.bulkAction = action.action;
this.cdRef.markForCheck();
switch (action.action) {
case(Action.Scan):
case(Action.RefreshMetadata):
case(Action.GenerateColorScape):
case (Action.Delete):
await this.applyBulkAction();
break;
case (Action.CopySettings):
// Prompt the user for the library then wait for them to manually trigger applyBulkAction
const ref = this.modalService.open(CopySettingsFromLibraryModalComponent, {size: 'lg', fullscreen: 'md'});
ref.componentInstance.libraries = this.libraries;
ref.closed.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((res: number | null) => {
if (res === null) return;
// res will be the library the user chose
this.bulkMode = true;
this.sourceCopyToLibrary = this.libraries.filter(l => l.id === res)[0];
this.cdRef.markForCheck();
});
break;
}
}
async handleAction(action: ActionItem<Library>, library: Library) {
switch (action.action) {
case(Action.Scan):
@ -185,4 +297,57 @@ export class ManageLibraryComponent implements OnInit {
action.callback(action, library);
}
}
setupSelections() {
this.selections = new SelectionModel<Library>(false, this.libraries);
this.cdRef.markForCheck();
}
toggleAll() {
this.selectAll = !this.selectAll;
this.libraries.forEach(s => this.selections.toggle(s, this.selectAll));
this.cdRef.markForCheck();
}
handleSelection(item: Library, index: number) {
if (this.isShiftDown && this.lastSelectedIndex !== null) {
// Bulk select items between the last selected item and the current one
const start = Math.min(this.lastSelectedIndex, index);
const end = Math.max(this.lastSelectedIndex, index);
for (let i = start; i <= end; i++) {
const library = this.libraries[i];
if (!this.selections.isSelected(library)) {
this.selections.toggle(library, true); // Select the item
}
}
} else {
// Toggle the clicked item
this.selections.toggle(item);
}
// Update the last selected index
this.lastSelectedIndex = index;
// Manage the state of "Select All" and "Has Some Selected"
const numberOfSelected = this.selections.selected().length;
this.selectAll = numberOfSelected === this.libraries.length;
this.cdRef.markForCheck();
}
resetBulkMode() {
this.bulkAction = null;
this.bulkMode = false;
this.sourceCopyToLibrary = null;
this.libraries.forEach(s => {
if (this.selections.isSelected(s)) {
this.selections.toggle(s, false)
}
});
this.selectAll = false;
this.cdRef.markForCheck();
}
}

View file

@ -1,7 +1,7 @@
import { ConfirmButton } from './confirm-button';
export class ConfirmConfig {
_type: string = 'confirm'; // internal only: confirm or alert (todo: use enum)
_type: 'confirm' | 'alert' = 'confirm';
header: string = 'Confirm';
content: string = '';
buttons: Array<ConfirmButton> = [];

View file

@ -3,6 +3,7 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { take } from 'rxjs/operators';
import { ConfirmDialogComponent } from './confirm-dialog/confirm-dialog.component';
import { ConfirmConfig } from './confirm-dialog/_models/confirm-config';
import {translate} from "@jsverse/transloco";
@Injectable({
providedIn: 'root'
@ -32,6 +33,7 @@ export class ConfirmService {
if (content !== undefined && config === undefined) {
config = this.defaultConfirm;
config.header = translate('confirm.confirm');
config.content = content;
}
if (content !== undefined && content !== '' && config!.content === '') {
@ -58,7 +60,8 @@ export class ConfirmService {
}
if (content !== undefined && config === undefined) {
config = this.defaultConfirm;
config = this.defaultAlert;
config.header = translate('confirm.alert');
config.content = content;
}

View file

@ -23,7 +23,7 @@ import {TopReadersComponent} from '../top-readers/top-readers.component';
import {StatListComponent} from '../stat-list/stat-list.component';
import {IconAndTitleComponent} from '../../../shared/icon-and-title/icon-and-title.component';
import {AsyncPipe, DecimalPipe, NgIf} from '@angular/common';
import {translate, TranslocoDirective, TranslocoService} from "@jsverse/transloco";
import {translate, TranslocoDirective} from "@jsverse/transloco";
import {FilterComparison} from "../../../_models/metadata/v2/filter-comparison";
import {FilterField} from "../../../_models/metadata/v2/filter-field";
import {

View file

@ -9,28 +9,34 @@
<label for="time-select-top-reads" class="form-check-label visually-hidden">{{t('time-selection-label')}}</label>
<select id="time-select-top-reads" class="form-select" formControlName="days"
[class.is-invalid]="formGroup.get('days')?.invalid && formGroup.get('days')?.touched">
<option *ngFor="let item of timePeriods" [value]="item.value">{{t(item.title)}}</option>
@for (item of timePeriods; track item.value) {
<option [value]="item.value">{{t(item.title)}}</option>
}
</select>
</div>
</form>
</div>
</div>
<ng-container>
<div class="grid row g-0">
<div class="card" *ngFor="let user of (users$ | async)">
<div class="card-header text-center">
{{user.username}}
@if (users$ | async; as users) {
<app-carousel-reel [alwaysShow]="false" [clickableTitle]="false" [items]="users">
<ng-template #carouselItem let-item>
<div class="card me-2">
<div class="card-header text-center">
{{item.username}}
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">{{t('comics-label', {value: item.comicsTime})}}</li>
<li class="list-group-item">{{t('manga-label', {value: item.mangaTime})}}</li>
<li class="list-group-item">{{t('books-label', {value: item.booksTime})}}</li>
</ul>
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">{{t('comics-label', {value: user.comicsTime})}}</li>
<li class="list-group-item">{{t('manga-label', {value: user.mangaTime})}}</li>
<li class="list-group-item">{{t('books-label', {value: user.booksTime})}}</li>
</ul>
</div>
</div>
</ng-container>
</ng-template>
</app-carousel-reel>
}

View file

@ -11,8 +11,9 @@ import { Observable, switchMap, shareReplay } from 'rxjs';
import { StatisticsService } from 'src/app/_services/statistics.service';
import { TopUserRead } from '../../_models/top-reads';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import { NgFor, AsyncPipe } from '@angular/common';
import { AsyncPipe } from '@angular/common';
import {TranslocoDirective} from "@jsverse/transloco";
import {CarouselReelComponent} from "../../../carousel/_components/carousel-reel/carousel-reel.component";
export const TimePeriods: Array<{title: string, value: number}> =
[{title: 'this-week', value: new Date().getDay() || 1},
@ -28,15 +29,16 @@ export const TimePeriods: Array<{title: string, value: number}> =
styleUrls: ['./top-readers.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [ReactiveFormsModule, NgFor, AsyncPipe, TranslocoDirective]
imports: [ReactiveFormsModule, AsyncPipe, TranslocoDirective, CarouselReelComponent]
})
export class TopReadersComponent implements OnInit {
private readonly destroyRef = inject(DestroyRef);
formGroup: FormGroup;
timePeriods = TimePeriods;
users$: Observable<TopUserRead[]>;
private readonly destroyRef = inject(DestroyRef);
constructor(private statsService: StatisticsService, private readonly cdRef: ChangeDetectorRef) {
this.formGroup = new FormGroup({