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:
parent
894b49bb76
commit
857e419e4e
77 changed files with 72523 additions and 30914 deletions
|
|
@ -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 = [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>>();
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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> = [];
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue