mirror of
https://github.com/adityachandelgit/BookLore.git
synced 2026-01-07 05:39:49 -06:00
Implement EPUB page spread feature for desktop (#1267)
* Implement EPUB page spread feature for desktop * Implement EPUB page spread feature for desktop
This commit is contained in:
@@ -113,6 +113,7 @@ public class BookLoreUser {
|
||||
private Float letterSpacing;
|
||||
private Float lineHeight;
|
||||
private String flow;
|
||||
private String spread;
|
||||
}
|
||||
|
||||
@Data
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -41,4 +41,7 @@ public class EpubViewerPreferencesEntity {
|
||||
|
||||
@Column(name = "flow")
|
||||
private String flow;
|
||||
|
||||
@Column(name = "spread")
|
||||
private String spread;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -393,116 +393,116 @@ public class MetadataRefreshService {
|
||||
public BookMetadata buildFetchMetadata(Long bookId, MetadataRefreshOptions refreshOptions, Map<MetadataProvider, BookMetadata> 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();
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -69,6 +69,7 @@ public class DefaultUserSettingsProvider {
|
||||
.letterSpacing(null)
|
||||
.lineHeight(null)
|
||||
.flow("paginated")
|
||||
.spread("double")
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE epub_viewer_preference
|
||||
ADD COLUMN IF NOT EXISTS spread VARCHAR(20) DEFAULT 'double';
|
||||
@@ -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<MetadataProvider, BookMetadata> metadataMap = new HashMap<>();
|
||||
|
||||
@@ -109,6 +109,36 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (selectedFlow === 'paginated' && !isMobileDevice()) {
|
||||
<p-divider></p-divider>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<label id="spread-label" class="block font-semibold text-gray-200">Page Spead:</label>
|
||||
<div class="flex gap-4" role="radiogroup" aria-labelledby="spread-label">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<p-radioButton
|
||||
name="spread"
|
||||
[value]="'single'"
|
||||
[(ngModel)]="selectedSpread"
|
||||
(onClick)="changeSpreadMode()"
|
||||
inputId="spread-single">
|
||||
</p-radioButton>
|
||||
<label for="spread-single" class="cursor-pointer select-none text-gray-200">Single</label>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<p-radioButton
|
||||
name="spread"
|
||||
[value]="'double'"
|
||||
[(ngModel)]="selectedSpread"
|
||||
(onClick)="changeSpreadMode()"
|
||||
inputId="spread-auto">
|
||||
</p-radioButton>
|
||||
<label for="spread-auto" class="cursor-pointer select-none text-gray-200">Double</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<p-divider></p-divider>
|
||||
|
||||
<div>
|
||||
|
||||
@@ -178,3 +178,4 @@
|
||||
::ng-deep .p-divider.p-divider-horizontal {
|
||||
margin: 0.25rem 0 0.5rem 0 !important;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -187,6 +187,7 @@ export interface EpubViewerSetting {
|
||||
flow: string;
|
||||
lineHeight: number;
|
||||
letterSpacing: number;
|
||||
spread: string;
|
||||
}
|
||||
|
||||
export interface CbxViewerSetting {
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
<table class="min-w-full table-auto border-collapse custom-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-4 py-1.5 text-left font-semibold text-zinc-300">Skip</th>
|
||||
<th class="px-4 py-1.5 text-left font-semibold text-zinc-300">Metadata Field</th>
|
||||
<th class="px-4 py-1.5 text-left font-semibold text-zinc-300" style="max-width: 70px; width: 70px;">Enabled</th>
|
||||
<th class="px-4 py-1.5 text-left font-semibold text-zinc-300" style="max-width: 127px; width: 127px;">Field</th>
|
||||
<th class="px-4 py-1.5 text-left font-semibold text-zinc-300">
|
||||
4th Priority
|
||||
<i class="pi pi-question-circle ml-1 text-xs"
|
||||
@@ -30,8 +30,8 @@
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="px-4 py-2"></td>
|
||||
<td class="px-4 py-2 text-sm text-zinc-400 italic">Set All:</td>
|
||||
<td class="px-4 py-2" style="max-width: 70px; width: 70px;"></td>
|
||||
<td class="px-4 py-2 text-sm text-zinc-400 italic" style="max-width: 127px; width: 127px;">Set All:</td>
|
||||
<td class="px-4 py-2">
|
||||
<p-select [options]="providersWithClear" [(ngModel)]="bulkP4"
|
||||
(ngModelChange)="setBulkProvider('p4', $event)"
|
||||
@@ -64,38 +64,37 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (field of nonProviderSpecificFields; track field) {
|
||||
<tr [hidden]="field === 'cover' && !refreshCovers"
|
||||
[class.opacity-50]="skipFields[field]">
|
||||
<td class="px-4 py-1.5">
|
||||
<p-checkbox [(ngModel)]="skipFields[field]" [binary]="true"
|
||||
pTooltip="Skip this field during metadata fetch"
|
||||
<tr [hidden]="field === 'cover' && !refreshCovers" [class.opacity-50]="!enabledFields[field]">
|
||||
<td class="px-4 py-1.5" style="max-width: 70px; width: 70px;">
|
||||
<p-checkbox [(ngModel)]="enabledFields[field]" [binary]="true"
|
||||
pTooltip="Enable this field during metadata fetch"
|
||||
tooltipPosition="top"></p-checkbox>
|
||||
</td>
|
||||
<td class="px-4 py-1.5 text-zinc-200">{{ formatLabel(field) }}</td>
|
||||
<td class="px-4 py-1.5 text-zinc-200" style="max-width: 127px; width: 127px;">{{ formatLabel(field) }}</td>
|
||||
<td class="px-4 py-1.5">
|
||||
<p-select [options]="providers" [(ngModel)]="fieldOptions[field].p4"
|
||||
[disabled]="skipFields[field]"
|
||||
[disabled]="!enabledFields[field]"
|
||||
placeholder="Unset" appendTo="body"
|
||||
class="w-full" size="small">
|
||||
</p-select>
|
||||
</td>
|
||||
<td class="px-4 py-1.5">
|
||||
<p-select [options]="providers" [(ngModel)]="fieldOptions[field].p3"
|
||||
[disabled]="skipFields[field]"
|
||||
[disabled]="!enabledFields[field]"
|
||||
placeholder="Unset" appendTo="body"
|
||||
class="w-full" size="small">
|
||||
</p-select>
|
||||
</td>
|
||||
<td class="px-4 py-1.5">
|
||||
<p-select [options]="providers" [(ngModel)]="fieldOptions[field].p2"
|
||||
[disabled]="skipFields[field]"
|
||||
[disabled]="!enabledFields[field]"
|
||||
placeholder="Unset" appendTo="body"
|
||||
class="w-full" size="small">
|
||||
</p-select>
|
||||
</td>
|
||||
<td class="px-4 py-1.5">
|
||||
<p-select [options]="providers" [(ngModel)]="fieldOptions[field].p1"
|
||||
[disabled]="skipFields[field]"
|
||||
[disabled]="!enabledFields[field]"
|
||||
placeholder="Unset" appendTo="body"
|
||||
class="w-full" size="small">
|
||||
</p-select>
|
||||
@@ -107,12 +106,12 @@
|
||||
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-lg font-semibold text-zinc-300">Provider-Specific Fields</h3>
|
||||
<p class="text-sm text-zinc-400">These fields are unique to specific providers and cannot have custom priority settings. Use the checkboxes to skip fetching these fields entirely.</p>
|
||||
<p class="text-sm text-zinc-400">These fields are unique to specific providers and cannot have custom priority settings. Use the checkboxes to enable/disable fetching these fields.</p>
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
@for (field of providerSpecificFields; track field) {
|
||||
<div class="flex items-center space-x-3 p-3 border border-zinc-600 rounded-lg">
|
||||
<p-checkbox [(ngModel)]="skipFields[field]" [binary]="true"
|
||||
pTooltip="Skip this field during metadata fetch"
|
||||
<p-checkbox [(ngModel)]="enabledFields[field]" [binary]="true"
|
||||
pTooltip="Enable this field during metadata fetch"
|
||||
tooltipPosition="top"></p-checkbox>
|
||||
<span class="text-sm text-zinc-300">{{ formatLabel(field) }}</span>
|
||||
</div>
|
||||
|
||||
@@ -50,7 +50,7 @@ export class MetadataAdvancedFetchOptionsComponent implements OnChanges {
|
||||
reviewBeforeApply: boolean = false;
|
||||
|
||||
fieldOptions: FieldOptions = this.initializeFieldOptions();
|
||||
skipFields: Record<keyof FieldOptions, boolean> = this.initializeSkipFields();
|
||||
enabledFields: Record<keyof FieldOptions, boolean> = this.initializeEnabledFields();
|
||||
|
||||
bulkP1: string | null = null;
|
||||
bulkP2: string | null = null;
|
||||
@@ -74,9 +74,9 @@ export class MetadataAdvancedFetchOptionsComponent implements OnChanges {
|
||||
}, {} as FieldOptions);
|
||||
}
|
||||
|
||||
private initializeSkipFields(): Record<keyof FieldOptions, boolean> {
|
||||
private initializeEnabledFields(): Record<keyof FieldOptions, boolean> {
|
||||
return this.fields.reduce((acc, field) => {
|
||||
acc[field] = false;
|
||||
acc[field] = true;
|
||||
return acc;
|
||||
}, {} as Record<keyof FieldOptions, boolean>);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -4,7 +4,7 @@ export interface MetadataRefreshOptions {
|
||||
mergeCategories: boolean;
|
||||
reviewBeforeApply: boolean;
|
||||
fieldOptions?: FieldOptions;
|
||||
skipFields?: Record<keyof FieldOptions, boolean>;
|
||||
enabledFields?: Record<keyof FieldOptions, boolean>;
|
||||
}
|
||||
|
||||
export interface FieldProvider {
|
||||
|
||||
@@ -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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -42,6 +43,7 @@
|
||||
</h3>
|
||||
<p class="section-description">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
.setting-label {
|
||||
margin-bottom: 0;
|
||||
flex-shrink: 0;
|
||||
min-width: 120px;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.radio-group {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,77 +1,115 @@
|
||||
<div class="epub-preferences-container">
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label-row">
|
||||
<label class="setting-label">Theme</label>
|
||||
<p-select
|
||||
size="small"
|
||||
[options]="themes"
|
||||
[(ngModel)]="selectedTheme"
|
||||
optionLabel="name"
|
||||
optionValue="key"
|
||||
placeholder="Select a Theme"
|
||||
class="w-full md:w-60">
|
||||
</p-select>
|
||||
</div>
|
||||
<p class="setting-description">
|
||||
Choose the visual theme for EPUB reading experience.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label-row">
|
||||
<label class="setting-label">Font</label>
|
||||
<p-select
|
||||
size="small"
|
||||
[options]="fonts"
|
||||
[(ngModel)]="selectedFont"
|
||||
optionLabel="name"
|
||||
optionValue="key"
|
||||
placeholder="Select a Font"
|
||||
class="w-full md:w-60">
|
||||
</p-select>
|
||||
</div>
|
||||
<p class="setting-description">
|
||||
Select the font family for text display.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label-row">
|
||||
<label class="setting-label">Flow</label>
|
||||
<p-select
|
||||
size="small"
|
||||
[options]="flowOptions"
|
||||
[(ngModel)]="selectedFlow"
|
||||
optionLabel="name"
|
||||
optionValue="key"
|
||||
placeholder="Select a Flow"
|
||||
class="w-full md:w-60">
|
||||
</p-select>
|
||||
</div>
|
||||
<p class="setting-description">
|
||||
Configure text flow and reading direction.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label-row">
|
||||
<label class="setting-label">Font Size</label>
|
||||
<div class="font-size-controls">
|
||||
<p-button icon="pi pi-minus" size="small" rounded (click)="decreaseFontSize()"></p-button>
|
||||
<span class="font-size-value">{{ fontSize }}%</span>
|
||||
<p-button icon="pi pi-plus" size="small" rounded (click)="increaseFontSize()"></p-button>
|
||||
<div class="p-4">
|
||||
<div class="epub-preferences-container">
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label-row">
|
||||
<label class="setting-label">Theme</label>
|
||||
<div class="radio-group">
|
||||
@for (theme of themes; track theme) {
|
||||
<div class="radio-option">
|
||||
<p-radiobutton
|
||||
[inputId]="'theme-' + theme.key"
|
||||
name="theme"
|
||||
[value]="theme.key"
|
||||
[(ngModel)]="selectedTheme">
|
||||
</p-radiobutton>
|
||||
<label [for]="'theme-' + theme.key">{{ theme.name }}</label>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<p class="setting-description">
|
||||
Choose the visual theme for EPUB reading experience.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label-row">
|
||||
<label class="setting-label">Font</label>
|
||||
<div class="radio-group">
|
||||
@for (font of fonts; track font) {
|
||||
<div class="radio-option">
|
||||
<p-radiobutton
|
||||
[inputId]="'font-' + font.key"
|
||||
name="font"
|
||||
[value]="font.key"
|
||||
[(ngModel)]="selectedFont">
|
||||
</p-radiobutton>
|
||||
<label [for]="'font-' + font.key">{{ font.name }}</label>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<p class="setting-description">
|
||||
Select the font family for text display.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label-row">
|
||||
<label class="setting-label">Flow</label>
|
||||
<div class="radio-group">
|
||||
@for (flow of flowOptions; track flow) {
|
||||
<div class="radio-option">
|
||||
<p-radiobutton
|
||||
[inputId]="'flow-' + flow.key"
|
||||
name="flow"
|
||||
[value]="flow.key"
|
||||
[(ngModel)]="selectedFlow">
|
||||
</p-radiobutton>
|
||||
<label [for]="'flow-' + flow.key">{{ flow.name }}</label>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<p class="setting-description">
|
||||
Configure text flow and reading direction.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label-row">
|
||||
<label class="setting-label">Page Spread</label>
|
||||
<div class="radio-group">
|
||||
@for (spread of spreadOptions; track spread) {
|
||||
<div class="radio-option">
|
||||
<p-radiobutton
|
||||
[inputId]="'spread-' + spread.key"
|
||||
name="spread"
|
||||
[value]="spread.key"
|
||||
[(ngModel)]="selectedSpread">
|
||||
</p-radiobutton>
|
||||
<label [for]="'spread-' + spread.key">{{ spread.name }}</label>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<p class="setting-description">
|
||||
Choose between single page or double page spread view.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label-row">
|
||||
<label class="setting-label">Font Size</label>
|
||||
<div class="font-size-controls">
|
||||
<p-button icon="pi pi-minus" size="small" rounded (click)="decreaseFontSize()"></p-button>
|
||||
<span class="font-size-value">{{ fontSize }}%</span>
|
||||
<p-button icon="pi pi-plus" size="small" rounded (click)="increaseFontSize()"></p-button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="setting-description">
|
||||
Adjust the text size for comfortable reading.
|
||||
</p>
|
||||
</div>
|
||||
<p class="setting-description">
|
||||
Adjust the text size for comfortable reading.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
.setting-label {
|
||||
margin-bottom: 0;
|
||||
flex-shrink: 0;
|
||||
min-width: 120px;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.radio-group {
|
||||
|
||||
@@ -55,6 +55,7 @@ export interface EpubReaderSetting {
|
||||
font: string;
|
||||
fontSize: number;
|
||||
flow: string;
|
||||
spread: string;
|
||||
lineHeight: number;
|
||||
margin: number;
|
||||
letterSpacing: number;
|
||||
|
||||
Reference in New Issue
Block a user