Adicione ao seu pubspec.yaml:
dependencies:
ganki: ^2.0.0Ou execute:
dart pub add ganki
# ou
flutter pub add gankiSe você estiver no Linux, pode precisar instalar o SQLite3:
# Ubuntu/Debian/PopOS
sudo apt-get install libsqlite3-dev
# Fedora/RedHat
sudo dnf install sqlite-devel
# Arch Linux
sudo pacman -S sqliteWindows e macOS: Nenhuma instalação adicional necessária! ✅
📖 Para mais detalhes, veja INSTALL.md
import 'package:ganki/ganki.dart';
void main() async {
// Criar nota com modelo básico
final note = Note(
model: basicModel,
fields: ['Capital do Brasil', 'Brasília'],
tags: ['geografia', 'capitais'],
);
// Criar deck e adicionar nota
final deck = Deck(
deckId: 2059400110,
name: 'Capitais do Mundo',
description: 'Aprenda capitais',
);
deck.addNote(note);
// Criar pacote e salvar
final package = Package(deck);
await package.writeToFile('capitais.apkg');
}Define a estrutura dos seus cards (campos e templates).
final customModel = Model(
modelId: 1607392319,
name: 'Vocabulário',
fields: [
{'name': 'Português'},
{'name': 'Inglês'},
{'name': 'Exemplo'},
],
templates: [
{
'name': 'Card 1',
'qfmt': '{{Português}}',
'afmt': '{{FrontSide}}<hr id="answer">{{Inglês}}<br><i>{{Exemplo}}</i>',
},
],
css: '.card { font-size: 20px; text-align: center; }',
);Importante:
modelIddeve ser único. Gere com:Random().nextInt(1 << 30) + (1 << 30)qfmt= formato da pergunta (question format)afmt= formato da resposta (answer format)
Representa um fato a memorizar.
final note = Note(
model: customModel,
fields: ['Olá', 'Hello', 'Olá, tudo bem?'],
tags: ['saudações', 'básico'],
guid: 'custom-guid-123', // opcional
);GUID: Identificador único. Se não fornecido, é gerado automaticamente a partir dos campos. Notas com mesmo GUID são consideradas a mesma nota (útil para atualizações).
Coleção de notas.
final deck = Deck(
deckId: 2059400110, // deve ser único
name: 'Meu Deck',
description: 'Descrição opcional',
);
deck.addNote(note1);
deck.addNote(note2);Arquivo .apkg final que pode ser importado no Anki.
// Um deck
final package = Package(deck);
// Múltiplos decks
final package = Package([deck1, deck2, deck3]);
// Com arquivos de mídia
final package = Package(deck, mediaFiles: [
'audio/hello.mp3',
'images/flag.jpg',
]);
await package.writeToFile('output.apkg');A biblioteca inclui modelos pré-definidos:
final note = Note(
model: basicModel,
fields: ['Frente', 'Verso'],
);final note = Note(
model: basicAndReversedCardModel,
fields: ['Português', 'English'],
);
// Gera 2 cards: PT→EN e EN→PTfinal note = Note(
model: basicOptionalReversedCardModel,
fields: ['Frente', 'Verso', 'sim'], // 'sim' ativa o reverso
);final note = Note(
model: clozeModel,
fields: [
'A capital do {{c1::Brasil}} é {{c2::Brasília}}',
'Extra info',
],
);
// Gera 2 cards: um para cada {{c1::}} e {{c2::}}final model = Model(
modelId: 1234567890,
name: 'Modelo Bonito',
fields: [
{'name': 'Pergunta'},
{'name': 'Resposta'},
],
templates: [
{
'name': 'Card 1',
'qfmt': '<div class="question">{{Pergunta}}</div>',
'afmt': '''
{{FrontSide}}
<hr id="answer">
<div class="answer">{{Resposta}}</div>
''',
},
],
css: '''
.card {
font-family: Arial;
font-size: 24px;
text-align: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
}
.question {
font-size: 28px;
font-weight: bold;
}
.answer {
font-size: 24px;
margin-top: 20px;
}
''',
);import 'dart:io';
import 'package:ganki/ganki.dart';
Future<void> importFromCsv(String csvPath) async {
final file = File(csvPath);
final lines = await file.readAsLines();
final deck = Deck(
deckId: 2059400110,
name: 'Imported from CSV',
);
for (final line in lines.skip(1)) { // pula header
final parts = line.split(',');
if (parts.length >= 2) {
deck.addNote(Note(
model: basicModel,
fields: [parts[0].trim(), parts[1].trim()],
));
}
}
final package = Package(deck);
await package.writeToFile('imported.apkg');
}final model = Model(
modelId: 1234567891,
name: 'Completo',
fields: [
{'name': 'Word'},
{'name': 'Translation'},
{'name': 'Example'},
{'name': 'Audio'},
],
templates: [
{
'name': 'Recognition',
'qfmt': '{{Word}}<br>[sound:{{Audio}}]',
'afmt': '{{FrontSide}}<hr>{{Translation}}<br><i>{{Example}}</i>',
},
{
'name': 'Production',
'qfmt': '{{Translation}}',
'afmt': '{{FrontSide}}<hr>{{Word}}<br>[sound:{{Audio}}]<br><i>{{Example}}</i>',
},
],
);final note = Note(
model: basicModel,
fields: [
'Como se diz "hello"?',
'Olá [sound:hello.mp3]<br><img src="hello.jpg">',
],
);
final package = Package(deck, mediaFiles: [
'media/hello.mp3',
'media/hello.jpg',
]);
await package.writeToFile('with_media.apkg');import 'dart:math';
int generateUniqueId() {
final random = Random();
return random.nextInt(1 << 30) + (1 << 30);
}
final deckId = generateUniqueId();
final modelId = generateUniqueId();Se seus campos contêm caracteres especiais (<, >, &), use:
import 'package:ganki/ganki.dart';
final safeText = escapeHtml('5 < 10 & 20 > 15');
// '5 < 10 & 20 > 15'Para builds determinísticos:
final timestamp = DateTime(2024, 1, 1).millisecondsSinceEpoch / 1000.0;
await package.writeToFile('output.apkg', timestamp: timestamp);Os templates usam a sintaxe Mustache:
{{Field}}- Insere o valor do campo{{#Field}}...{{/Field}}- Renderiza se Field não estiver vazio{{^Field}}...{{/Field}}- Renderiza se Field estiver vazio{{!comment}}- Comentário (não renderizado)
- IDs devem ser únicos - Use números aleatórios grandes para evitar conflitos
- Campos são HTML - Escape caracteres especiais se necessário
- Cloze precisa de 2 campos - Sempre forneça 2 campos ao clozeModel
- Arquivos de mídia - Devem existir no caminho especificado
- Sort Field - Por padrão é o primeiro campo, usado para ordenação no Anki
Certifique-se de que o número de campos na Note corresponde ao número de campos no Model.
Verifique se o template qfmt está correto e usa os nomes de campos corretos.
- Verifique se os IDs são únicos
- Teste com um deck simples primeiro
- Certifique-se de que não há erros no template
MIT License - veja LICENSE para detalhes.
Contribuições são bem-vindas! Abra issues ou pull requests no GitHub.
Veja a pasta example/ para mais exemplos de uso!
Ganki - Crie decks Anki programaticamente em Dart/Flutter! 🚀