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:
Balázs Szücs
2026-01-16 03:57:27 +01:00
committed by GitHub
parent 709f90dc68
commit 869bea9ca5
9 changed files with 95 additions and 34 deletions

View File

@@ -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) {

View File

@@ -74,6 +74,7 @@ public class BookLoreUser {
public String metadataCenterViewMode;
public boolean koReaderEnabled;
public boolean enableSeriesView;
public boolean autoSaveMetadata;
public DashboardConfig dashboardConfig;
@Data

View File

@@ -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;

View File

@@ -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);
}
}
}

View File

@@ -64,7 +64,8 @@ const mockUser: User = {
global: {sortKey: '', sortDir: 'ASC', view: 'GRID', coverSize: 1, seriesCollapsed: false},
overrides: []
},
koReaderEnabled: false
koReaderEnabled: false,
autoSaveMetadata: false
}
};

View File

@@ -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 => {

View File

@@ -144,6 +144,7 @@ export interface UserSettings {
tableColumnPreference?: TableColumnPreference[];
dashboardConfig?: DashboardConfig;
koReaderEnabled: boolean;
autoSaveMetadata: boolean;
}
export interface User {

View File

@@ -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">

View File

@@ -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',