diff --git a/frontend/src/app/features/book/service/sort.service.spec.ts b/frontend/src/app/features/book/service/sort.service.spec.ts deleted file mode 100644 index dfbebbd8d3..0000000000 --- a/frontend/src/app/features/book/service/sort.service.spec.ts +++ /dev/null @@ -1,88 +0,0 @@ -import {describe, expect, it, vi} from 'vitest'; - -import {SortDirection, SortOption} from '../model/sort.model'; -import {Book, ReadStatus} from '../model/book.model'; -import {SortService} from './sort.service'; - -function makeBook(id: number, overrides: Partial = {}): Book { - return { - id, - libraryId: 1, - libraryName: 'Library', - fileName: `Book ${id}`, - primaryFile: {id, bookId: id, bookType: 'EPUB'}, - metadata: { - bookId: id, - title: `Book ${id}`, - authors: [`Author ${id}`], - seriesName: `Series ${id}`, - publishedDate: '2024-01-01', - pageCount: id * 100, - }, - ...overrides, - }; -} - -describe('SortService', () => { - const service = new SortService(); - - it('returns the original array when no sort is provided', () => { - const books = [makeBook(1), makeBook(2)]; - - expect(service.applySort(books, null)).toBe(books); - expect(service.applyMultiSort(books, [])).toBe(books); - }); - - it('sorts by title and file name using natural ordering', () => { - const books = [ - makeBook(2, {fileName: 'Book 10', metadata: {bookId: 2, title: 'Book 10'}}), - makeBook(1, {fileName: 'Book 2', metadata: {bookId: 1, title: 'Book 2'}}), - ]; - const sortOption: SortOption = {label: 'Title', field: 'title', direction: SortDirection.ASCENDING}; - - expect(service.applySort(books, sortOption).map(book => book.id)).toEqual([1, 2]); - }); - - it('sorts by primaryFile file name if file name is missing', () => { - const books = [ - makeBook(2, {fileName: undefined, primaryFile: { id: 2, bookId: 2, fileName: 'Book 2' }, metadata: {bookId: 2, title: 'Book 10'}}), - makeBook(1, {fileName: 'Book 1', primaryFile: { id: 1, bookId: 1, fileName: 'Book 3' }, metadata: {bookId: 1, title: 'Book 2'}}), - ]; - const sortOption: SortOption = {label: 'File Name', field: 'fileName', direction: SortDirection.ASCENDING}; - - expect(service.applySort(books, sortOption).map(book => book.id)).toEqual([1, 2]); - }); - - it('sorts by array fields and read status rank', () => { - const books = [ - makeBook(1, {metadata: {bookId: 1, authors: ['Jane Zed']}, readStatus: ReadStatus.READ}), - makeBook(2, {metadata: {bookId: 2, authors: ['Adam Alpha']}, readStatus: ReadStatus.READING}), - makeBook(3, {metadata: {bookId: 3, authors: ['Jane Zed']}, readStatus: ReadStatus.UNREAD}), - ]; - - expect(service.applyMultiSort(books, [ - {label: 'Authors', field: 'authorSurnameVorname', direction: SortDirection.ASCENDING}, - ]).map(book => book.id)).toEqual([2, 1, 3]); - - expect(service.applyMultiSort(books, [ - {label: 'Status', field: 'readStatus', direction: SortDirection.DESCENDING}, - ]).map(book => book.id)).toEqual([1, 2, 3]); - }); - - it('keeps null values sorted after non-null values and warns on unknown fields', () => { - const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); - const books = [ - makeBook(1, {metadata: {bookId: 1}}), - makeBook(2, {metadata: {bookId: 2, publishedDate: '2023-01-01'}}), - ]; - - expect(service.applyMultiSort(books, [ - {label: 'Published', field: 'publishedDate', direction: SortDirection.ASCENDING}, - ]).map(book => book.id)).toEqual([2, 1]); - - expect(service.applyMultiSort(books, [ - {label: 'Unknown', field: 'doesNotExist', direction: SortDirection.ASCENDING}, - ])).toEqual(books); - expect(warnSpy).toHaveBeenCalledWith('[SortService] No extractor for field: doesNotExist'); - }); -}); diff --git a/frontend/src/app/features/book/service/sort.service.ts b/frontend/src/app/features/book/service/sort.service.ts deleted file mode 100644 index b182760768..0000000000 --- a/frontend/src/app/features/book/service/sort.service.ts +++ /dev/null @@ -1,177 +0,0 @@ -import {Injectable} from '@angular/core'; -import {Book, ReadStatus} from '../model/book.model'; -import {SortDirection, SortOption} from "../model/sort.model"; - -@Injectable({ - providedIn: 'root', -}) -export class SortService { - - private naturalCompare(a: string, b: string): number { - if (a == null && b == null) return 0; - if (a == null) return 1; - if (b == null) return -1; - - const aStr = a.toString(); - const bStr = b.toString(); - - const chunkRegex = /(\d+|\D+)/g; - - const aChunks = aStr.match(chunkRegex) || [aStr]; - const bChunks = bStr.match(chunkRegex) || [bStr]; - - const maxLength = Math.max(aChunks.length, bChunks.length); - - for (let i = 0; i < maxLength; i++) { - const aChunk = aChunks[i] || ''; - const bChunk = bChunks[i] || ''; - - if (aChunk === '' && bChunk === '') continue; - - const aIsNumeric = /^\d+$/.test(aChunk); - const bIsNumeric = /^\d+$/.test(bChunk); - - if (aIsNumeric && bIsNumeric) { - const aNum = parseInt(aChunk, 10); - const bNum = parseInt(bChunk, 10); - if (aNum !== bNum) { - return aNum - bNum; - } - } else { - const comparison = aChunk.localeCompare(bChunk); - if (comparison !== 0) { - return comparison; - } - } - } - - return aChunks.length - bChunks.length; - } - - private static readonly READ_STATUS_RANK: Record = { - [ReadStatus.UNSET]: 0, - [ReadStatus.UNREAD]: 1, - [ReadStatus.READING]: 2, - [ReadStatus.RE_READING]: 3, - [ReadStatus.PARTIALLY_READ]: 4, - [ReadStatus.PAUSED]: 5, - [ReadStatus.READ]: 6, - [ReadStatus.ABANDONED]: 7, - [ReadStatus.WONT_READ]: 8, - }; - - private readonly fieldExtractors: Record unknown> = { - title: (book) => book.metadata?.title?.toLowerCase() || null, - author: (book) => book.metadata?.authors?.map(a => a.toLowerCase()).join(", ") || null, - authorSurnameVorname: (book) => book.metadata?.authors?.map(a => { - const parts = a.trim().split(/\s+/); - if (parts.length < 2) return a.toLowerCase(); - const surname = parts.pop(); - const firstname = parts.join(" "); - return `${surname}, ${firstname}`.toLowerCase(); - }).join(", ") || null, - publishedDate: (book) => { - const date = book.metadata?.publishedDate; - return date === null || date === undefined ? null : new Date(date).getTime(); - }, - publisher: (book) => book.metadata?.publisher || null, - pageCount: (book) => book.metadata?.pageCount || null, - rating: (book) => book.metadata?.rating || null, - personalRating: (book) => book.personalRating || null, - reviewCount: (book) => book.metadata?.reviewCount || null, - amazonRating: (book) => book.metadata?.amazonRating || null, - amazonReviewCount: (book) => book.metadata?.amazonReviewCount || null, - goodreadsRating: (book) => book.metadata?.goodreadsRating || null, - goodreadsReviewCount: (book) => book.metadata?.goodreadsReviewCount || null, - hardcoverRating: (book) => book.metadata?.hardcoverRating || null, - hardcoverReviewCount: (book) => book.metadata?.hardcoverReviewCount || null, - ranobedbRating: (book) => book.metadata?.ranobedbRating || null, - locked: (book) => book.metadata?.allMetadataLocked ?? false, - lastReadTime: (book) => book.lastReadTime ? new Date(book.lastReadTime).getTime() : null, - addedOn: (book) => book.addedOn ? new Date(book.addedOn).getTime() : null, - fileSizeKb: (book) => book.fileSizeKb ?? book.primaryFile?.fileSizeKb ?? null, - fileName: (book) => book.fileName ?? book.primaryFile?.fileName ?? null, - filePath: (book) => book.filePath ?? book.primaryFile?.filePath ?? null, - random: () => Math.random(), - seriesName: (book) => book.metadata?.seriesName?.toLowerCase() || null, - seriesNumber: (book) => book.metadata?.seriesNumber ?? null, - readStatus: (book) => book.readStatus ? (SortService.READ_STATUS_RANK[book.readStatus] ?? null) : null, - dateFinished: (book) => book.dateFinished ? new Date(book.dateFinished).getTime() : null, - readingProgress: (book) => - book.epubProgress?.percentage - ?? book.pdfProgress?.percentage - ?? book.cbxProgress?.percentage - ?? book.audiobookProgress?.percentage - ?? book.koreaderProgress?.percentage - ?? book.koboProgress?.percentage - ?? null, - bookType: (book) => book.primaryFile?.bookType || null, - narrator: (book) => book.metadata?.narrator?.toLowerCase() || null, - }; - - applySort(books: Book[], selectedSort: SortOption | null): Book[] { - if (!selectedSort) return books; - return this.applyMultiSort(books, [selectedSort]); - } - - applyMultiSort(books: Book[], sortCriteria: SortOption[]): Book[] { - if (!sortCriteria || sortCriteria.length === 0) return books; - - return books.slice().sort((a, b) => { - for (const criterion of sortCriteria) { - const result = this.compareByCriterion(a, b, criterion); - if (result !== 0) return result; - } - return 0; - }); - } - - private compareByCriterion(a: Book, b: Book, criterion: SortOption): number { - const extractor = this.fieldExtractors[criterion.field]; - - if (!extractor) { - console.warn(`[SortService] No extractor for field: ${criterion.field}`); - return 0; - } - - const aValue = extractor(a); - const bValue = extractor(b); - - const result = this.compareValues(aValue, bValue); - - return criterion.direction === SortDirection.ASCENDING ? result : -result; - } - - private compareValues(aValue: unknown, bValue: unknown): number { - if (Array.isArray(aValue) && Array.isArray(bValue)) { - return this.compareArrays(aValue, bValue); - } else if (typeof aValue === 'string' && typeof bValue === 'string') { - return this.naturalCompare(aValue, bValue); - } else if (typeof aValue === 'number' && typeof bValue === 'number') { - return aValue - bValue; - } else { - if (aValue == null && bValue != null) return 1; - if (aValue != null && bValue == null) return -1; - return 0; - } - } - - private compareArrays(aValue: unknown[], bValue: unknown[]): number { - for (let i = 0; i < aValue.length; i++) { - const valA = aValue[i]; - const valB = bValue[i]; - - if (typeof valA === 'string' && typeof valB === 'string') { - const result = this.naturalCompare(valA, valB); - if (result !== 0) return result; - } else if (typeof valA === 'number' && typeof valB === 'number') { - const result = valA - valB; - if (result !== 0) return result; - } else { - if (valA == null && valB != null) return 1; - if (valA != null && valB == null) return -1; - } - } - return 0; - } -} diff --git a/frontend/src/app/features/dashboard/components/dashboard-scroller/dashboard-scroller.component.html b/frontend/src/app/features/dashboard/components/dashboard-scroller/dashboard-scroller.component.html index b45c029f57..10a9c91938 100644 --- a/frontend/src/app/features/dashboard/components/dashboard-scroller/dashboard-scroller.component.html +++ b/frontend/src/app/features/dashboard/components/dashboard-scroller/dashboard-scroller.component.html @@ -1,5 +1,5 @@ -
+

