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

View File

@@ -20,6 +20,7 @@
<div class="header-actions">
<p-button
label="Rescan"
size="small"
icon="pi pi-refresh"
severity="primary"
outlined
@@ -56,18 +57,32 @@
@if (bookdropFileUis.length !== 0) {
<div class="controls-row">
<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
size="small"
outlined
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"
class="bulkedit"
outlined
severity="help"
label="Bulk&nbsp;Edit"
@@ -77,28 +92,24 @@
pTooltip="Edit metadata fields in bulk for selected files"
tooltipPosition="top">
</p-button>
<p-button
size="small"
class="extractpattern"
outlined
severity="warn"
label="Extract&nbsp;Pattern"
icon="pi pi-sliders-h"
[disabled]="!hasSelectedFiles"
(click)="openPatternExtractDialog()"
pTooltip="Extract metadata from filenames using a pattern"
pTooltip="Extract metadata from selected filenames using a pattern"
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">
<i class="pi pi-copy"
pTooltip="Select library and subpath for all files:"
<i class="pi pi-book"
pTooltip="Specify library and subpath for selected files"
tooltipPosition="left"></i>
<p-select
@@ -126,8 +137,8 @@
icon="pi pi-check"
severity="info"
[disabled]="!canApplyDefaults"
(click)="applyDefaultsToAll()"
pTooltip="Apply selected library and subpath to all files"
(click)="applyLibraryDefaults()"
pTooltip="Apply library and subpath to selected files"
tooltipPosition="top">
</p-button>
</div>
@@ -253,28 +264,33 @@
@if (!loading) {
<p-divider></p-divider>
<!-- Desktop Buttons -->
<div class="footer">
<div class="footer-left">
@if (bookdropFileUis.length > 0) {
<p-button
label="Select&nbsp;All"
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-inputgroup class="selectall">
<p-inputgroup-addon>
<label class="text-sm" pTooltip="Number of files selected" tooltipPosition="top">{{ selectedCount }}</label>
</p-inputgroup-addon>
<p-button
label="Select&nbsp;All"
icon="pi pi-check-square"
severity="info"
(click)="selectAll(true)"
outlined
pTooltip="Select all files across all pages"
tooltipPosition="top">
</p-button>
</p-inputgroup>
<p-button
label="Clear"
icon="pi pi-times-circle"
severity="warn"
[disabled]="!hasSelectedFiles"
(click)="selectAll(false)"
outlined
pTooltip="Deselect all files across all pages you have navigated"
pTooltip="Deselect all files across all pages"
tooltipPosition="top">
</p-button>
} @else {
@@ -299,7 +315,7 @@
<div class="footer-right">
<p-button
[label]="'Reset ' + selectedCount"
label="Reset"
icon="pi pi-refresh"
severity="warn"
[disabled]="!hasSelectedFiles"
@@ -309,7 +325,7 @@
tooltipPosition="top">
</p-button>
<p-button
[label]="'Delete ' + selectedCount"
label="Delete"
icon="pi pi-times"
severity="danger"
[disabled]="!hasSelectedFiles"
@@ -319,90 +335,15 @@
tooltipPosition="top">
</p-button>
<p-button
[label]="saving ? ('Finalizing ' + selectedCount + '...') : ('Finalize ' + selectedCount)"
label="Finalize"
[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>
<!-- 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"
pTooltip="Move selected files into the chosen library and subpath"
tooltipPosition="top">
</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>

View File

@@ -193,10 +193,53 @@
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 {
display: flex;
gap: 1rem;
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 {
@@ -320,15 +363,18 @@ div.cover-image {
.footer {
@media (max-width: 768px) {
display: none;
}
}
flex-wrap: wrap;
.footer-mobile {
flex-wrap: wrap;
::ng-deep .p-button-label {
display: none;
}
@media (min-width: 768px) {
display: none;
::ng-deep .p-button {
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 {
min-width: 12.5rem;
justify-content: flex-end;
}
@@ -358,6 +403,10 @@ div.cover-image {
}
}
.p-paginator {
flex-wrap: nowrap;
}
.spacer {
min-width: 10rem;
}

View File

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

View File

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