mirror of
https://github.com/booklore-app/booklore.git
synced 2025-12-23 22:28:11 -05:00
fix: Bookdrop bulk edit mobile fixes (#1925)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 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 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 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 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 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 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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user