{{ title() | transloco }} @if (isMagicShelf()) { @@ -7,38 +7,54 @@

}

- @if (books() !== null && books()?.length === 0) { -
-
- + @switch (viewState()) { + @case ('idle') { +
+
-

{{ t('noBooksFound') }}

-
- } - - @if (books() && books()!.length > 0) { -
- @for (book of books(); track book.id; let i = $index) { -
- - -
- } -
- } - - @if (books() === null) { -
-
- + } + @case ('skeleton') { +
+ @for (skeleton of skeletons; track $index) { +
+ +
+ } +
+ } + @case ('error') { +
+ +

{{ t('loadError') }}

+
-

{{ t('loadError') }}

-
+ } + @case ('empty') { +
+ +

{{ t('noBooksFound') }}

+
+ } + @case ('books') { +
+ @for (rowBook of rowBooks(); track rowBook.book.id) { +
+ +
+ } +
+ + + } }
diff --git a/frontend/src/app/features/dashboard/components/dashboard-scroller/dashboard-scroller.component.scss b/frontend/src/app/features/dashboard/components/dashboard-scroller/dashboard-scroller.component.scss index faaac559b9..26477e9048 100644 --- a/frontend/src/app/features/dashboard/components/dashboard-scroller/dashboard-scroller.component.scss +++ b/frontend/src/app/features/dashboard/components/dashboard-scroller/dashboard-scroller.component.scss @@ -33,69 +33,6 @@ } } -.dashboard-scroller-no-books { - padding: 2.625rem 1.75rem; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - text-align: center; - position: relative; - z-index: 2; - - .empty-state-icon { - width: 25px; - height: 25px; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - margin-bottom: 1.75rem; - position: relative; - - i { - font-size: 1.75rem; - color: var(--color-text); - } - } - - p { - color: var(--color-text-secondary); - font-size: var(--app-text-base); - font-weight: 500; - max-width: 400px; - line-height: 1.7; - } -} - -.dashboard-scroller-strip { - display: flex; - overflow: auto hidden; - scroll-snap-type: x mandatory; - padding: 0.875rem 0.4375rem; - gap: 1.75rem; - width: 100%; - position: relative; - z-index: 2; -} - -.dashboard-scroller-card { - width: 116px; - flex: 0 0 124px; - scroll-snap-align: start; - position: relative; - transition: transform 0.15s ease; - - &.square-card { - width: 160px; - flex-basis: 160px; - } - - &:hover { - transform: translateY(-2px); - } -} - .magic-shelf-icon { margin-left: 0.4375rem; color: var(--color-primary) !important; @@ -103,34 +40,3 @@ background-clip: unset !important; -webkit-text-fill-color: var(--color-primary) !important; } - -@media (width <= 767px) { - .dashboard-scroller-card { - height: 184px; - width: 108px; - flex-basis: 108px; - - &.square-card { - width: 150px; - height: auto; - flex-basis: 150px; - } - } - - .dashboard-scroller-no-books { - padding: 3.5rem 0.875rem; - - .empty-state-icon { - width: 80px; - height: 80px; - - i { - font-size: 1.75rem; - } - } - - p { - font-size: 0.9625rem; - } - } -} diff --git a/frontend/src/app/features/dashboard/components/dashboard-scroller/dashboard-scroller.component.ts b/frontend/src/app/features/dashboard/components/dashboard-scroller/dashboard-scroller.component.ts index 3b5524236d..409de97c86 100644 --- a/frontend/src/app/features/dashboard/components/dashboard-scroller/dashboard-scroller.component.ts +++ b/frontend/src/app/features/dashboard/components/dashboard-scroller/dashboard-scroller.component.ts @@ -1,19 +1,36 @@ -import {ChangeDetectionStrategy, Component, computed, input} from '@angular/core'; -import {NgClass} from '@angular/common'; -import {Book} from '../../../book/model/book.model'; -import {ScrollerType} from '../../models/dashboard-config.model'; -import {LegacyBookCardComponent} from '../../../book/components/legacy-book-card/legacy-book-card.component'; -import {BookCardOverlayPreferenceService} from '../../../book/components/legacy-book-card/book-card-overlay-preference.service'; +import {ChangeDetectionStrategy, Component, computed, inject, input, viewChild} from '@angular/core'; +import {injectQuery} from '@tanstack/angular-query-experimental'; import {TranslocoDirective, TranslocoPipe} from '@jsverse/transloco'; +import {BookCardComponent} from '../../../book/components/cards/book-card.component'; +import {BOOK_CARD_COVER_ASPECT, bookCardHeightForWidth} from '../../../book/components/cards/book-card.layout'; +import {BookCardSkeletonComponent} from '../../../book/components/cards/book-card-skeleton.component'; +import {BookMenuComponent} from '../../../book/components/book-menu/book-menu.component'; +import {BookQueryService} from '../../../book/data/book-query.service'; +import {type BookSummary} from '../../../book/data/book-response.models'; +import {BookNavigationService} from '../../../book/service/book-navigation.service'; +import {UserService} from '../../../settings/user-management/user.service'; +import {createBrowseSkeletonDelay} from '../../../../shared/browse/skeleton-delay'; +import {ArtworkRevealGroupDirective} from '../../../../shared/components/cover/artwork-reveal-group.directive'; +import {AppButtonComponent} from '../../../../shared/ui/button/app-button.component'; +import {dashboardRowBooks, dashboardRowQueryParams} from '../../dashboard-row-query'; +import {type ScrollerConfig, ScrollerType} from '../../models/dashboard-config.model'; + +const CARD_BASE_WIDTH = 140; +const SKELETONS = Array.from({length: 8}); +type ScrollerViewState = 'idle' | 'skeleton' | 'error' | 'empty' | 'books'; + @Component({ selector: 'app-dashboard-scroller', changeDetection: ChangeDetectionStrategy.OnPush, templateUrl: './dashboard-scroller.component.html', styleUrls: ['./dashboard-scroller.component.scss'], imports: [ - LegacyBookCardComponent, - NgClass, + AppButtonComponent, + ArtworkRevealGroupDirective, + BookCardComponent, + BookCardSkeletonComponent, + BookMenuComponent, TranslocoDirective, TranslocoPipe ], @@ -21,12 +38,67 @@ import {TranslocoDirective, TranslocoPipe} from '@jsverse/transloco'; }) export class DashboardScrollerComponent { - readonly bookListType = input(null); - readonly title = input.required(); - readonly books = input(null); - readonly isMagicShelf = input(false); - readonly useSquareCovers = input(false); - readonly overlayPreferenceService = input.required(); + readonly config = input.required(); + + private readonly bookQuery = inject(BookQueryService); + private readonly userService = inject(UserService); + protected readonly bookNavigation = inject(BookNavigationService); + protected readonly bookMenu = viewChild(BookMenuComponent); + + private readonly params = computed(() => dashboardRowQueryParams(this.config())); + private readonly rowQuery = injectQuery(() => { + const params = this.params(); + return { + ...this.bookQuery.page(params ?? {facets: {}, facetLogic: 'or', sort: [], size: 1}), + enabled: params !== null, + }; + }); + + protected readonly title = computed(() => this.config().title); + protected readonly isMagicShelf = computed(() => this.config().type === ScrollerType.MAGIC_SHELF); + protected readonly squareCovers = computed(() => this.config().type === ScrollerType.LAST_LISTENED); + protected readonly cardWidth = computed(() => + this.squareCovers() ? Math.round(CARD_BASE_WIDTH * BOOK_CARD_COVER_ASPECT) : CARD_BASE_WIDTH); + protected readonly cardHeight = computed(() => + bookCardHeightForWidth(this.cardWidth(), {square: this.squareCovers(), metaLines: 2})); + protected readonly showFormatPill = computed(() => + this.userService.currentUser()?.userSettings.entityViewPreferences?.global.overlayBookType ?? true); + + protected readonly rowBooks = computed(() => + dashboardRowBooks(this.config(), this.params() === null ? [] : this.rowQuery.data()?.content ?? [])); + protected readonly books = computed(() => this.rowBooks().map(({book}) => book)); + private readonly skeletonVisible = createBrowseSkeletonDelay( + computed(() => this.rowQuery.status()), + computed(() => this.rowBooks().length > 0)); + + protected readonly viewState = computed(() => { + if (this.rowBooks().length > 0) { + return 'books'; + } + if (this.params() === null) { + return 'empty'; + } + switch (this.rowQuery.status()) { + case 'error': + return 'error'; + case 'success': + return 'empty'; + default: + return this.skeletonVisible() ? 'skeleton' : 'idle'; + } + }); + protected readonly menuOpenBookId = computed(() => this.bookMenu()?.openBookId() ?? null); + protected readonly skeletons = SKELETONS; + protected readonly stripClass = + 'relative z-[2] -mx-4 flex gap-4 overflow-x-auto overflow-y-hidden px-4 pt-2 pb-5'; + protected readonly noticeClass = + 'relative z-[2] flex flex-col items-center gap-7 px-7 py-10 text-center'; + + protected retry(): void { + void this.rowQuery.refetch(); + } - readonly forceEbookMode = computed(() => this.bookListType() === ScrollerType.LAST_READ); + protected openBook(book: BookSummary): void { + this.bookNavigation.openBook(book.id, this.books().map(({id}) => id)); + } } diff --git a/frontend/src/app/features/dashboard/components/main-dashboard/main-dashboard.component.html b/frontend/src/app/features/dashboard/components/main-dashboard/main-dashboard.component.html index e301e4f917..79811b4943 100644 --- a/frontend/src/app/features/dashboard/components/main-dashboard/main-dashboard.component.html +++ b/frontend/src/app/features/dashboard/components/main-dashboard/main-dashboard.component.html @@ -1,73 +1,45 @@
- @if (isBooksLoading()) { -
- - -
- } @else { -
-
- @if (isLibrariesEmpty()) { -
- @if (userService.currentUser()?.permissions; as permissions) { -
- @if (permissions.admin || permissions.canManageLibrary) { -
-

- {{ t('welcomeTitle') }}
- {{ t('welcomeSubtitle') }} -

-

- {{ t('welcomeDescription') }} -

- - -
- } -
- } -
- } @else { - @if (dashboardConfig(); as config) { - @for (scroller of enabledScrollers(); track scroller.id; let idx = $index) { - @if (idx < 2) { - - - } @else { - @defer (on viewport) { - - - } @placeholder { -
- } +
+
+ @if (isLibrariesEmpty()) { +
+ @if (userService.currentUser()?.permissions; as permissions) { +
+ @if (permissions.admin || permissions.canManageLibrary) { +
+

+ {{ t('welcomeTitle') }}
+ {{ t('welcomeSubtitle') }} +

+

+ {{ t('welcomeDescription') }} +

+ + +
} +
+ } +
+ } @else { + @for (scroller of enabledScrollers(); track scroller.id; let idx = $index) { + @if (idx < 2) { + + } @else { + @defer (on viewport) { + + } @placeholder { +
} } } -
+ }
- } +
diff --git a/frontend/src/app/features/dashboard/components/main-dashboard/main-dashboard.component.scss b/frontend/src/app/features/dashboard/components/main-dashboard/main-dashboard.component.scss index 03d49b4458..cf07423ff9 100644 --- a/frontend/src/app/features/dashboard/components/main-dashboard/main-dashboard.component.scss +++ b/frontend/src/app/features/dashboard/components/main-dashboard/main-dashboard.component.scss @@ -12,7 +12,6 @@ justify-content: center; min-height: 100%; padding: 0.9625rem 0.9625rem 0; - text-align: center; position: relative; z-index: 1; } @@ -23,13 +22,6 @@ } } -.loading-state { - display: flex; - align-items: center; - justify-content: center; - height: 100%; -} - .dashboard-inner { width: 100%; } @@ -60,6 +52,7 @@ border: 1px solid var(--color-border); position: relative; overflow: hidden; + text-align: center; } .dashboard .p-dialog { @@ -71,6 +64,6 @@ } .scroller-placeholder { - height: 260px; + height: 330px; margin-bottom: 1.75rem; } diff --git a/frontend/src/app/features/dashboard/components/main-dashboard/main-dashboard.component.ts b/frontend/src/app/features/dashboard/components/main-dashboard/main-dashboard.component.ts index e96c322beb..14e9782d7e 100644 --- a/frontend/src/app/features/dashboard/components/main-dashboard/main-dashboard.component.ts +++ b/frontend/src/app/features/dashboard/components/main-dashboard/main-dashboard.component.ts @@ -1,18 +1,12 @@ import {ChangeDetectionStrategy, Component, computed, inject} from '@angular/core'; import {Button} from '@openng/optimus-ui/button'; import {DashboardScrollerComponent} from '../dashboard-scroller/dashboard-scroller.component'; -import {BookService} from '../../../book/service/book.service'; import {UserService} from '../../../settings/user-management/user.service'; -import {ProgressSpinner} from '@openng/optimus-ui/progressspinner'; import {TranslocoDirective, TranslocoService} from '@jsverse/transloco'; import {DashboardConfigService} from '../../services/dashboard-config.service'; -import {ScrollerConfig, ScrollerType} from '../../models/dashboard-config.model'; import {DialogLauncherService} from '../../../../shared/services/dialog-launcher.service'; import {PageTitleService} from '../../../../shared/service/page-title.service'; import {LibraryService} from '../../../book/service/library.service'; -import {BookCardOverlayPreferenceService} from '../../../book/components/legacy-book-card/book-card-overlay-preference.service'; -import {DashboardBookService} from '../../services/dashboard-book.service'; -import {Book} from '../../../book/model/book.model'; @Component({ selector: 'app-main-dashboard', @@ -22,43 +16,30 @@ import {Book} from '../../../book/model/book.model'; imports: [ Button, DashboardScrollerComponent, - ProgressSpinner, TranslocoDirective ], standalone: true }) export class MainDashboardComponent { - private readonly bookService = inject(BookService); private readonly libraryService = inject(LibraryService); private readonly dialogLauncher = inject(DialogLauncherService); protected readonly userService = inject(UserService); private readonly dashboardConfigService = inject(DashboardConfigService); - private readonly dashboardBookService = inject(DashboardBookService); private readonly pageTitle = inject(PageTitleService); private readonly t = inject(TranslocoService); - protected readonly overlayPreferenceService = inject(BookCardOverlayPreferenceService); - readonly dashboardConfig = this.dashboardConfigService.config; - readonly isBooksLoading = this.bookService.isBooksLoading; readonly isLibrariesEmpty = computed(() => !this.libraryService.isLibrariesLoading() && this.libraryService.libraries().length === 0 ); - readonly enabledScrollers = computed(() => { - return this.dashboardConfig().scrollers.filter(s => s.enabled); - }); - - ScrollerType = ScrollerType; + protected readonly enabledScrollers = computed(() => + this.dashboardConfigService.config().scrollers.filter(scroller => scroller.enabled)); constructor() { this.pageTitle.setPageTitle(this.t.translate('dashboard.main.pageTitle')); } - getBooksForScroller(config: ScrollerConfig): Book[] { - return this.dashboardBookService.scrollerBooksMap().get(config.id) ?? []; - } - createNewLibrary() { void this.dialogLauncher.openLibraryCreateDialog().catch(() => undefined); } diff --git a/frontend/src/app/features/dashboard/dashboard-row-query.spec.ts b/frontend/src/app/features/dashboard/dashboard-row-query.spec.ts new file mode 100644 index 0000000000..251c5e1f76 --- /dev/null +++ b/frontend/src/app/features/dashboard/dashboard-row-query.spec.ts @@ -0,0 +1,52 @@ +import {describe, expect, it} from 'vitest'; + +import {type BookFileResponse, type BookSummary} from '../book/data/book-response.models'; +import {dashboardRowBooks} from './dashboard-row-query'; +import {type ScrollerConfig, ScrollerType} from './models/dashboard-config.model'; + +function file(bookId: number, id: number, bookType: 'EPUB' | 'CBX' | 'AUDIOBOOK'): BookFileResponse { + return {id, bookId, book: true, folderBased: false, bookType}; +} + +const READING: BookSummary = { + id: 1, + libraryId: 1, + libraryName: 'Library', + primaryFile: file(1, 10, 'EPUB'), + epubProgress: {cfi: null, href: null, contentSourceProgressPercent: null, percentage: 40, ttsPositionCfi: null}, +}; +const LISTENING: BookSummary = { + id: 2, + libraryId: 1, + libraryName: 'Library', + primaryFile: file(2, 20, 'EPUB'), + alternativeFormats: [file(2, 21, 'AUDIOBOOK')], + audiobookProgress: {positionMs: 10, trackIndex: 0, trackPositionMs: 10, percentage: 55}, +}; +const UNSTARTED: BookSummary = {id: 3, libraryId: 1, libraryName: 'Library', primaryFile: file(3, 30, 'EPUB')}; +const READING_ALTERNATIVE: BookSummary = { + id: 4, + libraryId: 1, + libraryName: 'Library', + primaryFile: file(4, 40, 'CBX'), + alternativeFormats: [file(4, 41, 'EPUB')], + epubProgress: {cfi: null, href: null, contentSourceProgressPercent: null, percentage: 55, ttsPositionCfi: null}, + koboProgress: {percentage: 3}, +}; + +function row(type: ScrollerType): ScrollerConfig { + return {id: 'row-1', type, title: 'Row', enabled: true, order: 1, maxItems: 20}; +} + +describe('dashboardRowBooks', () => { + it('keeps the books started in the row format, and says which file that is', () => { + const all = [READING, LISTENING, UNSTARTED, READING_ALTERNATIVE]; + + expect(dashboardRowBooks(row(ScrollerType.LAST_READ), all)).toEqual([ + {book: READING, file: READING.primaryFile}, + {book: READING_ALTERNATIVE, file: file(4, 41, 'EPUB')}, + ]); + expect(dashboardRowBooks(row(ScrollerType.LAST_LISTENED), all)) + .toEqual([{book: LISTENING, file: file(2, 21, 'AUDIOBOOK')}]); + }); +}); diff --git a/frontend/src/app/features/dashboard/dashboard-row-query.ts b/frontend/src/app/features/dashboard/dashboard-row-query.ts new file mode 100644 index 0000000000..42481ef2ae --- /dev/null +++ b/frontend/src/app/features/dashboard/dashboard-row-query.ts @@ -0,0 +1,131 @@ +import {bookGrimmoryProgress, bookProgressPercentage} from '../book/data/book-actions'; +import {bookSortTermsFromCriteria} from '../book/browse/book-browse-sort'; +import { + type BookPageParams, + type BookSortTerm, + DEFAULT_BOOK_SORT_TERMS, +} from '../book/data/book-query-params'; +import { + BOOK_FILE_TYPES, + type BookFileResponse, + type BookFileType, + type BookSummary, + type KnownBookReadStatus, +} from '../book/data/book-response.models'; +import {DEFAULT_MAX_ITEMS, type ScrollerConfig, ScrollerType} from './models/dashboard-config.model'; + +const IN_PROGRESS_STATUSES: readonly KnownBookReadStatus[] = ['READING', 'RE_READING', 'PAUSED']; +const RANDOM_EXCLUDED_STATUSES: readonly KnownBookReadStatus[] = [ + 'READ', + 'PARTIALLY_READ', + 'READING', + 'PAUSED', + 'WONT_READ', + 'ABANDONED', +]; +const EBOOK_FILE_TYPES: readonly BookFileType[] = BOOK_FILE_TYPES.filter(type => type !== 'AUDIOBOOK'); +const AUDIOBOOK_FILE_TYPES: readonly BookFileType[] = ['AUDIOBOOK']; +const PROGRESS_QUERY_SIZE_MULTIPLIER = 2; + +interface DashboardRowBook { + readonly book: BookSummary; + readonly file?: BookFileResponse; +} + +function progressFileTypes(type: ScrollerType): readonly BookFileType[] | null { + switch (type) { + case ScrollerType.LAST_READ: + return EBOOK_FILE_TYPES; + case ScrollerType.LAST_LISTENED: + return AUDIOBOOK_FILE_TYPES; + default: + return null; + } +} + +export function dashboardRowQueryParams(config: ScrollerConfig): BookPageParams | null { + const size = rowSize(config); + const fileTypes = progressFileTypes(config.type); + if (fileTypes !== null) { + return inProgressParams(fileTypes, size); + } + + switch (config.type) { + case ScrollerType.LATEST_ADDED: + return { + facets: {}, + facetLogic: 'or', + sort: [{key: 'addedOn', direction: 'desc'}], + size, + }; + case ScrollerType.RANDOM: + return { + facets: {read_status: RANDOM_EXCLUDED_STATUSES}, + facetLogic: 'not', + sort: [{key: 'random', direction: 'asc'}], + size, + }; + case ScrollerType.MAGIC_SHELF: + if (config.magicShelfId == null) { + return null; + } + return { + facets: {shelf: [`magic:${config.magicShelfId}`]}, + facetLogic: 'or', + sort: magicShelfSort(config), + size, + }; + default: + return null; + } +} + +export function dashboardRowBooks( + config: ScrollerConfig, + books: readonly BookSummary[], +): readonly DashboardRowBook[] { + const fileTypes = progressFileTypes(config.type); + if (fileTypes === null) { + return books.map(book => ({book})); + } + + return books + .flatMap(book => { + const file = startedFile(book, fileTypes); + return file ? [{book, file}] : []; + }) + .slice(0, rowSize(config)); +} + +function startedFile(book: BookSummary, fileTypes: readonly string[]): BookFileResponse | undefined { + const files = [book.primaryFile, ...(book.alternativeFormats ?? [])] + .filter(file => file?.bookType != null && fileTypes.includes(file.bookType)); + + return files.find(file => bookGrimmoryProgress(book, file) !== null) + ?? files.find(file => bookProgressPercentage(book, file) !== null); +} + +function rowSize(config: ScrollerConfig): number { + return config.maxItems || DEFAULT_MAX_ITEMS; +} + +function inProgressParams(fileTypes: readonly BookFileType[], rowSize: number): BookPageParams { + return { + facets: {read_status: IN_PROGRESS_STATUSES, file_type: fileTypes}, + facetLogic: 'or', + sort: [{key: 'lastReadTime', direction: 'desc'}], + size: rowSize * PROGRESS_QUERY_SIZE_MULTIPLIER, + }; +} + +function magicShelfSort(config: ScrollerConfig): readonly BookSortTerm[] { + if (!config.sortField) { + return DEFAULT_BOOK_SORT_TERMS; + } + + const terms = bookSortTermsFromCriteria([{ + field: config.sortField, + direction: config.sortDirection === 'desc' ? 'DESC' : 'ASC', + }]); + return terms.length > 0 ? terms : DEFAULT_BOOK_SORT_TERMS; +} diff --git a/frontend/src/app/features/dashboard/services/dashboard-book.service.ts b/frontend/src/app/features/dashboard/services/dashboard-book.service.ts deleted file mode 100644 index a530d255c6..0000000000 --- a/frontend/src/app/features/dashboard/services/dashboard-book.service.ts +++ /dev/null @@ -1,168 +0,0 @@ -import {computed, inject, Injectable} from '@angular/core'; -import {BookService} from '../../book/service/book.service'; -import {Book, ReadStatus} from '../../book/model/book.model'; -import {MagicShelfService} from '../../magic-shelf/service/magic-shelf.service'; -import {BookRuleEvaluatorService} from '../../magic-shelf/service/book-rule-evaluator.service'; -import {SortService} from '../../book/service/sort.service'; -import {ScrollerConfig, ScrollerType} from '../models/dashboard-config.model'; -import {SortDirection, SortOption} from '../../book/model/sort.model'; -import {DashboardConfigService} from './dashboard-config.service'; -import {GroupRule} from '../../magic-shelf/component/magic-shelf-component'; - -const DEFAULT_MAX_ITEMS = 20; - -@Injectable({ - providedIn: 'root' -}) -export class DashboardBookService { - private readonly bookService = inject(BookService); - private readonly magicShelfService = inject(MagicShelfService); - private readonly ruleEvaluatorService = inject(BookRuleEvaluatorService); - private readonly sortService = inject(SortService); - private readonly configService = inject(DashboardConfigService); - - /** - * Computed map of scroller ID to its filtered book list. - * This centralizes all dashboard filtering logic and keeps it reactive. - */ - readonly scrollerBooksMap = computed(() => { - const config = this.configService.config(); - const books = this.bookService.books(); - const shelves = this.magicShelfService.shelves(); - const scrollerMap = new Map(); - - for (const scroller of config.scrollers) { - if (!scroller.enabled) continue; - scrollerMap.set(scroller.id, this.getBooksForConfig(scroller, books, shelves)); - } - - return scrollerMap; - }); - - private getBooksForConfig(config: ScrollerConfig, books: Book[], magicShelves: {id?: number | null; filterJson: string}[]): Book[] { - switch (config.type) { - case ScrollerType.LAST_READ: - return this.getLastReadBooks(books, config.maxItems || DEFAULT_MAX_ITEMS); - case ScrollerType.LAST_LISTENED: - return this.getLastListenedBooks(books, config.maxItems || DEFAULT_MAX_ITEMS); - case ScrollerType.LATEST_ADDED: - return this.getLatestAddedBooks(books, config.maxItems || DEFAULT_MAX_ITEMS); - case ScrollerType.RANDOM: - return this.getRandomBooks(books, config.maxItems || DEFAULT_MAX_ITEMS); - case ScrollerType.MAGIC_SHELF: - return this.getMagicShelfBooks(config, books, magicShelves); - default: - return []; - } - } - - private getLastReadBooks(books: Book[], maxItems: number): Book[] { - const recentBooks = books.filter(book => - book.lastReadTime && - (book.readStatus === ReadStatus.READING || book.readStatus === ReadStatus.RE_READING || book.readStatus === ReadStatus.PAUSED) && - this.hasEbookProgress(book) - ); - - return recentBooks.sort((a, b) => { - const aTime = new Date(a.lastReadTime!).getTime(); - const bTime = new Date(b.lastReadTime!).getTime(); - return bTime - aTime; - }).slice(0, maxItems); - } - - private getLastListenedBooks(books: Book[], maxItems: number): Book[] { - const recentBooks = books.filter(book => - book.lastReadTime && - (book.readStatus === ReadStatus.READING || book.readStatus === ReadStatus.RE_READING || book.readStatus === ReadStatus.PAUSED) && - book.audiobookProgress - ); - - return recentBooks.sort((a, b) => { - const aTime = new Date(a.lastReadTime!).getTime(); - const bTime = new Date(b.lastReadTime!).getTime(); - return bTime - aTime; - }).slice(0, maxItems); - } - - private hasEbookProgress(book: Book): boolean { - return !!(book.epubProgress || book.pdfProgress || book.cbxProgress || book.koreaderProgress || book.koboProgress); - } - - private getLatestAddedBooks(books: Book[], maxItems: number): Book[] { - const addedBooks = books.filter(book => book.addedOn); - - return addedBooks.sort((a, b) => { - const aTime = new Date(a.addedOn!).getTime(); - const bTime = new Date(b.addedOn!).getTime(); - return bTime - aTime; - }).slice(0, maxItems); - } - - private getRandomBooks(books: Book[], maxItems: number): Book[] { - const excludedStatuses = new Set([ - ReadStatus.READ, - ReadStatus.PARTIALLY_READ, - ReadStatus.READING, - ReadStatus.PAUSED, - ReadStatus.WONT_READ, - ReadStatus.ABANDONED - ]); - - const candidates = books.filter(book => - !book.readStatus || !excludedStatuses.has(book.readStatus) - ); - - return this.shuffleBooks(candidates, maxItems); - } - - private getMagicShelfBooks( - config: ScrollerConfig, - books: Book[], - magicShelves: {id?: number | null; filterJson: string}[] - ): Book[] { - const shelf = magicShelves.find(currentShelf => currentShelf.id === config.magicShelfId); - if (!shelf) { - return []; - } - - let group: GroupRule; - try { - group = JSON.parse(shelf.filterJson); - } catch (e) { - console.error('Invalid filter JSON', e); - return []; - } - - let filteredBooks = books.filter(book => - this.ruleEvaluatorService.evaluateGroup(book, group, books) - ); - - if (config.sortField && config.sortDirection) { - const sortOption = this.createSortOption(config.sortField, config.sortDirection); - filteredBooks = this.sortService.applySort(filteredBooks, sortOption); - } - - if (config.maxItems) { - filteredBooks = filteredBooks.slice(0, config.maxItems); - } - - return filteredBooks; - } - - private createSortOption(field: string, direction: string): SortOption { - return { - field, - direction: direction === 'asc' ? SortDirection.ASCENDING : SortDirection.DESCENDING, - label: '' - }; - } - - private shuffleBooks(books: Book[], maxItems: number): Book[] { - const shuffled = [...books]; - for (let i = shuffled.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; - } - return shuffled.slice(0, maxItems); - } -} diff --git a/frontend/src/app/features/settings/view-preferences-parent/dashboard-preferences/dashboard-preferences.component.ts b/frontend/src/app/features/settings/view-preferences-parent/dashboard-preferences/dashboard-preferences.component.ts index 29efa5cf55..d5276b2362 100644 --- a/frontend/src/app/features/settings/view-preferences-parent/dashboard-preferences/dashboard-preferences.component.ts +++ b/frontend/src/app/features/settings/view-preferences-parent/dashboard-preferences/dashboard-preferences.component.ts @@ -53,8 +53,6 @@ export class DashboardPreferencesComponent { const t = (key: string) => this.translocoService.translate(`settingsView.dashboardScrollers.${key}`); return [ {label: t('sortFields.title'), value: 'title'}, - {label: t('sortFields.fileName'), value: 'fileName'}, - {label: t('sortFields.filePath'), value: 'filePath'}, {label: t('sortFields.addedOn'), value: 'addedOn'}, {label: t('sortFields.author'), value: 'author'}, {label: t('sortFields.authorSurnameVorname'), value: 'authorSurnameVorname'}, @@ -67,7 +65,6 @@ export class DashboardPreferencesComponent { {label: t('sortFields.readStatus'), value: 'readStatus'}, {label: t('sortFields.dateFinished'), value: 'dateFinished'}, {label: t('sortFields.readingProgress'), value: 'readingProgress'}, - {label: t('sortFields.bookType'), value: 'bookType'}, {label: t('sortFields.pageCount'), value: 'pageCount'} ]; }); diff --git a/frontend/src/app/shared/components/cover/cover.component.ts b/frontend/src/app/shared/components/cover/cover.component.ts index 00716b36d6..63dce2b8d1 100644 --- a/frontend/src/app/shared/components/cover/cover.component.ts +++ b/frontend/src/app/shared/components/cover/cover.component.ts @@ -74,8 +74,10 @@ export class CoverComponent { this.closePreview(); }); afterNextRender(() => { - if (!this.showImage() || this.preview()) { + if (this.preview()) { this.markReady(); + } else if (!this.showImage()) { + requestAnimationFrame(() => requestAnimationFrame(this.markReady)); } }); } diff --git a/frontend/src/i18n/cs.json b/frontend/src/i18n/cs.json index cc202a920a..88f50ca3ff 100644 --- a/frontend/src/i18n/cs.json +++ b/frontend/src/i18n/cs.json @@ -496,8 +496,6 @@ }, "sortFields": { "title": "", - "fileName": "", - "filePath": "", "addedOn": "", "author": "", "authorSurnameVorname": "", @@ -510,7 +508,6 @@ "readStatus": "", "dateFinished": "", "readingProgress": "", - "bookType": "", "pageCount": "" }, "sortDirections": { diff --git a/frontend/src/i18n/da.json b/frontend/src/i18n/da.json index 3f6410de18..044b3b1fd7 100644 --- a/frontend/src/i18n/da.json +++ b/frontend/src/i18n/da.json @@ -481,8 +481,6 @@ }, "sortFields": { "title": "", - "fileName": "", - "filePath": "", "addedOn": "", "author": "", "authorSurnameVorname": "", @@ -495,7 +493,6 @@ "readStatus": "", "dateFinished": "", "readingProgress": "", - "bookType": "", "pageCount": "" }, "sortDirections": { diff --git a/frontend/src/i18n/de.json b/frontend/src/i18n/de.json index c55bf2970c..149188de03 100644 --- a/frontend/src/i18n/de.json +++ b/frontend/src/i18n/de.json @@ -456,8 +456,6 @@ }, "sortFields": { "title": "Titel", - "fileName": "Dateiname", - "filePath": "Dateipfad", "addedOn": "Hinzugefügt am", "author": "Autor", "authorSurnameVorname": "Autor (Nachname)", @@ -470,7 +468,6 @@ "readStatus": "Lesestatus", "dateFinished": "Beendet am", "readingProgress": "Lesefortschritt", - "bookType": "Dateiformat", "pageCount": "Seiten" }, "sortDirections": { diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json index c7a0d0fe7f..d79ac63233 100644 --- a/frontend/src/i18n/en.json +++ b/frontend/src/i18n/en.json @@ -497,8 +497,6 @@ }, "sortFields": { "title": "Title", - "fileName": "File Name", - "filePath": "File Path", "addedOn": "Date Added", "author": "Author", "authorSurnameVorname": "Author (Surname)", @@ -511,7 +509,6 @@ "readStatus": "Read Status", "dateFinished": "Date Finished", "readingProgress": "Reading Progress", - "bookType": "File Format", "pageCount": "Pages" }, "sortDirections": { diff --git a/frontend/src/i18n/es.json b/frontend/src/i18n/es.json index 065f0e954c..e5f3fa20ba 100644 --- a/frontend/src/i18n/es.json +++ b/frontend/src/i18n/es.json @@ -456,8 +456,6 @@ }, "sortFields": { "title": "Título", - "fileName": "Nombre de archivo", - "filePath": "Ruta de archivo", "addedOn": "Fecha de adición", "author": "Autor", "authorSurnameVorname": "Autor (Apellido)", @@ -470,7 +468,6 @@ "readStatus": "Estado de lectura", "dateFinished": "Fecha de finalización", "readingProgress": "Progreso de lectura", - "bookType": "Formato de archivo", "pageCount": "Páginas" }, "sortDirections": { diff --git a/frontend/src/i18n/fr.json b/frontend/src/i18n/fr.json index 14b2f422cd..1247000f4c 100644 --- a/frontend/src/i18n/fr.json +++ b/frontend/src/i18n/fr.json @@ -456,8 +456,6 @@ }, "sortFields": { "title": "Titre", - "fileName": "Nom de fichier", - "filePath": "Chemin du fichier", "addedOn": "Date d'ajout", "author": "Auteur", "authorSurnameVorname": "Auteur (Nom de famille)", @@ -470,7 +468,6 @@ "readStatus": "Statut de lecture", "dateFinished": "Date de fin", "readingProgress": "Progression de lecture", - "bookType": "Type de livre", "pageCount": "Pages" }, "sortDirections": { diff --git a/frontend/src/i18n/hr.json b/frontend/src/i18n/hr.json index 3ce4d9bfb2..e6fe1ea52c 100644 --- a/frontend/src/i18n/hr.json +++ b/frontend/src/i18n/hr.json @@ -456,8 +456,6 @@ }, "sortFields": { "title": "Naslov", - "fileName": "Naziv datoteke", - "filePath": "Putanja datoteke", "addedOn": "Datum dodavanja", "author": "Autor", "authorSurnameVorname": "Autor (prezime)", @@ -470,7 +468,6 @@ "readStatus": "Status čitanja", "dateFinished": "Datum završetka", "readingProgress": "Napredak čitanja", - "bookType": "Format datoteke", "pageCount": "Stranice" }, "sortDirections": { diff --git a/frontend/src/i18n/hu.json b/frontend/src/i18n/hu.json index 39f697055e..9f441739ff 100644 --- a/frontend/src/i18n/hu.json +++ b/frontend/src/i18n/hu.json @@ -481,8 +481,6 @@ }, "sortFields": { "title": "Cím", - "fileName": "Fájlnév", - "filePath": "Elérési Útvonal", "addedOn": "Hozzáadás Dátuma", "author": "Szerző", "authorSurnameVorname": "Szerző (családnév)", @@ -495,7 +493,6 @@ "readStatus": "Olvasás Állapota", "dateFinished": "Befejezés Dátuma", "readingProgress": "Olvasás Előrehaladottsága", - "bookType": "Fájlformátum", "pageCount": "Oldalak" }, "sortDirections": { diff --git a/frontend/src/i18n/id.json b/frontend/src/i18n/id.json index 2111be3a51..f4cb6387b4 100644 --- a/frontend/src/i18n/id.json +++ b/frontend/src/i18n/id.json @@ -455,8 +455,6 @@ }, "sortFields": { "title": "Judul", - "fileName": "", - "filePath": "", "addedOn": "", "author": "Penulis", "authorSurnameVorname": "", @@ -469,7 +467,6 @@ "readStatus": "", "dateFinished": "", "readingProgress": "", - "bookType": "", "pageCount": "Halaman" }, "sortDirections": { diff --git a/frontend/src/i18n/it.json b/frontend/src/i18n/it.json index 122a7ed767..b62bc6f5dd 100644 --- a/frontend/src/i18n/it.json +++ b/frontend/src/i18n/it.json @@ -456,8 +456,6 @@ }, "sortFields": { "title": "Titolo", - "fileName": "Nome file", - "filePath": "Percorso file", "addedOn": "Data aggiunta", "author": "Autore", "authorSurnameVorname": "Autore (Cognome)", @@ -470,7 +468,6 @@ "readStatus": "Stato di lettura", "dateFinished": "Data completamento", "readingProgress": "Progresso di lettura", - "bookType": "Tipo di libro", "pageCount": "Pagine" }, "sortDirections": { diff --git a/frontend/src/i18n/ja.json b/frontend/src/i18n/ja.json index 828c6bb419..c496182eca 100644 --- a/frontend/src/i18n/ja.json +++ b/frontend/src/i18n/ja.json @@ -456,8 +456,6 @@ }, "sortFields": { "title": "タイトル", - "fileName": "ファイル名", - "filePath": "ファイルパス", "addedOn": "追加日", "author": "著者", "authorSurnameVorname": "著者(姓)", @@ -470,7 +468,6 @@ "readStatus": "読書状態", "dateFinished": "完了日", "readingProgress": "読書進捗", - "bookType": "本の種類", "pageCount": "ページ数" }, "sortDirections": { diff --git a/frontend/src/i18n/ko.json b/frontend/src/i18n/ko.json index cc202a920a..88f50ca3ff 100644 --- a/frontend/src/i18n/ko.json +++ b/frontend/src/i18n/ko.json @@ -496,8 +496,6 @@ }, "sortFields": { "title": "", - "fileName": "", - "filePath": "", "addedOn": "", "author": "", "authorSurnameVorname": "", @@ -510,7 +508,6 @@ "readStatus": "", "dateFinished": "", "readingProgress": "", - "bookType": "", "pageCount": "" }, "sortDirections": { diff --git a/frontend/src/i18n/nl.json b/frontend/src/i18n/nl.json index 97c3b97409..6f82497375 100644 --- a/frontend/src/i18n/nl.json +++ b/frontend/src/i18n/nl.json @@ -483,8 +483,6 @@ }, "sortFields": { "title": "Titel", - "fileName": "Bestandsnaam", - "filePath": "Bestandspad", "addedOn": "Datum toegevoegd", "author": "Auteur", "authorSurnameVorname": "Auteur (Achternaam)", @@ -497,7 +495,6 @@ "readStatus": "Leesstatus", "dateFinished": "Datum voltooid", "readingProgress": "Leesvoortgang", - "bookType": "Boektype", "pageCount": "Pagina's" }, "sortDirections": { diff --git a/frontend/src/i18n/pl.json b/frontend/src/i18n/pl.json index 38dd0eab29..9002b541cd 100644 --- a/frontend/src/i18n/pl.json +++ b/frontend/src/i18n/pl.json @@ -456,8 +456,6 @@ }, "sortFields": { "title": "Tytuł", - "fileName": "Nazwa pliku", - "filePath": "Ścieżka pliku", "addedOn": "Data dodania", "author": "Autor", "authorSurnameVorname": "Autor (Nazwisko)", @@ -470,7 +468,6 @@ "readStatus": "Status czytania", "dateFinished": "Data ukończenia", "readingProgress": "Postęp czytania", - "bookType": "Typ książki", "pageCount": "Strony" }, "sortDirections": { diff --git a/frontend/src/i18n/pt.json b/frontend/src/i18n/pt.json index ba0dd59960..9c7612dd7f 100644 --- a/frontend/src/i18n/pt.json +++ b/frontend/src/i18n/pt.json @@ -456,8 +456,6 @@ }, "sortFields": { "title": "Título", - "fileName": "Nome do Arquivo", - "filePath": "Caminho do Arquivo", "addedOn": "Data de Adição", "author": "Autor", "authorSurnameVorname": "Autor (Sobrenome)", @@ -470,7 +468,6 @@ "readStatus": "Status de Leitura", "dateFinished": "Data de Conclusão", "readingProgress": "Progresso de Leitura", - "bookType": "Tipo de Livro", "pageCount": "Páginas" }, "sortDirections": { diff --git a/frontend/src/i18n/ru.json b/frontend/src/i18n/ru.json index 1f257dc52b..e7998bf8aa 100644 --- a/frontend/src/i18n/ru.json +++ b/frontend/src/i18n/ru.json @@ -456,8 +456,6 @@ }, "sortFields": { "title": "Название", - "fileName": "Имя файла", - "filePath": "Путь к файлу", "addedOn": "Дата добавления", "author": "Автор", "authorSurnameVorname": "Автор (фамилия)", @@ -470,7 +468,6 @@ "readStatus": "Статус чтения", "dateFinished": "Дата завершения", "readingProgress": "Прогресс чтения", - "bookType": "Тип книги", "pageCount": "Страницы" }, "sortDirections": { diff --git a/frontend/src/i18n/sk.json b/frontend/src/i18n/sk.json index 611652bf37..6d5ff9eedd 100644 --- a/frontend/src/i18n/sk.json +++ b/frontend/src/i18n/sk.json @@ -483,8 +483,6 @@ }, "sortFields": { "title": "", - "fileName": "", - "filePath": "", "addedOn": "", "author": "", "authorSurnameVorname": "", @@ -497,7 +495,6 @@ "readStatus": "", "dateFinished": "", "readingProgress": "", - "bookType": "", "pageCount": "" }, "sortDirections": { diff --git a/frontend/src/i18n/sl.json b/frontend/src/i18n/sl.json index 27fec5b7e6..9fb4eb04e9 100644 --- a/frontend/src/i18n/sl.json +++ b/frontend/src/i18n/sl.json @@ -456,8 +456,6 @@ }, "sortFields": { "title": "Naslov", - "fileName": "Ime datoteke", - "filePath": "Pot datoteke", "addedOn": "Datum dodajanja", "author": "Avtor", "authorSurnameVorname": "Avtor (Priimek)", @@ -470,7 +468,6 @@ "readStatus": "Status branja", "dateFinished": "Datum dokončanja", "readingProgress": "Napredek branja", - "bookType": "Oblika datoteke", "pageCount": "Strani" }, "sortDirections": { diff --git a/frontend/src/i18n/sv.json b/frontend/src/i18n/sv.json index d1812211c5..80ca44a8a9 100644 --- a/frontend/src/i18n/sv.json +++ b/frontend/src/i18n/sv.json @@ -456,8 +456,6 @@ }, "sortFields": { "title": "Titel", - "fileName": "Filnamn", - "filePath": "Filsökväg", "addedOn": "Tillagd datum", "author": "Författare", "authorSurnameVorname": "Författare (efternamn)", @@ -470,7 +468,6 @@ "readStatus": "Lässtatus", "dateFinished": "Avslutad datum", "readingProgress": "Läsframsteg", - "bookType": "Filformat", "pageCount": "Sidor" }, "sortDirections": { diff --git a/frontend/src/i18n/uk.json b/frontend/src/i18n/uk.json index f6a534da59..cb63e059c1 100644 --- a/frontend/src/i18n/uk.json +++ b/frontend/src/i18n/uk.json @@ -456,8 +456,6 @@ }, "sortFields": { "title": "Назва", - "fileName": "Назва файлу", - "filePath": "Шлях до файлу", "addedOn": "Дата додавання", "author": "Автор", "authorSurnameVorname": "Автор (Прізвище)", @@ -470,7 +468,6 @@ "readStatus": "Статус читання", "dateFinished": "Дата завершення", "readingProgress": "Прогрес читання", - "bookType": "Тип книги", "pageCount": "Сторінки" }, "sortDirections": { diff --git a/frontend/src/i18n/zh.json b/frontend/src/i18n/zh.json index 6486a0ffef..3f6892d711 100644 --- a/frontend/src/i18n/zh.json +++ b/frontend/src/i18n/zh.json @@ -456,8 +456,6 @@ }, "sortFields": { "title": "标题", - "fileName": "文件名", - "filePath": "文件路径", "addedOn": "添加日期", "author": "作者", "authorSurnameVorname": "作者(姓氏)", @@ -470,7 +468,6 @@ "readStatus": "阅读状态", "dateFinished": "完成日期", "readingProgress": "阅读进度", - "bookType": "书籍类型", "pageCount": "页数" }, "sortDirections": {