mirror of
https://github.com/adityachandelgit/BookLore.git
synced 2026-01-05 22:59:51 -06:00
Improve bookdrop UI (#1768)
* Improve bookdrop UI * Rename resetAll as it doesn't apply to all now * Fix display of copied metadata fields
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
@if (fetchedMetadata) {
|
||||
<form [formGroup]="metadataForm" class="flex flex-col h-[35rem] w-full">
|
||||
@if (fetchedMetadata && fetchedMetadata.title) {
|
||||
<form [formGroup]="metadataForm" class="metapicker flex flex-col w-full">
|
||||
<div class="flex-grow overflow-auto">
|
||||
<div class="relative flex items-center justify-between pb-3">
|
||||
<div class="absolute left-1/2 transform -translate-x-1/2 flex items-center pl-24">
|
||||
<p class="pr-6">Current Metadata</p>
|
||||
<div>
|
||||
<div class="metaheader relative flex items-center">
|
||||
<label class="w-[12%]"></label>
|
||||
<div class="flex w-full items-center">
|
||||
<p class="!w-1/2 pr-3" style="text-align:right">Current Metadata</p>
|
||||
<div class="midbuttons">
|
||||
<p-button
|
||||
size="small"
|
||||
severity="success"
|
||||
icon="pi pi-angle-left"
|
||||
class="mx-2"
|
||||
@@ -15,6 +17,7 @@
|
||||
(onClick)="copyMissing()"
|
||||
></p-button>
|
||||
<p-button
|
||||
size="small"
|
||||
severity="success"
|
||||
icon="pi pi-angle-double-left"
|
||||
class="mx-2"
|
||||
@@ -24,27 +27,29 @@
|
||||
(onClick)="copyAll()"
|
||||
></p-button>
|
||||
</div>
|
||||
<p class="pl-6">Fetched Metadata</p>
|
||||
<p class="!w-1/2 pl-3">Fetched Metadata</p>
|
||||
</div>
|
||||
<div class="ml-auto">
|
||||
<div class="ml-auto absolute" style="right:1rem">
|
||||
<p-button
|
||||
severity="danger"
|
||||
size="small"
|
||||
severity="warn"
|
||||
icon="pi pi-refresh"
|
||||
label="Reset"
|
||||
[outlined]="true"
|
||||
pTooltip="Reset all fields to original values"
|
||||
tooltipPosition="bottom"
|
||||
(onClick)="resetAll()"
|
||||
(onClick)="confirmReset()"
|
||||
></p-button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="metacontent">
|
||||
<div class="flex items-center py-1">
|
||||
<label class="w-[6.5%]"></label>
|
||||
<label class="w-[12%] text-sm">Cover</label>
|
||||
<div class="flex w-full items-center justify-center">
|
||||
<p-image class="thumbnail" [src]="metadataForm.get('thumbnailUrl')?.value" alt="Image" appendTo="body" lazyLoad [preview]="true"></p-image>
|
||||
<input type="hidden" id="thumbnailUrl" formControlName="thumbnailUrl" class="!w-1/2"/>
|
||||
<p-button
|
||||
size="small"
|
||||
[icon]="isValueSaved('thumbnailUrl') ? 'pi pi-check' : (hoveredFields['thumbnailUrl'] && isValueCopied('thumbnailUrl') ? 'pi pi-times' : 'pi pi-arrow-left')"
|
||||
[outlined]="true"
|
||||
[ngClass]="
|
||||
@@ -62,7 +67,7 @@
|
||||
</div>
|
||||
@for (field of metadataFieldsTop; track field) {
|
||||
<div class="flex items-center py-1">
|
||||
<label for="{{field.controlName}}" class="w-[6.5%]">{{ field.label }}</label>
|
||||
<label for="{{field.controlName}}" class="w-[12%] text-sm">{{ field.label }}</label>
|
||||
<div class="flex w-full">
|
||||
<input
|
||||
pSize="small"
|
||||
@@ -76,6 +81,7 @@
|
||||
}"
|
||||
/>
|
||||
<p-button
|
||||
size="small"
|
||||
[icon]="isValueSaved(field.controlName) ? 'pi pi-check' : (hoveredFields[field.controlName] && isValueCopied(field.controlName) ? 'pi pi-times' : 'pi pi-arrow-left')"
|
||||
[outlined]="true"
|
||||
[ngClass]="
|
||||
@@ -93,15 +99,20 @@
|
||||
}
|
||||
@for (field of metadataChips; track field) {
|
||||
<div class="flex items-center py-1">
|
||||
<label for="{{field.controlName}}" class="w-[6.5%]">{{ field.label }}</label>
|
||||
<label for="{{field.controlName}}" class="w-[12%] text-sm">{{ field.label }}</label>
|
||||
<div class="flex w-full items-center">
|
||||
<div class="w-full"
|
||||
[ngClass]="{
|
||||
'outlined-input-green': isValueCopied(field.controlName) && !hoveredFields[field.controlName],
|
||||
}">
|
||||
<p-autoComplete formControlName="{{field.controlName}}" [multiple]="true" [typeahead]="false" [dropdown]="false" [forceSelection]="false" class="w-full" (onBlur)="onAutoCompleteBlur(field.controlName, $event)"></p-autoComplete>
|
||||
</div>
|
||||
<p-autoComplete
|
||||
size="small"
|
||||
formControlName="{{field.controlName}}"
|
||||
[multiple]="true"
|
||||
[typeahead]="false"
|
||||
[dropdown]="false"
|
||||
[forceSelection]="false"
|
||||
class="w-full"
|
||||
[ngClass]="{'outlined-input-green': isValueCopied(field.controlName) && !hoveredFields[field.controlName]}"
|
||||
(onBlur)="onAutoCompleteBlur(field.controlName, $event)"/>
|
||||
<p-button
|
||||
size="small"
|
||||
[icon]="isValueSaved(field.controlName) ? 'pi pi-check' : (hoveredFields[field.controlName] && isValueCopied(field.controlName) ? 'pi pi-times' : 'pi pi-arrow-left')"
|
||||
[outlined]="true"
|
||||
[ngClass]="
|
||||
@@ -113,28 +124,34 @@
|
||||
(click)="hoveredFields[field.controlName] && isValueCopied(field.controlName) ? resetField(field.controlName) : copyFetchedToCurrent(field.controlName)"
|
||||
(mouseenter)="onMouseEnter(field.controlName)"
|
||||
(mouseleave)="onMouseLeave(field.controlName)"/>
|
||||
|
||||
<div class="w-full">
|
||||
<p-autoComplete
|
||||
[ngModel]="fetchedMetadata[field.fetchedKey] ?? []"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
[disabled]="true"
|
||||
[multiple]="true" [typeahead]="false" [dropdown]="false" [forceSelection]="false" class="w-full">
|
||||
</p-autoComplete>
|
||||
</div>
|
||||
<p-autoComplete
|
||||
size="small"
|
||||
[ngModel]="fetchedMetadata[field.fetchedKey] ?? []"
|
||||
[ngModelOptions]="{standalone:true}"
|
||||
[disabled]="true"
|
||||
[multiple]="true"
|
||||
[typeahead]="false"
|
||||
[dropdown]="false"
|
||||
[forceSelection]="false"
|
||||
class="w-full"/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@for (field of metadataDescription; track field) {
|
||||
<div class="flex items-center py-1">
|
||||
<label for="{{field.controlName}}" class="w-[6.5%]">{{ field.label }}</label>
|
||||
<label for="{{field.controlName}}" class="w-[12%] text-sm">{{ field.label }}</label>
|
||||
<div class="flex w-full items-center">
|
||||
<textarea rows="2" pTextarea id="{{field.controlName}}" formControlName="{{field.controlName}}" class="!w-1/2"
|
||||
[ngClass]="{
|
||||
'outlined-input-green': isValueCopied(field.controlName) && !hoveredFields[field.controlName],
|
||||
}"
|
||||
></textarea>
|
||||
<textarea
|
||||
rows="2"
|
||||
pSize="small"
|
||||
pTextarea
|
||||
id="{{field.controlName}}"
|
||||
formControlName="{{field.controlName}}"
|
||||
class="!w-1/2"
|
||||
[ngClass]="{'outlined-input-green': isValueCopied(field.controlName) && !hoveredFields[field.controlName]}"
|
||||
></textarea>
|
||||
<p-button
|
||||
size="small"
|
||||
[icon]="isValueSaved(field.controlName) ? 'pi pi-check' : (hoveredFields[field.controlName] && isValueCopied(field.controlName) ? 'pi pi-times' : 'pi pi-arrow-left')"
|
||||
[outlined]="true"
|
||||
[ngClass]="
|
||||
@@ -146,13 +163,19 @@
|
||||
(click)="hoveredFields[field.controlName] && isValueCopied(field.controlName) ? resetField(field.controlName) : copyFetchedToCurrent(field.controlName)"
|
||||
(mouseenter)="onMouseEnter(field.controlName)"
|
||||
(mouseleave)="onMouseLeave(field.controlName)"/>
|
||||
<textarea rows="2" pInputText [value]="fetchedMetadata[field.fetchedKey] ?? null" class="!w-1/2" readonly></textarea>
|
||||
<textarea
|
||||
rows="2"
|
||||
pSize="small"
|
||||
pInputText
|
||||
[value]="fetchedMetadata[field.fetchedKey] ?? null"
|
||||
class="!w-1/2"
|
||||
readonly></textarea>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@for (field of metadataFieldsBottom; track field) {
|
||||
<div class="flex items-center py-1">
|
||||
<label for="{{field.controlName}}" class="w-[6.5%]">{{ field.label }}</label>
|
||||
<label for="{{field.controlName}}" class="w-[12%] text-sm">{{ field.label }}</label>
|
||||
<div class="flex w-full">
|
||||
<input pInputText pSize="small" id="{{field.controlName}}" formControlName="{{field.controlName}}" class="!w-1/2"
|
||||
[ngClass]="{
|
||||
@@ -160,6 +183,7 @@
|
||||
}"
|
||||
/>
|
||||
<p-button
|
||||
size="small"
|
||||
[icon]="isValueSaved(field.controlName) ? 'pi pi-check' : (hoveredFields[field.controlName] && isValueCopied(field.controlName) ? 'pi pi-times' : 'pi pi-arrow-left')"
|
||||
[outlined]="true"
|
||||
[ngClass]="
|
||||
@@ -179,68 +203,86 @@
|
||||
</div>
|
||||
</form>
|
||||
} @else {
|
||||
<form [formGroup]="metadataForm" class="flex h-[30rem] w-full">
|
||||
<div class="flex-grow overflow-auto pr-4">
|
||||
<p class="pb-2">Current Metadata:</p>
|
||||
|
||||
@for (field of metadataFieldsTop; track field) {
|
||||
<div class="flex items-center py-1">
|
||||
<label for="{{field.controlName}}" class="w-[15%]">{{ field.label }}</label>
|
||||
<div class="flex w-full">
|
||||
<input
|
||||
pSize="small"
|
||||
fluid
|
||||
pInputText
|
||||
id="{{field.controlName}}"
|
||||
formControlName="{{field.controlName}}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@for (field of metadataChips; track field) {
|
||||
<div class="flex items-center py-1">
|
||||
<label for="{{field.controlName}}" class="w-[15%]">{{ field.label }}</label>
|
||||
<div class="flex w-full">
|
||||
<p-autoComplete formControlName="{{field.controlName}}" [multiple]="true" [typeahead]="false" [dropdown]="false" [forceSelection]="false" class="w-full" (onBlur)="onAutoCompleteBlur(field.controlName, $event)"></p-autoComplete>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@for (field of metadataDescription; track field) {
|
||||
<div class="flex items-center py-1">
|
||||
<label for="{{field.controlName}}" class="w-[15%]">{{ field.label }}</label>
|
||||
<div class="flex w-full">
|
||||
<textarea fluid rows="2" pTextarea id="{{field.controlName}}" formControlName="{{field.controlName}}"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@for (field of metadataFieldsBottom; track field) {
|
||||
<div class="flex items-center py-1">
|
||||
<label for="{{field.controlName}}" class="w-[15%]">{{ field.label }}</label>
|
||||
<div class="flex w-full">
|
||||
<input
|
||||
fluid
|
||||
pInputText
|
||||
pSize="small"
|
||||
id="{{field.controlName}}"
|
||||
formControlName="{{field.controlName}}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="w-[20%] h-full flex items-start justify-center p-4">
|
||||
<div class="w-full h-full">
|
||||
<img
|
||||
[src]="metadataForm.get('thumbnailUrl')?.value"
|
||||
alt="Book Thumbnail"
|
||||
class="w-full h-full object-contain"
|
||||
/>
|
||||
<input type="hidden" id="thumbnailUrl" formControlName="thumbnailUrl"/>
|
||||
<form [formGroup]="metadataForm" class="metapicker flex w-full">
|
||||
<div class="flex-grow overflow-auto">
|
||||
<div class="metaheader relative flex items-center">
|
||||
<p class="text-sm flex items-center" style="gap: 0.5rem">
|
||||
<i class="pi metadata-status pi-exclamation-triangle not-applied"></i>
|
||||
Unable to fetch new metadata for this file
|
||||
</p>
|
||||
<p-button
|
||||
size="small"
|
||||
severity="warn"
|
||||
icon="pi pi-refresh"
|
||||
label="Reset"
|
||||
[outlined]="true"
|
||||
pTooltip="Reset all fields to original values"
|
||||
tooltipPosition="bottom"
|
||||
(onClick)="confirmReset()"
|
||||
></p-button>
|
||||
</div>
|
||||
<div class="flex flex-row">
|
||||
<div class="w-[25%] h-full flex items-start justify-center pl-4">
|
||||
<div class="w-full h-full">
|
||||
<img
|
||||
[src]="metadataForm.get('thumbnailUrl')?.value"
|
||||
alt="Book Thumbnail"
|
||||
class="w-full h-full object-contain"
|
||||
/>
|
||||
<input type="hidden" id="thumbnailUrl" formControlName="thumbnailUrl"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="metacontent w-[75%]">
|
||||
@for (field of metadataFieldsTop; track field) {
|
||||
<div class="flex items-center py-1">
|
||||
<label for="{{field.controlName}}" class="w-[15%] text-sm">{{ field.label }}</label>
|
||||
<div class="flex w-full">
|
||||
<input
|
||||
pSize="small"
|
||||
fluid
|
||||
pInputText
|
||||
id="{{field.controlName}}"
|
||||
formControlName="{{field.controlName}}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@for (field of metadataChips; track field) {
|
||||
<div class="flex items-center py-1">
|
||||
<label for="{{field.controlName}}" class="w-[15%] text-sm">{{ field.label }}</label>
|
||||
<div class="flex w-full">
|
||||
<p-autoComplete formControlName="{{field.controlName}}" [multiple]="true" [typeahead]="false" [dropdown]="false" [forceSelection]="false" class="w-full" (onBlur)="onAutoCompleteBlur(field.controlName, $event)"></p-autoComplete>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@for (field of metadataDescription; track field) {
|
||||
<div class="flex items-center py-1">
|
||||
<label for="{{field.controlName}}" class="w-[15%] text-sm">{{ field.label }}</label>
|
||||
<div class="flex w-full">
|
||||
<textarea fluid rows="2" pTextarea id="{{field.controlName}}" formControlName="{{field.controlName}}"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@for (field of metadataFieldsBottom; track field) {
|
||||
<div class="flex items-center py-1">
|
||||
<label for="{{field.controlName}}" class="w-[15%] text-sm">{{ field.label }}</label>
|
||||
<div class="flex w-full">
|
||||
<input
|
||||
fluid
|
||||
pInputText
|
||||
pSize="small"
|
||||
id="{{field.controlName}}"
|
||||
formControlName="{{field.controlName}}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
|
||||
@@ -22,10 +22,49 @@
|
||||
--p-button-outlined-primary-color: #E32636;
|
||||
}
|
||||
|
||||
.outlined-input-green {
|
||||
border: 0.75px solid forestgreen !important;
|
||||
input.outlined-input-green,
|
||||
textarea.outlined-input-green,
|
||||
::ng-deep p-autocomplete.outlined-input-green ul.p-autocomplete-input-multiple {
|
||||
border: 1px solid forestgreen !important;
|
||||
}
|
||||
|
||||
::ng-deep .p-inputchips {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
::ng-deep .p-autocomplete .p-chip .p-chip-label {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.metapicker {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.metaheader {
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.metaheader .midbuttons {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.metacontent {
|
||||
padding: 0 1rem 1rem;
|
||||
}
|
||||
|
||||
.metadata-status {
|
||||
&.copied {
|
||||
color: rgb(34, 197, 94);
|
||||
}
|
||||
|
||||
&.no-metadata {
|
||||
color: rgb(59, 130, 246);
|
||||
}
|
||||
|
||||
&.not-applied {
|
||||
color: rgb(239, 68, 68);
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import {Textarea} from 'primeng/textarea';
|
||||
import {AutoComplete} from 'primeng/autocomplete';
|
||||
import {Image} from 'primeng/image';
|
||||
import {LazyLoadImageModule} from 'ng-lazyload-image';
|
||||
import {ConfirmationService} from 'primeng/api';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bookdrop-file-metadata-picker-component',
|
||||
@@ -30,6 +31,8 @@ import {LazyLoadImageModule} from 'ng-lazyload-image';
|
||||
})
|
||||
export class BookdropFileMetadataPickerComponent {
|
||||
|
||||
private readonly confirmationService = inject(ConfirmationService);
|
||||
|
||||
@Input() fetchedMetadata!: BookMetadata;
|
||||
@Input() originalMetadata?: BookMetadata;
|
||||
@Input() metadataForm!: FormGroup;
|
||||
@@ -97,10 +100,10 @@ export class BookdropFileMetadataPickerComponent {
|
||||
});
|
||||
}
|
||||
|
||||
copyAll() {
|
||||
copyAll(includeCover: boolean = true): void {
|
||||
if (this.fetchedMetadata) {
|
||||
Object.keys(this.fetchedMetadata).forEach((field) => {
|
||||
if (this.fetchedMetadata[field] && field !== 'thumbnailUrl') {
|
||||
if (this.fetchedMetadata[field] && (includeCover || field !== 'thumbnailUrl')) {
|
||||
this.copyFetchedToCurrent(field);
|
||||
}
|
||||
});
|
||||
@@ -164,6 +167,16 @@ export class BookdropFileMetadataPickerComponent {
|
||||
}
|
||||
}
|
||||
|
||||
confirmReset(): void {
|
||||
this.confirmationService.confirm({
|
||||
message: 'Are you sure you want to reset all metadata changes made to this file?',
|
||||
header: 'Reset Metadata Changes?',
|
||||
icon: 'pi pi-exclamation-triangle',
|
||||
acceptButtonStyleClass: 'p-button-danger',
|
||||
accept: () => this.resetAll()
|
||||
});
|
||||
}
|
||||
|
||||
resetAll() {
|
||||
if (this.originalMetadata) {
|
||||
this.metadataForm.patchValue({
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
<div class="header-actions">
|
||||
<p-button
|
||||
label="Rescan Bookdrop"
|
||||
label="Rescan"
|
||||
icon="pi pi-refresh"
|
||||
severity="primary"
|
||||
outlined
|
||||
@@ -56,72 +56,53 @@
|
||||
|
||||
@if (bookdropFileUis.length !== 0) {
|
||||
<div class="controls-row">
|
||||
|
||||
<div class="default-controls">
|
||||
<span>Library for All Files:</span>
|
||||
<p-select
|
||||
size="small"
|
||||
[options]="libraryOptions"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
placeholder="Select Default Library"
|
||||
[(ngModel)]="defaultLibraryId">
|
||||
</p-select>
|
||||
|
||||
<span>Subpath for All Files:</span>
|
||||
<p-select
|
||||
size="small"
|
||||
[options]="selectedLibraryPaths"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
placeholder="Select Default Subpath"
|
||||
[(ngModel)]="defaultPathId">
|
||||
</p-select>
|
||||
|
||||
<p-button
|
||||
size="small"
|
||||
label="Apply to All"
|
||||
icon="pi pi-check"
|
||||
[disabled]="!canApplyDefaults"
|
||||
(click)="applyDefaultsToAll()"
|
||||
pTooltip="Apply selected library and subpath to all files"
|
||||
tooltipPosition="top">
|
||||
</p-button>
|
||||
</div>
|
||||
|
||||
<div class="action-buttons">
|
||||
<p-button
|
||||
size="small"
|
||||
outlined
|
||||
severity="info"
|
||||
label="Apply (w/ Cover)"
|
||||
label="Import Metadata"
|
||||
icon="pi pi-copy"
|
||||
(click)="copyAll(true)"
|
||||
pTooltip="For all files, replace current metadata with fetched metadata, including cover images"
|
||||
(click)="copyAll()"
|
||||
pTooltip="Replace current metadata with fetched metadata on all files"
|
||||
tooltipPosition="top">
|
||||
</p-button>
|
||||
<span pTooltip="Include book covers when importing fetched metadata"><p-checkbox
|
||||
inputId="includecovers"
|
||||
[binary]="true"
|
||||
[(ngModel)]="includeCoversOnCopy">
|
||||
</p-checkbox>
|
||||
<label for="includecovers" class="text-sm" style="margin-left: 0.5em;">Covers</label></span>
|
||||
</div>
|
||||
|
||||
<div class="default-controls">
|
||||
<p-select
|
||||
size="small"
|
||||
[options]="libraryOptions"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
placeholder="Default Library"
|
||||
[(ngModel)]="defaultLibraryId">
|
||||
</p-select>
|
||||
|
||||
<p-select
|
||||
size="small"
|
||||
[options]="selectedLibraryPaths"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
placeholder="Default Subpath"
|
||||
[(ngModel)]="defaultPathId">
|
||||
</p-select>
|
||||
|
||||
<p-button
|
||||
size="small"
|
||||
outlined
|
||||
severity="info"
|
||||
label="Apply (no Cover)"
|
||||
icon="pi pi-copy"
|
||||
(click)="copyAll(false)"
|
||||
pTooltip="For all files, replace current metadata with fetched metadata, excluding cover images"
|
||||
label="Apply"
|
||||
icon="pi pi-check"
|
||||
[disabled]="!canApplyDefaults"
|
||||
(click)="applyDefaultsToAll()"
|
||||
pTooltip="Apply selected library and subpath to all files"
|
||||
tooltipPosition="top">
|
||||
</p-button>
|
||||
|
||||
<p-button
|
||||
size="small"
|
||||
outlined
|
||||
severity="warn"
|
||||
label="Reset"
|
||||
icon="pi pi-refresh"
|
||||
(click)="resetAll()"
|
||||
pTooltip="Reset all metadata changes"
|
||||
tooltipPosition="left">
|
||||
</p-button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -182,7 +163,7 @@
|
||||
}"
|
||||
[pTooltip]="copiedFlags[file.file.id]
|
||||
? 'Fetched metadata has been applied.'
|
||||
: !file.file.fetchedMetadata
|
||||
: (!file.file.fetchedMetadata || !file.file.fetchedMetadata.title)
|
||||
? 'No fetched metadata available. Original metadata will be used.'
|
||||
: 'Fetched metadata hasn’t been applied yet. Open metadata picker to review.'"
|
||||
tooltipPosition="top">
|
||||
@@ -242,7 +223,7 @@
|
||||
<div class="footer-left">
|
||||
@if (bookdropFileUis.length > 0) {
|
||||
<p-button
|
||||
label="Select All"
|
||||
label="Select All"
|
||||
icon="pi pi-check-square"
|
||||
severity="info"
|
||||
(click)="selectAll(true)"
|
||||
@@ -252,7 +233,7 @@
|
||||
</p-button>
|
||||
|
||||
<p-button
|
||||
label="Clear All"
|
||||
label="Clear"
|
||||
icon="pi pi-times-circle"
|
||||
severity="warn"
|
||||
(click)="selectAll(false)"
|
||||
@@ -271,14 +252,26 @@
|
||||
[totalRecords]="totalRecords"
|
||||
[first]="currentPage * pageSize"
|
||||
(onPageChange)="loadPage($event.page ?? 0)"
|
||||
[showCurrentPageReport]="true"
|
||||
currentPageReportTemplate="Showing files {first} - {last} of {totalRecords}">
|
||||
[showJumpToPageDropdown]="true"
|
||||
[showPageLinks]="false"
|
||||
[showFirstLastIcon]="false"
|
||||
currentPageReportTemplate="Page {currentPage} of {totalPages}">
|
||||
</p-paginator>
|
||||
</div>
|
||||
|
||||
<div class="footer-right">
|
||||
<p-button
|
||||
[label]="'Delete ' + selectedCount + ' File' + (selectedCount !== 1 ? 's' : '')"
|
||||
[label]="'Reset ' + selectedCount"
|
||||
icon="pi pi-refresh"
|
||||
severity="warn"
|
||||
[disabled]="!hasSelectedFiles"
|
||||
outlined
|
||||
(click)="confirmReset()"
|
||||
pTooltip="Discard all changes made to metadata of selected files"
|
||||
tooltipPosition="top">
|
||||
</p-button>
|
||||
<p-button
|
||||
[label]="'Delete ' + selectedCount"
|
||||
icon="pi pi-times"
|
||||
severity="danger"
|
||||
[disabled]="!hasSelectedFiles"
|
||||
@@ -288,7 +281,7 @@
|
||||
tooltipPosition="top">
|
||||
</p-button>
|
||||
<p-button
|
||||
[label]="saving ? ('Finalizing ' + selectedCount + ' File' + (selectedCount !== 1 ? 's' : '') + '...') : ('Finalize ' + selectedCount + ' File' + (selectedCount !== 1 ? 's' : ''))"
|
||||
[label]="saving ? ('Finalizing ' + selectedCount + '...') : ('Finalize ' + selectedCount)"
|
||||
[icon]="saving ? 'pi pi-spin pi-spinner' : 'pi pi-save'"
|
||||
severity="success"
|
||||
outlined
|
||||
|
||||
@@ -9,12 +9,8 @@
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
background: var(--card-background);
|
||||
min-width: 100rem;
|
||||
min-width: 60rem;
|
||||
border: 1px solid var(--p-content-border-color);
|
||||
|
||||
> * + * {
|
||||
margin: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
@@ -96,7 +92,9 @@
|
||||
}
|
||||
|
||||
.controls-section {
|
||||
padding: 0 1.5rem 0.5rem;
|
||||
padding: 0 1.5rem 1.5rem;
|
||||
margin: 0;
|
||||
border-bottom: 1px solid var(--p-content-border-color);
|
||||
}
|
||||
|
||||
.saving-overlay {
|
||||
@@ -145,12 +143,6 @@
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
font-size: 0.875rem;
|
||||
color: rgb(209, 213, 219);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
p-select {
|
||||
min-width: 8rem;
|
||||
max-width: 16rem;
|
||||
@@ -160,20 +152,23 @@
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
font-size: 0.875rem;
|
||||
color: rgb(209, 213, 219);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.content-area {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1rem 1rem 1rem;
|
||||
padding: 1rem;
|
||||
|
||||
> * + * {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
padding: 1rem 1.5rem 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
@@ -249,7 +244,6 @@
|
||||
}
|
||||
|
||||
.details-section {
|
||||
padding: 2rem 3rem;
|
||||
border-left: 1px solid var(--border-color);
|
||||
border-right: 1px solid var(--border-color);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
|
||||
@@ -86,6 +86,7 @@ export class BookdropFileReviewComponent implements OnInit {
|
||||
copiedFlags: Record<number, boolean> = {};
|
||||
loading = true;
|
||||
saving = false;
|
||||
includeCoversOnCopy = true;
|
||||
|
||||
pageSize = 50;
|
||||
totalRecords = 0;
|
||||
@@ -231,13 +232,13 @@ export class BookdropFileReviewComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
copyAll(includeThumbnail: boolean): void {
|
||||
copyAll(): void {
|
||||
Object.values(this.fileUiCache).forEach(fileUi => {
|
||||
const fetched = fileUi.file.fetchedMetadata;
|
||||
const form = fileUi.metadataForm;
|
||||
if (!fetched) return;
|
||||
for (const key of Object.keys(fetched)) {
|
||||
if (!includeThumbnail && key === 'thumbnailUrl') continue;
|
||||
if (!this.includeCoversOnCopy && key === 'thumbnailUrl') continue;
|
||||
const value = fetched[key as keyof typeof fetched];
|
||||
if (value != null) {
|
||||
form.get(key)?.setValue(value);
|
||||
@@ -248,8 +249,16 @@ export class BookdropFileReviewComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
resetAll(): void {
|
||||
Object.values(this.fileUiCache).forEach(fileUi => {
|
||||
resetMetadata(): void {
|
||||
const selectedFiles = Object.values(this.fileUiCache).filter(file => {
|
||||
if (this.selectAllAcrossPages) {
|
||||
return !this.excludedFiles.has(file.file.id);
|
||||
} else {
|
||||
return file.selected;
|
||||
}
|
||||
});
|
||||
|
||||
const files = selectedFiles.map(fileUi => {
|
||||
const original = fileUi.file.originalMetadata;
|
||||
fileUi.metadataForm.patchValue({
|
||||
title: original?.title || null,
|
||||
@@ -283,8 +292,8 @@ export class BookdropFileReviewComponent implements OnInit {
|
||||
});
|
||||
fileUi.copiedFields = {};
|
||||
fileUi.savedFields = {};
|
||||
this.copiedFlags[fileUi.file.id] = false;
|
||||
});
|
||||
this.copiedFlags = {};
|
||||
}
|
||||
|
||||
selectAll(selected: boolean): void {
|
||||
@@ -317,6 +326,26 @@ export class BookdropFileReviewComponent implements OnInit {
|
||||
this.syncCurrentPageSelection();
|
||||
}
|
||||
|
||||
confirmReset(): void {
|
||||
const selectedCount = this.selectedCount;
|
||||
if (selectedCount === 0) {
|
||||
this.messageService.add({
|
||||
severity: 'warn',
|
||||
summary: 'No files selected',
|
||||
detail: 'Please select files to reset metadata.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.confirmationService.confirm({
|
||||
message: 'Are you sure you want to reset all metadata changes made to the selected files?',
|
||||
header: 'Confirm Reset',
|
||||
icon: 'pi pi-exclamation-triangle',
|
||||
acceptButtonStyleClass: 'p-button-danger',
|
||||
accept: () => this.resetMetadata()
|
||||
});
|
||||
}
|
||||
|
||||
confirmFinalize(): void {
|
||||
const selectedCount = this.selectedCount;
|
||||
if (selectedCount === 0) {
|
||||
@@ -332,8 +361,7 @@ export class BookdropFileReviewComponent implements OnInit {
|
||||
message: `Are you sure you want to finalize the import of ${selectedCount} file${selectedCount !== 1 ? 's' : ''}?`,
|
||||
header: 'Confirm Finalize',
|
||||
icon: 'pi pi-exclamation-triangle',
|
||||
acceptLabel: 'Yes',
|
||||
rejectLabel: 'Cancel',
|
||||
acceptButtonStyleClass: 'p-button-danger',
|
||||
accept: () => this.finalizeImport(),
|
||||
});
|
||||
}
|
||||
@@ -353,6 +381,7 @@ export class BookdropFileReviewComponent implements OnInit {
|
||||
message: `Are you sure you want to delete ${selectedCount} selected Bookdrop file${selectedCount !== 1 ? 's' : ''}? This action cannot be undone.`,
|
||||
header: 'Confirm Delete',
|
||||
icon: 'pi pi-exclamation-triangle',
|
||||
acceptButtonStyleClass: 'p-button-danger',
|
||||
accept: () => {
|
||||
const payload: any = {
|
||||
selectAll: this.selectAllAcrossPages,
|
||||
|
||||
Reference in New Issue
Block a user