Add a user-facing option to enable or disable series view (#1748)

This commit is contained in:
Aditya Chandel
2025-12-03 21:17:06 -07:00
committed by GitHub
parent 9b6079a3b5
commit f22f9bd1e0
15 changed files with 106 additions and 28 deletions

View File

@@ -69,6 +69,7 @@ public class BookLoreUserTransformer {
switch (settingKey) {
case FILTER_SORTING_MODE -> userSettings.setFilterSortingMode(value);
case METADATA_CENTER_VIEW_MODE -> userSettings.setMetadataCenterViewMode(value);
case ENABLE_SERIES_VIEW -> userSettings.setEnableSeriesView(Boolean.parseBoolean(value));
}
}
} catch (IllegalArgumentException e) {

View File

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

View File

@@ -16,7 +16,8 @@ public enum UserSettingKey {
DASHBOARD_CONFIG("dashboardConfig", true),
FILTER_SORTING_MODE("filterSortingMode", false),
METADATA_CENTER_VIEW_MODE("metadataCenterViewMode", false);
METADATA_CENTER_VIEW_MODE("metadataCenterViewMode", false),
ENABLE_SERIES_VIEW("enableSeriesView", false);
private final String dbKey;

View File

@@ -262,7 +262,7 @@
[index]="books.indexOf(book)"
[book]="book"
[isCheckboxEnabled]="true"
[readButtonHidden]="!!book.seriesCount"
[seriesViewEnabled]="seriesViewEnabled"
[onBookSelect]="handleBookSelect.bind(this)"
[isSeriesCollapsed]="seriesCollapseFilter.isSeriesCollapsed"
(checkboxClick)="onCheckboxClicked($event)"

View File

@@ -841,4 +841,8 @@ export class BookBrowserComponent implements OnInit, AfterViewInit {
})
);
}
get seriesViewEnabled(): boolean {
return Boolean(this.userService.getCurrentUser()?.userSettings?.enableSeriesView);
}
}

View File

