Fix/hardcover api key per user (#1943)

* feat: add hardcover API key and sync settings to KoboSync and KoboUserSettings entities

* feat(hardcover): enhance sync functionality to use per-user API keys

- Updated HardcoverSyncService to utilize user-specific Hardcover API keys for syncing reading progress.
- Modified syncProgressToHardcover method to accept userId, allowing for personalized sync settings.
- Improved logging to include userId in sync operations for better traceability.
- Adjusted KoboReadingStateService to pass userId when triggering sync to Hardcover.

* feat(database): update kobo_location_source column size and add hardcover settings

- Increased the size of the kobo_location_source column to accommodate longer location strings from Kobo devices.
- Added new columns for hardcover API key and sync settings in the kobo_user_settings table to enhance user customization.

* refactor(hardcover): update tests to use user-specific Kobo settings

- Replaced AppSettingService with KoboSettingsService in HardcoverSyncServiceTest to utilize user-specific settings.
- Modified syncProgressToHardcover method calls to include userId for personalized sync operations.
- Added a new test case to handle scenarios where user settings are not found, ensuring robust error handling.

---------

Co-authored-by: akiraslingshot <akiraslingshot@gmail.com>
This commit is contained in:
Giancarlo Perrone
2025-12-19 17:56:41 -08:00
committed by GitHub
parent 54108754f9
commit 32a1a2ac34
11 changed files with 271 additions and 117 deletions

View File

@@ -12,4 +12,6 @@ public class KoboSyncSettings {
private Float progressMarkAsReadingThreshold;
private Float progressMarkAsFinishedThreshold;
private boolean autoAddToShelf;
private String hardcoverApiKey;
private boolean hardcoverSyncEnabled;
}

View File

@@ -37,4 +37,11 @@ public class KoboUserSettingsEntity {
@Column(name = "auto_add_to_shelf")
@Builder.Default
private boolean autoAddToShelf = false;
@Column(name = "hardcover_api_key", length = 2048)
private String hardcoverApiKey;
@Column(name = "hardcover_sync_enabled")
@Builder.Default
private boolean hardcoverSyncEnabled = false;
}

View File

@@ -69,7 +69,7 @@ public class UserBookProgressEntity {
@Column(name = "kobo_location_type", length = 50)
private String koboLocationType;
@Column(name = "kobo_location_source", length = 50)
@Column(name = "kobo_location_source", length = 512)
private String koboLocationSource;
@Enumerated(EnumType.STRING)

View File

@@ -1,13 +1,14 @@
package com.adityachandel.booklore.service.hardcover;
import com.adityachandel.booklore.model.dto.settings.MetadataProviderSettings;
import com.adityachandel.booklore.model.dto.KoboSyncSettings;
import com.adityachandel.booklore.model.entity.BookEntity;
import com.adityachandel.booklore.model.entity.BookMetadataEntity;
import com.adityachandel.booklore.repository.BookRepository;
import com.adityachandel.booklore.service.appsettings.AppSettingService;
import com.adityachandel.booklore.service.kobo.KoboSettingsService;
import com.adityachandel.booklore.service.metadata.parser.hardcover.GraphQLRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.scheduling.annotation.Async;
@@ -23,8 +24,8 @@ import java.util.Map;
/**
* Service to sync reading progress to Hardcover.
* Uses the global Hardcover API token from Metadata Provider Settings.
* Sync only activates if the token is configured and Hardcover is enabled.
* Uses per-user Hardcover API tokens for reading progress sync.
* Each user can configure their own Hardcover API key in Kobo settings.
*/
@Slf4j
@Service
@@ -35,12 +36,15 @@ public class HardcoverSyncService {
private static final int STATUS_READ = 3;
private final RestClient restClient;
private final AppSettingService appSettingService;
private final KoboSettingsService koboSettingsService;
private final BookRepository bookRepository;
// Thread-local to hold the current API token for GraphQL requests
private final ThreadLocal<String> currentApiToken = new ThreadLocal<>();
@Autowired
public HardcoverSyncService(AppSettingService appSettingService, BookRepository bookRepository) {
this.appSettingService = appSettingService;
public HardcoverSyncService(@Lazy KoboSettingsService koboSettingsService, BookRepository bookRepository) {
this.koboSettingsService = koboSettingsService;
this.bookRepository = bookRepository;
this.restClient = RestClient.builder()
.baseUrl(HARDCOVER_API_URL)
@@ -50,19 +54,28 @@ public class HardcoverSyncService {
/**
* Asynchronously sync Kobo reading progress to Hardcover.
* This method is non-blocking and will not fail the calling process if sync fails.
* Uses the user's personal Hardcover API key if configured.
*
* @param bookId The book ID to sync progress for
* @param progressPercent The reading progress as a percentage (0-100)
* @param userId The user ID whose reading progress is being synced
*/
@Async
@Transactional(readOnly = true)
public void syncProgressToHardcover(Long bookId, Float progressPercent) {
public void syncProgressToHardcover(Long bookId, Float progressPercent, Long userId) {
try {
if (!isHardcoverSyncEnabled()) {
log.trace("Hardcover sync skipped: not enabled or no API token configured");
// Get user's Hardcover settings
KoboSyncSettings userSettings = koboSettingsService.getSettingsByUserId(userId);
if (!isHardcoverSyncEnabledForUser(userSettings)) {
log.trace("Hardcover sync skipped for user {}: not enabled or no API token configured", userId);
return;
}
// Set the user's API token for this sync operation
currentApiToken.set(userSettings.getHardcoverApiKey());
try {
if (progressPercent == null) {
log.debug("Hardcover sync skipped: no progress to sync");
return;
@@ -107,8 +120,8 @@ public class HardcoverSyncService {
progressPages = Math.round((progressPercent / 100.0f) * hardcoverBook.pages);
progressPages = Math.max(0, Math.min(hardcoverBook.pages, progressPages));
}
log.info("Progress calculation: progressPercent={}%, totalPages={}, progressPages={}",
progressPercent, hardcoverBook.pages, progressPages);
log.info("Progress calculation: userId={}, progressPercent={}%, totalPages={}, progressPages={}",
userId, progressPercent, hardcoverBook.pages, progressPages);
// Step 1: Add/update the book in user's library
Integer userBookId = insertOrGetUserBook(hardcoverBook.bookId, hardcoverBook.editionId, statusId);
@@ -121,34 +134,36 @@ public class HardcoverSyncService {
boolean success = upsertReadingProgress(userBookId, hardcoverBook.editionId, progressPages);
if (success) {
log.info("Synced progress to Hardcover: book={}, hardcoverBookId={}, progress={}% ({}pages)",
bookId, hardcoverBook.bookId, Math.round(progressPercent), progressPages);
log.info("Synced progress to Hardcover: userId={}, book={}, hardcoverBookId={}, progress={}% ({}pages)",
userId, bookId, hardcoverBook.bookId, Math.round(progressPercent), progressPages);
}
} finally {
// Clean up thread-local
currentApiToken.remove();
}
} catch (Exception e) {
log.error("Failed to sync progress to Hardcover for book {}: {}",
bookId, e.getMessage());
log.error("Failed to sync progress to Hardcover for book {} (user {}): {}",
bookId, userId, e.getMessage());
}
}
private boolean isHardcoverSyncEnabled() {
MetadataProviderSettings.Hardcover hardcoverSettings =
appSettingService.getAppSettings().getMetadataProviderSettings().getHardcover();
if (hardcoverSettings == null) {
/**
* Check if Hardcover sync is enabled for a specific user.
*/
private boolean isHardcoverSyncEnabledForUser(KoboSyncSettings userSettings) {
if (userSettings == null) {
return false;
}
return hardcoverSettings.isEnabled()
&& hardcoverSettings.getApiKey() != null
&& !hardcoverSettings.getApiKey().isBlank();
return userSettings.isHardcoverSyncEnabled()
&& userSettings.getHardcoverApiKey() != null
&& !userSettings.getHardcoverApiKey().isBlank();
}
private String getApiToken() {
return appSettingService.getAppSettings()
.getMetadataProviderSettings()
.getHardcover()
.getApiKey();
return currentApiToken.get();
}
/**

View File

@@ -154,6 +154,9 @@ public class KoboReadingStateService {
KoboReadingState.CurrentBookmark.Location location = bookmark.getLocation();
if (location != null) {
log.debug("Kobo location data: value={}, type={}, source={} (length={})",
location.getValue(), location.getType(), location.getSource(),
location.getSource() != null ? location.getSource().length() : 0);
progress.setKoboLocation(location.getValue());
progress.setKoboLocationType(location.getType());
progress.setKoboLocationSource(location.getSource());
@@ -171,8 +174,8 @@ public class KoboReadingStateService {
progressRepository.save(progress);
log.debug("Synced Kobo progress: bookId={}, progress={}%", bookId, progress.getKoboProgressPercent());
// Sync progress to Hardcover asynchronously (if enabled)
hardcoverSyncService.syncProgressToHardcover(book.getId(), progress.getKoboProgressPercent());
// Sync progress to Hardcover asynchronously (if enabled for this user)
hardcoverSyncService.syncProgressToHardcover(book.getId(), progress.getKoboProgressPercent(), userId);
} catch (NumberFormatException e) {
log.warn("Invalid entitlement ID format: {}", readingState.getEntitlementId());
}

View File

@@ -82,6 +82,10 @@ public class KoboSettingsService {
entity.setAutoAddToShelf(settings.isAutoAddToShelf());
// Update Hardcover settings
entity.setHardcoverApiKey(settings.getHardcoverApiKey());
entity.setHardcoverSyncEnabled(settings.isHardcoverSyncEnabled());
repository.save(entity);
return mapToDto(entity);
}
@@ -122,6 +126,19 @@ public class KoboSettingsService {
dto.setProgressMarkAsReadingThreshold(entity.getProgressMarkAsReadingThreshold());
dto.setProgressMarkAsFinishedThreshold(entity.getProgressMarkAsFinishedThreshold());
dto.setAutoAddToShelf(entity.isAutoAddToShelf());
dto.setHardcoverApiKey(entity.getHardcoverApiKey());
dto.setHardcoverSyncEnabled(entity.isHardcoverSyncEnabled());
return dto;
}
/**
* Get Hardcover settings for a specific user by ID.
* Used by HardcoverSyncService to get user-specific API key.
*/
@Transactional(readOnly = true)
public KoboSyncSettings getSettingsByUserId(Long userId) {
return repository.findByUserId(userId)
.map(this::mapToDto)
.orElse(null);
}
}

View File

@@ -0,0 +1,6 @@
-- Add per-user Hardcover settings to kobo_user_settings table
ALTER TABLE kobo_user_settings ADD COLUMN hardcover_api_key VARCHAR(2048);
ALTER TABLE kobo_user_settings ADD COLUMN hardcover_sync_enabled BOOLEAN DEFAULT FALSE;
-- Fix kobo_location_source column size (Kobo devices can send longer location strings)
ALTER TABLE user_book_progress MODIFY COLUMN kobo_location_source VARCHAR(512);

View File

@@ -1,11 +1,10 @@
package com.adityachandel.booklore.service.hardcover;
import com.adityachandel.booklore.model.dto.settings.AppSettings;
import com.adityachandel.booklore.model.dto.settings.MetadataProviderSettings;
import com.adityachandel.booklore.model.dto.KoboSyncSettings;
import com.adityachandel.booklore.model.entity.BookEntity;
import com.adityachandel.booklore.model.entity.BookMetadataEntity;
import com.adityachandel.booklore.repository.BookRepository;
import com.adityachandel.booklore.service.appsettings.AppSettingService;
import com.adityachandel.booklore.service.kobo.KoboSettingsService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
@@ -31,7 +30,7 @@ import static org.mockito.Mockito.*;
class HardcoverSyncServiceTest {
@Mock
private AppSettingService appSettingService;
private KoboSettingsService koboSettingsService;
@Mock
private BookRepository bookRepository;
@@ -52,15 +51,15 @@ class HardcoverSyncServiceTest {
private BookEntity testBook;
private BookMetadataEntity testMetadata;
private AppSettings appSettings;
private MetadataProviderSettings.Hardcover hardcoverSettings;
private KoboSyncSettings koboSyncSettings;
private static final Long TEST_BOOK_ID = 100L;
private static final Long TEST_USER_ID = 1L;
@BeforeEach
void setUp() throws Exception {
// Create service with mocked dependencies
service = new HardcoverSyncService(appSettingService, bookRepository);
service = new HardcoverSyncService(koboSettingsService, bookRepository);
// Inject our mocked restClient using reflection
Field restClientField = HardcoverSyncService.class.getDeclaredField("restClient");
@@ -75,15 +74,12 @@ class HardcoverSyncServiceTest {
testMetadata.setPageCount(300);
testBook.setMetadata(testMetadata);
appSettings = new AppSettings();
MetadataProviderSettings metadataSettings = new MetadataProviderSettings();
hardcoverSettings = new MetadataProviderSettings.Hardcover();
hardcoverSettings.setEnabled(true);
hardcoverSettings.setApiKey("test-api-key");
metadataSettings.setHardcover(hardcoverSettings);
appSettings.setMetadataProviderSettings(metadataSettings);
// Setup Kobo sync settings with Hardcover enabled
koboSyncSettings = new KoboSyncSettings();
koboSyncSettings.setHardcoverSyncEnabled(true);
koboSyncSettings.setHardcoverApiKey("test-api-key");
when(appSettingService.getAppSettings()).thenReturn(appSettings);
when(koboSettingsService.getSettingsByUserId(TEST_USER_ID)).thenReturn(koboSyncSettings);
when(bookRepository.findById(TEST_BOOK_ID)).thenReturn(Optional.of(testBook));
// Setup RestClient mock chain - handles multiple calls
@@ -97,11 +93,11 @@ class HardcoverSyncServiceTest {
// === Tests for skipping sync (no API calls should be made) ===
@Test
@DisplayName("Should skip sync when Hardcover is not enabled")
@DisplayName("Should skip sync when Hardcover sync is not enabled for user")
void syncProgressToHardcover_whenHardcoverDisabled_shouldSkip() {
hardcoverSettings.setEnabled(false);
koboSyncSettings.setHardcoverSyncEnabled(false);
service.syncProgressToHardcover(TEST_BOOK_ID, 50.0f);
service.syncProgressToHardcover(TEST_BOOK_ID, 50.0f, TEST_USER_ID);
verify(restClient, never()).post();
}
@@ -109,9 +105,9 @@ class HardcoverSyncServiceTest {
@Test
@DisplayName("Should skip sync when API key is missing")
void syncProgressToHardcover_whenApiKeyMissing_shouldSkip() {
hardcoverSettings.setApiKey(null);
koboSyncSettings.setHardcoverApiKey(null);
service.syncProgressToHardcover(TEST_BOOK_ID, 50.0f);
service.syncProgressToHardcover(TEST_BOOK_ID, 50.0f, TEST_USER_ID);
verify(restClient, never()).post();
}
@@ -119,9 +115,9 @@ class HardcoverSyncServiceTest {
@Test
@DisplayName("Should skip sync when API key is blank")
void syncProgressToHardcover_whenApiKeyBlank_shouldSkip() {
hardcoverSettings.setApiKey(" ");
koboSyncSettings.setHardcoverApiKey(" ");
service.syncProgressToHardcover(TEST_BOOK_ID, 50.0f);
service.syncProgressToHardcover(TEST_BOOK_ID, 50.0f, TEST_USER_ID);
verify(restClient, never()).post();
}
@@ -129,7 +125,7 @@ class HardcoverSyncServiceTest {
@Test
@DisplayName("Should skip sync when progress is null")
void syncProgressToHardcover_whenProgressNull_shouldSkip() {
service.syncProgressToHardcover(TEST_BOOK_ID, null);
service.syncProgressToHardcover(TEST_BOOK_ID, null, TEST_USER_ID);
verify(restClient, never()).post();
}
@@ -139,7 +135,7 @@ class HardcoverSyncServiceTest {
void syncProgressToHardcover_whenBookNotFound_shouldSkip() {
when(bookRepository.findById(TEST_BOOK_ID)).thenReturn(Optional.empty());
service.syncProgressToHardcover(TEST_BOOK_ID, 50.0f);
service.syncProgressToHardcover(TEST_BOOK_ID, 50.0f, TEST_USER_ID);
verify(restClient, never()).post();
}
@@ -149,7 +145,7 @@ class HardcoverSyncServiceTest {
void syncProgressToHardcover_whenNoMetadata_shouldSkip() {
testBook.setMetadata(null);
service.syncProgressToHardcover(TEST_BOOK_ID, 50.0f);
service.syncProgressToHardcover(TEST_BOOK_ID, 50.0f, TEST_USER_ID);
verify(restClient, never()).post();
}
@@ -160,7 +156,7 @@ class HardcoverSyncServiceTest {
testMetadata.setIsbn13(null);
testMetadata.setIsbn10(null);
service.syncProgressToHardcover(TEST_BOOK_ID, 50.0f);
service.syncProgressToHardcover(TEST_BOOK_ID, 50.0f, TEST_USER_ID);
verify(restClient, never()).post();
}
@@ -179,7 +175,7 @@ class HardcoverSyncServiceTest {
.thenReturn(createEmptyUserBookReadsResponse())
.thenReturn(createInsertUserBookReadResponse());
service.syncProgressToHardcover(TEST_BOOK_ID, 50.0f);
service.syncProgressToHardcover(TEST_BOOK_ID, 50.0f, TEST_USER_ID);
// Verify API was called at least once (using stored ID, no search needed)
verify(restClient, atLeastOnce()).post();
@@ -195,7 +191,7 @@ class HardcoverSyncServiceTest {
.thenReturn(createEmptyUserBookReadsResponse())
.thenReturn(createInsertUserBookReadResponse());
service.syncProgressToHardcover(TEST_BOOK_ID, 50.0f);
service.syncProgressToHardcover(TEST_BOOK_ID, 50.0f, TEST_USER_ID);
// Verify API was called at least once
verify(restClient, atLeastOnce()).post();
@@ -207,7 +203,7 @@ class HardcoverSyncServiceTest {
// Mock: search returns empty results
when(responseSpec.body(Map.class)).thenReturn(createEmptySearchResponse());
service.syncProgressToHardcover(TEST_BOOK_ID, 50.0f);
service.syncProgressToHardcover(TEST_BOOK_ID, 50.0f, TEST_USER_ID);
// Should call search only
verify(restClient, times(1)).post();
@@ -224,7 +220,7 @@ class HardcoverSyncServiceTest {
.thenReturn(createEmptyUserBookReadsResponse())
.thenReturn(createInsertUserBookReadResponse());
service.syncProgressToHardcover(TEST_BOOK_ID, 99.0f);
service.syncProgressToHardcover(TEST_BOOK_ID, 99.0f, TEST_USER_ID);
verify(restClient, atLeastOnce()).post();
}
@@ -240,7 +236,7 @@ class HardcoverSyncServiceTest {
.thenReturn(createEmptyUserBookReadsResponse())
.thenReturn(createInsertUserBookReadResponse());
service.syncProgressToHardcover(TEST_BOOK_ID, 50.0f);
service.syncProgressToHardcover(TEST_BOOK_ID, 50.0f, TEST_USER_ID);
verify(restClient, atLeastOnce()).post();
}
@@ -257,7 +253,7 @@ class HardcoverSyncServiceTest {
.thenReturn(createEmptyUserBookReadsResponse())
.thenReturn(createInsertUserBookReadResponse());
service.syncProgressToHardcover(TEST_BOOK_ID, 50.0f);
service.syncProgressToHardcover(TEST_BOOK_ID, 50.0f, TEST_USER_ID);
verify(restClient, atLeastOnce()).post();
}
@@ -273,7 +269,7 @@ class HardcoverSyncServiceTest {
.thenReturn(createFindUserBookReadResponse(6001))
.thenReturn(createUpdateUserBookReadResponse());
service.syncProgressToHardcover(TEST_BOOK_ID, 50.0f);
service.syncProgressToHardcover(TEST_BOOK_ID, 50.0f, TEST_USER_ID);
verify(restClient, atLeastOnce()).post();
}
@@ -290,7 +286,7 @@ class HardcoverSyncServiceTest {
.thenReturn(createEmptyUserBookReadsResponse())
.thenReturn(createInsertUserBookReadResponse());
service.syncProgressToHardcover(TEST_BOOK_ID, 50.0f);
service.syncProgressToHardcover(TEST_BOOK_ID, 50.0f, TEST_USER_ID);
verify(restClient, atLeastOnce()).post();
}
@@ -304,7 +300,7 @@ class HardcoverSyncServiceTest {
when(responseSpec.body(Map.class)).thenReturn(Map.of("errors", List.of(Map.of("message", "Unauthorized"))));
assertDoesNotThrow(() -> service.syncProgressToHardcover(TEST_BOOK_ID, 50.0f));
assertDoesNotThrow(() -> service.syncProgressToHardcover(TEST_BOOK_ID, 50.0f, TEST_USER_ID));
}
@Test
@@ -314,7 +310,17 @@ class HardcoverSyncServiceTest {
when(responseSpec.body(Map.class)).thenReturn(null);
assertDoesNotThrow(() -> service.syncProgressToHardcover(TEST_BOOK_ID, 50.0f));
assertDoesNotThrow(() -> service.syncProgressToHardcover(TEST_BOOK_ID, 50.0f, TEST_USER_ID));
}
@Test
@DisplayName("Should skip sync when user settings not found")
void syncProgressToHardcover_whenUserSettingsNotFound_shouldSkip() {
when(koboSettingsService.getSettingsByUserId(TEST_USER_ID)).thenReturn(null);
service.syncProgressToHardcover(TEST_BOOK_ID, 50.0f, TEST_USER_ID);
verify(restClient, never()).post();
}
// === Helper methods to create mock responses ===

View File

@@ -174,6 +174,82 @@
</div>
</div>
@if (koboSyncSettings.syncEnabled) {
<div class="preferences-section">
<div class="section-header">
<h3 class="section-title">
<i class="pi pi-book"></i>
Hardcover Integration
</h3>
<p class="section-description">
Sync your Kobo reading progress to your personal Hardcover account.
</p>
</div>
<div class="settings-card">
<div class="setting-item">
<div class="setting-info">
<div class="setting-label-row">
<label class="setting-label">Sync Reading Progress to Hardcover</label>
<p-toggle-switch
id="hardcoverSyncEnabled"
name="hardcoverSyncEnabled"
[(ngModel)]="koboSyncSettings.hardcoverSyncEnabled"
(ngModelChange)="onHardcoverSyncToggle()">
</p-toggle-switch>
</div>
<p class="setting-description">
When enabled, your reading progress from Kobo will be automatically synced to your Hardcover account.
Each user can configure their own Hardcover account.
</p>
</div>
</div>
@if (koboSyncSettings.hardcoverSyncEnabled) {
<div class="setting-item">
<div class="setting-info">
<div class="setting-label-row">
<label class="setting-label">Hardcover API Key</label>
<div class="token-input-group">
<input
pInputText
id="hardcoverApiKey"
[(ngModel)]="koboSyncSettings.hardcoverApiKey"
[type]="showHardcoverApiKey ? 'text' : 'password'"
name="hardcoverApiKey"
class="token-input"
placeholder="Enter your Hardcover API key"
(blur)="onHardcoverApiKeyChange()"
/>
<p-button
icon="pi pi-copy"
outlined="true"
severity="info"
size="small"
(onClick)="copyText(koboSyncSettings.hardcoverApiKey ?? '', 'API Key')"
[disabled]="!koboSyncSettings.hardcoverApiKey">
</p-button>
<p-button
[icon]="showHardcoverApiKey ? 'pi pi-eye-slash' : 'pi pi-eye'"
outlined="true"
severity="info"
size="small"
(onClick)="toggleShowHardcoverApiKey()">
</p-button>
</div>
</div>
<p class="setting-description">
Your personal Hardcover API key. Get it from
<a href="https://hardcover.app/account/api" target="_blank" rel="noopener">hardcover.app/account/api</a>.
This key is used only for syncing your reading progress and is not shared with other users.
</p>
</div>
</div>
}
</div>
</div>
}
@if (isAdmin) {
<div class="preferences-section">
<div class="section-header">

View File

@@ -43,6 +43,7 @@ export class KoboSyncSettingsComponent implements OnInit, OnDestroy {
isAdmin = false;
credentialsSaved = false;
showToken = false;
showHardcoverApiKey = false;
koboSettings: KoboSettings = {
convertToKepub: false,
@@ -58,7 +59,9 @@ export class KoboSyncSettingsComponent implements OnInit, OnDestroy {
syncEnabled: false,
progressMarkAsReadingThreshold: 1,
progressMarkAsFinishedThreshold: 99,
autoAddToShelf: true
autoAddToShelf: true,
hardcoverApiKey: '',
hardcoverSyncEnabled: false
}
ngOnInit() {
@@ -119,6 +122,8 @@ export class KoboSyncSettingsComponent implements OnInit, OnDestroy {
this.koboSyncSettings.progressMarkAsReadingThreshold = settings.progressMarkAsReadingThreshold ?? 1;
this.koboSyncSettings.progressMarkAsFinishedThreshold = settings.progressMarkAsFinishedThreshold ?? 99;
this.koboSyncSettings.autoAddToShelf = settings.autoAddToShelf ?? false;
this.koboSyncSettings.hardcoverApiKey = settings.hardcoverApiKey ?? '';
this.koboSyncSettings.hardcoverSyncEnabled = settings.hardcoverSyncEnabled ?? false;
this.credentialsSaved = !!settings.token;
},
error: () => {
@@ -167,6 +172,21 @@ export class KoboSyncSettingsComponent implements OnInit, OnDestroy {
this.showToken = !this.showToken;
}
toggleShowHardcoverApiKey() {
this.showHardcoverApiKey = !this.showHardcoverApiKey;
}
onHardcoverSyncToggle() {
const message = this.koboSyncSettings.hardcoverSyncEnabled
? 'Hardcover sync enabled'
: 'Hardcover sync disabled';
this.updateKoboSettings(message);
}
onHardcoverApiKeyChange() {
this.updateKoboSettings('Hardcover API key updated');
}
confirmRegenerateToken() {
this.confirmationService.confirm({
message: 'This will generate a new token and invalidate the previous one. Continue?',

View File

@@ -9,6 +9,8 @@ export interface KoboSyncSettings {
progressMarkAsReadingThreshold?: number;
progressMarkAsFinishedThreshold?: number;
autoAddToShelf: boolean;
hardcoverApiKey?: string;
hardcoverSyncEnabled?: boolean;
}
@Injectable({