From becfb6a72ea14d5eef6fd356a14ca7ce291ef6e9 Mon Sep 17 00:00:00 2001 From: neon <18559658+neonsolstice@users.noreply.github.com> Date: Thu, 13 Aug 2026 19:08:53 +0000 Subject: [PATCH] fix(settings): display complete endpoint path in koreader sync url The KOReader settings page displayed the origin-only base URL without the /api/v1/koreader path suffix, causing manual URL copy and built-in progress sync setup to fail due to incomplete endpoint paths. Updates the UI to display the full KOReader sync URL with the API endpoint path included, consolidates redundant copy fields, and updates plugin URL normalization to handle full stock sync URLs seamlessly. Closes #981 --- .../features/settings/KoreaderSettings.vue | 67 +++++++------------ .../__tests__/KoreaderSettings.spec.ts | 14 ++-- client/src/locales/en.json | 2 + koreader-plugin/spec/bookorbit_api_test.lua | 5 ++ 4 files changed, 38 insertions(+), 50 deletions(-) diff --git a/client/src/features/settings/KoreaderSettings.vue b/client/src/features/settings/KoreaderSettings.vue index a72aa7eac..09baf33de 100644 --- a/client/src/features/settings/KoreaderSettings.vue +++ b/client/src/features/settings/KoreaderSettings.vue @@ -120,7 +120,7 @@ const creating = ref(false) const showPassword = ref(false) const deleteConfirmOpen = ref(false) const helpOpen = ref(false) -const copiedUrlTarget = ref<'plugin' | 'stock' | null>(null) +const urlCopied = ref(false) const selectedUnmatched = ref(null) const selectedManualLink = ref(null) const linkSearchQuery = ref('') @@ -152,10 +152,7 @@ onUnmounted(() => { if (copiedUrlTimer) clearTimeout(copiedUrlTimer) }) -const syncUrl = computed(() => getSyncUrl()) -const stockSyncUrl = computed(() => new URL('/api/v1/koreader', syncUrl.value).toString()) -const urlCopied = computed(() => copiedUrlTarget.value === 'plugin') -const stockUrlCopied = computed(() => copiedUrlTarget.value === 'stock') +const syncUrl = computed(() => new URL('/api/v1/koreader', getSyncUrl()).toString()) const hasCredentials = computed(() => !!credentials.value) const deviceCount = computed(() => syncStatus.value?.devices.length ?? 0) const totalSyncedBooks = computed(() => syncStatus.value?.totalSyncedBooks ?? 0) @@ -383,30 +380,22 @@ async function handleDelete() { } } -async function copyUrlWithFeedback(url: string, target: 'plugin' | 'stock') { - const copied = await copyToClipboard(url) +async function handleCopySyncUrl() { + const copied = await copyToClipboard(syncUrl.value) if (!copied) { toast.error(t('settings.reader.koreader.syncUrlCopyFailed')) return } - copiedUrlTarget.value = target + urlCopied.value = true toast.success(t('settings.reader.koreader.syncUrlCopied')) if (copiedUrlTimer) clearTimeout(copiedUrlTimer) copiedUrlTimer = setTimeout(() => { - copiedUrlTarget.value = null + urlCopied.value = false copiedUrlTimer = null }, 2000) } -async function handleCopyUrl() { - await copyUrlWithFeedback(syncUrl.value, 'plugin') -} - -async function handleCopyStockUrl() { - await copyUrlWithFeedback(stockSyncUrl.value, 'stock') -} - async function handleRefresh() { try { await fetchSyncStatus(true) @@ -793,13 +782,24 @@ async function handleDownloadPlugin() {

-
- - +
+
+ +
+
+ +

+ {{ t('settings.reader.koreader.syncUrlHint') }} +