@@ -4,11 +4,11 @@
(mouseout)="isHovered = false"
(click)="onCardClick($event)">
<div class="cover-container" [ngClass]="{ 'shimmer': !isImageLoaded, 'center-info-btn': readButtonHidden }">
<div class="cover-container" [ngClass]="{ 'shimmer': !isImageLoaded, 'center-info-btn': isSeriesViewActive() }">
<div
class="cover-container"
[ngClass]="{
'center-info-btn': readButtonHidden,
'center-info-btn': isSeriesViewActive(),
'loaded': isImageLoaded
}">
<img
@@ -32,13 +32,13 @@
</div>
}
@if (book.seriesCount && book.seriesCount! >= 1) {
@if (isSeriesViewActive()) {
<p-button [rounded]="true" icon="pi pi-info" class="info-btn" (click)="openSeriesInfo()"></p-button>
} @else {
<p-button [rounded]="true" icon="pi pi-info" class="info-btn" (click)="openBookInfo(book)"></p-button>
}
<p-button [hidden]="readButtonHidden" [rounded]="true" icon="pi pi-book" class="read-btn" (click)="readBook(book)"></p-button>
<p-button [hidden]="isSeriesViewActive()" [rounded]="true" icon="pi pi-book" class="read-btn" (click)="readBook(book)"></p-button>
@if (isCheckboxEnabled) {
<p-checkbox

View File

@@ -45,7 +45,7 @@ export class BookCardComponent implements OnInit, OnChanges, OnDestroy {
@Input() onBookSelect?: (bookId: number, selected: boolean) => void;
@Input() isSelected: boolean = false;
@Input() bottomBarHidden: boolean = false;
@Input() readButtonHidden: boolean = false;
@Input() seriesViewEnabled: boolean = false;
@Input() isSeriesCollapsed: boolean = false;
@ViewChild('checkboxElem') checkboxElem!: ElementRef<HTMLInputElement>;
@@ -768,4 +768,8 @@ export class BookCardComponent implements OnInit, OnChanges, OnDestroy {
shouldShowStatusIcon(): boolean {
return this.readStatusHelper.shouldShowStatusIcon(this.book.readStatus);
}
isSeriesViewActive(): boolean {
return this.seriesViewEnabled && !!this.book.seriesCount && this.book.seriesCount! >= 1;
}
}

View File

@@ -22,6 +22,7 @@
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;

View File

@@ -125,7 +125,7 @@
[pTooltip]="getRatingTooltip(book, 'amazon')"
tooltipPosition="top"
>
<img src="https://www.amazon.com/favicon.ico" alt="Amazon" class="rating-favicon" />
<img src="https://www.amazon.com/favicon.ico" alt="Amazon" class="rating-favicon"/>
<span class="rating-value">{{ getRatingPercent(book.metadata!.amazonRating) }}%</span>
</a>
}
@@ -139,7 +139,7 @@
[pTooltip]="getRatingTooltip(book, 'goodreads')"
tooltipPosition="top"
>
<img src="https://www.goodreads.com/favicon.ico" alt="Goodreads" class="rating-favicon" />
<img src="https://www.goodreads.com/favicon.ico" alt="Goodreads" class="rating-favicon"/>
<span class="rating-value">{{ getRatingPercent(book.metadata!.goodreadsRating) }}%</span>
</a>
}
@@ -153,7 +153,7 @@
[pTooltip]="getRatingTooltip(book, 'hardcover')"
tooltipPosition="top"
>
<img src="https://assets.hardcover.app/static/favicon.ico" alt="Hardcover" class="rating-favicon" />
<img src="https://assets.hardcover.app/static/favicon.ico" alt="Hardcover" class="rating-favicon"/>
<span class="rating-value">{{ getRatingPercent(book.metadata!.hardcoverRating) }}%</span>
</a>
}
@@ -165,7 +165,7 @@
target="_blank"
rel="noopener noreferrer"
>
<img src="https://books.google.com/favicon.ico" alt="Google Books" class="rating-favicon" />
<img src="https://books.google.com/favicon.ico" alt="Google Books" class="rating-favicon"/>
</a>
}
@@ -176,7 +176,7 @@
target="_blank"
rel="noopener noreferrer"
>
<img src="https://www.comicvine.com/favicon.ico" alt="Comic Vine" class="rating-favicon" />
<img src="https://www.comicvine.com/favicon.ico" alt="Comic Vine" class="rating-favicon"/>
</a>
}
</div>
@@ -328,7 +328,9 @@
@if (book?.metadata?.isbn13 || book?.metadata?.isbn10) {
<span class="metadata-value">
{{ book.metadata!.isbn13 }}
@if (book?.metadata?.isbn13 && book?.metadata?.isbn10) { / }
@if (book?.metadata?.isbn13 && book?.metadata?.isbn10) {
/
}
{{ book.metadata!.isbn10 }}
</span>
} @else {
@@ -523,7 +525,7 @@
<p-tabs lazy="true" class="custom-p-tabs" [value]="defaultTabValue" scrollable>
<p-tablist>
@if (bookInSeries.length > 0) {
@if (bookInSeries.length > 1) {
<p-tab [value]="1">
<i class="pi pi-ethereum"></i> More in Series
</p-tab>
@@ -541,12 +543,14 @@
<p-tabpanels>
<p-tabpanel [value]="1">
<div class="tab-content">
@if (bookInSeries.length > 0) {
@if (bookInSeries.length > 1) {
<div class="dashboard-scroller-infinite" infiniteScroll [infiniteScrollDistance]="2" [infiniteScrollThrottle]="50" [horizontal]="true">
@for (bookInSeriesItem of bookInSeries; track bookInSeriesItem) {
<div class="dashboard-scroller-card">
<app-book-card-lite-component [book]="bookInSeriesItem" [isActive]="bookInSeriesItem.id === book.id"></app-book-card-lite-component>
</div>
@if (bookInSeriesItem.id !== book.id) {
<div class="dashboard-scroller-card">
<app-book-card-lite-component [book]="bookInSeriesItem" [isActive]="bookInSeriesItem.id === book.id"></app-book-card-lite-component>
</div>
}
}
</div>
}

View File

@@ -348,7 +348,7 @@ export class MetadataViewerComponent implements OnInit, OnChanges {
}
get defaultTabValue(): number {
return this.bookInSeries && this.bookInSeries.length > 0 ? 1 : 2;
return this.bookInSeries && this.bookInSeries.length > 1 ? 1 : 2;
}
toggleExpand(): void {

View File

@@ -129,6 +129,7 @@ export interface UserSettings {
sidebarShelfSorting: SidebarShelfSorting;
filterSortingMode: 'alphabetical' | 'count';
metadataCenterViewMode: 'route' | 'dialog';
enableSeriesView: boolean;
entityViewPreferences: EntityViewPreferences;
tableColumnPreference?: TableColumnPreference[];
dashboardConfig?: DashboardConfig;
@@ -173,7 +174,7 @@ export class UserService {
private http = inject(HttpClient);
private authService = inject(AuthService);
private userStateSubject = new BehaviorSubject<UserState>({
userStateSubject = new BehaviorSubject<UserState>({
user: null,
loaded: false,
error: null,

View File

@@ -1,11 +1,11 @@
<div class="main-container enclosing-container">
<div class="settings-header">
<h2 class="settings-title">
<i class="pi pi-window-maximize"></i>
Open Metadata Center
<i class="pi pi-cog"></i>
View Preferences
</h2>
<p class="settings-description">
Decide how you want to view book details - inline as a full page or in a pop-up dialog.
Customize how you view metadata and series information.
</p>
</div>
@@ -16,7 +16,7 @@
<div class="setting-info">
<div class="setting-label-row">
<label class="setting-label">
Display Mode
Metadata Center Display Mode
<i class="pi pi-info-circle text-sky-600"
pTooltip="Choose whether to open the Metadata Center in the current page or as a popup dialog window."
tooltipPosition="right"
@@ -47,6 +47,42 @@
</p>
</div>
</div>
<!-- Series View Mode Setting -->
<div class="setting-item">
<div class="setting-info">
<div class="setting-label-row">
<label class="setting-label">
Series View Mode
<i class="pi pi-info-circle text-sky-600"
pTooltip="Enable or disable the series view mode for displaying series information."
tooltipPosition="right"
style="cursor: pointer;">
</i>
</label>
<div class="radio-group">
<div class="radio-option">
<p-radioButton name="seriesViewMode"
[value]="true"
[(ngModel)]="seriesViewMode"
inputId="seriesViewModeTrue"
(onClick)="onSeriesViewModeChange(true)"></p-radioButton>
<label for="seriesViewModeTrue">Enabled</label>
</div>
<div class="radio-option">
<p-radioButton name="seriesViewMode"
[value]="false"
[(ngModel)]="seriesViewMode"
inputId="seriesViewModeFalse"
(onClick)="onSeriesViewModeChange(false)"></p-radioButton>
<label for="seriesViewModeFalse">Disabled</label>
</div>
</div>
</div>
<p class="setting-description">
Enable or disable the series view mode for displaying series information.
</p>
</div>
</div>
</div>
</div>
</div>

View File

@@ -10,8 +10,8 @@
}
.settings-header {
margin-top: 1rem;
margin-bottom: 2rem;
margin-top: 1.25rem;
margin-bottom: 1rem;
}
.settings-title {

View File

@@ -19,6 +19,7 @@ import {Subject} from 'rxjs';
})
export class MetaCenterViewModeComponent implements OnInit, OnDestroy {
viewMode: 'route' | 'dialog' = 'route';
seriesViewMode: boolean = false;
private userService = inject(UserService);
private messageService = inject(MessageService);
@@ -34,6 +35,10 @@ export class MetaCenterViewModeComponent implements OnInit, OnDestroy {
if (preference === 'dialog' || preference === 'route') {
this.viewMode = preference;
}
const seriesPref = userState.user?.userSettings?.enableSeriesView;
if (typeof seriesPref === 'boolean') {
this.seriesViewMode = seriesPref;
}
});
}
@@ -47,6 +52,11 @@ export class MetaCenterViewModeComponent implements OnInit, OnDestroy {
this.savePreference(value);
}
onSeriesViewModeChange(value: boolean): void {
this.seriesViewMode = value;
this.saveSeriesViewPreference(value);
}
private savePreference(value: 'route' | 'dialog'): void {
const user = this.userService.getCurrentUser();
if (!user) return;
@@ -61,4 +71,19 @@ export class MetaCenterViewModeComponent implements OnInit, OnDestroy {
life: 1500,
});
}
private saveSeriesViewPreference(value: boolean): void {
const user = this.userService.getCurrentUser();
if (!user) return;
user.userSettings.enableSeriesView = value;
this.userService.updateUserSetting(user.id, 'enableSeriesView', value);
this.messageService.add({
severity: 'success',
summary: 'Preferences Updated',
detail: 'Your series view mode preference has been saved.',
life: 1500,
});
}
}

View File

@@ -10,8 +10,8 @@
}
.settings-header {
margin-top: 1rem;
margin-bottom: 2rem;
margin-top: 1.25rem;
margin-bottom: 1rem;
}
.settings-title {