feat: add v2 character card import/export
- Expanded Character struct with all v2 spec fields (description, scenario, mes_example, post_history_instructions, alternate_greetings, character_book, tags, creator, character_version, creator_notes, extensions) - Created CharacterCardV2 serialization structs following spec at github.com/malfoyslastname/character-card-spec-v2 - Implemented PNG metadata utilities: * read_character_card_from_png() - extracts and decodes character data from PNG tEXt chunks * write_character_card_to_png() - embeds character data into PNG files * create_placeholder_png() - generates gradient placeholder images for avatarless characters - Added Tauri commands: * import_character_card - opens file picker, imports PNG with automatic name conflict handling * export_character_card - exports character as v2 PNG card with embedded metadata - Added Import/Export buttons to character settings UI - Full backward compatibility with existing characters using serde defaults - Added dependencies: png 0.17, base64 0.21, image 0.24
This commit is contained in:
@@ -191,7 +191,20 @@
|
||||
</button>
|
||||
<button type="button" id="delete-character-btn" class="btn-danger">
|
||||
Delete Character
|
||||
</button> </form>
|
||||
</button>
|
||||
|
||||
<div class="form-group" style="margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--border-color);">
|
||||
<label>Character Card Import/Export</label>
|
||||
<div style="display: flex; gap: 0.5rem;">
|
||||
<button type="button" id="import-character-btn" class="btn-secondary" style="flex: 1;">
|
||||
Import v2 Card
|
||||
</button>
|
||||
<button type="button" id="export-character-btn" class="btn-secondary" style="flex: 1;">
|
||||
Export v2 Card
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
47
src/main.js
47
src/main.js
@@ -971,6 +971,8 @@ function setupAppControls() {
|
||||
});
|
||||
document.getElementById('upload-avatar-btn').addEventListener('click', handleAvatarUpload);
|
||||
document.getElementById('remove-avatar-btn').addEventListener('click', handleAvatarRemove);
|
||||
document.getElementById('import-character-btn').addEventListener('click', handleImportCharacter);
|
||||
document.getElementById('export-character-btn').addEventListener('click', handleExportCharacter);
|
||||
}
|
||||
|
||||
// Keyboard shortcuts
|
||||
@@ -1077,6 +1079,51 @@ async function handleDeleteCharacter() {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle character card import
|
||||
async function handleImportCharacter() {
|
||||
const characterMsg = document.getElementById('character-message');
|
||||
try {
|
||||
const importedCharacter = await invoke('import_character_card');
|
||||
characterMsg.textContent = `Successfully imported ${importedCharacter.name}!`;
|
||||
characterMsg.className = 'validation-message success';
|
||||
|
||||
// Reload characters and switch to the imported one
|
||||
await loadCharacters();
|
||||
await loadCharacterSettings();
|
||||
|
||||
setTimeout(() => {
|
||||
characterMsg.style.display = 'none';
|
||||
}, 3000);
|
||||
} catch (error) {
|
||||
console.error('Failed to import character:', error);
|
||||
if (error && !error.toString().includes('No file selected') && !error.toString().includes('cancelled')) {
|
||||
characterMsg.textContent = `Failed to import: ${error}`;
|
||||
characterMsg.className = 'validation-message error';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle character card export
|
||||
async function handleExportCharacter() {
|
||||
const characterMsg = document.getElementById('character-message');
|
||||
try {
|
||||
const characterId = document.getElementById('character-settings-select').value;
|
||||
const outputPath = await invoke('export_character_card', { characterId });
|
||||
characterMsg.textContent = `Successfully exported to ${outputPath}`;
|
||||
characterMsg.className = 'validation-message success';
|
||||
|
||||
setTimeout(() => {
|
||||
characterMsg.style.display = 'none';
|
||||
}, 3000);
|
||||
} catch (error) {
|
||||
console.error('Failed to export character:', error);
|
||||
if (error && !error.toString().includes('cancelled')) {
|
||||
characterMsg.textContent = `Failed to export: ${error}`;
|
||||
characterMsg.className = 'validation-message error';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load chat history
|
||||
async function loadChatHistory() {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user