fix: Bookdrop bulk edit mobile fixes (#1925)

This commit is contained in:
Muppetteer
2025-12-19 05:08:32 +11:00
committed by GitHub
parent 055e86df18
commit 7c2736229f
5 changed files with 142 additions and 157 deletions

View File

@@ -2,7 +2,11 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 1rem;
padding: 1rem; padding: 0 1rem;
@media (max-width: 768px) {
padding: 0;
}
.helper-text { .helper-text {
font-size: 0.875rem; font-size: 0.875rem;
@@ -73,7 +77,7 @@
.field-row { .field-row {
display: grid; display: grid;
grid-template-columns: auto 120px 1fr; grid-template-columns: auto 6rem 1fr;
gap: 0.75rem; gap: 0.75rem;
align-items: center; align-items: center;
} }

View File

@@ -20,6 +20,7 @@
<div class="header-actions"> <div class="header-actions">
<p-button <p-button
label="Rescan" label="Rescan"
size="small"
icon="pi pi-refresh" icon="pi pi-refresh"
severity="primary" severity="primary"
outlined outlined
@@ -56,18 +57,32 @@
@if (bookdropFileUis.length !== 0) { @if (bookdropFileUis.length !== 0) {
<div class="controls-row"> <div class="controls-row">
<div class="action-buttons"> <div class="action-buttons">
<p-inputgroup class="importmetadata" [ngClass]="!hasSelectedFiles ? 'disabled' : ''">
<p-button
size="small"
outlined
severity="info"
label="Import&nbsp;Metadata"
icon="pi pi-copy"
[disabled]="!hasSelectedFiles"
(click)="copyMetadata()"
pTooltip="Import fetched metadata for selected files"
tooltipPosition="top">
</p-button>
<p-inputgroup-addon pTooltip="Include book covers when importing fetched metadata" tooltipPosition="top">
<p-checkbox
inputId="includecovers"
[disabled]="!hasSelectedFiles"
[binary]="true"
[(ngModel)]="includeCoversOnCopy">
</p-checkbox>
<label for="includecovers"><i class="pi pi-image pl-2"></i></label>
</p-inputgroup-addon>
</p-inputgroup>
<p-button <p-button
size="small" size="small"
outlined class="bulkedit"
severity="info"
label="Import&nbsp;Metadata"
icon="pi pi-copy"
(click)="copyAll()"
pTooltip="Replace current metadata with fetched metadata on all files"
tooltipPosition="top">
</p-button>
<p-button
size="small"
outlined outlined
severity="help" severity="help"
label="Bulk&nbsp;Edit" label="Bulk&nbsp;Edit"
@@ -77,28 +92,24 @@
pTooltip="Edit metadata fields in bulk for selected files" pTooltip="Edit metadata fields in bulk for selected files"
tooltipPosition="top"> tooltipPosition="top">
</p-button> </p-button>
<p-button <p-button
size="small" size="small"
class="extractpattern"
outlined outlined
severity="warn" severity="warn"
label="Extract&nbsp;Pattern" label="Extract&nbsp;Pattern"
icon="pi pi-sliders-h" icon="pi pi-sliders-h"
[disabled]="!hasSelectedFiles" [disabled]="!hasSelectedFiles"
(click)="openPatternExtractDialog()" (click)="openPatternExtractDialog()"
pTooltip="Extract metadata from filenames using a pattern" pTooltip="Extract metadata from selected filenames using a pattern"
tooltipPosition="top"> tooltipPosition="top">
</p-button> </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>
<div class="default-controls"> <div class="default-controls">
<i class="pi pi-copy" <i class="pi pi-book"
pTooltip="Select library and subpath for all files:" pTooltip="Specify library and subpath for selected files"
tooltipPosition="left"></i> tooltipPosition="left"></i>
<p-select <p-select
@@ -126,8 +137,8 @@
icon="pi pi-check" icon="pi pi-check"
severity="info" severity="info"
[disabled]="!canApplyDefaults" [disabled]="!canApplyDefaults"
(click)="applyDefaultsToAll()" (click)="applyLibraryDefaults()"
pTooltip="Apply selected library and subpath to all files" pTooltip="Apply library and subpath to selected files"
tooltipPosition="top"> tooltipPosition="top">
</p-button> </p-button>
</div> </div>
@@ -253,28 +264,33 @@
@if (!loading) { @if (!loading) {
<p-divider></p-divider> <p-divider></p-divider>
<!-- Desktop Buttons -->
<div class="footer"> <div class="footer">
<div class="footer-left"> <div class="footer-left">
@if (bookdropFileUis.length > 0) { @if (bookdropFileUis.length > 0) {
<p-button <p-inputgroup class="selectall">
label="Select&nbsp;All" <p-inputgroup-addon>
icon="pi pi-check-square" <label class="text-sm" pTooltip="Number of files selected" tooltipPosition="top">{{ selectedCount }}</label>
severity="info" </p-inputgroup-addon>
(click)="selectAll(true)" <p-button
outlined label="Select&nbsp;All"
pTooltip="Select all files across all pages you have navigated" icon="pi pi-check-square"
tooltipPosition="top"> severity="info"
</p-button> (click)="selectAll(true)"
outlined
pTooltip="Select all files across all pages"
tooltipPosition="top">
</p-button>
</p-inputgroup>
<p-button <p-button
label="Clear" label="Clear"
icon="pi pi-times-circle" icon="pi pi-times-circle"
severity="warn" severity="warn"
[disabled]="!hasSelectedFiles"
(click)="selectAll(false)" (click)="selectAll(false)"
outlined outlined
pTooltip="Deselect all files across all pages you have navigated" pTooltip="Deselect all files across all pages"
tooltipPosition="top"> tooltipPosition="top">
</p-button> </p-button>
} @else { } @else {
@@ -299,7 +315,7 @@
<div class="footer-right"> <div class="footer-right">
<p-button <p-button
[label]="'Reset ' + selectedCount" label="Reset"
icon="pi pi-refresh" icon="pi pi-refresh"
severity="warn" severity="warn"
[disabled]="!hasSelectedFiles" [disabled]="!hasSelectedFiles"
@@ -309,7 +325,7 @@
tooltipPosition="top"> tooltipPosition="top">
</p-button> </p-button>
<p-button <p-button
[label]="'Delete ' + selectedCount" label="Delete"
icon="pi pi-times" icon="pi pi-times"
severity="danger" severity="danger"
[disabled]="!hasSelectedFiles" [disabled]="!hasSelectedFiles"
@@ -319,90 +335,15 @@
tooltipPosition="top"> tooltipPosition="top">
</p-button> </p-button>
<p-button <p-button
[label]="saving ? ('Finalizing ' + selectedCount + '...') : ('Finalize ' + selectedCount)" label="Finalize"
[icon]="saving ? 'pi pi-spin pi-spinner' : 'pi pi-save'" [icon]="saving ? 'pi pi-spin pi-spinner' : 'pi pi-save'"
severity="success" severity="success"
outlined outlined
[disabled]="!canFinalize || saving" [disabled]="!canFinalize || saving"
(click)="confirmFinalize()" (click)="confirmFinalize()"
pTooltip="Move selected files into the chosen library and subpath"> pTooltip="Move selected files into the chosen library and subpath"
</p-button>
</div>
</div>
<!-- Mobile Buttons -->
<div class="footer-mobile">
@if (totalRecords > pageSize) {
<div class="footer-center">
<p-paginator
[rows]="pageSize"
[totalRecords]="totalRecords"
[first]="currentPage * pageSize"
(onPageChange)="loadPage($event.page ?? 0)"
[showJumpToPageDropdown]="true"
[showPageLinks]="false"
[showFirstLastIcon]="false"
currentPageReportTemplate="Page {currentPage} of {totalPages}">
</p-paginator>
</div>
}
<div class="footer-left">
@if (bookdropFileUis.length > 0) {
<p-button
icon="pi pi-check-square"
severity="info"
(click)="selectAll(true)"
outlined
pTooltip="Select all files across all pages you have navigated"
tooltipPosition="top">
</p-button>
<p-button
icon="pi pi-times-circle"
severity="warn"
(click)="selectAll(false)"
outlined
pTooltip="Deselect all files across all pages you have navigated"
tooltipPosition="top">
</p-button>
} @else {
<div class="spacer"></div>
}
</div>
<div class="footer-right">
@if (selectedCount > 0) {
<label class="text-sm">{{ selectedCount }}</label>
}
<p-button
icon="pi pi-refresh"
severity="warn"
[disabled]="!hasSelectedFiles"
outlined
(click)="confirmReset()"
pTooltip="Discard all changes made to metadata of selected files"
tooltipPosition="top"> tooltipPosition="top">
</p-button> </p-button>
<p-button
icon="pi pi-times"
severity="danger"
[disabled]="!hasSelectedFiles"
outlined
(click)="confirmDelete()"
pTooltip="Permanently delete selected Bookdrop files and discard any changes"
tooltipPosition="top">
</p-button>
<p-button
[icon]="saving ? 'pi pi-spin pi-spinner' : 'pi pi-save'"
severity="success"
outlined
[disabled]="!canFinalize || saving"
(click)="confirmFinalize()"
pTooltip="Move selected files into the chosen library and subpath">
</p-button>
</div> </div>
</div> </div>

View File

@@ -193,10 +193,53 @@
color: var(--p-button-outlined-info-color); color: var(--p-button-outlined-info-color);
} }
p-inputgroup-addon {
background-color: transparent;
border-color: var(--p-button-outlined-info-border-color) !important;
color: var(--p-button-outlined-info-color);
padding: 0 0.5em;
label {
display: flex;
color: var(--p-button-outlined-info-color) !important;
}
}
p-inputgroup.disabled {
p-inputgroup-addon {
opacity: var(--p-disabled-opacity) !important;
}
}
.action-buttons { .action-buttons {
display: flex; display: flex;
gap: 1rem; gap: 1rem;
align-items: center; align-items: center;
@media (max-width: 640px) {
flex-wrap: wrap;
width: 100%;
p-inputgroup.importmetadata {
width: 100%;
p-button {
flex-grow: 1;
::ng-deep button {
width: 100%;
}
}
}
p-button.bulkedit, p-button.extractpattern {
width: calc(50% - 0.5rem);
::ng-deep button {
width: 100%;
}
}
}
} }
.content-area { .content-area {
@@ -320,15 +363,18 @@ div.cover-image {
.footer { .footer {
@media (max-width: 768px) { @media (max-width: 768px) {
display: none; flex-wrap: wrap;
}
}
.footer-mobile { ::ng-deep .p-button-label {
flex-wrap: wrap; display: none;
}
@media (min-width: 768px) { ::ng-deep .p-button {
display: none; width: var(--p-button-icon-only-width);
padding-inline-start: 0;
padding-inline-end: 0;
gap: 0;
}
} }
} }
@@ -344,7 +390,6 @@ div.cover-image {
} }
.footer-right { .footer-right {
min-width: 12.5rem;
justify-content: flex-end; justify-content: flex-end;
} }
@@ -358,6 +403,10 @@ div.cover-image {
} }
} }
.p-paginator {
flex-wrap: nowrap;
}
.spacer { .spacer {
min-width: 10rem; min-width: 10rem;
} }

View File

@@ -15,7 +15,8 @@ import {Tooltip} from 'primeng/tooltip';
import {Divider} from 'primeng/divider'; import {Divider} from 'primeng/divider';
import {ConfirmationService, MessageService} from 'primeng/api'; import {ConfirmationService, MessageService} from 'primeng/api';
import {Observable, Subscription} from 'rxjs'; import {Observable, Subscription} from 'rxjs';
import {InputGroup} from 'primeng/inputgroup';
import {InputGroupAddonModule} from 'primeng/inputgroupaddon';
import {AppSettings} from '../../../../shared/model/app-settings.model'; import {AppSettings} from '../../../../shared/model/app-settings.model';
import {AppSettingsService} from '../../../../shared/service/app-settings.service'; import {AppSettingsService} from '../../../../shared/service/app-settings.service';
import {BookMetadata} from '../../../book/model/book.model'; import {BookMetadata} from '../../../book/model/book.model';
@@ -25,7 +26,6 @@ import {NgClass} from '@angular/common';
import {Paginator} from 'primeng/paginator'; import {Paginator} from 'primeng/paginator';
import {ActivatedRoute} from '@angular/router'; import {ActivatedRoute} from '@angular/router';
import {BookdropFileMetadataPickerComponent} from '../bookdrop-file-metadata-picker/bookdrop-file-metadata-picker.component'; import {BookdropFileMetadataPickerComponent} from '../bookdrop-file-metadata-picker/bookdrop-file-metadata-picker.component';
import {BookdropFinalizeResultDialogComponent} from '../bookdrop-finalize-result-dialog/bookdrop-finalize-result-dialog.component';
import {BookdropBulkEditDialogComponent, BulkEditResult} from '../bookdrop-bulk-edit-dialog/bookdrop-bulk-edit-dialog.component'; import {BookdropBulkEditDialogComponent, BulkEditResult} from '../bookdrop-bulk-edit-dialog/bookdrop-bulk-edit-dialog.component';
import {BookdropPatternExtractDialogComponent} from '../bookdrop-pattern-extract-dialog/bookdrop-pattern-extract-dialog.component'; import {BookdropPatternExtractDialogComponent} from '../bookdrop-pattern-extract-dialog/bookdrop-pattern-extract-dialog.component';
import {DialogLauncherService} from '../../../../shared/services/dialog-launcher.service'; import {DialogLauncherService} from '../../../../shared/services/dialog-launcher.service';
@@ -58,6 +58,8 @@ export interface BookdropFileUI {
Checkbox, Checkbox,
NgClass, NgClass,
Paginator, Paginator,
InputGroup,
InputGroupAddonModule,
], ],
}) })
export class BookdropFileReviewComponent implements OnInit { export class BookdropFileReviewComponent implements OnInit {
@@ -107,7 +109,6 @@ export class BookdropFileReviewComponent implements OnInit {
.subscribe(); .subscribe();
this.libraryService.libraryState$ this.libraryService.libraryState$
.pipe(filter(state => !!state?.loaded), take(1))
.subscribe(state => { .subscribe(state => {
this.libraries = state.libraries ?? []; this.libraries = state.libraries ?? [];
}); });
@@ -220,44 +221,41 @@ export class BookdropFileReviewComponent implements OnInit {
this.copiedFlags[fileId] = copied; this.copiedFlags[fileId] = copied;
} }
applyDefaultsToAll(): void { applyLibraryDefaults(): void {
if (!this.defaultLibraryId || !this.libraries) return; if (!this.defaultLibraryId || !this.libraries) return;
const selectedLib = this.libraries.find(l => String(l.id) === this.defaultLibraryId); const selectedLib = this.libraries.find(l => String(l.id) === this.defaultLibraryId);
const selectedPaths = selectedLib?.paths ?? []; const selectedPaths = selectedLib?.paths ?? [];
Object.values(this.fileUiCache).forEach(file => { this.getSelectedFiles().map(fileUi => {
file.selectedLibraryId = this.defaultLibraryId; const cachedfUi = this.fileUiCache[fileUi.file.id];
file.availablePaths = selectedPaths.map(path => ({id: String(path.id), name: path.path})); cachedfUi.selectedLibraryId = this.defaultLibraryId;
file.selectedPathId = this.defaultPathId ?? null; cachedfUi.availablePaths = selectedPaths.map(path => ({id: String(path.id), name: path.path}));
cachedfUi.selectedPathId = this.defaultPathId ?? null;
}); });
} }
copyAll(): void { copyMetadata(): void {
Object.values(this.fileUiCache).forEach(fileUi => { const files = this.getSelectedFiles().map(fileUi => {
const fetched = fileUi.file.fetchedMetadata; const cachedfUi = this.fileUiCache[fileUi.file.id];
const form = fileUi.metadataForm; const fetched = cachedfUi.file.fetchedMetadata;
const form = cachedfUi.metadataForm;
if (!fetched) return; if (!fetched) return;
for (const key of Object.keys(fetched)) { for (const key of Object.keys(fetched)) {
if (!this.includeCoversOnCopy && key === 'thumbnailUrl') continue; if (!this.includeCoversOnCopy && key === 'thumbnailUrl') continue;
const value = fetched[key as keyof typeof fetched]; const value = fetched[key as keyof typeof fetched];
if (value != null) { if (value != null) {
form.get(key)?.setValue(value); form.get(key)?.setValue(value);
fileUi.copiedFields[key] = true; cachedfUi.copiedFields[key] = true;
} }
} }
this.onMetadataCopied(fileUi.file.id, true); this.onMetadataCopied(cachedfUi.file.id, true);
}); });
} }
resetMetadata(): void { resetMetadata(): void {
const selectedFiles = Object.values(this.fileUiCache).filter(file => { const selectedFiles = this.getSelectedFiles();
if (this.selectAllAcrossPages) {
return !this.excludedFiles.has(file.file.id);
} else {
return file.selected;
}
});
const files = selectedFiles.map(fileUi => { const files = selectedFiles.map(fileUi => {
const original = fileUi.file.originalMetadata; const original = fileUi.file.originalMetadata;
@@ -404,12 +402,7 @@ export class BookdropFileReviewComponent implements OnInit {
detail: 'Selected Bookdrop files were deleted successfully.', detail: 'Selected Bookdrop files were deleted successfully.',
}); });
const toDelete = Object.values(this.fileUiCache).filter(file => { this.getSelectedFiles().forEach(file => delete this.fileUiCache[file.file.id]);
return this.selectAllAcrossPages
? !this.excludedFiles.has(file.file.id)
: file.selected;
});
toDelete.forEach(file => delete this.fileUiCache[file.file.id]);
this.selectAllAcrossPages = false; this.selectAllAcrossPages = false;
this.excludedFiles.clear(); this.excludedFiles.clear();
@@ -453,13 +446,7 @@ export class BookdropFileReviewComponent implements OnInit {
private finalizeImport(): void { private finalizeImport(): void {
this.saving = true; this.saving = true;
const selectedFiles = Object.values(this.fileUiCache).filter(file => { const selectedFiles = this.getSelectedFiles();
if (this.selectAllAcrossPages) {
return !this.excludedFiles.has(file.file.id);
} else {
return file.selected;
}
});
const files = selectedFiles.map(fileUi => { const files = selectedFiles.map(fileUi => {
const rawMetadata = fileUi.metadataForm.value; const rawMetadata = fileUi.metadataForm.value;

View File

@@ -2,9 +2,13 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 1rem;
padding: 0.5rem; padding: 0 1rem;
max-height: 70vh; max-height: 70vh;
overflow-y: auto;
@media (max-width: 768px) {
padding: 0;
max-height: 95vh;
}
} }
.info-banner { .info-banner {