Skip to content

Commit ff8c57f

Browse files
Gnathonicclaude
andcommitted
feat: Add AnkiConnect CORS permission request
When connection fails due to CORS, automatically request permission from Anki which triggers a popup for the user to grant access. This is much more user-friendly than requiring manual config edits. Based on implementation from Mangatan-WebUI (MPL-2.0) https://github.com/KolbyML/Mangatan-WebUI Closes #151 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 31a1c84 commit ff8c57f

1 file changed

Lines changed: 49 additions & 6 deletions

File tree

src/lib/anki-connect/index.ts

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
/**
2+
* AnkiConnect integration for creating and updating Anki cards.
3+
*
4+
* The CORS permission request pattern is based on code from Mangatan-WebUI
5+
* by KolbyML, licensed under Mozilla Public License 2.0.
6+
* https://github.com/KolbyML/Mangatan-WebUI
7+
*/
8+
19
import type {
210
Settings,
311
AnkiConnectSettings,
@@ -415,14 +423,33 @@ export function getModelConfig(
415423

416424
export type ConnectionTestResult = {
417425
success: boolean;
418-
error?: 'network' | 'cors' | 'invalid_response' | 'anki_error';
426+
error?: 'network' | 'cors' | 'invalid_response' | 'anki_error' | 'permission_denied';
419427
message: string;
420428
version?: number;
421429
};
422430

431+
/**
432+
* Requests permission from AnkiConnect.
433+
* This triggers a popup in Anki asking the user to grant permission to this website.
434+
* Returns true if permission was granted, false otherwise.
435+
*/
436+
async function requestAnkiPermission(url: string): Promise<boolean> {
437+
try {
438+
const res = await fetch(url, {
439+
method: 'POST',
440+
body: JSON.stringify({ action: 'requestPermission', version: 6 })
441+
});
442+
const json = await res.json();
443+
return json.result?.permission === 'granted';
444+
} catch {
445+
return false;
446+
}
447+
}
448+
423449
/**
424450
* Tests the AnkiConnect connection and returns detailed error information.
425451
* Uses the "version" action which is a simple ping that returns the API version.
452+
* If CORS blocks the request, attempts to request permission from Anki.
426453
*/
427454
export async function testConnection(testUrl?: string): Promise<ConnectionTestResult> {
428455
const url = testUrl || get(settings).ankiConnectSettings.url || 'http://127.0.0.1:8765';
@@ -453,14 +480,21 @@ export async function testConnection(testUrl?: string): Promise<ConnectionTestRe
453480
const errorMessage = e?.message ?? String(e);
454481

455482
// CORS errors typically show as "Failed to fetch" or similar network errors
456-
// but we can check if it's a TypeError which often indicates CORS
457483
if (e instanceof TypeError && errorMessage.includes('Failed to fetch')) {
458-
// Could be CORS or network - provide guidance for both
484+
// Try requesting permission from Anki - this triggers a popup in Anki
485+
const granted = await requestAnkiPermission(url);
486+
487+
if (granted) {
488+
// Permission granted, retry the connection
489+
return testConnection(testUrl);
490+
}
491+
492+
// Permission not granted or request failed
459493
return {
460494
success: false,
461495
error: 'cors',
462496
message:
463-
'Cannot connect. Either Anki is not running, the URL is wrong, or CORS is not configured. Add this site to webCorsOriginList in AnkiConnect settings.'
497+
'Connection blocked. If Anki showed a permission popup, click "Yes" and try again. Otherwise, add this site to webCorsOriginList in AnkiConnect settings.'
464498
};
465499
}
466500

@@ -483,7 +517,7 @@ export async function testConnection(testUrl?: string): Promise<ConnectionTestRe
483517
export async function ankiConnect(
484518
action: string,
485519
params: Record<string, any>,
486-
options?: { silent?: boolean }
520+
options?: { silent?: boolean; retried?: boolean }
487521
) {
488522
const url = get(settings).ankiConnectSettings.url || 'http://127.0.0.1:8765';
489523

@@ -509,8 +543,17 @@ export async function ankiConnect(
509543
const errorMessage = e?.message ?? String(e);
510544

511545
if (e instanceof TypeError && errorMessage.includes('Failed to fetch')) {
546+
// Try requesting permission if we haven't already retried
547+
if (!options?.retried) {
548+
const granted = await requestAnkiPermission(url);
549+
if (granted) {
550+
// Retry the request
551+
return ankiConnect(action, params, { ...options, retried: true });
552+
}
553+
}
554+
512555
showSnackbar(
513-
'Error: Cannot connect to AnkiConnect. Check that Anki is running and CORS is configured.'
556+
'Error: Cannot connect to AnkiConnect. If Anki showed a permission popup, click "Yes" and try again.'
514557
);
515558
} else {
516559
showSnackbar(`Error: ${errorMessage}`);

0 commit comments

Comments
 (0)