mirror of
https://github.com/booklore-app/booklore.git
synced 2025-12-23 22:28:11 -05:00
fix: Bookdrop UI mobile support (#1911)
* fix: Bookdrop UI mobile support * fix: Wrap long select option items in mobile mode
This commit is contained in:
@@ -2,9 +2,9 @@
|
||||
<form [formGroup]="metadataForm" class="metapicker flex flex-col w-full">
|
||||
<div class="flex-grow overflow-auto">
|
||||
<div class="metaheader relative flex items-center">
|
||||
<label class="w-[12%]"></label>
|
||||
<label class="md:w-[8rem]"></label>
|
||||
<div class="flex w-full items-center">
|
||||
<p class="!w-1/2 pr-3" style="text-align:right">Current Metadata</p>
|
||||
<p class="w-1/2 pr-3" style="text-align:right">File Metadata</p>
|
||||
<div class="midbuttons">
|
||||
<p-button
|
||||
size="small"
|
||||
@@ -12,7 +12,7 @@
|
||||
icon="pi pi-angle-left"
|
||||
class="mx-2"
|
||||
[outlined]="true"
|
||||
pTooltip="Move all missing fields"
|
||||
pTooltip="Copy missing fields from fetched metadata"
|
||||
tooltipPosition="bottom"
|
||||
(onClick)="copyMissing()"
|
||||
></p-button>
|
||||
@@ -22,32 +22,31 @@
|
||||
icon="pi pi-angle-double-left"
|
||||
class="mx-2"
|
||||
[outlined]="true"
|
||||
pTooltip="Move all fields"
|
||||
pTooltip="Overwrite all fields with fetched metadata"
|
||||
tooltipPosition="bottom"
|
||||
(onClick)="copyAll()"
|
||||
></p-button>
|
||||
</div>
|
||||
<p class="!w-1/2 pl-3">Fetched Metadata</p>
|
||||
<p class="w-1/2 pl-3">Fetched</p>
|
||||
</div>
|
||||
<div class="ml-auto absolute" style="right:1rem">
|
||||
<p-button
|
||||
size="small"
|
||||
severity="warn"
|
||||
icon="pi pi-refresh"
|
||||
label="Reset"
|
||||
[outlined]="true"
|
||||
pTooltip="Reset all fields to original values"
|
||||
tooltipPosition="bottom"
|
||||
tooltipPosition="left"
|
||||
(onClick)="confirmReset()"
|
||||
></p-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="metacontent">
|
||||
<div class="flex items-center py-1">
|
||||
<label class="w-[12%] text-sm">Cover</label>
|
||||
<div class="meta-row flex items-center">
|
||||
<label class="md:w-[8rem] 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"/>
|
||||
<input type="hidden" id="thumbnailUrl" formControlName="thumbnailUrl" class="md:!w-1/2"/>
|
||||
<p-button
|
||||
size="small"
|
||||
[icon]="isValueSaved('thumbnailUrl') ? 'pi pi-check' : (hoveredFields['thumbnailUrl'] && isValueCopied('thumbnailUrl') ? 'pi pi-times' : 'pi pi-arrow-left')"
|
||||
@@ -61,21 +60,21 @@
|
||||
(click)="hoveredFields['thumbnailUrl'] && isValueCopied('thumbnailUrl') ? resetField('thumbnailUrl') : copyFetchedToCurrent('thumbnailUrl')"
|
||||
(mouseenter)="onMouseEnter('thumbnailUrl')"
|
||||
(mouseleave)="onMouseLeave('thumbnailUrl')"/>
|
||||
<input type="hidden" [value]="fetchedMetadata.thumbnailUrl" class="!w-1/2" readonly/>
|
||||
<input type="hidden" [value]="fetchedMetadata.thumbnailUrl" class="md:!w-1/2" readonly/>
|
||||
<p-image class="thumbnail" [src]="fetchedMetadata.thumbnailUrl!" alt="Image" appendTo="body" lazyLoad [preview]="true"></p-image>
|
||||
</div>
|
||||
</div>
|
||||
@for (field of metadataFieldsTop; track field) {
|
||||
<div class="flex items-center py-1">
|
||||
<label for="{{field.controlName}}" class="w-[12%] text-sm">{{ field.label }}</label>
|
||||
<div class="flex w-full">
|
||||
<div class="meta-row flex items-center field-{{field.controlName}}">
|
||||
<label for="{{field.controlName}}" class="md:w-[8rem] text-sm">{{ field.label }}</label>
|
||||
<div class="meta-fields flex w-full">
|
||||
<input
|
||||
pSize="small"
|
||||
fluid
|
||||
pInputText
|
||||
id="{{field.controlName}}"
|
||||
formControlName="{{field.controlName}}"
|
||||
class="!w-1/2"
|
||||
class="dest md:!w-1/2"
|
||||
[ngClass]="{
|
||||
'outlined-input-green': isValueCopied(field.controlName) && !hoveredFields[field.controlName],
|
||||
}"
|
||||
@@ -87,20 +86,29 @@
|
||||
[ngClass]="
|
||||
{
|
||||
'green-outlined-button': isValueCopied(field.controlName) && !hoveredFields[field.controlName],
|
||||
'red-outlined-button': isValueCopied(field.controlName) && hoveredFields[field.controlName]
|
||||
'red-outlined-button': isValueCopied(field.controlName) && hoveredFields[field.controlName],
|
||||
'notneeded' : !fetchedMetadata[field.fetchedKey]
|
||||
}"
|
||||
class="arrow-button"
|
||||
(click)="hoveredFields[field.controlName] && isValueCopied(field.controlName) ? resetField(field.controlName) : copyFetchedToCurrent(field.controlName)"
|
||||
(mouseenter)="onMouseEnter(field.controlName)"
|
||||
(mouseleave)="onMouseLeave(field.controlName)"/>
|
||||
<input pSize="small" pInputText [value]="fetchedMetadata[field.fetchedKey] ?? null" class="!w-1/2" readonly/>
|
||||
<input
|
||||
pSize="small"
|
||||
pInputText
|
||||
[value]="fetchedMetadata[field.fetchedKey] ?? null"
|
||||
class="src md:!w-1/2"
|
||||
disabled
|
||||
[ngClass]="{
|
||||
'notneeded': !fetchedMetadata[field.fetchedKey],
|
||||
}"/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@for (field of metadataChips; track field) {
|
||||
<div class="flex items-center py-1">
|
||||
<label for="{{field.controlName}}" class="w-[12%] text-sm">{{ field.label }}</label>
|
||||
<div class="flex w-full items-center">
|
||||
<div class="meta-row flex items-center field-{{field.controlName}}">
|
||||
<label for="{{field.controlName}}" class="md:w-[8rem] text-sm">{{ field.label }}</label>
|
||||
<div class="meta-fields flex w-full items-center">
|
||||
<p-autoComplete
|
||||
size="small"
|
||||
formControlName="{{field.controlName}}"
|
||||
@@ -108,7 +116,7 @@
|
||||
[typeahead]="false"
|
||||
[dropdown]="false"
|
||||
[forceSelection]="false"
|
||||
class="w-full"
|
||||
class="dest w-full"
|
||||
[ngClass]="{'outlined-input-green': isValueCopied(field.controlName) && !hoveredFields[field.controlName]}"
|
||||
(onBlur)="onAutoCompleteBlur(field.controlName, $event)"/>
|
||||
<p-button
|
||||
@@ -118,7 +126,8 @@
|
||||
[ngClass]="
|
||||
{
|
||||
'green-outlined-button': isValueCopied(field.controlName) && !hoveredFields[field.controlName],
|
||||
'red-outlined-button': isValueCopied(field.controlName) && hoveredFields[field.controlName]
|
||||
'red-outlined-button': isValueCopied(field.controlName) && hoveredFields[field.controlName],
|
||||
'notneeded' : !fetchedMetadata[field.fetchedKey]
|
||||
}"
|
||||
class="arrow-button"
|
||||
(click)="hoveredFields[field.controlName] && isValueCopied(field.controlName) ? resetField(field.controlName) : copyFetchedToCurrent(field.controlName)"
|
||||
@@ -133,21 +142,24 @@
|
||||
[typeahead]="false"
|
||||
[dropdown]="false"
|
||||
[forceSelection]="false"
|
||||
class="w-full"/>
|
||||
class="src w-full"
|
||||
[ngClass]="{
|
||||
'notneeded': !fetchedMetadata[field.fetchedKey],
|
||||
}"/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@for (field of metadataDescription; track field) {
|
||||
<div class="flex items-center py-1">
|
||||
<label for="{{field.controlName}}" class="w-[12%] text-sm">{{ field.label }}</label>
|
||||
<div class="flex w-full items-center">
|
||||
<div class="meta-row flex items-center field-{{field.controlName}}">
|
||||
<label for="{{field.controlName}}" class="md:w-[8rem] text-sm">{{ field.label }}</label>
|
||||
<div class="meta-fields flex w-full items-center">
|
||||
<textarea
|
||||
rows="2"
|
||||
rows="4"
|
||||
pSize="small"
|
||||
pTextarea
|
||||
id="{{field.controlName}}"
|
||||
formControlName="{{field.controlName}}"
|
||||
class="!w-1/2"
|
||||
class="dest md:!w-1/2"
|
||||
[ngClass]="{'outlined-input-green': isValueCopied(field.controlName) && !hoveredFields[field.controlName]}"
|
||||
></textarea>
|
||||
<p-button
|
||||
@@ -157,27 +169,36 @@
|
||||
[ngClass]="
|
||||
{
|
||||
'green-outlined-button': isValueCopied(field.controlName) && !hoveredFields[field.controlName],
|
||||
'red-outlined-button': isValueCopied(field.controlName) && hoveredFields[field.controlName]
|
||||
'red-outlined-button': isValueCopied(field.controlName) && hoveredFields[field.controlName],
|
||||
'notneeded' : !fetchedMetadata[field.fetchedKey]
|
||||
}"
|
||||
class="arrow-button"
|
||||
(click)="hoveredFields[field.controlName] && isValueCopied(field.controlName) ? resetField(field.controlName) : copyFetchedToCurrent(field.controlName)"
|
||||
(mouseenter)="onMouseEnter(field.controlName)"
|
||||
(mouseleave)="onMouseLeave(field.controlName)"/>
|
||||
<textarea
|
||||
rows="2"
|
||||
rows="4"
|
||||
pSize="small"
|
||||
pInputText
|
||||
[value]="fetchedMetadata[field.fetchedKey] ?? null"
|
||||
class="!w-1/2"
|
||||
readonly></textarea>
|
||||
class="src md:!w-1/2"
|
||||
disabled
|
||||
[ngClass]="{
|
||||
'notneeded': !fetchedMetadata[field.fetchedKey],
|
||||
}"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@for (field of metadataFieldsBottom; track field) {
|
||||
<div class="flex items-center py-1">
|
||||
<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"
|
||||
<div class="meta-row flex items-cente field-{{field.controlName}}">
|
||||
<label for="{{field.controlName}}" class="md:w-[8rem] text-sm">{{ field.label }}</label>
|
||||
<div class="meta-fields flex w-full">
|
||||
<input
|
||||
pInputText
|
||||
pSize="small"
|
||||
id="{{field.controlName}}"
|
||||
formControlName="{{field.controlName}}"
|
||||
class="dest md:!w-1/2"
|
||||
[ngClass]="{
|
||||
'outlined-input-green': isValueCopied(field.controlName) && !hoveredFields[field.controlName],
|
||||
}"
|
||||
@@ -189,13 +210,22 @@
|
||||
[ngClass]="
|
||||
{
|
||||
'green-outlined-button': isValueCopied(field.controlName) && !hoveredFields[field.controlName],
|
||||
'red-outlined-button': isValueCopied(field.controlName) && hoveredFields[field.controlName]
|
||||
'red-outlined-button': isValueCopied(field.controlName) && hoveredFields[field.controlName],
|
||||
'notneeded' : !fetchedMetadata[field.fetchedKey]
|
||||
}"
|
||||
class="arrow-button"
|
||||
(click)="hoveredFields[field.controlName] && isValueCopied(field.controlName) ? resetField(field.controlName) : copyFetchedToCurrent(field.controlName)"
|
||||
(mouseenter)="onMouseEnter(field.controlName)"
|
||||
(mouseleave)="onMouseLeave(field.controlName)"/>
|
||||
<input pInputText pSize="small" [value]="fetchedMetadata[field.fetchedKey] ?? null" class="!w-1/2" readonly/>
|
||||
<input
|
||||
pInputText
|
||||
pSize="small"
|
||||
[value]="fetchedMetadata[field.fetchedKey] ?? null"
|
||||
class="src md:!w-1/2"
|
||||
disabled
|
||||
[ngClass]="{
|
||||
'notneeded': !fetchedMetadata[field.fetchedKey],
|
||||
}"/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -203,40 +233,38 @@
|
||||
</div>
|
||||
</form>
|
||||
} @else {
|
||||
<form [formGroup]="metadataForm" class="metapicker flex w-full">
|
||||
<form [formGroup]="metadataForm" class="metaeditor 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
|
||||
Unable to fetch 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"
|
||||
tooltipPosition="left"
|
||||
(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">
|
||||
<div class="flex flex-col p-4 md:flex-row">
|
||||
<div class="flex items-start justify-center md:w-[20%] md:h-full">
|
||||
<div class="w-full md:h-full">
|
||||
<img
|
||||
[src]="metadataForm.get('thumbnailUrl')?.value"
|
||||
alt="Book Thumbnail"
|
||||
class="w-full h-full object-contain"
|
||||
class="thumbnail w-full md:h-full object-contain"
|
||||
/>
|
||||
<input type="hidden" id="thumbnailUrl" formControlName="thumbnailUrl"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="metacontent w-[75%]">
|
||||
<div class="metacontent md:w-[80%]">
|
||||
@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="meta-row flex items-center">
|
||||
<label for="{{field.controlName}}" class="w-[8rem] text-sm">{{ field.label }}</label>
|
||||
<div class="flex w-full">
|
||||
<input
|
||||
pSize="small"
|
||||
@@ -250,8 +278,8 @@
|
||||
}
|
||||
|
||||
@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="meta-row flex items-center">
|
||||
<label for="{{field.controlName}}" class="w-[8rem] 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>
|
||||
@@ -259,17 +287,17 @@
|
||||
}
|
||||
|
||||
@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="meta-row flex items-center">
|
||||
<label for="{{field.controlName}}" class="w-[8rem] text-sm">{{ field.label }}</label>
|
||||
<div class="flex w-full">
|
||||
<textarea fluid rows="2" pTextarea id="{{field.controlName}}" formControlName="{{field.controlName}}"></textarea>
|
||||
<textarea fluid rows="4" pSize="small" 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="meta-row flex items-center">
|
||||
<label for="{{field.controlName}}" class="w-[8rem] text-sm">{{ field.label }}</label>
|
||||
<div class="flex w-full">
|
||||
<input
|
||||
fluid
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
.thumbnail {
|
||||
width: 10.46875rem;
|
||||
height: 14.65625rem;
|
||||
width: 10rem;
|
||||
height: 14rem;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
margin-left: 2.5rem;
|
||||
margin-right: 2.5rem;
|
||||
margin: 0 auto 1rem;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
max-width: 50%;
|
||||
}
|
||||
}
|
||||
.metaeditor .thumbnail {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.arrow-button {
|
||||
@@ -36,14 +43,22 @@ textarea.outlined-input-green,
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.metapicker {
|
||||
::ng-deep .p-autocomplete-input-chip {
|
||||
width: 2rem;
|
||||
}
|
||||
|
||||
.metapicker, .metaeditor {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.metapicker label,
|
||||
.metaeditor label {
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
|
||||
.metaheader {
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
@@ -52,7 +67,14 @@ textarea.outlined-input-green,
|
||||
}
|
||||
|
||||
.metacontent {
|
||||
padding: 0 1rem 1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
.metaeditor .metacontent {
|
||||
padding: 0 0 0 1rem;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.metadata-status {
|
||||
@@ -68,3 +90,90 @@ textarea.outlined-input-green,
|
||||
color: rgb(239, 68, 68);
|
||||
}
|
||||
}
|
||||
|
||||
.meta-row {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.meta-row {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.metapicker .meta-row {
|
||||
flex-wrap: wrap;
|
||||
|
||||
label {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.metapicker .meta-fields {
|
||||
justify-content: center;
|
||||
|
||||
.dest {
|
||||
width: 50%;
|
||||
order: 1;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
p-button {
|
||||
padding: 0;
|
||||
order: 3;
|
||||
}
|
||||
|
||||
::ng-deep button {
|
||||
border-left-width: 0;
|
||||
border-radius: 0 6px 6px 0;
|
||||
}
|
||||
|
||||
.src {
|
||||
width: 50%;
|
||||
order: 2;
|
||||
border-radius: 6px 0 0 6px;
|
||||
margin-left: 0.25rem;
|
||||
|
||||
::ng-deep ul {
|
||||
border-radius: 6px 0 0 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.metapicker {
|
||||
.field-title,
|
||||
.field-subtitle,
|
||||
.field-publisher,
|
||||
.field-authors,
|
||||
.field-categories,
|
||||
.field-moods,
|
||||
.field-tags,
|
||||
.field-description,
|
||||
.field-seriesName {
|
||||
.meta-fields {
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.dest {
|
||||
width: 100% !important;
|
||||
margin: 0 0 2px 0;
|
||||
}
|
||||
|
||||
.src {
|
||||
flex: 1 !important;
|
||||
width: auto !important;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.field-authors,
|
||||
.field-categories,
|
||||
.field-moods,
|
||||
.field-tags {
|
||||
::ng-deep button {
|
||||
line-height: 2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@ export class BookdropFileMetadataPickerComponent {
|
||||
{label: 'Title', controlName: 'title', fetchedKey: 'title'},
|
||||
{label: 'Subtitle', controlName: 'subtitle', fetchedKey: 'subtitle'},
|
||||
{label: 'Publisher', controlName: 'publisher', fetchedKey: 'publisher'},
|
||||
{label: 'Published', controlName: 'publishedDate', fetchedKey: 'publishedDate'}
|
||||
{label: 'Publish Date', controlName: 'publishedDate', fetchedKey: 'publishedDate'}
|
||||
];
|
||||
|
||||
metadataChips = [
|
||||
@@ -62,21 +62,21 @@ export class BookdropFileMetadataPickerComponent {
|
||||
];
|
||||
|
||||
metadataFieldsBottom = [
|
||||
{label: 'Series', controlName: 'seriesName', lockedKey: 'seriesNameLocked', fetchedKey: 'seriesName'},
|
||||
{label: 'Book #', controlName: 'seriesNumber', lockedKey: 'seriesNumberLocked', fetchedKey: 'seriesNumber'},
|
||||
{label: 'Total Books', controlName: 'seriesTotal', lockedKey: 'seriesTotalLocked', fetchedKey: 'seriesTotal'},
|
||||
{label: 'Series Name', controlName: 'seriesName', lockedKey: 'seriesNameLocked', fetchedKey: 'seriesName'},
|
||||
{label: 'Series #', controlName: 'seriesNumber', lockedKey: 'seriesNumberLocked', fetchedKey: 'seriesNumber'},
|
||||
{label: 'Series Total', controlName: 'seriesTotal', lockedKey: 'seriesTotalLocked', fetchedKey: 'seriesTotal'},
|
||||
{label: 'Language', controlName: 'language', lockedKey: 'languageLocked', fetchedKey: 'language'},
|
||||
{label: 'ISBN-10', controlName: 'isbn10', lockedKey: 'isbn10Locked', fetchedKey: 'isbn10'},
|
||||
{label: 'ISBN-13', controlName: 'isbn13', lockedKey: 'isbn13Locked', fetchedKey: 'isbn13'},
|
||||
{label: 'ASIN', controlName: 'asin', lockedKey: 'asinLocked', fetchedKey: 'asin'},
|
||||
{label: 'Amz Reviews', controlName: 'amazonReviewCount', lockedKey: 'amazonReviewCountLocked', fetchedKey: 'amazonReviewCount'},
|
||||
{label: 'Amz Rating', controlName: 'amazonRating', lockedKey: 'amazonRatingLocked', fetchedKey: 'amazonRating'},
|
||||
{label: 'GR ID', controlName: 'goodreadsId', lockedKey: 'goodreadsIdLocked', fetchedKey: 'goodreadsId'},
|
||||
{label: 'GR Reviews', controlName: 'goodreadsReviewCount', lockedKey: 'goodreadsReviewCountLocked', fetchedKey: 'goodreadsReviewCount'},
|
||||
{label: 'GR Rating', controlName: 'goodreadsRating', lockedKey: 'goodreadsRatingLocked', fetchedKey: 'goodreadsRating'},
|
||||
{label: 'HC ID', controlName: 'hardcoverId', lockedKey: 'hardcoverIdLocked', fetchedKey: 'hardcoverId'},
|
||||
{label: 'HC Reviews', controlName: 'hardcoverReviewCount', lockedKey: 'hardcoverReviewCountLocked', fetchedKey: 'hardcoverReviewCount'},
|
||||
{label: 'HC Rating', controlName: 'hardcoverRating', lockedKey: 'hardcoverRatingLocked', fetchedKey: 'hardcoverRating'},
|
||||
{label: 'Amazon ASIN', controlName: 'asin', lockedKey: 'asinLocked', fetchedKey: 'asin'},
|
||||
{label: 'Amazon #', controlName: 'amazonReviewCount', lockedKey: 'amazonReviewCountLocked', fetchedKey: 'amazonReviewCount'},
|
||||
{label: 'Amazon ★', controlName: 'amazonRating', lockedKey: 'amazonRatingLocked', fetchedKey: 'amazonRating'},
|
||||
{label: 'Goodreads ID', controlName: 'goodreadsId', lockedKey: 'goodreadsIdLocked', fetchedKey: 'goodreadsId'},
|
||||
{label: 'Goodreads #', controlName: 'goodreadsReviewCount', lockedKey: 'goodreadsReviewCountLocked', fetchedKey: 'goodreadsReviewCount'},
|
||||
{label: 'Goodreads ★', controlName: 'goodreadsRating', lockedKey: 'goodreadsRatingLocked', fetchedKey: 'goodreadsRating'},
|
||||
{label: 'Hardcover ID', controlName: 'hardcoverId', lockedKey: 'hardcoverIdLocked', fetchedKey: 'hardcoverId'},
|
||||
{label: 'Hardcover #', controlName: 'hardcoverReviewCount', lockedKey: 'hardcoverReviewCountLocked', fetchedKey: 'hardcoverReviewCount'},
|
||||
{label: 'Hardcover ★', controlName: 'hardcoverRating', lockedKey: 'hardcoverRatingLocked', fetchedKey: 'hardcoverRating'},
|
||||
{label: 'Google ID', controlName: 'googleId', lockedKey: 'googleIdLocked', fetchedKey: 'googleId'},
|
||||
{label: 'Comicvine ID', controlName: 'comicvineId', lockedKey: 'comicvineIdLocked', fetchedKey: 'comicvineId'},
|
||||
{label: 'Pages', controlName: 'pageCount', lockedKey: 'pageCountLocked', fetchedKey: 'pageCount'}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
<div class="container">
|
||||
<div class="main-card">
|
||||
<div class="header">
|
||||
<div class="header-content">
|
||||
@@ -30,7 +29,7 @@
|
||||
</p-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
@if (loading) {
|
||||
|
||||
<div class="loading-overlay">
|
||||
@@ -76,12 +75,17 @@
|
||||
</div>
|
||||
|
||||
<div class="default-controls">
|
||||
<i class="pi pi-copy"
|
||||
pTooltip="Select library and subpath for all files:"
|
||||
tooltipPosition="left"></i>
|
||||
|
||||
<p-select
|
||||
size="small"
|
||||
[options]="libraryOptions"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
placeholder="Default Library"
|
||||
placeholder="Library"
|
||||
appendTo="body"
|
||||
[(ngModel)]="defaultLibraryId">
|
||||
</p-select>
|
||||
|
||||
@@ -90,14 +94,15 @@
|
||||
[options]="selectedLibraryPaths"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
placeholder="Default Subpath"
|
||||
placeholder="Subpath"
|
||||
appendTo="body"
|
||||
[(ngModel)]="defaultPathId">
|
||||
</p-select>
|
||||
|
||||
<p-button
|
||||
size="small"
|
||||
label="Apply"
|
||||
icon="pi pi-check"
|
||||
severity="info"
|
||||
[disabled]="!canApplyDefaults"
|
||||
(click)="applyDefaultsToAll()"
|
||||
pTooltip="Apply selected library and subpath to all files"
|
||||
@@ -125,13 +130,6 @@
|
||||
(ngModelChange)="toggleFileSelection(file.file.id, $event)">
|
||||
</p-checkbox>
|
||||
|
||||
<i
|
||||
class="pi pi-circle-fill status-indicator"
|
||||
[ngStyle]="{'color': file.file.fetchedMetadata ? 'green' : 'darkorange'}"
|
||||
pTooltip="{{file.file.fetchedMetadata ? 'Fetched metadata is available.' : 'No fetched metadata available.'}}"
|
||||
tooltipPosition="top">
|
||||
</i>
|
||||
|
||||
@if (file.metadataForm.get('thumbnailUrl')?.value) {
|
||||
<img
|
||||
[src]="file.metadataForm.get('thumbnailUrl')?.value"
|
||||
@@ -139,6 +137,8 @@
|
||||
title="Original cover"
|
||||
class="cover-image"
|
||||
(click)="file.showDetails = !file.showDetails"/>
|
||||
} @else {
|
||||
<div title="No cover found" class="cover-image text-sm">?</div>
|
||||
}
|
||||
|
||||
@if (file.file.fetchedMetadata?.thumbnailUrl) {
|
||||
@@ -148,12 +148,15 @@
|
||||
title="Fetched cover"
|
||||
class="cover-image"
|
||||
(click)="file.showDetails = !file.showDetails"/>
|
||||
} @else {
|
||||
<div title="No cover fetched" class="cover-image text-sm">?</div>
|
||||
}
|
||||
|
||||
<div class="file-name" (click)="file.showDetails = !file.showDetails">
|
||||
{{ file.file.fileName }}
|
||||
</div>
|
||||
|
||||
<div class="file-library">
|
||||
<i
|
||||
class="pi metadata-status"
|
||||
[ngClass]="{
|
||||
@@ -174,8 +177,9 @@
|
||||
[options]="libraryOptions"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
placeholder="Select Library"
|
||||
placeholder="Library"
|
||||
class="library-select"
|
||||
appendTo="body"
|
||||
[(ngModel)]="file.selectedLibraryId"
|
||||
(onChange)="onLibraryChange(file)">
|
||||
</p-select>
|
||||
@@ -185,7 +189,7 @@
|
||||
[options]="file.availablePaths"
|
||||
optionLabel="name"
|
||||
optionValue="id"
|
||||
placeholder="Select Subpath"
|
||||
placeholder="Subpath"
|
||||
class="path-select"
|
||||
appendTo="body"
|
||||
[(ngModel)]="file.selectedPathId">
|
||||
@@ -193,11 +197,18 @@
|
||||
|
||||
<p-button
|
||||
size="small"
|
||||
[variant]="file.file.fetchedMetadata?.title ? undefined : 'outlined'"
|
||||
[icon]="file.showDetails ? 'pi pi-chevron-up' : 'pi pi-chevron-down'"
|
||||
(click)="file.showDetails = !file.showDetails"
|
||||
tooltipPosition="top">
|
||||
[pTooltip]="file.showDetails
|
||||
? 'Hide metadata'
|
||||
: file.file.fetchedMetadata?.title
|
||||
? 'Review and copy fetched metadata'
|
||||
: 'Show file metadata (no fetched metadata)'"
|
||||
tooltipPosition="left">
|
||||
</p-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (file.showDetails) {
|
||||
<app-bookdrop-file-metadata-picker-component
|
||||
@@ -215,9 +226,13 @@
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (!loading) {
|
||||
<p-divider></p-divider>
|
||||
|
||||
<!-- Desktop Buttons -->
|
||||
<div class="footer">
|
||||
|
||||
<div class="footer-left">
|
||||
@@ -246,6 +261,7 @@
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (totalRecords > pageSize) {
|
||||
<div class="footer-center">
|
||||
<p-paginator
|
||||
[rows]="pageSize"
|
||||
@@ -258,6 +274,7 @@
|
||||
currentPageReportTemplate="Page {currentPage} of {totalPages}">
|
||||
</p-paginator>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="footer-right">
|
||||
<p-button
|
||||
@@ -292,6 +309,81 @@
|
||||
</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">
|
||||
</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>
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
.container {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.main-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -9,22 +5,24 @@
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
background: var(--card-background);
|
||||
min-width: 60rem;
|
||||
border: 1px solid var(--p-content-border-color);
|
||||
|
||||
@media (max-width: 768px) {
|
||||
height: calc(100dvh - 4.9rem);
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 1.5rem 1rem 1rem;
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-bottom: 1px solid var(--p-content-border-color);
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
padding: 1.5rem 1.5rem 1rem;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid var(--p-content-border-color);
|
||||
gap: 1rem;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,13 +44,19 @@
|
||||
color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
flex-shrink: 1;
|
||||
|
||||
p {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
margin-top: 1rem;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
margin-top: 0;
|
||||
@media (max-width: 768px) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +66,13 @@
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
@@ -82,7 +93,7 @@
|
||||
}
|
||||
|
||||
span {
|
||||
color: rgb(209, 213, 219);
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
|
||||
p-progressSpinner {
|
||||
@@ -92,7 +103,7 @@
|
||||
}
|
||||
|
||||
.controls-section {
|
||||
padding: 0 1.5rem 1.5rem;
|
||||
padding: 1rem;
|
||||
margin: 0;
|
||||
border-bottom: 1px solid var(--p-content-border-color);
|
||||
}
|
||||
@@ -135,40 +146,62 @@
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 0 0.25rem;
|
||||
flex-wrap: wrap;
|
||||
|
||||
label {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-color-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.default-controls {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
p-select {
|
||||
min-width: 8rem;
|
||||
max-width: 16rem;
|
||||
width: 8rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
width: 100%;
|
||||
|
||||
label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
p-select {
|
||||
flex-basis: 50%;
|
||||
max-width: 250px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep .p-select-overlay {
|
||||
@media (max-width: 768px) {
|
||||
max-width: 70vw;
|
||||
|
||||
.p-select-option {
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.default-controls i.pi {
|
||||
color: var(--p-button-outlined-info-color);
|
||||
}
|
||||
|
||||
.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;
|
||||
|
||||
> * + * {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
padding: 0 0 1px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
@@ -191,9 +224,9 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 0.75rem 0.75rem 0 0;
|
||||
padding: 0.5rem 1rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
@@ -212,6 +245,17 @@
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
div.cover-image {
|
||||
display: block;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
text-align: center;
|
||||
line-height: 2rem;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
@@ -220,7 +264,11 @@
|
||||
font-size: 0.875rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
white-space: wrap;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.metadata-status {
|
||||
@@ -237,34 +285,58 @@
|
||||
}
|
||||
}
|
||||
|
||||
.file-library {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.library-select,
|
||||
.path-select {
|
||||
min-width: 8rem;
|
||||
max-width: 16rem;
|
||||
width: 8rem;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
flex-basis: 50%;
|
||||
max-width: 250px;
|
||||
}
|
||||
}
|
||||
|
||||
.details-section {
|
||||
border-left: 1px solid var(--border-color);
|
||||
border-right: 1px solid var(--border-color);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
border-top: none;
|
||||
border-radius: 0 0 0.625rem 0.625rem;
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 0.5rem 1rem;
|
||||
.footer, .footer-mobile {
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.footer {
|
||||
@media (max-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.footer-mobile {
|
||||
flex-wrap: wrap;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.footer-left,
|
||||
.footer-right {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
min-width: 10rem;
|
||||
}
|
||||
|
||||
.footer-left {
|
||||
@@ -280,6 +352,10 @@
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.spacer {
|
||||
|
||||
@@ -21,7 +21,7 @@ import {AppSettingsService} from '../../../../shared/service/app-settings.servic
|
||||
import {BookMetadata} from '../../../book/model/book.model';
|
||||
import {UrlHelperService} from '../../../../shared/service/url-helper.service';
|
||||
import {Checkbox} from 'primeng/checkbox';
|
||||
import {NgClass, NgStyle} from '@angular/common';
|
||||
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';
|
||||
@@ -53,7 +53,6 @@ export interface BookdropFileUI {
|
||||
Tooltip,
|
||||
Divider,
|
||||
Checkbox,
|
||||
NgStyle,
|
||||
NgClass,
|
||||
Paginator,
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user