diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/BookLoreUser.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/BookLoreUser.java index 011bbc035..b8fe745bd 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/BookLoreUser.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/BookLoreUser.java @@ -113,6 +113,7 @@ public class BookLoreUser { private Float letterSpacing; private Float lineHeight; private String flow; + private String spread; } @Data diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/EpubViewerPreferences.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/EpubViewerPreferences.java index f6360a2e1..258f67bf1 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/EpubViewerPreferences.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/EpubViewerPreferences.java @@ -10,6 +10,7 @@ public class EpubViewerPreferences { private String theme; private String font; private String flow; + private String spread; private Integer fontSize; private Float letterSpacing; private Float lineHeight; diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/request/MetadataRefreshOptions.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/request/MetadataRefreshOptions.java index a58084e16..e0b385114 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/request/MetadataRefreshOptions.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/request/MetadataRefreshOptions.java @@ -20,8 +20,8 @@ public class MetadataRefreshOptions { private Boolean reviewBeforeApply; @NotNull(message = "Field options cannot be null") private FieldOptions fieldOptions; - @NotNull(message = "Skip fields cannot be null") - private SkipFields skipFields; + @NotNull(message = "Enabled fields cannot be null") + private EnabledFields enabledFields; @Getter @Setter @@ -76,7 +76,7 @@ public class MetadataRefreshOptions { @NoArgsConstructor @AllArgsConstructor @Builder - public static class SkipFields { + public static class EnabledFields { private boolean title; private boolean subtitle; private boolean description; diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/entity/EpubViewerPreferencesEntity.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/entity/EpubViewerPreferencesEntity.java index 412c987a2..5e6fb116d 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/model/entity/EpubViewerPreferencesEntity.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/model/entity/EpubViewerPreferencesEntity.java @@ -41,4 +41,7 @@ public class EpubViewerPreferencesEntity { @Column(name = "flow") private String flow; + + @Column(name = "spread") + private String spread; } \ No newline at end of file diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/BookService.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/BookService.java index 9b02a4a7d..5cacb26de 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/BookService.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/BookService.java @@ -193,6 +193,7 @@ public class BookService { .fontSize(epubPref.getFontSize()) .theme(epubPref.getTheme()) .flow(epubPref.getFlow()) + .spread(epubPref.getSpread()) .letterSpacing(epubPref.getLetterSpacing()) .lineHeight(epubPref.getLineHeight()) .build())); @@ -272,6 +273,7 @@ public class BookService { epubPrefs.setFontSize(epubSettings.getFontSize()); epubPrefs.setTheme(epubSettings.getTheme()); epubPrefs.setFlow(epubSettings.getFlow()); + epubPrefs.setSpread(epubSettings.getSpread()); epubPrefs.setLetterSpacing(epubSettings.getLetterSpacing()); epubPrefs.setLineHeight(epubSettings.getLineHeight()); epubViewerPreferencesRepository.save(epubPrefs); diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/appsettings/SettingPersistenceHelper.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/appsettings/SettingPersistenceHelper.java index d6c56d31a..53c9cffc9 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/appsettings/SettingPersistenceHelper.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/appsettings/SettingPersistenceHelper.java @@ -148,35 +148,35 @@ public class SettingPersistenceHelper { .tags(nullProvider) .build(); - MetadataRefreshOptions.SkipFields skipFields = MetadataRefreshOptions.SkipFields.builder() - .title(false) - .subtitle(false) - .description(false) - .authors(false) - .publisher(false) - .publishedDate(false) - .seriesName(false) - .seriesNumber(false) - .seriesTotal(false) - .isbn13(false) - .isbn10(false) - .language(false) - .categories(false) - .cover(false) - .pageCount(false) - .asin(false) - .goodreadsId(false) - .comicvineId(false) - .hardcoverId(false) - .googleId(false) - .amazonRating(false) - .amazonReviewCount(false) - .goodreadsRating(false) - .goodreadsReviewCount(false) - .hardcoverRating(false) - .hardcoverReviewCount(false) - .moods(false) - .tags(false) + MetadataRefreshOptions.EnabledFields enabledFields = MetadataRefreshOptions.EnabledFields.builder() + .title(true) + .subtitle(true) + .description(true) + .authors(true) + .publisher(true) + .publishedDate(true) + .seriesName(true) + .seriesNumber(true) + .seriesTotal(true) + .isbn13(true) + .isbn10(true) + .language(true) + .categories(true) + .cover(true) + .pageCount(true) + .asin(true) + .goodreadsId(true) + .comicvineId(true) + .hardcoverId(true) + .googleId(true) + .amazonRating(true) + .amazonReviewCount(true) + .goodreadsRating(true) + .goodreadsReviewCount(true) + .hardcoverRating(true) + .hardcoverReviewCount(true) + .moods(true) + .tags(true) .build(); return MetadataRefreshOptions.builder() @@ -185,7 +185,7 @@ public class SettingPersistenceHelper { .mergeCategories(true) .reviewBeforeApply(false) .fieldOptions(fieldOptions) - .skipFields(skipFields) + .enabledFields(enabledFields) .build(); } diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/MetadataRefreshService.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/MetadataRefreshService.java index 808c66f12..a5010b098 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/MetadataRefreshService.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/metadata/MetadataRefreshService.java @@ -393,116 +393,116 @@ public class MetadataRefreshService { public BookMetadata buildFetchMetadata(Long bookId, MetadataRefreshOptions refreshOptions, Map metadataMap) { BookMetadata metadata = BookMetadata.builder().bookId(bookId).build(); MetadataRefreshOptions.FieldOptions fieldOptions = refreshOptions.getFieldOptions(); - MetadataRefreshOptions.SkipFields skipFields = refreshOptions.getSkipFields(); + MetadataRefreshOptions.EnabledFields enabledFields = refreshOptions.getEnabledFields(); - if (!skipFields.isTitle()) { + if (enabledFields.isTitle()) { metadata.setTitle(resolveFieldAsString(metadataMap, fieldOptions.getTitle(), BookMetadata::getTitle)); } - if (!skipFields.isSubtitle()) { + if (enabledFields.isSubtitle()) { metadata.setSubtitle(resolveFieldAsString(metadataMap, fieldOptions.getSubtitle(), BookMetadata::getSubtitle)); } - if (!skipFields.isDescription()) { + if (enabledFields.isDescription()) { metadata.setDescription(resolveFieldAsString(metadataMap, fieldOptions.getDescription(), BookMetadata::getDescription)); } - if (!skipFields.isAuthors()) { + if (enabledFields.isAuthors()) { metadata.setAuthors(resolveFieldAsList(metadataMap, fieldOptions.getAuthors(), BookMetadata::getAuthors)); } - if (!skipFields.isPublisher()) { + if (enabledFields.isPublisher()) { metadata.setPublisher(resolveFieldAsString(metadataMap, fieldOptions.getPublisher(), BookMetadata::getPublisher)); } - if (!skipFields.isPublishedDate()) { + if (enabledFields.isPublishedDate()) { metadata.setPublishedDate(resolveField(metadataMap, fieldOptions.getPublishedDate(), BookMetadata::getPublishedDate)); } - if (!skipFields.isSeriesName()) { + if (enabledFields.isSeriesName()) { metadata.setSeriesName(resolveFieldAsString(metadataMap, fieldOptions.getSeriesName(), BookMetadata::getSeriesName)); } - if (!skipFields.isSeriesNumber()) { + if (enabledFields.isSeriesNumber()) { metadata.setSeriesNumber(resolveField(metadataMap, fieldOptions.getSeriesNumber(), BookMetadata::getSeriesNumber)); } - if (!skipFields.isSeriesTotal()) { + if (enabledFields.isSeriesTotal()) { metadata.setSeriesTotal(resolveFieldAsInteger(metadataMap, fieldOptions.getSeriesTotal(), BookMetadata::getSeriesTotal)); } - if (!skipFields.isIsbn13()) { + if (enabledFields.isIsbn13()) { metadata.setIsbn13(resolveFieldAsString(metadataMap, fieldOptions.getIsbn13(), BookMetadata::getIsbn13)); } - if (!skipFields.isIsbn10()) { + if (enabledFields.isIsbn10()) { metadata.setIsbn10(resolveFieldAsString(metadataMap, fieldOptions.getIsbn10(), BookMetadata::getIsbn10)); } - if (!skipFields.isLanguage()) { + if (enabledFields.isLanguage()) { metadata.setLanguage(resolveFieldAsString(metadataMap, fieldOptions.getLanguage(), BookMetadata::getLanguage)); } - if (!skipFields.isPageCount()) { + if (enabledFields.isPageCount()) { metadata.setPageCount(resolveFieldAsInteger(metadataMap, fieldOptions.getPageCount(), BookMetadata::getPageCount)); } - if (!skipFields.isCover()) { + if (enabledFields.isCover()) { metadata.setThumbnailUrl(resolveFieldAsString(metadataMap, fieldOptions.getCover(), BookMetadata::getThumbnailUrl)); } - if (!skipFields.isAmazonRating()) { + if (enabledFields.isAmazonRating()) { if (metadataMap.containsKey(Amazon)) { metadata.setAmazonRating(metadataMap.get(Amazon).getAmazonRating()); } } - if (!skipFields.isAmazonReviewCount()) { + if (enabledFields.isAmazonReviewCount()) { if (metadataMap.containsKey(Amazon)) { metadata.setAmazonReviewCount(metadataMap.get(Amazon).getAmazonReviewCount()); } } - if (!skipFields.isGoodreadsRating()) { + if (enabledFields.isGoodreadsRating()) { if (metadataMap.containsKey(GoodReads)) { metadata.setGoodreadsRating(metadataMap.get(GoodReads).getGoodreadsRating()); } } - if (!skipFields.isGoodreadsReviewCount()) { + if (enabledFields.isGoodreadsReviewCount()) { if (metadataMap.containsKey(GoodReads)) { metadata.setGoodreadsReviewCount(metadataMap.get(GoodReads).getGoodreadsReviewCount()); } } - if (!skipFields.isHardcoverRating()) { + if (enabledFields.isHardcoverRating()) { if (metadataMap.containsKey(Hardcover)) { metadata.setHardcoverRating(metadataMap.get(Hardcover).getHardcoverRating()); } } - if (!skipFields.isHardcoverReviewCount()) { + if (enabledFields.isHardcoverReviewCount()) { if (metadataMap.containsKey(Hardcover)) { metadata.setHardcoverReviewCount(metadataMap.get(Hardcover).getHardcoverReviewCount()); } } - if (!skipFields.isAsin()) { + if (enabledFields.isAsin()) { if (metadataMap.containsKey(Amazon)) { metadata.setAsin(metadataMap.get(Amazon).getAsin()); } } - if (!skipFields.isGoodreadsId()) { + if (enabledFields.isGoodreadsId()) { if (metadataMap.containsKey(GoodReads)) { metadata.setGoodreadsId(metadataMap.get(GoodReads).getGoodreadsId()); } } - if (!skipFields.isHardcoverId()) { + if (enabledFields.isHardcoverId()) { if (metadataMap.containsKey(Hardcover)) { metadata.setHardcoverId(metadataMap.get(Hardcover).getHardcoverId()); } } - if (!skipFields.isGoogleId()) { + if (enabledFields.isGoogleId()) { if (metadataMap.containsKey(Google)) { metadata.setGoogleId(metadataMap.get(Google).getGoogleId()); } } - if (!skipFields.isComicvineId()) { + if (enabledFields.isComicvineId()) { if (metadataMap.containsKey(Comicvine)) { metadata.setComicvineId(metadataMap.get(Comicvine).getComicvineId()); } } - if (!skipFields.isMoods()) { + if (enabledFields.isMoods()) { if (metadataMap.containsKey(Hardcover)) { metadata.setMoods(metadataMap.get(Hardcover).getMoods()); } } - if (!skipFields.isTags()) { + if (enabledFields.isTags()) { if (metadataMap.containsKey(Hardcover)) { metadata.setTags(metadataMap.get(Hardcover).getTags()); } } - if (!skipFields.isCategories()) { + if (enabledFields.isCategories()) { if (refreshOptions.isMergeCategories()) { metadata.setCategories(getAllCategories(metadataMap, fieldOptions.getCategories(), BookMetadata::getCategories)); } else { @@ -596,4 +596,5 @@ public class MetadataRefreshService { case BOOKS -> request.getBookIds(); }; } -} \ No newline at end of file +} + diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/service/user/DefaultUserSettingsProvider.java b/booklore-api/src/main/java/com/adityachandel/booklore/service/user/DefaultUserSettingsProvider.java index 5ccded2f1..75b18e0c5 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/service/user/DefaultUserSettingsProvider.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/service/user/DefaultUserSettingsProvider.java @@ -69,6 +69,7 @@ public class DefaultUserSettingsProvider { .letterSpacing(null) .lineHeight(null) .flow("paginated") + .spread("double") .build(); } diff --git a/booklore-api/src/main/resources/db/migration/V54__Add_Spread_Column_Epub.sql b/booklore-api/src/main/resources/db/migration/V54__Add_Spread_Column_Epub.sql new file mode 100644 index 000000000..5ee28f020 --- /dev/null +++ b/booklore-api/src/main/resources/db/migration/V54__Add_Spread_Column_Epub.sql @@ -0,0 +1,2 @@ +ALTER TABLE epub_viewer_preference + ADD COLUMN IF NOT EXISTS spread VARCHAR(20) DEFAULT 'double'; \ No newline at end of file diff --git a/booklore-api/src/test/java/com/adityachandel/booklore/service/metadata/MetadataRefreshServiceTest.java b/booklore-api/src/test/java/com/adityachandel/booklore/service/metadata/MetadataRefreshServiceTest.java index fba06ea77..4344f023b 100644 --- a/booklore-api/src/test/java/com/adityachandel/booklore/service/metadata/MetadataRefreshServiceTest.java +++ b/booklore-api/src/test/java/com/adityachandel/booklore/service/metadata/MetadataRefreshServiceTest.java @@ -82,6 +82,39 @@ class MetadataRefreshServiceTest { setupTestEntities(); } + private MetadataRefreshOptions.EnabledFields allEnabledFields() { + return MetadataRefreshOptions.EnabledFields.builder() + .title(true) + .subtitle(true) + .description(true) + .authors(true) + .publisher(true) + .publishedDate(true) + .seriesName(true) + .seriesNumber(true) + .seriesTotal(true) + .isbn13(true) + .isbn10(true) + .language(true) + .categories(true) + .cover(true) + .pageCount(true) + .asin(true) + .goodreadsId(true) + .comicvineId(true) + .hardcoverId(true) + .googleId(true) + .amazonRating(true) + .amazonReviewCount(true) + .goodreadsRating(true) + .goodreadsReviewCount(true) + .hardcoverRating(true) + .hardcoverReviewCount(true) + .moods(true) + .tags(true) + .build(); + } + private void setupDefaultOptions() { MetadataRefreshOptions.FieldProvider titleProvider = MetadataRefreshOptions.FieldProvider.builder() .p3(MetadataProvider.Google) @@ -116,14 +149,14 @@ class MetadataRefreshServiceTest { .cover(coverProvider) .build(); - MetadataRefreshOptions.SkipFields skipFields = MetadataRefreshOptions.SkipFields.builder().build(); + MetadataRefreshOptions.EnabledFields skipFields = allEnabledFields(); defaultOptions = MetadataRefreshOptions.builder() .refreshCovers(true) .mergeCategories(false) .reviewBeforeApply(false) .fieldOptions(fieldOptions) - .skipFields(skipFields) + .enabledFields(skipFields) .build(); } @@ -136,7 +169,7 @@ class MetadataRefreshServiceTest { .title(titleProvider) .build(); - MetadataRefreshOptions.SkipFields skipFields = MetadataRefreshOptions.SkipFields.builder().build(); + MetadataRefreshOptions.EnabledFields skipFields = allEnabledFields(); libraryOptions = MetadataRefreshOptions.builder() .libraryId(1L) @@ -144,7 +177,7 @@ class MetadataRefreshServiceTest { .mergeCategories(true) .reviewBeforeApply(true) .fieldOptions(fieldOptions) - .skipFields(skipFields) + .enabledFields(skipFields) .build(); } @@ -160,7 +193,6 @@ class MetadataRefreshServiceTest { testLibrary.setId(1L); testLibrary.setName("Test Library"); - // Create AuthorEntity for proper type compatibility AuthorEntity authorEntity = new AuthorEntity(); authorEntity.setName("Test Author"); @@ -252,21 +284,20 @@ class MetadataRefreshServiceTest { @Test void testRefreshMetadata_WithRequestOptions_ShouldUseRequestOptions() { - // Given MetadataRefreshOptions.FieldProvider titleProvider = MetadataRefreshOptions.FieldProvider.builder() .p1(MetadataProvider.Hardcover) .build(); MetadataRefreshOptions.FieldOptions fieldOptions = MetadataRefreshOptions.FieldOptions.builder() .title(titleProvider) .build(); - MetadataRefreshOptions.SkipFields skipFields = MetadataRefreshOptions.SkipFields.builder().build(); + MetadataRefreshOptions.EnabledFields skipFields = allEnabledFields(); MetadataRefreshOptions requestOptions = MetadataRefreshOptions.builder() .refreshCovers(true) .mergeCategories(false) .reviewBeforeApply(false) .fieldOptions(fieldOptions) - .skipFields(skipFields) + .enabledFields(skipFields) .build(); MetadataRefreshRequest request = MetadataRefreshRequest.builder() @@ -322,14 +353,14 @@ class MetadataRefreshServiceTest { @Test void testRefreshMetadata_WithReviewMode_ShouldCreateTaskAndProposals() throws JsonProcessingException { - MetadataRefreshOptions.SkipFields skipFields = MetadataRefreshOptions.SkipFields.builder().build(); + MetadataRefreshOptions.EnabledFields skipFields = allEnabledFields(); MetadataRefreshOptions reviewOptions = MetadataRefreshOptions.builder() .refreshCovers(true) .mergeCategories(false) .reviewBeforeApply(true) .fieldOptions(defaultOptions.getFieldOptions()) - .skipFields(skipFields) + .enabledFields(skipFields) .build(); MetadataRefreshRequest request = MetadataRefreshRequest.builder() @@ -522,14 +553,14 @@ class MetadataRefreshServiceTest { .cover(coverProvider) .build(); - MetadataRefreshOptions.SkipFields skipFields = MetadataRefreshOptions.SkipFields.builder().build(); + MetadataRefreshOptions.EnabledFields skipFields = allEnabledFields(); MetadataRefreshOptions mergeOptions = MetadataRefreshOptions.builder() .refreshCovers(true) .mergeCategories(true) .reviewBeforeApply(false) .fieldOptions(fieldOptions) - .skipFields(skipFields) + .enabledFields(skipFields) .build(); Map metadataMap = new HashMap<>(); diff --git a/booklore-ui/src/app/book/components/epub-viewer/component/epub-viewer.component.html b/booklore-ui/src/app/book/components/epub-viewer/component/epub-viewer.component.html index c76c78411..807eda4c8 100644 --- a/booklore-ui/src/app/book/components/epub-viewer/component/epub-viewer.component.html +++ b/booklore-ui/src/app/book/components/epub-viewer/component/epub-viewer.component.html @@ -109,6 +109,36 @@ + @if (selectedFlow === 'paginated' && !isMobileDevice()) { + + +
+ +
+
+ + + +
+
+ + + +
+
+
+ } +
diff --git a/booklore-ui/src/app/book/components/epub-viewer/component/epub-viewer.component.scss b/booklore-ui/src/app/book/components/epub-viewer/component/epub-viewer.component.scss index 083c31eac..c51dd2920 100644 --- a/booklore-ui/src/app/book/components/epub-viewer/component/epub-viewer.component.scss +++ b/booklore-ui/src/app/book/components/epub-viewer/component/epub-viewer.component.scss @@ -178,3 +178,4 @@ ::ng-deep .p-divider.p-divider-horizontal { margin: 0.25rem 0 0.5rem 0 !important; } + diff --git a/booklore-ui/src/app/book/components/epub-viewer/component/epub-viewer.component.ts b/booklore-ui/src/app/book/components/epub-viewer/component/epub-viewer.component.ts index dd97a35ae..755e8906b 100644 --- a/booklore-ui/src/app/book/components/epub-viewer/component/epub-viewer.component.ts +++ b/booklore-ui/src/app/book/components/epub-viewer/component/epub-viewer.component.ts @@ -49,6 +49,7 @@ export class EpubViewerComponent implements OnInit, OnDestroy { selectedFlow?: string = 'paginated'; selectedTheme?: string = 'white'; selectedFontType?: string | null = null; + selectedSpread?: string = 'double'; lineHeight?: number; letterSpacing?: number; @@ -115,6 +116,7 @@ export class EpubViewerComponent implements OnInit, OnDestroy { const resolvedTheme = settingScope === 'Global' ? globalSettings.theme : individualSetting?.theme; const resolvedLineHeight = settingScope === 'Global' ? globalSettings.lineHeight : individualSetting?.lineHeight; const resolvedLetterSpacing = settingScope === 'Global' ? globalSettings.letterSpacing : individualSetting?.letterSpacing; + const resolvedSpread = settingScope === 'Global' ? globalSettings.spread || 'double' : individualSetting?.spread || 'double'; if (resolvedTheme != null) this.selectedTheme = resolvedTheme; if (resolvedFontFamily != null) this.selectedFontType = resolvedFontFamily; @@ -122,12 +124,14 @@ export class EpubViewerComponent implements OnInit, OnDestroy { if (resolvedLineHeight != null) this.lineHeight = resolvedLineHeight; if (resolvedLetterSpacing != null) this.letterSpacing = resolvedLetterSpacing; if (resolvedFlow != null) this.selectedFlow = resolvedFlow; + if (resolvedSpread != null) this.selectedSpread = resolvedSpread; this.rendition = this.book.renderTo(this.epubContainer.nativeElement, { flow: this.selectedFlow ?? 'paginated', manager: this.selectedFlow === 'scrolled' ? 'continuous' : 'default', width: '100%', height: '100%', + spread: this.selectedFlow === 'paginated' && !this.isMobileDevice() ? (this.selectedSpread === 'single' ? 'none' : this.selectedSpread) : 'none', allowScriptedContent: true, }); @@ -193,11 +197,35 @@ export class EpubViewerComponent implements OnInit, OnDestroy { manager: this.selectedFlow === 'scrolled' ? 'continuous' : 'default', width: '100%', height: '100%', + spread: this.selectedFlow === 'paginated' && !this.isMobileDevice() ? (this.selectedSpread === 'single' ? 'none' : this.selectedSpread) : 'none', allowScriptedContent: true, }); + this.rendition.themes.override('font-size', `${this.fontSize}%`); this.applyCombinedTheme(); + this.setupKeyListener(); + this.setupTouchListeners(); + this.rendition.display(cfi || undefined); + this.updateViewerSetting(); + } + changeSpreadMode(): void { + if (!this.rendition || !this.book || this.selectedFlow === 'scrolled' || this.isMobileDevice()) return; + + const cfi = this.rendition.currentLocation()?.start?.cfi; + this.rendition.destroy(); + + this.rendition = this.book.renderTo(this.epubContainer.nativeElement, { + flow: this.selectedFlow, + manager: 'default', + width: '100%', + height: '100%', + spread: this.selectedSpread === 'single' ? 'none' : this.selectedSpread, + allowScriptedContent: true, + }); + + this.rendition.themes.override('font-size', `${this.fontSize}%`); + this.applyCombinedTheme(); this.setupKeyListener(); this.setupTouchListeners(); this.rendition.display(cfi || undefined); @@ -255,6 +283,7 @@ export class EpubViewerComponent implements OnInit, OnDestroy { if (this.selectedFontType) epubSettings.font = this.selectedFontType; if (this.fontSize) epubSettings.fontSize = this.fontSize; if (this.selectedFlow) epubSettings.flow = this.selectedFlow; + if (this.selectedSpread === 'single' || this.selectedSpread === 'double') epubSettings.spread = this.selectedSpread; if (this.lineHeight) epubSettings.lineHeight = this.lineHeight; if (this.letterSpacing) epubSettings.letterSpacing = this.letterSpacing; @@ -287,19 +316,21 @@ export class EpubViewerComponent implements OnInit, OnDestroy { private setupTouchListeners(): void { if (!this.isMobileDevice() || this.selectedFlow === 'scrolled') return; - this.rendition.on('rendered', () => { + const container = this.epubContainer.nativeElement; + container.removeEventListener('touchstart', this.onTouchStart.bind(this)); + container.removeEventListener('touchend', this.onTouchEnd.bind(this)); + + container.addEventListener('touchstart', this.onTouchStart.bind(this), {passive: true}); + container.addEventListener('touchend', this.onTouchEnd.bind(this), {passive: true}); + + setTimeout(() => { const iframe = this.epubContainer.nativeElement.querySelector('iframe'); if (iframe && iframe.contentDocument) { const iframeDoc = iframe.contentDocument; - iframeDoc.addEventListener('touchstart', this.onTouchStart.bind(this), {passive: true}); iframeDoc.addEventListener('touchend', this.onTouchEnd.bind(this), {passive: true}); } - }); - - const container = this.epubContainer.nativeElement; - container.addEventListener('touchstart', this.onTouchStart.bind(this), {passive: true}); - container.addEventListener('touchend', this.onTouchEnd.bind(this), {passive: true}); + }, 500); } onTouchStart(event: TouchEvent): void { @@ -328,7 +359,7 @@ export class EpubViewerComponent implements OnInit, OnDestroy { } } - private isMobileDevice(): boolean { + public isMobileDevice(): boolean { return window.innerWidth <= 768; } diff --git a/booklore-ui/src/app/book/model/book.model.ts b/booklore-ui/src/app/book/model/book.model.ts index 081235f9f..6d87e5aac 100644 --- a/booklore-ui/src/app/book/model/book.model.ts +++ b/booklore-ui/src/app/book/model/book.model.ts @@ -187,6 +187,7 @@ export interface EpubViewerSetting { flow: string; lineHeight: number; letterSpacing: number; + spread: string; } export interface CbxViewerSetting { diff --git a/booklore-ui/src/app/metadata/metadata-options-dialog/metadata-advanced-fetch-options/metadata-advanced-fetch-options.component.html b/booklore-ui/src/app/metadata/metadata-options-dialog/metadata-advanced-fetch-options/metadata-advanced-fetch-options.component.html index 773dc9865..c79e5c284 100644 --- a/booklore-ui/src/app/metadata/metadata-options-dialog/metadata-advanced-fetch-options/metadata-advanced-fetch-options.component.html +++ b/booklore-ui/src/app/metadata/metadata-options-dialog/metadata-advanced-fetch-options/metadata-advanced-fetch-options.component.html @@ -2,8 +2,8 @@ - - + + - - + + @for (field of nonProviderSpecificFields; track field) { - - - +
SkipMetadata FieldEnabledField 4th Priority
Set All:Set All:
- + + {{ formatLabel(field) }}{{ formatLabel(field) }} @@ -107,12 +106,12 @@

Provider-Specific Fields

-

These fields are unique to specific providers and cannot have custom priority settings. Use the checkboxes to skip fetching these fields entirely.

+

These fields are unique to specific providers and cannot have custom priority settings. Use the checkboxes to enable/disable fetching these fields.

@for (field of providerSpecificFields; track field) {
- {{ formatLabel(field) }}
diff --git a/booklore-ui/src/app/metadata/metadata-options-dialog/metadata-advanced-fetch-options/metadata-advanced-fetch-options.component.ts b/booklore-ui/src/app/metadata/metadata-options-dialog/metadata-advanced-fetch-options/metadata-advanced-fetch-options.component.ts index c4a6e6864..5d340a5a1 100644 --- a/booklore-ui/src/app/metadata/metadata-options-dialog/metadata-advanced-fetch-options/metadata-advanced-fetch-options.component.ts +++ b/booklore-ui/src/app/metadata/metadata-options-dialog/metadata-advanced-fetch-options/metadata-advanced-fetch-options.component.ts @@ -50,7 +50,7 @@ export class MetadataAdvancedFetchOptionsComponent implements OnChanges { reviewBeforeApply: boolean = false; fieldOptions: FieldOptions = this.initializeFieldOptions(); - skipFields: Record = this.initializeSkipFields(); + enabledFields: Record = this.initializeEnabledFields(); bulkP1: string | null = null; bulkP2: string | null = null; @@ -74,9 +74,9 @@ export class MetadataAdvancedFetchOptionsComponent implements OnChanges { }, {} as FieldOptions); } - private initializeSkipFields(): Record { + private initializeEnabledFields(): Record { return this.fields.reduce((acc, field) => { - acc[field] = false; + acc[field] = true; return acc; }, {} as Record); } @@ -97,10 +97,10 @@ export class MetadataAdvancedFetchOptionsComponent implements OnChanges { } this.fieldOptions = backendFieldOptions; - if (this.currentMetadataOptions.skipFields) { - this.skipFields = {...this.skipFields, ...this.currentMetadataOptions.skipFields}; + if (this.currentMetadataOptions.enabledFields) { + this.enabledFields = {...this.enabledFields, ...this.currentMetadataOptions.enabledFields}; } else { - this.skipFields = this.initializeSkipFields(); + this.enabledFields = this.initializeEnabledFields(); } } } @@ -120,7 +120,7 @@ export class MetadataAdvancedFetchOptionsComponent implements OnChanges { submit() { const allFieldsHaveProvider = Object.entries(this.fieldOptions).every(([field, opt]) => - this.skipFields[field as keyof FieldOptions] || + !this.enabledFields[field as keyof FieldOptions] || this.isProviderSpecificField(field as keyof FieldOptions) || opt.p1 !== null || opt.p2 !== null || opt.p3 !== null || opt.p4 !== null ); @@ -134,7 +134,7 @@ export class MetadataAdvancedFetchOptionsComponent implements OnChanges { mergeCategories: this.mergeCategories, reviewBeforeApply: this.reviewBeforeApply, fieldOptions: this.fieldOptions, - skipFields: this.skipFields + enabledFields: this.enabledFields }; this.metadataOptionsSubmitted.emit(metadataRefreshOptions); @@ -146,7 +146,7 @@ export class MetadataAdvancedFetchOptionsComponent implements OnChanges { this.messageService.add({ severity: 'error', summary: 'Error', - detail: 'At least one provider (P1–P4) must be selected for each non-skipped book field.', + detail: 'At least one provider (P1–P4) must be selected for each enabled book field.', life: 5000 }); } @@ -158,7 +158,7 @@ export class MetadataAdvancedFetchOptionsComponent implements OnChanges { const value = provider === 'Clear All' ? null : provider; for (const field of this.nonProviderSpecificFields) { - if (!this.skipFields[field]) { + if (this.enabledFields[field]) { this.fieldOptions[field][priority] = value; } } @@ -189,7 +189,7 @@ export class MetadataAdvancedFetchOptionsComponent implements OnChanges { p4: null }; } - this.skipFields = this.initializeSkipFields(); + this.enabledFields = this.initializeEnabledFields(); // Reset bulk selectors this.bulkP1 = null; diff --git a/booklore-ui/src/app/metadata/model/request/metadata-refresh-options.model.ts b/booklore-ui/src/app/metadata/model/request/metadata-refresh-options.model.ts index 737993f72..8f1cd97a3 100644 --- a/booklore-ui/src/app/metadata/model/request/metadata-refresh-options.model.ts +++ b/booklore-ui/src/app/metadata/model/request/metadata-refresh-options.model.ts @@ -4,7 +4,7 @@ export interface MetadataRefreshOptions { mergeCategories: boolean; reviewBeforeApply: boolean; fieldOptions?: FieldOptions; - skipFields?: Record; + enabledFields?: Record; } export interface FieldProvider { diff --git a/booklore-ui/src/app/settings/library-metadata-settings-component/library-metadata-settings.component.html b/booklore-ui/src/app/settings/library-metadata-settings-component/library-metadata-settings.component.html index 35a34ff30..3f62426d1 100644 --- a/booklore-ui/src/app/settings/library-metadata-settings-component/library-metadata-settings.component.html +++ b/booklore-ui/src/app/settings/library-metadata-settings-component/library-metadata-settings.component.html @@ -22,6 +22,7 @@ The system checks your 1st priority provider first - if that provider doesn't have the specific field (like description or author), it automatically moves to your 2nd priority, then 3rd, and finally 4th. Leave a priority empty to skip it entirely. For example, if Amazon (1st) has no description but Google Books (2nd) does, Google's description will be used. + Use the Enable checkboxes to completely disable fetching for specific fields - disabled fields will be skipped entirely regardless of provider settings.

@@ -42,6 +43,7 @@

Override the default priority settings for specific libraries. For example, you might prefer Amazon for fiction but Google Books for technical books. Each library can have its own provider priority order while falling back to defaults for unspecified fields. + You can also enable or disable specific metadata fields per library - useful if certain libraries don't need specific data types like descriptions or covers.

diff --git a/booklore-ui/src/app/settings/reader-preferences/cbx-reader-preferences-component/cbx-reader-preferences-component.scss b/booklore-ui/src/app/settings/reader-preferences/cbx-reader-preferences-component/cbx-reader-preferences-component.scss index 8e6450366..2e740666f 100644 --- a/booklore-ui/src/app/settings/reader-preferences/cbx-reader-preferences-component/cbx-reader-preferences-component.scss +++ b/booklore-ui/src/app/settings/reader-preferences/cbx-reader-preferences-component/cbx-reader-preferences-component.scss @@ -43,7 +43,7 @@ .setting-label { margin-bottom: 0; flex-shrink: 0; - min-width: 120px; + min-width: 100px; } .radio-group { diff --git a/booklore-ui/src/app/settings/reader-preferences/cbx-reader-preferences-component/cbx-reader-preferences-component.ts b/booklore-ui/src/app/settings/reader-preferences/cbx-reader-preferences-component/cbx-reader-preferences-component.ts index 8ab7436bb..0b0bf286b 100644 --- a/booklore-ui/src/app/settings/reader-preferences/cbx-reader-preferences-component/cbx-reader-preferences-component.ts +++ b/booklore-ui/src/app/settings/reader-preferences/cbx-reader-preferences-component/cbx-reader-preferences-component.ts @@ -21,18 +21,19 @@ export class CbxReaderPreferencesComponent { private readonly readerPreferencesService = inject(ReaderPreferencesService); readonly cbxSpreads = [ - { name: 'Even', key: 'EVEN' }, - { name: 'Odd', key: 'ODD' } + {name: 'Even', key: 'EVEN'}, + {name: 'Odd', key: 'ODD'} ]; readonly cbxViewModes = [ - { name: 'Single Page', key: 'SINGLE_PAGE' }, - { name: 'Two Page', key: 'TWO_PAGE' }, + {name: 'Single Page', key: 'SINGLE_PAGE'}, + {name: 'Two Page', key: 'TWO_PAGE'}, ]; get selectedCbxSpread(): CbxPageSpread { return this.userSettings.cbxReaderSetting.pageSpread; } + set selectedCbxSpread(value: CbxPageSpread) { this.userSettings.cbxReaderSetting.pageSpread = value; this.readerPreferencesService.updatePreference(['cbxReaderSetting', 'pageSpread'], value); @@ -41,6 +42,7 @@ export class CbxReaderPreferencesComponent { get selectedCbxViewMode(): CbxPageViewMode { return this.userSettings.cbxReaderSetting.pageViewMode; } + set selectedCbxViewMode(value: CbxPageViewMode) { this.userSettings.cbxReaderSetting.pageViewMode = value; this.readerPreferencesService.updatePreference(['cbxReaderSetting', 'pageViewMode'], value); diff --git a/booklore-ui/src/app/settings/reader-preferences/epub-reader-preferences-component/epub-reader-preferences-component.html b/booklore-ui/src/app/settings/reader-preferences/epub-reader-preferences-component/epub-reader-preferences-component.html index 48c6cf11f..aff22c9ff 100644 --- a/booklore-ui/src/app/settings/reader-preferences/epub-reader-preferences-component/epub-reader-preferences-component.html +++ b/booklore-ui/src/app/settings/reader-preferences/epub-reader-preferences-component/epub-reader-preferences-component.html @@ -1,77 +1,115 @@ -
-
-
-
- - - -
-

- Choose the visual theme for EPUB reading experience. -

-
-
- -
-
-
- - - -
-

- Select the font family for text display. -

-
-
- -
-
-
- - - -
-

- Configure text flow and reading direction. -

-
-
- -
-
-
- -
- - {{ fontSize }}% - +
+
+
+
+
+ +
+ @for (theme of themes; track theme) { +
+ + + +
+ } +
+

+ Choose the visual theme for EPUB reading experience. +

+
+
+ +
+
+
+ +
+ @for (font of fonts; track font) { +
+ + + +
+ } +
+
+

+ Select the font family for text display. +

+
+
+ +
+
+
+ +
+ @for (flow of flowOptions; track flow) { +
+ + + +
+ } +
+
+

+ Configure text flow and reading direction. +

+
+
+ +
+
+
+ +
+ @for (spread of spreadOptions; track spread) { +
+ + + +
+ } +
+
+

+ Choose between single page or double page spread view. +

+
+
+ +
+
+
+ +
+ + {{ fontSize }}% + +
+
+

+ Adjust the text size for comfortable reading. +

-

- Adjust the text size for comfortable reading. -

diff --git a/booklore-ui/src/app/settings/reader-preferences/epub-reader-preferences-component/epub-reader-preferences-component.scss b/booklore-ui/src/app/settings/reader-preferences/epub-reader-preferences-component/epub-reader-preferences-component.scss index a014fbbed..bd27e4209 100644 --- a/booklore-ui/src/app/settings/reader-preferences/epub-reader-preferences-component/epub-reader-preferences-component.scss +++ b/booklore-ui/src/app/settings/reader-preferences/epub-reader-preferences-component/epub-reader-preferences-component.scss @@ -1,7 +1,7 @@ .epub-preferences-container { display: flex; flex-direction: column; - gap: 1rem; + gap: 1.5rem; } .setting-item { @@ -46,26 +46,13 @@ min-width: 100px; } - p-select { - flex: 1; - min-width: 200px; - max-width: 300px; + .radio-group { + display: flex; + gap: 1.5rem; @media (max-width: 768px) { - min-width: 180px; - } - } - - .font-size-controls { - display: flex; - align-items: center; - gap: 0.75rem; - - .font-size-value { - min-width: 3rem; - text-align: center; - font-weight: 500; - color: var(--p-text-color); + flex-direction: column; + gap: 0.75rem; } } } @@ -77,3 +64,38 @@ margin: 0; } } + +.radio-group { + display: flex; + gap: 1.5rem; + + @media (max-width: 768px) { + flex-direction: column; + gap: 0.75rem; + } +} + +.radio-option { + display: flex; + align-items: center; + gap: 0.5rem; + + label { + font-size: 0.875rem; + color: var(--p-text-color); + cursor: pointer; + } +} + +.font-size-controls { + display: flex; + align-items: center; + gap: 1rem; + + .font-size-value { + min-width: 3rem; + text-align: center; + font-weight: 500; + color: var(--p-text-color); + } +} diff --git a/booklore-ui/src/app/settings/reader-preferences/epub-reader-preferences-component/epub-reader-preferences-component.ts b/booklore-ui/src/app/settings/reader-preferences/epub-reader-preferences-component/epub-reader-preferences-component.ts index f68ca3e08..10fe02518 100644 --- a/booklore-ui/src/app/settings/reader-preferences/epub-reader-preferences-component/epub-reader-preferences-component.ts +++ b/booklore-ui/src/app/settings/reader-preferences/epub-reader-preferences-component/epub-reader-preferences-component.ts @@ -1,6 +1,6 @@ import {Component, inject, Input} from '@angular/core'; import {Button} from 'primeng/button'; -import {Select} from 'primeng/select'; +import {RadioButton} from 'primeng/radiobutton'; import {FormsModule} from '@angular/forms'; import {ReaderPreferencesService} from '../reader-preferences-service'; import {UserSettings} from '../../user-management/user.service'; @@ -9,7 +9,7 @@ import {UserSettings} from '../../user-management/user.service'; selector: 'app-epub-reader-preferences-component', imports: [ Button, - Select, + RadioButton, FormsModule ], templateUrl: './epub-reader-preferences-component.html', @@ -31,13 +31,16 @@ export class EpubReaderPreferencesComponent { ]; readonly flowOptions = [ - {name: 'Book Default', key: null}, {name: 'Paginated', key: 'paginated'}, {name: 'Scrolled', key: 'scrolled'} ]; + readonly spreadOptions = [ + {name: 'Single Page', key: 'single'}, + {name: 'Double Page', key: 'double'} + ]; + readonly themes = [ - {name: 'Book Default', key: null}, {name: 'White', key: 'white'}, {name: 'Black', key: 'black'}, {name: 'Grey', key: 'grey'}, @@ -77,6 +80,17 @@ export class EpubReaderPreferencesComponent { this.readerPreferencesService.updatePreference(['epubReaderSetting', 'flow'], value); } + get selectedSpread(): string | null { + return this.userSettings.epubReaderSetting.spread; + } + + set selectedSpread(value: string | null) { + if (typeof value === "string") { + this.userSettings.epubReaderSetting.spread = value; + } + this.readerPreferencesService.updatePreference(['epubReaderSetting', 'spread'], value); + } + get fontSize(): number { return this.userSettings.epubReaderSetting.fontSize; } diff --git a/booklore-ui/src/app/settings/reader-preferences/pdf-reader-preferences-component/pdf-reader-preferences-component.scss b/booklore-ui/src/app/settings/reader-preferences/pdf-reader-preferences-component/pdf-reader-preferences-component.scss index 88afb754f..212d8bd35 100644 --- a/booklore-ui/src/app/settings/reader-preferences/pdf-reader-preferences-component/pdf-reader-preferences-component.scss +++ b/booklore-ui/src/app/settings/reader-preferences/pdf-reader-preferences-component/pdf-reader-preferences-component.scss @@ -43,7 +43,7 @@ .setting-label { margin-bottom: 0; flex-shrink: 0; - min-width: 120px; + min-width: 100px; } .radio-group { diff --git a/booklore-ui/src/app/settings/user-management/user.service.ts b/booklore-ui/src/app/settings/user-management/user.service.ts index e3c276f2b..05d0f7ea1 100644 --- a/booklore-ui/src/app/settings/user-management/user.service.ts +++ b/booklore-ui/src/app/settings/user-management/user.service.ts @@ -55,6 +55,7 @@ export interface EpubReaderSetting { font: string; fontSize: number; flow: string; + spread: string; lineHeight: number; margin: number; letterSpacing: number;