Skip to content

Commit a564bc2

Browse files
committed
refactor(ui): book browser page on the shared browse layer
1 parent 5f47709 commit a564bc2

89 files changed

Lines changed: 6052 additions & 2161 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

frontend/src/app/app.routes.spec.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,25 @@ describe('app routes', () => {
2626
it('defines the authenticated shell routes behind the auth guard', () => {
2727
const shellRoute = routes.find(route => route.path === '' && Array.isArray(route.children));
2828
const children = shellRoute?.children ?? [];
29+
const browsePage = (path: string) => children.find(route => route.path === path)
30+
?.children?.find(route => route.path === '');
31+
const browseFilterPage = (path: string) => children.find(route => route.path === path)
32+
?.children?.find(route => route.path === 'filter');
2933

3034
expect(children).toHaveLength(21);
35+
expect(children.find(route => route.path === 'browse/filter')).toBeUndefined();
3136
expect(shellRoute?.canActivateChild).toEqual([AuthChildGuard]);
3237
expect(children.find(route => route.path === 'dashboard')?.canActivate).toBeUndefined();
3338
expect(children.find(route => route.path === 'all-books')?.canActivate).toBeUndefined();
3439
expect(children.find(route => route.path === 'magic-shelf/:magicShelfId/books')?.canActivate).toBeUndefined();
3540
expect(children.find(route => route.path === 'notebook')?.canActivate).toBeUndefined();
36-
expect(typeof children.find(route => route.path === 'all-books')?.loadComponent).toBe('function');
37-
expect(typeof children.find(route => route.path === 'library/:libraryId/books')?.loadComponent).toBe('function');
38-
expect(typeof children.find(route => route.path === 'shelf/:shelfId/books')?.loadComponent).toBe('function');
39-
expect(typeof children.find(route => route.path === 'unshelved-books')?.loadComponent).toBe('function');
40-
expect(typeof children.find(route => route.path === 'magic-shelf/:magicShelfId/books')?.loadComponent).toBe('function');
41+
expect(typeof browsePage('all-books')?.loadComponent).toBe('function');
42+
expect(typeof browseFilterPage('all-books')?.loadComponent).toBe('function');
43+
expect(children.find(route => route.path === 'browse')).toBeUndefined();
44+
expect(typeof browsePage('library/:libraryId/books')?.loadComponent).toBe('function');
45+
expect(typeof browsePage('shelf/:shelfId/books')?.loadComponent).toBe('function');
46+
expect(typeof browsePage('unshelved-books')?.loadComponent).toBe('function');
47+
expect(typeof browsePage('magic-shelf/:magicShelfId/books')?.loadComponent).toBe('function');
4148
expect(typeof children.find(route => route.path === 'design-system')?.loadComponent).toBe('function');
4249
expect(typeof children.find(route => route.path === 'design-system/form/library')?.loadComponent).toBe('function');
4350
expect(typeof children.find(route => route.path === 'design-system/form/device')?.loadComponent).toBe('function');

frontend/src/app/app.routes.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,17 @@ import {BookdropGuard} from './core/security/guards/bookdrop.guard';
1414
import {LibraryStatsGuard} from './core/security/guards/library-stats.guard';
1515
import {UserStatsGuard} from './core/security/guards/user-stats.guard';
1616
import {EditMetadataGuard} from './core/security/guards/edit-metdata.guard';
17+
import {type BookBrowseRouteData} from './features/book/browse/book-browse-scope';
1718

18-
const loadBookBrowserComponent = () =>
19-
import('./features/book/components/book-browser/book-browser.component').then(m => m.BookBrowserComponent);
19+
const loadBookBrowsePage = () =>
20+
import('./features/book/browse/book-browse-page.component').then(m => m.BookBrowsePageComponent);
21+
const loadBookBrowseFilterPage = () =>
22+
import('./features/book/browse/book-browse-filter-page.component').then(m => m.BookBrowseFilterPageComponent);
23+
24+
const bookBrowseRoutes = (data: BookBrowseRouteData = {}) => [
25+
{path: '', loadComponent: loadBookBrowsePage, ...(data.browseScope ? {data} : {})},
26+
{path: 'filter', loadComponent: loadBookBrowseFilterPage, ...(data.browseScope ? {data} : {})},
27+
];
2028

2129
export const routes: Routes = [
2230
{
@@ -37,16 +45,16 @@ export const routes: Routes = [
3745
canActivateChild: [AuthChildGuard],
3846
children: [
3947
{path: 'dashboard', component: MainDashboardComponent},
40-
{path: 'all-books', loadComponent: loadBookBrowserComponent},
48+
{path: 'all-books', children: bookBrowseRoutes()},
4149
{path: 'settings', loadComponent: () => import('./features/settings/settings.component').then(m => m.SettingsComponent)},
42-
{path: 'library/:libraryId/books', loadComponent: loadBookBrowserComponent},
43-
{path: 'shelf/:shelfId/books', loadComponent: loadBookBrowserComponent},
44-
{path: 'unshelved-books', loadComponent: loadBookBrowserComponent},
50+
{path: 'library/:libraryId/books', children: bookBrowseRoutes()},
51+
{path: 'shelf/:shelfId/books', children: bookBrowseRoutes()},
52+
{path: 'unshelved-books', children: bookBrowseRoutes({browseScope: 'unshelved'})},
4553
{path: 'series', loadComponent: () => import('./features/series-browser/components/series-browser/series-browser.component').then(m => m.SeriesBrowserComponent)},
4654
{path: 'series/:seriesName', loadComponent: () => import('./features/book/components/series-page/series-page.component').then(m => m.SeriesPageComponent)},
4755
{path: 'authors', loadComponent: () => import('./features/author-browser/components/author-browser/author-browser.component').then(m => m.AuthorBrowserComponent)},
4856
{path: 'author/:authorId', loadComponent: () => import('./features/author-browser/components/author-detail/author-detail.component').then(m => m.AuthorDetailComponent)},
49-
{path: 'magic-shelf/:magicShelfId/books', loadComponent: loadBookBrowserComponent},
57+
{path: 'magic-shelf/:magicShelfId/books', children: bookBrowseRoutes()},
5058
{path: 'book/:bookId', loadComponent: () => import('./features/metadata/component/book-metadata-center/book-metadata-center.component').then(m => m.BookMetadataCenterComponent)},
5159
{path: 'bookdrop', loadComponent: () => import('./features/bookdrop/component/bookdrop-file-review/bookdrop-file-review.component').then(m => m.BookdropFileReviewComponent), canActivate: [BookdropGuard]},
5260
{path: 'metadata-manager', loadComponent: () => import('./features/metadata/component/metadata-manager/metadata-manager.component').then(m => m.MetadataManagerComponent), canActivate: [EditMetadataGuard]},
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
@if (selection().active()) {
2+
<app-browse-bulk-actions-bar
3+
[count]="selection().count()"
4+
[total]="total()"
5+
[moreMenu]="bulkMoreMenu"
6+
[moreAlways]="moreMenuOnlyItems()"
7+
[moreDisabled]="isResolving()"
8+
(clearSelection)="selection().clear()"
9+
(selectAll)="selection().selectAll()">
10+
<app-button
11+
appBrowseBulkActionsItem="addToShelf"
12+
variant="ghost"
13+
size="md"
14+
[disabled]="isResolving()"
15+
[label]="'book.menu.addToShelf' | transloco"
16+
[appMenuTriggerFor]="bulkShelfMenuHost.menu()">
17+
<svg lucideBookmark aria-hidden="true"></svg>
18+
</app-button>
19+
<app-button
20+
appBrowseBulkActionsItem="markAs"
21+
variant="ghost"
22+
size="md"
23+
[disabled]="isResolving()"
24+
[label]="'book.menu.markAs' | transloco"
25+
[appMenuTriggerFor]="bulkMarkAsMenu">
26+
<svg lucideCheck aria-hidden="true"></svg>
27+
</app-button>
28+
@if (bulkPermissions().canEditMetadata) {
29+
<app-button
30+
appBrowseBulkActionsItem="edit"
31+
variant="ghost"
32+
size="md"
33+
[disabled]="isResolving()"
34+
[label]="'common.edit' | transloco"
35+
[appMenuTriggerFor]="bulkEditMenu">
36+
<svg lucidePenLine aria-hidden="true"></svg>
37+
</app-button>
38+
}
39+
@if (bulkMetadataAvailable()) {
40+
<app-button
41+
appBrowseBulkActionsItem="metadata"
42+
variant="ghost"
43+
size="md"
44+
[disabled]="isResolving()"
45+
[label]="'book.menu.metadata' | transloco"
46+
[appMenuTriggerFor]="bulkMetadataMenu">
47+
<svg lucideDatabase aria-hidden="true"></svg>
48+
</app-button>
49+
}
50+
@if (actionPermissions().canDeleteBook) {
51+
<span appBrowseBulkActionsItem="delete" appBrowseBulkActionsTrailing class="flex items-center gap-1">
52+
<app-browse-bulk-actions-divider />
53+
<app-button
54+
variant="ghost"
55+
tone="danger"
56+
size="md"
57+
[label]="'common.delete' | transloco"
58+
[loading]="bulkDeleting()"
59+
[disabled]="isResolving()"
60+
(clicked)="commands.delete()" />
61+
</span>
62+
}
63+
</app-browse-bulk-actions-bar>
64+
65+
<app-shelf-membership-menu
66+
#bulkShelfMenuHost
67+
[shelves]="bulkShelves()"
68+
(toggleShelf)="commands.toggleShelf($event.shelfId, $event.checked)"
69+
(createShelf)="commands.createShelf()"
70+
(removeFromAllShelves)="commands.removeFromAllShelves(bulkShelfIds())" />
71+
72+
<app-menu #bulkMarkAsMenu [ariaLabel]="'book.menu.markAs' | transloco">
73+
@for (status of readStatusTargets; track status) {
74+
<app-menu-item [value]="status" [disabled]="isResolving()" (selected)="commands.markAs(status)">{{ statusLabelKey(status) | transloco }}</app-menu-item>
75+
}
76+
<app-menu-separator />
77+
<app-menu-item value="clear-read-status" [disabled]="isResolving()" (selected)="commands.markAs(clearReadStatus)">
78+
{{ clearReadStatusLabelKey | transloco }}
79+
</app-menu-item>
80+
</app-menu>
81+
82+
<app-menu #bulkEditMenu [ariaLabel]="'common.edit' | transloco">
83+
<app-menu-item value="edit-all" [disabled]="isResolving()" (selected)="commands.editAll()">{{ 'browse.bulk.editAll' | transloco }}</app-menu-item>
84+
<app-menu-item value="edit-one-by-one" [disabled]="isResolving()" (selected)="commands.editOneByOne()">{{ 'browse.bulk.editOneByOne' | transloco }}</app-menu-item>
85+
</app-menu>
86+
87+
<app-menu #bulkMetadataMenu [ariaLabel]="'book.menu.metadata' | transloco">
88+
@if (bulkPermissions().canAutoFetchMetadata) {
89+
<app-menu-item value="fetch-metadata" [disabled]="isResolving()" (selected)="commands.fetchMetadata()">{{ 'book.menu.fetchMetadata' | transloco }}</app-menu-item>
90+
}
91+
@if (bulkPermissions().canCustomFetchMetadata) {
92+
<app-menu-item value="fetch-metadata-with-options" [disabled]="isResolving()" (selected)="commands.fetchMetadataWithOptions()">{{ 'book.menu.fetchMetadataWithOptions' | transloco }}</app-menu-item>
93+
}
94+
<app-menu-separator [class]="autoSeparatorClass" />
95+
@if (bulkPermissions().canLockUnlockMetadata) {
96+
<app-menu-item value="lock-metadata" [disabled]="isResolving()" (selected)="commands.setMetadataLocks(true)">{{ 'metadata.editor.lockAllBtn' | transloco }}</app-menu-item>
97+
<app-menu-item value="unlock-metadata" [disabled]="isResolving()" (selected)="commands.setMetadataLocks(false)">{{ 'metadata.editor.unlockAllBtn' | transloco }}</app-menu-item>
98+
<app-menu-item value="lock-unlock-metadata" [disabled]="isResolving()" (selected)="commands.lockUnlockMetadata()">{{ 'browse.bulk.lockUnlockMetadata' | transloco }}</app-menu-item>
99+
}
100+
<app-menu-separator [class]="autoSeparatorClass" />
101+
@if (bulkPermissions().canRegenerateCover) {
102+
<app-menu-item value="restore-cover" [disabled]="isResolving()" (selected)="commands.changeCovers('regenerate')">{{ 'book.menu.restoreCover' | transloco }}</app-menu-item>
103+
<app-menu-item value="generate-cover" [disabled]="isResolving()" (selected)="commands.changeCovers('generate')">{{ 'book.menu.generateCover' | transloco }}</app-menu-item>
104+
}
105+
</app-menu>
106+
107+
<app-menu #bulkMoreMenu [ariaLabel]="'browse.moreActions' | transloco">
108+
@if (overflowed().has('addToShelf')) {
109+
<app-menu-item value="add-to-shelf" [disabled]="isResolving()" [submenu]="bulkShelfMenuHost.ariaMenu()">{{ 'book.menu.addToShelf' | transloco }}</app-menu-item>
110+
}
111+
@if (overflowed().has('markAs')) {
112+
<app-menu-item value="mark-as" [disabled]="isResolving()" [submenu]="bulkMarkAsMenu.menu">{{ 'book.menu.markAs' | transloco }}</app-menu-item>
113+
}
114+
@if (overflowed().has('edit')) {
115+
<app-menu-item value="edit" [disabled]="isResolving()" [submenu]="bulkEditMenu.menu">{{ 'common.edit' | transloco }}</app-menu-item>
116+
}
117+
@if (overflowed().has('metadata')) {
118+
<app-menu-item value="metadata" [disabled]="isResolving()" [submenu]="bulkMetadataMenu.menu">{{ 'book.menu.metadata' | transloco }}</app-menu-item>
119+
}
120+
<app-menu-separator [class]="autoSeparatorClass" />
121+
@if (actionPermissions().canResetGrimmoryProgress) {
122+
<app-menu-item value="reset-grimmory-progress" [disabled]="isResolving()" (selected)="commands.resetProgress('GRIMMORY')">{{ 'book.menu.resetGrimmoryProgress' | transloco }}</app-menu-item>
123+
}
124+
@if (actionPermissions().canResetKoreaderProgress) {
125+
<app-menu-item value="reset-koreader-progress" [disabled]="isResolving()" (selected)="commands.resetProgress('KOREADER')">{{ 'book.menu.resetKoreaderProgress' | transloco }}</app-menu-item>
126+
}
127+
<app-menu-separator [class]="autoSeparatorClass" />
128+
@if (bulkPermissions().canOrganizeFiles) {
129+
<app-menu-item value="organize-files" [disabled]="isResolving()" (selected)="commands.organizeFiles()">{{ 'browse.bulk.organizeFiles' | transloco }}</app-menu-item>
130+
}
131+
@if (bulkPermissions().canAttachFiles) {
132+
<app-menu-item value="attach-files" [disabled]="!bulkAttachEligible() || isResolving()" (selected)="commands.attachFiles()">
133+
{{ 'book.fileAttacher.attachFilesBulk' | transloco }}
134+
</app-menu-item>
135+
}
136+
<app-menu-separator [class]="autoSeparatorClass" />
137+
@if (overflowed().has('delete')) {
138+
<app-menu-item value="delete" variant="destructive" [loading]="bulkDeleting()" [disabled]="isResolving()" (selected)="commands.delete()">
139+
{{ 'common.delete' | transloco }}
140+
</app-menu-item>
141+
}
142+
</app-menu>
143+
}

0 commit comments

Comments
 (0)