From 869bea9ca51a641e62208214fac2161e79c6b23a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Sz=C3=BCcs?= <127139797+balazs-szucs@users.noreply.github.com> Date: Fri, 16 Jan 2026 03:57:27 +0100 Subject: [PATCH] feat(metadata): add auto-save metadata feature in editor settings (#2274) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(metadata): add auto-save metadata feature in editor settings Signed-off-by: Balázs Szücs * feat(reader-preferences): add autoSaveMetadata property to user preferences Signed-off-by: Balázs Szücs * feat(user-service): add autoSaveMetadata property to user preferences in tests Signed-off-by: Balázs Szücs --------- Signed-off-by: Balázs Szücs --- .../custom/BookLoreUserTransformer.java | 1 + .../booklore/model/dto/BookLoreUser.java | 1 + .../model/dto/settings/UserSettingKey.java | 3 +- .../metadata-editor.component.ts | 63 ++++++++++++------- .../reader-preferences.service.spec.ts | 3 +- .../user-management/user.service.spec.ts | 24 ++++--- .../settings/user-management/user.service.ts | 1 + .../view-preferences.component.html | 26 ++++++++ .../view-preferences.component.ts | 7 ++- 9 files changed, 95 insertions(+), 34 deletions(-) diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/mapper/custom/BookLoreUserTransformer.java b/booklore-api/src/main/java/com/adityachandel/booklore/mapper/custom/BookLoreUserTransformer.java index a42d56394..6f4f88150 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/mapper/custom/BookLoreUserTransformer.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/mapper/custom/BookLoreUserTransformer.java @@ -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) { diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/BookLoreUser.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/BookLoreUser.java index e34723e33..c4eb93e0a 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/BookLoreUser.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/BookLoreUser.java @@ -74,6 +74,7 @@ public class BookLoreUser { public String metadataCenterViewMode; public boolean koReaderEnabled; public boolean enableSeriesView; + public boolean autoSaveMetadata; public DashboardConfig dashboardConfig; @Data diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/settings/UserSettingKey.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/settings/UserSettingKey.java index 6ce1feed5..f4f67b905 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/settings/UserSettingKey.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/settings/UserSettingKey.java @@ -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; diff --git a/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-editor/metadata-editor.component.ts b/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-editor/metadata-editor.component.ts index ef69e41ea..7e68b202b 100644 --- a/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-editor/metadata-editor.component.ts +++ b/booklore-ui/src/app/features/metadata/component/book-metadata-center/metadata-editor/metadata-editor.component.ts @@ -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(); 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 { 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); + } } } diff --git a/booklore-ui/src/app/features/settings/reader-preferences/reader-preferences.service.spec.ts b/booklore-ui/src/app/features/settings/reader-preferences/reader-preferences.service.spec.ts index 0e064aca8..2f8b88e40 100644 --- a/booklore-ui/src/app/features/settings/reader-preferences/reader-preferences.service.spec.ts +++ b/booklore-ui/src/app/features/settings/reader-preferences/reader-preferences.service.spec.ts @@ -64,7 +64,8 @@ const mockUser: User = { global: {sortKey: '', sortDir: 'ASC', view: 'GRID', coverSize: 1, seriesCollapsed: false}, overrides: [] }, - koReaderEnabled: false + koReaderEnabled: false, + autoSaveMetadata: false } }; diff --git a/booklore-ui/src/app/features/settings/user-management/user.service.spec.ts b/booklore-ui/src/app/features/settings/user-management/user.service.spec.ts index a9dfaf4a1..43228be20 100644 --- a/booklore-ui/src/app/features/settings/user-management/user.service.spec.ts +++ b/booklore-ui/src/app/features/settings/user-management/user.service.spec.ts @@ -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 => { diff --git a/booklore-ui/src/app/features/settings/user-management/user.service.ts b/booklore-ui/src/app/features/settings/user-management/user.service.ts index 482da3505..6750d21af 100644 --- a/booklore-ui/src/app/features/settings/user-management/user.service.ts +++ b/booklore-ui/src/app/features/settings/user-management/user.service.ts @@ -144,6 +144,7 @@ export interface UserSettings { tableColumnPreference?: TableColumnPreference[]; dashboardConfig?: DashboardConfig; koReaderEnabled: boolean; + autoSaveMetadata: boolean; } export interface User { diff --git a/booklore-ui/src/app/features/settings/view-preferences-parent/view-preferences/view-preferences.component.html b/booklore-ui/src/app/features/settings/view-preferences-parent/view-preferences/view-preferences.component.html index 1aeb85192..b4b8f7907 100644 --- a/booklore-ui/src/app/features/settings/view-preferences-parent/view-preferences/view-preferences.component.html +++ b/booklore-ui/src/app/features/settings/view-preferences-parent/view-preferences/view-preferences.component.html @@ -43,6 +43,32 @@ +
+
+

+ + Metadata Editor Settings +

+

+ Configure behavior for the metadata editor. +

+
+ +
+
+
+ +

+ Automatically save changes when navigating to the next or previous book in the metadata editor. +

+
+
+ +
+
+
+
+

diff --git a/booklore-ui/src/app/features/settings/view-preferences-parent/view-preferences/view-preferences.component.ts b/booklore-ui/src/app/features/settings/view-preferences-parent/view-preferences/view-preferences.component.ts index 4404c4c68..a09943464 100644 --- a/booklore-ui/src/app/features/settings/view-preferences-parent/view-preferences/view-preferences.component.ts +++ b/booklore-ui/src/app/features/settings/view-preferences-parent/view-preferences/view-preferences.component.ts @@ -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',