Skip to content

Commit 53f448f

Browse files
authored
Merge pull request #2458 from booklore-app/develop
Merge develop into master for release
2 parents 74fa38b + c23e38b commit 53f448f

File tree

30 files changed

+578
-169
lines changed

30 files changed

+578
-169
lines changed

booklore-api/src/main/java/com/adityachandel/booklore/repository/BookOpdsRepository.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ m.searchText LIKE CONCAT('%', :text, '%')
122122
""")
123123
Page<Long> findBookIdsByMetadataSearchAndShelfIds(@Param("text") String text, @Param("shelfIds") Collection<Long> shelfIds, Pageable pageable);
124124

125-
@EntityGraph(attributePaths = {"metadata", "metadata.authors", "metadata.categories", "additionalFiles", "shelves"})
125+
@EntityGraph(attributePaths = {"metadata", "metadata.authors", "metadata.categories", "bookFiles", "shelves"})
126126
@Query("SELECT DISTINCT b FROM BookEntity b JOIN b.shelves s WHERE b.id IN :ids AND s.id IN :shelfIds AND (b.deleted IS NULL OR b.deleted = false)")
127127
List<BookEntity> findAllWithFullMetadataByIdsAndShelfIds(@Param("ids") Collection<Long> ids, @Param("shelfIds") Collection<Long> shelfIds);
128128

@@ -133,7 +133,7 @@ m.searchText LIKE CONCAT('%', :text, '%')
133133
@Query("SELECT DISTINCT b.id FROM BookEntity b JOIN b.shelves s WHERE s.id IN :shelfIds AND (b.deleted IS NULL OR b.deleted = false) ORDER BY b.addedOn DESC")
134134
Page<Long> findBookIdsByShelfIds(@Param("shelfIds") Collection<Long> shelfIds, Pageable pageable);
135135

136-
@EntityGraph(attributePaths = {"metadata", "additionalFiles", "shelves"})
136+
@EntityGraph(attributePaths = {"metadata", "bookFiles", "shelves"})
137137
@Query("SELECT DISTINCT b FROM BookEntity b JOIN b.shelves s WHERE b.id IN :ids AND s.id IN :shelfIds AND (b.deleted IS NULL OR b.deleted = false)")
138138
List<BookEntity> findAllWithMetadataByIdsAndShelfIds(@Param("ids") Collection<Long> ids, @Param("shelfIds") Collection<Long> shelfIds);
139139

booklore-ui/ngsw-config.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"resources": {
99
"files": [
1010
"/favicon.ico",
11+
"/assets/favicon.svg",
1112
"/index.csr.html",
1213
"/index.html",
1314
"/manifest.webmanifest",

booklore-ui/src/app/app.component.html

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,31 @@
11
@if (loading) {
22
<div class="splash-screen">
33
<div class="splash-content">
4-
<img src="assets/favicon.svg" alt="Booklore logo" class="logo" />
5-
<h1>Loading Booklore…</h1>
6-
<p>Please wait while we get things ready.</p>
7-
<div class="loader"></div>
4+
<svg class="logo" viewBox="0 0 126 126" xmlns="http://www.w3.org/2000/svg">
5+
<path d="M59 4.79297C71.5051 11.5557 80 24.7854 80 40C80 40.5959 79.987 41.1888 79.9609 41.7783C79.8609 44.0406 81.7355 46 84 46C106.091 46 124 63.9086 124 86C124 108.091 106.091 126 84 126H10C4.47715 126 0 121.523 0 116V39.0068L0.0126953 38.9941C0.357624 25.0252 7.86506 12.8347 19 5.95215V63.832C19 64.8345 20.0676 65.4391 20.9121 64.9902L21.0771 64.8867L38.2227 52.3428C38.6819 52.0068 39.3064 52.0068 39.7656 52.3428L56.9229 64.8945L57.0879 64.998C57.9324 65.447 59 64.8423 59 63.8398V4.79297Z" fill="#818cf8"/>
6+
<path d="M40 0C43.8745 0 47.6199 0.552381 51.1631 1.58008V50.9697L44.3926 46.0176L44.0879 45.8037C40.9061 43.6679 36.7098 43.7393 33.5957 46.0176L26.8369 50.9619V2.21875C30.9593 0.782634 35.3881 0 40 0Z" fill="white"/>
7+
</svg>
8+
@if (offline) {
9+
<h1>You're Offline</h1>
10+
<p>BookLore needs a connection to the server to load.</p>
11+
<p>Check your network and try again.</p>
12+
} @else {
13+
<h1>Loading Booklore…</h1>
14+
<p>Please wait while we get things ready.</p>
15+
<div class="loader"></div>
16+
}
17+
</div>
18+
</div>
19+
} @else if (offline) {
20+
<div class="splash-screen">
21+
<div class="splash-content">
22+
<svg class="logo" viewBox="0 0 126 126" xmlns="http://www.w3.org/2000/svg">
23+
<path d="M59 4.79297C71.5051 11.5557 80 24.7854 80 40C80 40.5959 79.987 41.1888 79.9609 41.7783C79.8609 44.0406 81.7355 46 84 46C106.091 46 124 63.9086 124 86C124 108.091 106.091 126 84 126H10C4.47715 126 0 121.523 0 116V39.0068L0.0126953 38.9941C0.357624 25.0252 7.86506 12.8347 19 5.95215V63.832C19 64.8345 20.0676 65.4391 20.9121 64.9902L21.0771 64.8867L38.2227 52.3428C38.6819 52.0068 39.3064 52.0068 39.7656 52.3428L56.9229 64.8945L57.0879 64.998C57.9324 65.447 59 64.8423 59 63.8398V4.79297Z" fill="#818cf8"/>
24+
<path d="M40 0C43.8745 0 47.6199 0.552381 51.1631 1.58008V50.9697L44.3926 46.0176L44.0879 45.8037C40.9061 43.6679 36.7098 43.7393 33.5957 46.0176L26.8369 50.9619V2.21875C30.9593 0.782634 35.3881 0 40 0Z" fill="white"/>
25+
</svg>
26+
<h1>You're Offline</h1>
27+
<p>Your connection was lost. Some features may not work.</p>
28+
<button class="retry-btn" (click)="reload()">Retry</button>
829
</div>
930
</div>
1031
} @else {

booklore-ui/src/app/app.component.scss

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,19 @@ p {
4747
transform: rotate(360deg);
4848
}
4949
}
50+
51+
.retry-btn {
52+
margin-top: 1rem;
53+
padding: 0.6rem 1.5rem;
54+
background-color: #818cf8;
55+
color: #fff;
56+
border: none;
57+
border-radius: 6px;
58+
font-size: 0.95rem;
59+
cursor: pointer;
60+
transition: background-color 0.2s;
61+
62+
&:hover {
63+
background-color: #6366f1;
64+
}
65+
}

booklore-ui/src/app/app.component.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {scan, withLatestFrom} from 'rxjs/operators';
2828
export class AppComponent implements OnInit, OnDestroy {
2929

3030
loading = true;
31+
offline = !navigator.onLine;
3132
private subscriptions: Subscription[] = [];
3233
private subscriptionsInitialized = false;
3334

@@ -43,6 +44,9 @@ export class AppComponent implements OnInit, OnDestroy {
4344
private libraryLoadingService = inject(LibraryLoadingService);
4445

4546
ngOnInit(): void {
47+
window.addEventListener('online', this.onOnline);
48+
window.addEventListener('offline', this.onOffline);
49+
4650
this.authInit.initialized$.subscribe(ready => {
4751
this.loading = !ready;
4852
if (ready && !this.subscriptionsInitialized) {
@@ -52,6 +56,18 @@ export class AppComponent implements OnInit, OnDestroy {
5256
});
5357
}
5458

59+
private onOnline = () => {
60+
this.offline = false;
61+
};
62+
63+
private onOffline = () => {
64+
this.offline = true;
65+
};
66+
67+
reload(): void {
68+
window.location.reload();
69+
}
70+
5571
private setupWebSocketSubscriptions(): void {
5672
this.subscriptions.push(
5773
this.rxStompService.watch('/user/queue/book-add').pipe(
@@ -125,6 +141,8 @@ export class AppComponent implements OnInit, OnDestroy {
125141
}
126142

127143
ngOnDestroy(): void {
144+
window.removeEventListener('online', this.onOnline);
145+
window.removeEventListener('offline', this.onOffline);
128146
this.subscriptions.forEach(sub => sub.unsubscribe());
129147
this.libraryLoadingService.hide();
130148
}
Lines changed: 55 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,78 @@
1-
import { Injectable } from '@angular/core';
2-
import { ActivatedRouteSnapshot, DetachedRouteHandle, RouteReuseStrategy } from '@angular/router';
1+
import {inject, Injectable} from '@angular/core';
2+
import {ActivatedRouteSnapshot, DetachedRouteHandle, RouteReuseStrategy} from '@angular/router';
3+
import {BookBrowserScrollService} from '../features/book/components/book-browser/book-browser-scroll.service';
34

45
@Injectable({
56
providedIn: 'root',
67
})
78
export class CustomReuseStrategy implements RouteReuseStrategy {
89
private storedRoutes = new Map<string, DetachedRouteHandle>();
10+
private scrollService = inject(BookBrowserScrollService);
11+
12+
private readonly BOOK_BROWSER_PATHS = [
13+
'all-books',
14+
'unshelved-books',
15+
'library/:libraryId/books',
16+
'shelf/:shelfId/books',
17+
'magic-shelf/:magicShelfId/books'
18+
];
19+
20+
private readonly BOOK_DETAILS_PATH = 'book/:bookId';
21+
22+
private getRouteKey(route: ActivatedRouteSnapshot): string {
23+
const path = route.routeConfig?.path || '';
24+
return this.scrollService.createKey(path, route.params);
25+
}
26+
27+
private isBookBrowserRoute(route: ActivatedRouteSnapshot): boolean {
28+
const path = route.routeConfig?.path;
29+
return this.BOOK_BROWSER_PATHS.includes(path || '');
30+
}
31+
32+
private isBookDetailsRoute(route: ActivatedRouteSnapshot): boolean {
33+
return route.routeConfig?.path === this.BOOK_DETAILS_PATH;
34+
}
935

10-
// Only detach the route if it's for the book details page
1136
shouldDetach(route: ActivatedRouteSnapshot): boolean {
12-
return route.routeConfig?.path === 'book/:id'; // Match the path of the route you want to reuse
37+
return this.isBookBrowserRoute(route);
1338
}
1439

15-
// Store the route component instance when detaching
1640
store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle | null): void {
17-
if (handle) {
18-
// Save the handle if we are detaching this route
19-
this.storedRoutes.set(route.routeConfig?.path || '', handle);
41+
if (handle && this.isBookBrowserRoute(route)) {
42+
const key = this.getRouteKey(route);
43+
this.storedRoutes.set(key, handle);
2044
}
2145
}
2246

23-
// Check if we should attach the route (reuse it) when navigating back to it
2447
shouldAttach(route: ActivatedRouteSnapshot): boolean {
25-
// Attach the route only if there's a stored instance for this route
26-
return !!this.storedRoutes.get(route.routeConfig?.path || '');
48+
if (!this.isBookBrowserRoute(route)) {
49+
return false;
50+
}
51+
const key = this.getRouteKey(route);
52+
return this.storedRoutes.has(key);
2753
}
2854

29-
// Retrieve the stored route component instance
3055
retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null {
31-
return this.storedRoutes.get(route.routeConfig?.path || '') || null;
56+
const key = this.getRouteKey(route);
57+
const handle = this.storedRoutes.get(key) || null;
58+
59+
if (handle) {
60+
const savedPosition = this.scrollService.getPosition(key);
61+
if (savedPosition !== undefined) {
62+
setTimeout(() => {
63+
const scrollElement = document.querySelector('.virtual-scroller');
64+
if (scrollElement) {
65+
(scrollElement as HTMLElement).scrollTop = savedPosition;
66+
}
67+
}, 0);
68+
}
69+
}
70+
71+
return handle;
3272
}
3373

34-
// Determine if the route should be reused based on its configuration
3574
shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
36-
// Reuse the route if the path and parameters match
37-
return future.routeConfig === curr.routeConfig && future.params['id'] === curr.params['id'];
75+
return future.routeConfig === curr.routeConfig &&
76+
JSON.stringify(future.params) === JSON.stringify(curr.params);
3877
}
3978
}

booklore-ui/src/app/core/security/auth-initializer.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const OIDC_BYPASS_KEY = 'booklore-oidc-bypass';
88
const OIDC_ERROR_COUNT_KEY = 'booklore-oidc-error-count';
99
const MAX_OIDC_RETRIES = 3;
1010
const OIDC_TIMEOUT_MS = 5000;
11+
const SETTINGS_TIMEOUT_MS = 10000;
1112

1213
function withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
1314
return Promise.race([
@@ -26,8 +27,23 @@ export function initializeAuthFactory() {
2627
const authInitService = inject(AuthInitializationService);
2728

2829
return new Promise<void>((resolve) => {
30+
if (!navigator.onLine) {
31+
console.warn('[Auth] App is offline, skipping auth initialization');
32+
authInitService.markAsInitialized();
33+
resolve();
34+
return;
35+
}
36+
37+
const settingsTimeout = setTimeout(() => {
38+
console.warn('[Auth] Public settings fetch timed out, falling back to local auth');
39+
sub.unsubscribe();
40+
authInitService.markAsInitialized();
41+
resolve();
42+
}, SETTINGS_TIMEOUT_MS);
43+
2944
const sub = appSettingsService.publicAppSettings$.subscribe(publicSettings => {
3045
if (publicSettings) {
46+
clearTimeout(settingsTimeout);
3147
const forceLocalOnly = new URLSearchParams(window.location.search).get('localOnly') === 'true';
3248
const oidcBypassed = localStorage.getItem(OIDC_BYPASS_KEY) === 'true';
3349
const errorCount = parseInt(localStorage.getItem(OIDC_ERROR_COUNT_KEY) || '0', 10);
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import {Injectable} from '@angular/core';
2+
3+
@Injectable({
4+
providedIn: 'root'
5+
})
6+
export class BookBrowserScrollService {
7+
private scrollPositions = new Map<string, number>();
8+
9+
savePosition(key: string, position: number): void {
10+
this.scrollPositions.set(key, position);
11+
}
12+
13+
getPosition(key: string): number | undefined {
14+
return this.scrollPositions.get(key);
15+
}
16+
17+
clearPosition(key: string): void {
18+
this.scrollPositions.delete(key);
19+
}
20+
21+
createKey(path: string, params: Record<string, string>): string {
22+
const paramValues = Object.values(params).join('-');
23+
return paramValues ? `${path}:${paramValues}` : path;
24+
}
25+
}

booklore-ui/src/app/features/book/components/book-browser/book-browser.component.html

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -111,20 +111,43 @@
111111
<label for="collapse-series-checkbox" class="display-settings-label">Collapse series</label>
112112
</div>
113113
</div>
114-
<div class="display-settings-section">
115-
<label class="display-settings-label">Grid item size</label>
116-
<p-slider
117-
[(ngModel)]="coverScalePreferenceService.scaleFactor"
118-
[min]="0.5"
119-
[max]="1.5"
120-
[step]="0.01"
121-
[style]="{ width: '100%' }"
122-
(onChange)="updateScale()">
123-
</p-slider>
124-
<div class="scale-value-display">
125-
{{ coverScalePreferenceService.scaleFactor.toFixed(2) }}x
114+
@if (isMobile) {
115+
<div class="display-settings-section">
116+
<label class="display-settings-label">Grid columns</label>
117+
<div class="column-options">
118+
<button
119+
type="button"
120+
class="column-option-btn"
121+
[class.active]="mobileColumnCount === 2"
122+
(click)="setMobileColumns(2)">2</button>
123+
<button
124+
type="button"
125+
class="column-option-btn"
126+
[class.active]="mobileColumnCount === 3"
127+
(click)="setMobileColumns(3)">3</button>
128+
<button
129+
type="button"
130+
class="column-option-btn"
131+
[class.active]="mobileColumnCount === 4"
132+
(click)="setMobileColumns(4)">4</button>
133+
</div>
126134
</div>
127-
</div>
135+
} @else {
136+
<div class="display-settings-section">
137+
<label class="display-settings-label">Grid item size</label>
138+
<p-slider
139+
[(ngModel)]="coverScalePreferenceService.scaleFactor"
140+
[min]="0.5"
141+
[max]="1.5"
142+
[step]="0.01"
143+
[style]="{ width: '100%' }"
144+
(onChange)="updateScale()">
145+
</p-slider>
146+
<div class="scale-value-display">
147+
{{ coverScalePreferenceService.scaleFactor.toFixed(2) }}x
148+
</div>
149+
</div>
150+
}
128151
<div class="display-settings-section">
129152
<div class="display-settings-checkbox-row">
130153
<p-checkbox

0 commit comments

Comments
 (0)