+
-
- - -
- +
  • {{ t('settings.reader.koreader.stockStep2') }}
  • {{ t('settings.reader.koreader.stockStep3') }}
  • diff --git a/client/src/features/settings/__tests__/KoreaderSettings.spec.ts b/client/src/features/settings/__tests__/KoreaderSettings.spec.ts index ac110eba1..2a06f7c6c 100644 --- a/client/src/features/settings/__tests__/KoreaderSettings.spec.ts +++ b/client/src/features/settings/__tests__/KoreaderSettings.spec.ts @@ -323,8 +323,9 @@ describe('KoreaderSettings', () => { expect(wrapper.text()).toContain('reader-user') expect(wrapper.text()).toContain('14 books') expect(wrapper.text()).toContain('1 device') - const syncUrlInput = wrapper.find('input[readonly]').element as HTMLInputElement - expect(syncUrlInput.value).toBe('https://bookorbit.example') + expect(wrapper.text()).toContain('KOReader sync URL') + expect(wrapper.text()).toContain("Works with the BookOrbit plugin and KOReader's built-in progress sync.") + expect((wrapper.find('#koreader-sync-url').element as HTMLInputElement).value).toBe('https://bookorbit.example/api/v1/koreader') expect(wrapper.text()).toContain('Kobo Libra 2') expect(wrapper.text()).toContain('Project Hail Mary') expect(wrapper.text()).toContain('Latest plugin: v1.6.0') @@ -351,11 +352,10 @@ describe('KoreaderSettings', () => { await buttonByText(wrapper, 'KOReader setup steps')!.trigger('click') expect(wrapper.text()).toContain('Download the preconfigured plugin above.') - const stockUrlInput = wrapper.find('#koreader-stock-server-url') - expect(wrapper.find('label[for="koreader-stock-server-url"]').text()).toBe('Set the custom sync server to the following URL:') - expect((stockUrlInput.element as HTMLInputElement).value).toBe('https://bookorbit.example/api/v1/koreader') + expect(wrapper.text()).toContain('Set the custom sync server to the URL shown above.') + expect(wrapper.findAll('#koreader-sync-url')).toHaveLength(1) - const copyButton = wrapper.find('#koreader-stock-server-url-copy') + const copyButton = wrapper.find('#koreader-sync-url-copy') await copyButton.trigger('click') expect(vi.mocked(copyToClipboard)).toHaveBeenLastCalledWith('https://bookorbit.example/api/v1/koreader') @@ -469,7 +469,7 @@ describe('KoreaderSettings', () => { expect(koreaderMock.fetchUnmatchedBooks).toHaveBeenCalledTimes(2) expect(koreaderMock.fetchManualHashLinks).toHaveBeenCalledTimes(2) expect(koreaderMock.updateCredentials).toHaveBeenCalledWith({ syncEnabled: false }) - expect(vi.mocked(copyToClipboard)).toHaveBeenCalledWith('https://bookorbit.example') + expect(vi.mocked(copyToClipboard)).toHaveBeenCalledWith('https://bookorbit.example/api/v1/koreader') expect(koreaderMock.downloadPluginPackage).toHaveBeenCalledTimes(1) expect(wrapper.text()).toContain('Delete KOReader credentials?') diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 8d3861003..27d4dd834 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -2105,6 +2105,8 @@ "credentialsCreatedLabel": "Credentials created", "setup": "Setup", "pluginServerUrl": "Plugin server URL", + "syncUrl": "KOReader sync URL", + "syncUrlHint": "Works with the BookOrbit plugin and KOReader's built-in progress sync.", "copied": "Copied", "copyUrl": "Copy URL", "preconfiguredPlugin": "Preconfigured BookOrbit plugin", diff --git a/koreader-plugin/spec/bookorbit_api_test.lua b/koreader-plugin/spec/bookorbit_api_test.lua index 85441c4f3..c8336cdbf 100644 --- a/koreader-plugin/spec/bookorbit_api_test.lua +++ b/koreader-plugin/spec/bookorbit_api_test.lua @@ -108,6 +108,11 @@ local function assertEqual(actual, expected, label) end end +assertEqual(BookOrbitApi.normalizeServerUrl("https://bookorbit.example.com"), + "https://bookorbit.example.com/api/v1", "origin-only server URL is normalized") +assertEqual(BookOrbitApi.normalizeServerUrl("https://bookorbit.example.com/api/v1/koreader"), + "https://bookorbit.example.com/api/v1", "stock sync URL is normalized") + local decoded, err = BookOrbitApi.decodeResponse({ "" }) assertEqual(type(decoded), "table", "empty body decodes to table") assertEqual(err, nil, "empty body has no error")