mirror of
https://github.com/booklore-app/booklore.git
synced 2026-02-18 00:17:53 +01:00
feat(metadata): add auto-save metadata feature in editor settings (#2274)
* feat(metadata): add auto-save metadata feature in editor settings Signed-off-by: Balázs Szücs <bszucs1209@gmail.com> * feat(reader-preferences): add autoSaveMetadata property to user preferences Signed-off-by: Balázs Szücs <bszucs1209@gmail.com> * feat(user-service): add autoSaveMetadata property to user preferences in tests Signed-off-by: Balázs Szücs <bszucs1209@gmail.com> --------- Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
This commit is contained in:
@@ -65,6 +65,7 @@ public class BookLoreUserTransformer {
|
||||
case FILTER_SORTING_MODE -> userSettings.setFilterSortingMode(value);
|
||||
case METADATA_CENTER_VIEW_MODE -> userSettings.setMetadataCenterViewMode(value);
|
||||
case ENABLE_SERIES_VIEW -> userSettings.setEnableSeriesView(Boolean.parseBoolean(value));
|
||||
case AUTO_SAVE_METADATA -> userSettings.setAutoSaveMetadata(Boolean.parseBoolean(value));
|
||||
}
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
|
||||
@@ -74,6 +74,7 @@ public class BookLoreUser {
|
||||
public String metadataCenterViewMode;
|
||||
public boolean koReaderEnabled;
|
||||
public boolean enableSeriesView;
|
||||
public boolean autoSaveMetadata;
|
||||
public DashboardConfig dashboardConfig;
|
||||
|
||||
@Data
|
||||
|
||||
@@ -20,7 +20,8 @@ public enum UserSettingKey {
|
||||
METADATA_CENTER_VIEW_MODE("metadataCenterViewMode", false),
|
||||
ENABLE_SERIES_VIEW("enableSeriesView", false),
|
||||
HARDCOVER_API_KEY("hardcoverApiKey", false),
|
||||
HARDCOVER_SYNC_ENABLED("hardcoverSyncEnabled", false);
|
||||
HARDCOVER_SYNC_ENABLED("hardcoverSyncEnabled", false),
|
||||
AUTO_SAVE_METADATA("autoSaveMetadata", false);
|
||||
|
||||
|
||||
private final String dbKey;
|
||||
|
||||
@@ -13,7 +13,7 @@ import {HttpResponse} from "@angular/common/http";
|
||||
import {BookService} from "../../../../book/service/book.service";
|
||||
import {ProgressSpinner} from "primeng/progressspinner";
|
||||
import {Tooltip} from "primeng/tooltip";
|
||||
import {filter, finalize, take} from "rxjs/operators";
|
||||
import {filter, finalize, take, tap} from "rxjs/operators";
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import {MetadataRefreshType} from "../../../model/request/metadata-refresh-type.enum";
|
||||
import {AutoComplete, AutoCompleteSelectEvent} from "primeng/autocomplete";
|
||||
@@ -80,6 +80,7 @@ export class MetadataEditorComponent implements OnInit {
|
||||
|
||||
refreshingBookIds = new Set<number>();
|
||||
isAutoFetching = false;
|
||||
autoSaveEnabled = false;
|
||||
|
||||
originalMetadata!: BookMetadata;
|
||||
|
||||
@@ -236,6 +237,7 @@ export class MetadataEditorComponent implements OnInit {
|
||||
)
|
||||
.subscribe(userState => {
|
||||
this.metadataCenterViewMode = userState.user?.userSettings.metadataCenterViewMode ?? 'route';
|
||||
this.autoSaveEnabled = userState.user?.userSettings.autoSaveMetadata ?? false;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -419,32 +421,39 @@ export class MetadataEditorComponent implements OnInit {
|
||||
}
|
||||
|
||||
onSave(): void {
|
||||
this.saveMetadata().subscribe();
|
||||
}
|
||||
|
||||
saveMetadata(): Observable<void> {
|
||||
this.isSaving = true;
|
||||
this.bookService
|
||||
return this.bookService
|
||||
.updateBookMetadata(
|
||||
this.currentBookId,
|
||||
this.buildMetadataWrapper(undefined),
|
||||
false
|
||||
)
|
||||
.subscribe({
|
||||
next: (response) => {
|
||||
this.isSaving = false;
|
||||
this.messageService.add({
|
||||
severity: "info",
|
||||
summary: "Success",
|
||||
detail: "Book metadata updated",
|
||||
});
|
||||
this.prepareAutoComplete();
|
||||
},
|
||||
error: (err) => {
|
||||
this.isSaving = false;
|
||||
this.messageService.add({
|
||||
severity: "error",
|
||||
summary: "Error",
|
||||
detail: err?.error?.message || "Failed to update book metadata",
|
||||
});
|
||||
},
|
||||
});
|
||||
.pipe(
|
||||
tap({
|
||||
next: (response: any) => {
|
||||
this.isSaving = false;
|
||||
this.messageService.add({
|
||||
severity: "info",
|
||||
summary: "Success",
|
||||
detail: "Book metadata updated",
|
||||
});
|
||||
this.prepareAutoComplete();
|
||||
this.metadataForm.markAsPristine();
|
||||
},
|
||||
error: (err: any) => {
|
||||
this.isSaving = false;
|
||||
this.messageService.add({
|
||||
severity: "error",
|
||||
summary: "Error",
|
||||
detail: err?.error?.message || "Failed to update book metadata",
|
||||
});
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
toggleLock(field: string): void {
|
||||
@@ -795,14 +804,22 @@ export class MetadataEditorComponent implements OnInit {
|
||||
navigatePrevious(): void {
|
||||
const prevBookId = this.bookNavigationService.getPreviousBookId();
|
||||
if (prevBookId) {
|
||||
this.navigateToBook(prevBookId);
|
||||
if (this.autoSaveEnabled && this.metadataForm.dirty) {
|
||||
this.saveMetadata().subscribe(() => this.navigateToBook(prevBookId));
|
||||
} else {
|
||||
this.navigateToBook(prevBookId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
navigateNext(): void {
|
||||
const nextBookId = this.bookNavigationService.getNextBookId();
|
||||
if (nextBookId) {
|
||||
this.navigateToBook(nextBookId);
|
||||
if (this.autoSaveEnabled && this.metadataForm.dirty) {
|
||||
this.saveMetadata().subscribe(() => this.navigateToBook(nextBookId));
|
||||
} else {
|
||||
this.navigateToBook(nextBookId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -64,7 +64,8 @@ const mockUser: User = {
|
||||
global: {sortKey: '', sortDir: 'ASC', view: 'GRID', coverSize: 1, seriesCollapsed: false},
|
||||
overrides: []
|
||||
},
|
||||
koReaderEnabled: false
|
||||
koReaderEnabled: false,
|
||||
autoSaveMetadata: false
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -79,7 +79,8 @@ describe('UserService', () => {
|
||||
global: {sortKey: '', sortDir: 'ASC', view: 'GRID', coverSize: 1, seriesCollapsed: false},
|
||||
overrides: []
|
||||
},
|
||||
koReaderEnabled: false
|
||||
koReaderEnabled: false,
|
||||
autoSaveMetadata: false
|
||||
}
|
||||
};
|
||||
|
||||
@@ -319,7 +320,8 @@ describe('UserService - API Contract Tests', () => {
|
||||
metadataCenterViewMode: 'route',
|
||||
enableSeriesView: true,
|
||||
entityViewPreferences: {global: {sortKey: '', sortDir: 'ASC' as const, view: 'GRID' as const, coverSize: 1, seriesCollapsed: false}, overrides: []},
|
||||
koReaderEnabled: false
|
||||
koReaderEnabled: false,
|
||||
autoSaveMetadata: false
|
||||
}
|
||||
};
|
||||
|
||||
@@ -394,7 +396,8 @@ describe('UserService - API Contract Tests', () => {
|
||||
metadataCenterViewMode: 'route',
|
||||
enableSeriesView: true,
|
||||
entityViewPreferences: {global: {sortKey: '', sortDir: 'ASC' as const, view: 'GRID' as const, coverSize: 1, seriesCollapsed: false}, overrides: []},
|
||||
koReaderEnabled: false
|
||||
koReaderEnabled: false,
|
||||
autoSaveMetadata: false
|
||||
}
|
||||
};
|
||||
|
||||
@@ -466,7 +469,8 @@ describe('UserService - API Contract Tests', () => {
|
||||
metadataCenterViewMode: 'route',
|
||||
enableSeriesView: true,
|
||||
entityViewPreferences: {global: {sortKey: '', sortDir: 'ASC' as const, view: 'GRID' as const, coverSize: 1, seriesCollapsed: false}, overrides: []},
|
||||
koReaderEnabled: false
|
||||
koReaderEnabled: false,
|
||||
autoSaveMetadata: false
|
||||
}
|
||||
};
|
||||
|
||||
@@ -638,7 +642,8 @@ describe('UserService - API Contract Tests', () => {
|
||||
metadataCenterViewMode: 'route' as const,
|
||||
enableSeriesView: true,
|
||||
entityViewPreferences: {global: {sortKey: '', sortDir: 'ASC' as const, view: 'GRID' as const, coverSize: 1, seriesCollapsed: false}, overrides: []},
|
||||
koReaderEnabled: false
|
||||
koReaderEnabled: false,
|
||||
autoSaveMetadata: false
|
||||
}
|
||||
};
|
||||
service.createUser(userData).subscribe();
|
||||
@@ -814,7 +819,8 @@ describe('UserService - API Contract Tests', () => {
|
||||
metadataCenterViewMode: 'route',
|
||||
enableSeriesView: true,
|
||||
entityViewPreferences: {global: {sortKey: '', sortDir: 'ASC', view: 'GRID', coverSize: 1, seriesCollapsed: false}, overrides: []},
|
||||
koReaderEnabled: false
|
||||
koReaderEnabled: false,
|
||||
autoSaveMetadata: false
|
||||
}
|
||||
}];
|
||||
|
||||
@@ -875,7 +881,8 @@ describe('UserService - API Contract Tests', () => {
|
||||
metadataCenterViewMode: 'route',
|
||||
enableSeriesView: true,
|
||||
entityViewPreferences: {global: {sortKey: '', sortDir: 'ASC' as const, view: 'GRID' as const, coverSize: 1, seriesCollapsed: false}, overrides: []},
|
||||
koReaderEnabled: false
|
||||
koReaderEnabled: false,
|
||||
autoSaveMetadata: false
|
||||
}
|
||||
};
|
||||
|
||||
@@ -937,7 +944,8 @@ describe('UserService - API Contract Tests', () => {
|
||||
metadataCenterViewMode: 'route' as const,
|
||||
enableSeriesView: true,
|
||||
entityViewPreferences: {global: {sortKey: '', sortDir: 'ASC' as const, view: 'GRID' as const, coverSize: 1, seriesCollapsed: false}, overrides: []},
|
||||
koReaderEnabled: false
|
||||
koReaderEnabled: false,
|
||||
autoSaveMetadata: false
|
||||
}
|
||||
};
|
||||
service.createUser(userData).subscribe(result => {
|
||||
|
||||
@@ -144,6 +144,7 @@ export interface UserSettings {
|
||||
tableColumnPreference?: TableColumnPreference[];
|
||||
dashboardConfig?: DashboardConfig;
|
||||
koReaderEnabled: boolean;
|
||||
autoSaveMetadata: boolean;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
|
||||
@@ -43,6 +43,32 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="preferences-section">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title">
|
||||
<i class="pi pi-pencil"></i>
|
||||
Metadata Editor Settings
|
||||
</h3>
|
||||
<p class="section-description">
|
||||
Configure behavior for the metadata editor.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="settings-card">
|
||||
<div class="setting-item no-border">
|
||||
<div class="setting-info">
|
||||
<label class="setting-label">Auto-save on Navigation</label>
|
||||
<p class="setting-description">
|
||||
Automatically save changes when navigating to the next or previous book in the metadata editor.
|
||||
</p>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<p-toggleswitch [(ngModel)]="autoSaveMetadata"></p-toggleswitch>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="preferences-section">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title">
|
||||
|
||||
@@ -13,6 +13,7 @@ import {FormsModule} from '@angular/forms';
|
||||
import {ToastModule} from 'primeng/toast';
|
||||
import {Tooltip} from 'primeng/tooltip';
|
||||
import {filter, take, takeUntil} from 'rxjs/operators';
|
||||
import {ToggleSwitch} from 'primeng/toggleswitch';
|
||||
|
||||
@Component({
|
||||
selector: 'app-view-preferences',
|
||||
@@ -23,7 +24,8 @@ import {filter, take, takeUntil} from 'rxjs/operators';
|
||||
Button,
|
||||
TableModule,
|
||||
ToastModule,
|
||||
Tooltip
|
||||
Tooltip,
|
||||
ToggleSwitch
|
||||
],
|
||||
templateUrl: './view-preferences.component.html',
|
||||
styleUrl: './view-preferences.component.scss'
|
||||
@@ -75,6 +77,7 @@ export class ViewPreferencesComponent implements OnInit, OnDestroy {
|
||||
selectedSort: string = 'title';
|
||||
selectedSortDir: 'ASC' | 'DESC' = 'ASC';
|
||||
selectedView: 'GRID' | 'TABLE' = 'GRID';
|
||||
autoSaveMetadata: boolean = false;
|
||||
|
||||
overrides: {
|
||||
entityType: 'LIBRARY' | 'SHELF' | 'MAGIC_SHELF';
|
||||
@@ -109,6 +112,7 @@ export class ViewPreferencesComponent implements OnInit, OnDestroy {
|
||||
this.selectedSort = global?.sortKey ?? 'title';
|
||||
this.selectedSortDir = global?.sortDir ?? 'ASC';
|
||||
this.selectedView = global?.view ?? 'GRID';
|
||||
this.autoSaveMetadata = userState.user?.userSettings?.autoSaveMetadata ?? false;
|
||||
|
||||
this.overrides = (prefs?.overrides ?? []).map(o => ({
|
||||
entityType: o.entityType,
|
||||
@@ -219,6 +223,7 @@ export class ViewPreferencesComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
|
||||
this.userService.updateUserSetting(this.user.id, 'entityViewPreferences', prefs);
|
||||
this.userService.updateUserSetting(this.user.id, 'autoSaveMetadata', this.autoSaveMetadata);
|
||||
|
||||
this.messageService.add({
|
||||
severity: 'success',
|
||||
|
||||
Reference in New Issue
Block a user