mirror of
https://github.com/booklore-app/booklore.git
synced 2025-12-23 22:28:11 -05:00
Add a user-facing option to enable or disable series view (#1748)
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -52,6 +52,7 @@ public class BookLoreUser {
|
||||
public String filterSortingMode;
|
||||
public String metadataCenterViewMode;
|
||||
public boolean koReaderEnabled;
|
||||
public boolean enableSeriesView;
|
||||
public DashboardConfig dashboardConfig;
|
||||
|
||||
@Data
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -841,4 +841,8 @@ export class BookBrowserComponent implements OnInit, AfterViewInit {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
get seriesViewEnabled(): boolean {
|
||||
return Boolean(this.userService.getCurrentUser()?.userSettings?.enableSeriesView);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
}
|
||||
|
||||
.settings-header {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
margin-top: 1.25rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.settings-title {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
}
|
||||
|
||||
.settings-header {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
margin-top: 1.25rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.settings-title {
|
||||
|
||||
Reference in New Issue
Block a user