Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
0ec1115
feat: Redesign AnkiConnect settings with dynamic field mapping
Gnathonic Jan 7, 2026
b65e184
fix: Apply missed merge changes from main
Gnathonic Jan 7, 2026
d5a7dad
fix: Move crop preset option to own section with correct text
Gnathonic Jan 7, 2026
5b02803
docs: Update changelog for v1.3.0 AnkiConnect redesign
Gnathonic Jan 7, 2026
a4d5b83
chore: Bump version to 1.3.0
Gnathonic Jan 7, 2026
dee2a5b
Merge branch 'develop' into feat/anki-connect-redesign
Gnathonic Jan 22, 2026
53d550b
Merge branch 'develop' into feat/anki-connect-redesign
Gnathonic Jan 23, 2026
31a1c84
feat: Unified AnkiConnect field modal with three modes
Gnathonic Jan 23, 2026
ff8c57f
feat: Add AnkiConnect CORS permission request
Gnathonic Jan 23, 2026
6ebccf1
refactor: Remove redundant AnkiConnect enabled toggle
Gnathonic Jan 23, 2026
c3255c8
fix: Show resolved deck name in modal label
Gnathonic Jan 23, 2026
82a2b65
fix: Align modal header and collapsible row labels
Gnathonic Jan 23, 2026
1570986
fix: Improve row styling - de-emphasize labels, emphasize values
Gnathonic Jan 23, 2026
994cd69
fix: Allow scroll wheel in modals and preserve crop on resize
Gnathonic Jan 23, 2026
bd318d2
fix: Truncate long field previews and clamp crop to image bounds
Gnathonic Jan 23, 2026
3888cb6
fix: Require model name in update mode with retry logic
Gnathonic Jan 23, 2026
46661d5
fix: Use correct model name for field fetching in modal
Gnathonic Jan 23, 2026
98aa25b
fix: Disable image dragging in cropper
Gnathonic Jan 23, 2026
19f0772
feat(anki): Implement tiered accordion layout in AnkiFieldModal
Gnathonic Jan 23, 2026
57a3f73
feat(anki): Add field action badges (Append/Unchanged/Replace Value)
Gnathonic Jan 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
# Changelog

## [1.3.0] - 2026-01-06

### Added

- **Dynamic AnkiConnect field mapping** - Configure which template populates each Anki field with a visual UI
- **Template variables for fields** - Use `{selection}`, `{sentence}`, `{image}`, `{series}`, `{volume}` in any field
- **Mixed field templates** - Combine text and images in the same field (e.g., `{sentence} {image}`)
- **Android mode detection** - Auto-detects AnkiConnect Android limitations with manual override option
- **Connection-gated AnkiConnect settings** - Settings only appear after connecting, with live model/deck/field data from Anki

### Changed

- **AnkiConnect settings redesign** - Complete overhaul with connection status, enable toggle, and organized sections
- **Card mode selection** - Now uses radio buttons instead of dropdown for clearer selection

## [1.2.3] - 2026-01-15

### Added
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mokuro-reader",
"version": "1.2.3",
"version": "1.3.0",
"private": true,
"scripts": {
"dev": "vite dev",
Expand Down
105 changes: 101 additions & 4 deletions src/lib/anki-connect/cropper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,40 @@ export function expandTextBoxBounds(
];
}

type CropperModal = {
/**
* Modal modes for the unified Anki field modal:
* - configure: Edit raw template strings for field mapping (no cropper)
* - create: Create new card with resolved templates (shows cropper)
* - update: Update existing card with {existing} support (shows cropper)
*/
export type AnkiModalMode = 'configure' | 'create' | 'update';

type AnkiFieldModal = {
open: boolean;
mode: AnkiModalMode;
image?: string;
selectedText?: string; // The selected/highlighted text (for Front field)
sentence?: string; // The full sentence/all lines (for Sentence field)
tags?: string;
metadata?: VolumeMetadata;
pageNumber?: number;
pageFilename?: string; // The filename of the current page image
pages?: Page[]; // Available pages with textboxes for visual selection
textBox?: [number, number, number, number]; // [xmin, ymin, xmax, ymax] for initial crop
previousValues?: Record<string, string>; // field name -> previous value
previousTags?: string[]; // existing tags on the card being updated
previousCardId?: number;
modelName?: string; // For update mode: the model name from the card being updated
};

export const cropperStore = writable<CropperModal | undefined>(undefined);
// Backwards compatibility alias
type CropperModal = AnkiFieldModal;

export const cropperStore = writable<AnkiFieldModal | undefined>(undefined);

/**
* Legacy function for backwards compatibility - opens modal in create mode
*/
export function showCropper(
image: string,
selectedText?: string,
Expand All @@ -47,19 +68,95 @@ export function showCropper(
metadata?: VolumeMetadata,
pages?: Page[],
textBox?: [number, number, number, number]
) {
openCreateModal(image, selectedText, sentence, tags, metadata, pages, textBox);
}

/**
* Opens the modal in configure mode (no cropper, raw templates)
* @param modelName - Optional model name to configure. If not provided, uses the selected model from settings.
*/
export function openConfigureModal(modelName?: string) {
cropperStore.set({
open: true,
mode: 'configure',
modelName
});
}

/**
* Opens the modal in create mode (cropper + resolved templates)
*/
export function openCreateModal(
image: string,
selectedText?: string,
sentence?: string,
tags?: string,
metadata?: VolumeMetadata,
pages?: Page[],
textBox?: [number, number, number, number],
pageNumber?: number,
pageFilename?: string
) {
cropperStore.set({
open: true,
mode: 'create',
image,
selectedText,
sentence,
tags,
metadata,
pages,
textBox
textBox,
pageNumber,
pageFilename
});
}

/**
* Opens the modal in update mode (cropper + {existing} support)
*/
export function openUpdateModal(
image: string,
previousValues: Record<string, string>,
previousCardId: number,
modelName: string,
previousTags?: string[],
selectedText?: string,
sentence?: string,
tags?: string,
metadata?: VolumeMetadata,
pages?: Page[],
textBox?: [number, number, number, number],
pageNumber?: number,
pageFilename?: string
) {
cropperStore.set({
open: true,
mode: 'update',
image,
selectedText,
sentence,
tags,
metadata,
pages,
textBox,
pageNumber,
pageFilename,
previousValues,
previousTags,
previousCardId,
modelName
});
}

/**
* Closes the modal
*/
export function closeAnkiModal() {
cropperStore.set({ open: false, mode: 'create' });
}

async function createImage(url: string) {
return new Promise<HTMLImageElement>((resolve, reject) => {
const image = new Image();
Expand Down Expand Up @@ -126,7 +223,7 @@ export async function getCroppedImg(
settings.ankiConnectSettings.heightField
);
const blob = await canvas.convertToBlob({
type: 'image/webp',
type: 'image/jpeg',
quality: settings.ankiConnectSettings.qualityField
});

Expand Down
Loading
Loading