anki_record 0.1.1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +2 -0
- data/CHANGELOG.md +33 -4
- data/Gemfile +1 -1
- data/Gemfile.lock +5 -3
- data/README.md +147 -11
- data/anki_record.gemspec +2 -6
- data/lib/anki_record/anki_package/anki_package.rb +245 -0
- data/lib/anki_record/anki_package/database_setup_constants.rb +91 -0
- data/lib/anki_record/card/card.rb +108 -0
- data/lib/anki_record/card/card_attributes.rb +39 -0
- data/lib/anki_record/{card_template.rb → card_template/card_template.rb} +20 -47
- data/lib/anki_record/card_template/card_template_attributes.rb +69 -0
- data/lib/anki_record/collection/collection.rb +180 -0
- data/lib/anki_record/collection/collection_attributes.rb +35 -0
- data/lib/anki_record/deck/deck.rb +101 -0
- data/lib/anki_record/deck/deck_attributes.rb +30 -0
- data/lib/anki_record/deck/deck_defaults.rb +19 -0
- data/lib/anki_record/{deck_options_group.rb → deck_options_group/deck_options_group.rb} +10 -23
- data/lib/anki_record/deck_options_group/deck_options_group_attributes.rb +23 -0
- data/lib/anki_record/helpers/checksum_helper.rb +17 -0
- data/lib/anki_record/helpers/data_query_helper.rb +13 -0
- data/lib/anki_record/helpers/shared_constants_helper.rb +1 -3
- data/lib/anki_record/helpers/time_helper.rb +7 -5
- data/lib/anki_record/note/note.rb +181 -0
- data/lib/anki_record/note/note_attributes.rb +56 -0
- data/lib/anki_record/note_field/note_field.rb +62 -0
- data/lib/anki_record/note_field/note_field_attributes.rb +39 -0
- data/lib/anki_record/note_field/note_field_defaults.rb +19 -0
- data/lib/anki_record/note_type/note_type.rb +161 -0
- data/lib/anki_record/note_type/note_type_attributes.rb +80 -0
- data/lib/anki_record/note_type/note_type_defaults.rb +38 -0
- data/lib/anki_record/version.rb +1 -1
- data/lib/anki_record.rb +1 -15
- metadata +32 -20
- data/.rdoc_options +0 -27
- data/lib/anki_record/anki_package.rb +0 -183
- data/lib/anki_record/collection.rb +0 -65
- data/lib/anki_record/db/anki_schema_definition.rb +0 -77
- data/lib/anki_record/db/clean_collection21_record.rb +0 -10
- data/lib/anki_record/db/clean_collection2_record.rb +0 -10
- data/lib/anki_record/deck.rb +0 -88
- data/lib/anki_record/note_field.rb +0 -63
- data/lib/anki_record/note_type.rb +0 -147
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnkiRecord
|
4
|
+
ANKI_SCHEMA_DEFINITION = <<~SQL # :nodoc:
|
5
|
+
CREATE TABLE col (
|
6
|
+
id integer PRIMARY KEY,
|
7
|
+
crt integer NOT NULL,
|
8
|
+
mod integer NOT NULL,
|
9
|
+
scm integer NOT NULL,
|
10
|
+
ver integer NOT NULL,
|
11
|
+
dty integer NOT NULL,
|
12
|
+
usn integer NOT NULL,
|
13
|
+
ls integer NOT NULL,
|
14
|
+
conf text NOT NULL,
|
15
|
+
models text NOT NULL,
|
16
|
+
decks text NOT NULL,
|
17
|
+
dconf text NOT NULL,
|
18
|
+
tags text NOT NULL
|
19
|
+
);
|
20
|
+
CREATE TABLE notes (
|
21
|
+
id integer PRIMARY KEY,
|
22
|
+
guid text NOT NULL,
|
23
|
+
mid integer NOT NULL,
|
24
|
+
mod integer NOT NULL,
|
25
|
+
usn integer NOT NULL,
|
26
|
+
tags text NOT NULL,
|
27
|
+
flds text NOT NULL,
|
28
|
+
sfld integer NOT NULL,
|
29
|
+
csum integer NOT NULL,
|
30
|
+
flags integer NOT NULL,
|
31
|
+
data text NOT NULL
|
32
|
+
);
|
33
|
+
CREATE TABLE cards (
|
34
|
+
id integer PRIMARY KEY,
|
35
|
+
nid integer NOT NULL,
|
36
|
+
did integer NOT NULL,
|
37
|
+
ord integer NOT NULL,
|
38
|
+
mod integer NOT NULL,
|
39
|
+
usn integer NOT NULL,
|
40
|
+
type integer NOT NULL,
|
41
|
+
queue integer NOT NULL,
|
42
|
+
due integer NOT NULL,
|
43
|
+
ivl integer NOT NULL,
|
44
|
+
factor integer NOT NULL,
|
45
|
+
reps integer NOT NULL,
|
46
|
+
lapses integer NOT NULL,
|
47
|
+
left integer NOT NULL,
|
48
|
+
odue integer NOT NULL,
|
49
|
+
odid integer NOT NULL,
|
50
|
+
flags integer NOT NULL,
|
51
|
+
data text NOT NULL
|
52
|
+
);
|
53
|
+
CREATE TABLE revlog (
|
54
|
+
id integer PRIMARY KEY,
|
55
|
+
cid integer NOT NULL,
|
56
|
+
usn integer NOT NULL,
|
57
|
+
ease integer NOT NULL,
|
58
|
+
ivl integer NOT NULL,
|
59
|
+
lastIvl integer NOT NULL,
|
60
|
+
factor integer NOT NULL,
|
61
|
+
time integer NOT NULL,
|
62
|
+
type integer NOT NULL
|
63
|
+
);
|
64
|
+
CREATE INDEX ix_notes_usn ON notes (usn);
|
65
|
+
CREATE INDEX ix_cards_usn ON cards (usn);
|
66
|
+
CREATE INDEX ix_revlog_usn ON revlog (usn);
|
67
|
+
CREATE INDEX ix_cards_nid ON cards (nid);
|
68
|
+
CREATE INDEX ix_cards_sched ON cards (did, queue, due);
|
69
|
+
CREATE INDEX ix_revlog_cid ON revlog (cid);
|
70
|
+
CREATE INDEX ix_notes_csum ON notes (csum);
|
71
|
+
CREATE TABLE graves (
|
72
|
+
usn integer NOT NULL,
|
73
|
+
oid integer NOT NULL,
|
74
|
+
type integer NOT NULL
|
75
|
+
);
|
76
|
+
SQL
|
77
|
+
|
78
|
+
##
|
79
|
+
# This is the SQL insert statement (as a Ruby string/here document) for the
|
80
|
+
# default col record in the collection.anki2 database exported from a new Anki 2.1.54 profile
|
81
|
+
INSERT_COLLECTION_ANKI_2_COL_RECORD = <<~SQL
|
82
|
+
INSERT INTO col VALUES(1,1676883600,1676902390012,1676902390005,11,0,0,0,'{"addToCur":true,"_deck_1_lastNotetype":1676902390008,"nextPos":2,"sortType":"noteFld","newSpread":0,"schedVer":2,"collapseTime":1200,"estTimes":true,"curDeck":1,"creationOffset":300,"dayLearnFirst":false,"timeLim":0,"activeDecks":[1],"sortBackwards":false,"_nt_1676902390008_lastDeck":1,"curModel":1676902390008,"dueCounts":true}','{"1676902390010":{"id":1676902390010,"name":"Basic (optional reversed card)","type":0,"mod":0,"usn":0,"sortf":0,"did":null,"tmpls":[{"name":"Card 1","ord":0,"qfmt":"{{Front}}","afmt":"{{FrontSide}}\\n\\n<hr id=answer>\\n\\n{{Back}}","bqfmt":"","bafmt":"","did":null,"bfont":"","bsize":0},{"name":"Card 2","ord":1,"qfmt":"{{#Add Reverse}}{{Back}}{{/Add Reverse}}","afmt":"{{FrontSide}}\\n\\n<hr id=answer>\\n\\n{{Front}}","bqfmt":"","bafmt":"","did":null,"bfont":"","bsize":0}],"flds":[{"name":"Front","ord":0,"sticky":false,"rtl":false,"font":"Arial","size":20,"description":""},{"name":"Back","ord":1,"sticky":false,"rtl":false,"font":"Arial","size":20,"description":""},{"name":"Add Reverse","ord":2,"sticky":false,"rtl":false,"font":"Arial","size":20,"description":""}],"css":".card {\\n font-family: arial;\\n font-size: 20px;\\n text-align: center;\\n color: black;\\n background-color: white;\\n}\\n","latexPre":"\\\\documentclass[12pt]{article}\\n\\\\special{papersize=3in,5in}\\n\\\\usepackage[utf8]{inputenc}\\n\\\\usepackage{amssymb,amsmath}\\n\\\\pagestyle{empty}\\n\\\\setlength{\\\\parindent}{0in}\\n\\\\begin{document}\\n","latexPost":"\\\\end{document}","latexsvg":false,"req":[[0,"any",[0]],[1,"all",[1,2]]]},"1676902390009":{"id":1676902390009,"name":"Basic (and reversed card)","type":0,"mod":0,"usn":0,"sortf":0,"did":null,"tmpls":[{"name":"Card 1","ord":0,"qfmt":"{{Front}}","afmt":"{{FrontSide}}\\n\\n<hr id=answer>\\n\\n{{Back}}","bqfmt":"","bafmt":"","did":null,"bfont":"","bsize":0},{"name":"Card 2","ord":1,"qfmt":"{{Back}}","afmt":"{{FrontSide}}\\n\\n<hr id=answer>\\n\\n{{Front}}","bqfmt":"","bafmt":"","did":null,"bfont":"","bsize":0}],"flds":[{"name":"Front","ord":0,"sticky":false,"rtl":false,"font":"Arial","size":20,"description":""},{"name":"Back","ord":1,"sticky":false,"rtl":false,"font":"Arial","size":20,"description":""}],"css":".card {\\n font-family: arial;\\n font-size: 20px;\\n text-align: center;\\n color: black;\\n background-color: white;\\n}\\n","latexPre":"\\\\documentclass[12pt]{article}\\n\\\\special{papersize=3in,5in}\\n\\\\usepackage[utf8]{inputenc}\\n\\\\usepackage{amssymb,amsmath}\\n\\\\pagestyle{empty}\\n\\\\setlength{\\\\parindent}{0in}\\n\\\\begin{document}\\n","latexPost":"\\\\end{document}","latexsvg":false,"req":[[0,"any",[0]],[1,"any",[1]]]},"1676902390008":{"id":1676902390008,"name":"Basic","type":0,"mod":0,"usn":0,"sortf":0,"did":null,"tmpls":[{"name":"Card 1","ord":0,"qfmt":"{{Front}}","afmt":"{{FrontSide}}\\n\\n<hr id=answer>\\n\\n{{Back}}","bqfmt":"","bafmt":"","did":null,"bfont":"","bsize":0}],"flds":[{"name":"Front","ord":0,"sticky":false,"rtl":false,"font":"Arial","size":20,"description":""},{"name":"Back","ord":1,"sticky":false,"rtl":false,"font":"Arial","size":20,"description":""}],"css":".card {\\n font-family: arial;\\n font-size: 20px;\\n text-align: center;\\n color: black;\\n background-color: white;\\n}\\n","latexPre":"\\\\documentclass[12pt]{article}\\n\\\\special{papersize=3in,5in}\\n\\\\usepackage[utf8]{inputenc}\\n\\\\usepackage{amssymb,amsmath}\\n\\\\pagestyle{empty}\\n\\\\setlength{\\\\parindent}{0in}\\n\\\\begin{document}\\n","latexPost":"\\\\end{document}","latexsvg":false,"req":[[0,"any",[0]]]},"1676902390011":{"id":1676902390011,"name":"Basic (type in the answer)","type":0,"mod":0,"usn":0,"sortf":0,"did":null,"tmpls":[{"name":"Card 1","ord":0,"qfmt":"{{Front}}\\n\\n{{type:Back}}","afmt":"{{Front}}\\n\\n<hr id=answer>\\n\\n{{type:Back}}","bqfmt":"","bafmt":"","did":null,"bfont":"","bsize":0}],"flds":[{"name":"Front","ord":0,"sticky":false,"rtl":false,"font":"Arial","size":20,"description":""},{"name":"Back","ord":1,"sticky":false,"rtl":false,"font":"Arial","size":20,"description":""}],"css":".card {\\n font-family: arial;\\n font-size: 20px;\\n text-align: center;\\n color: black;\\n background-color: white;\\n}\\n","latexPre":"\\\\documentclass[12pt]{article}\\n\\\\special{papersize=3in,5in}\\n\\\\usepackage[utf8]{inputenc}\\n\\\\usepackage{amssymb,amsmath}\\n\\\\pagestyle{empty}\\n\\\\setlength{\\\\parindent}{0in}\\n\\\\begin{document}\\n","latexPost":"\\\\end{document}","latexsvg":false,"req":[[0,"any",[0,1]]]},"1676902390012":{"id":1676902390012,"name":"Cloze","type":1,"mod":0,"usn":0,"sortf":0,"did":null,"tmpls":[{"name":"Cloze","ord":0,"qfmt":"{{cloze:Text}}","afmt":"{{cloze:Text}}<br>\\n{{Back Extra}}","bqfmt":"","bafmt":"","did":null,"bfont":"","bsize":0}],"flds":[{"name":"Text","ord":0,"sticky":false,"rtl":false,"font":"Arial","size":20,"description":""},{"name":"Back Extra","ord":1,"sticky":false,"rtl":false,"font":"Arial","size":20,"description":""}],"css":".card {\\n font-family: arial;\\n font-size: 20px;\\n text-align: center;\\n color: black;\\n background-color: white;\\n}\\n.cloze {\\n font-weight: bold;\\n color: blue;\\n}\\n.nightMode .cloze {\\n color: lightblue;\\n}\\n","latexPre":"\\\\documentclass[12pt]{article}\\n\\\\special{papersize=3in,5in}\\n\\\\usepackage[utf8]{inputenc}\\n\\\\usepackage{amssymb,amsmath}\\n\\\\pagestyle{empty}\\n\\\\setlength{\\\\parindent}{0in}\\n\\\\begin{document}\\n","latexPost":"\\\\end{document}","latexsvg":false,"req":[[0,"any",[0]]]}}','{"1":{"id":1,"mod":0,"name":"Default","usn":0,"lrnToday":[0,0],"revToday":[0,0],"newToday":[0,0],"timeToday":[0,0],"collapsed":true,"browserCollapsed":true,"desc":"","dyn":0,"conf":1,"extendNew":0,"extendRev":0}}','{"1":{"id":1,"mod":0,"name":"Default","usn":0,"maxTaken":60,"autoplay":true,"timer":0,"replayq":true,"new":{"bury":false,"delays":[1.0,10.0],"initialFactor":2500,"ints":[1,4,0],"order":1,"perDay":20},"rev":{"bury":false,"ease4":1.3,"ivlFct":1.0,"maxIvl":36500,"perDay":200,"hardFactor":1.2},"lapse":{"delays":[10.0],"leechAction":1,"leechFails":8,"minInt":1,"mult":0.0},"dyn":false,"newMix":0,"newPerDayMinimum":0,"interdayLearningMix":0,"reviewOrder":0,"newSortOrder":0,"newGatherPriority":0,"buryInterdayLearning":false}}','{}');
|
83
|
+
SQL
|
84
|
+
|
85
|
+
##
|
86
|
+
# This is the SQL insert statement (as a Ruby string/here document) for the
|
87
|
+
# default col record in the collection.anki21 database exported from a new Anki 2.1.54 profile
|
88
|
+
INSERT_COLLECTION_ANKI_21_COL_RECORD = <<~SQL
|
89
|
+
INSERT INTO col VALUES(1,1676883600,0,1676902364657,11,0,0,0,'{"activeDecks":[1],"curDeck":1,"nextPos":1,"schedVer":2,"sortType":"noteFld","estTimes":true,"collapseTime":1200,"dayLearnFirst":false,"curModel":1676902364661,"sortBackwards":false,"newSpread":0,"creationOffset":300,"timeLim":0,"addToCur":true,"dueCounts":true}','{"1676902364664":{"id":1676902364664,"name":"Basic (type in the answer)","type":0,"mod":0,"usn":0,"sortf":0,"did":null,"tmpls":[{"name":"Card 1","ord":0,"qfmt":"{{Front}}\\n\\n{{type:Back}}","afmt":"{{Front}}\\n\\n<hr id=answer>\\n\\n{{type:Back}}","bqfmt":"","bafmt":"","did":null,"bfont":"","bsize":0}],"flds":[{"name":"Front","ord":0,"sticky":false,"rtl":false,"font":"Arial","size":20,"description":""},{"name":"Back","ord":1,"sticky":false,"rtl":false,"font":"Arial","size":20,"description":""}],"css":".card {\\n font-family: arial;\\n font-size: 20px;\\n text-align: center;\\n color: black;\\n background-color: white;\\n}\\n","latexPre":"\\\\documentclass[12pt]{article}\\n\\\\special{papersize=3in,5in}\\n\\\\usepackage[utf8]{inputenc}\\n\\\\usepackage{amssymb,amsmath}\\n\\\\pagestyle{empty}\\n\\\\setlength{\\\\parindent}{0in}\\n\\\\begin{document}\\n","latexPost":"\\\\end{document}","latexsvg":false,"req":[[0,"any",[0,1]]]},"1676902364663":{"id":1676902364663,"name":"Basic (optional reversed card)","type":0,"mod":0,"usn":0,"sortf":0,"did":null,"tmpls":[{"name":"Card 1","ord":0,"qfmt":"{{Front}}","afmt":"{{FrontSide}}\\n\\n<hr id=answer>\\n\\n{{Back}}","bqfmt":"","bafmt":"","did":null,"bfont":"","bsize":0},{"name":"Card 2","ord":1,"qfmt":"{{#Add Reverse}}{{Back}}{{/Add Reverse}}","afmt":"{{FrontSide}}\\n\\n<hr id=answer>\\n\\n{{Front}}","bqfmt":"","bafmt":"","did":null,"bfont":"","bsize":0}],"flds":[{"name":"Front","ord":0,"sticky":false,"rtl":false,"font":"Arial","size":20,"description":""},{"name":"Back","ord":1,"sticky":false,"rtl":false,"font":"Arial","size":20,"description":""},{"name":"Add Reverse","ord":2,"sticky":false,"rtl":false,"font":"Arial","size":20,"description":""}],"css":".card {\\n font-family: arial;\\n font-size: 20px;\\n text-align: center;\\n color: black;\\n background-color: white;\\n}\\n","latexPre":"\\\\documentclass[12pt]{article}\\n\\\\special{papersize=3in,5in}\\n\\\\usepackage[utf8]{inputenc}\\n\\\\usepackage{amssymb,amsmath}\\n\\\\pagestyle{empty}\\n\\\\setlength{\\\\parindent}{0in}\\n\\\\begin{document}\\n","latexPost":"\\\\end{document}","latexsvg":false,"req":[[0,"any",[0]],[1,"all",[1,2]]]},"1676902364665":{"id":1676902364665,"name":"Cloze","type":1,"mod":0,"usn":0,"sortf":0,"did":null,"tmpls":[{"name":"Cloze","ord":0,"qfmt":"{{cloze:Text}}","afmt":"{{cloze:Text}}<br>\\n{{Back Extra}}","bqfmt":"","bafmt":"","did":null,"bfont":"","bsize":0}],"flds":[{"name":"Text","ord":0,"sticky":false,"rtl":false,"font":"Arial","size":20,"description":""},{"name":"Back Extra","ord":1,"sticky":false,"rtl":false,"font":"Arial","size":20,"description":""}],"css":".card {\\n font-family: arial;\\n font-size: 20px;\\n text-align: center;\\n color: black;\\n background-color: white;\\n}\\n.cloze {\\n font-weight: bold;\\n color: blue;\\n}\\n.nightMode .cloze {\\n color: lightblue;\\n}\\n","latexPre":"\\\\documentclass[12pt]{article}\\n\\\\special{papersize=3in,5in}\\n\\\\usepackage[utf8]{inputenc}\\n\\\\usepackage{amssymb,amsmath}\\n\\\\pagestyle{empty}\\n\\\\setlength{\\\\parindent}{0in}\\n\\\\begin{document}\\n","latexPost":"\\\\end{document}","latexsvg":false,"req":[[0,"any",[0]]]},"1676902364661":{"id":1676902364661,"name":"Basic","type":0,"mod":0,"usn":0,"sortf":0,"did":null,"tmpls":[{"name":"Card 1","ord":0,"qfmt":"{{Front}}","afmt":"{{FrontSide}}\\n\\n<hr id=answer>\\n\\n{{Back}}","bqfmt":"","bafmt":"","did":null,"bfont":"","bsize":0}],"flds":[{"name":"Front","ord":0,"sticky":false,"rtl":false,"font":"Arial","size":20,"description":""},{"name":"Back","ord":1,"sticky":false,"rtl":false,"font":"Arial","size":20,"description":""}],"css":".card {\\n font-family: arial;\\n font-size: 20px;\\n text-align: center;\\n color: black;\\n background-color: white;\\n}\\n","latexPre":"\\\\documentclass[12pt]{article}\\n\\\\special{papersize=3in,5in}\\n\\\\usepackage[utf8]{inputenc}\\n\\\\usepackage{amssymb,amsmath}\\n\\\\pagestyle{empty}\\n\\\\setlength{\\\\parindent}{0in}\\n\\\\begin{document}\\n","latexPost":"\\\\end{document}","latexsvg":false,"req":[[0,"any",[0]]]},"1676902364662":{"id":1676902364662,"name":"Basic (and reversed card)","type":0,"mod":0,"usn":0,"sortf":0,"did":null,"tmpls":[{"name":"Card 1","ord":0,"qfmt":"{{Front}}","afmt":"{{FrontSide}}\\n\\n<hr id=answer>\\n\\n{{Back}}","bqfmt":"","bafmt":"","did":null,"bfont":"","bsize":0},{"name":"Card 2","ord":1,"qfmt":"{{Back}}","afmt":"{{FrontSide}}\\n\\n<hr id=answer>\\n\\n{{Front}}","bqfmt":"","bafmt":"","did":null,"bfont":"","bsize":0}],"flds":[{"name":"Front","ord":0,"sticky":false,"rtl":false,"font":"Arial","size":20,"description":""},{"name":"Back","ord":1,"sticky":false,"rtl":false,"font":"Arial","size":20,"description":""}],"css":".card {\\n font-family: arial;\\n font-size: 20px;\\n text-align: center;\\n color: black;\\n background-color: white;\\n}\\n","latexPre":"\\\\documentclass[12pt]{article}\\n\\\\special{papersize=3in,5in}\\n\\\\usepackage[utf8]{inputenc}\\n\\\\usepackage{amssymb,amsmath}\\n\\\\pagestyle{empty}\\n\\\\setlength{\\\\parindent}{0in}\\n\\\\begin{document}\\n","latexPost":"\\\\end{document}","latexsvg":false,"req":[[0,"any",[0]],[1,"any",[1]]]}}','{"1":{"id":1,"mod":0,"name":"Default","usn":0,"lrnToday":[0,0],"revToday":[0,0],"newToday":[0,0],"timeToday":[0,0],"collapsed":true,"browserCollapsed":true,"desc":"","dyn":0,"conf":1,"extendNew":0,"extendRev":0}}','{"1":{"id":1,"mod":0,"name":"Default","usn":0,"maxTaken":60,"autoplay":true,"timer":0,"replayq":true,"new":{"bury":false,"delays":[1.0,10.0],"initialFactor":2500,"ints":[1,4,0],"order":1,"perDay":20},"rev":{"bury":false,"ease4":1.3,"ivlFct":1.0,"maxIvl":36500,"perDay":200,"hardFactor":1.2},"lapse":{"delays":[10.0],"leechAction":1,"leechFails":8,"minInt":1,"mult":0.0},"dyn":false,"newMix":0,"newPerDayMinimum":0,"interdayLearningMix":0,"reviewOrder":0,"newSortOrder":0,"newGatherPriority":0,"buryInterdayLearning":false}}','{}');
|
90
|
+
SQL
|
91
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../helpers/shared_constants_helper"
|
4
|
+
require_relative "../helpers/time_helper"
|
5
|
+
require_relative "card_attributes"
|
6
|
+
|
7
|
+
module AnkiRecord
|
8
|
+
##
|
9
|
+
# Card represents an Anki card.
|
10
|
+
class Card
|
11
|
+
include CardAttributes
|
12
|
+
include TimeHelper
|
13
|
+
include SharedConstantsHelper
|
14
|
+
|
15
|
+
def initialize(note:, card_template: nil, card_data: nil) # :nodoc:
|
16
|
+
@note = note
|
17
|
+
if card_template
|
18
|
+
setup_instance_variables_for_new_card(card_template: card_template)
|
19
|
+
elsif card_data
|
20
|
+
setup_instance_variables_from_existing(card_data: card_data)
|
21
|
+
else
|
22
|
+
raise ArgumentError
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def setup_instance_variables_for_new_card(card_template:)
|
29
|
+
raise ArgumentError unless @note.note_type == card_template.note_type
|
30
|
+
|
31
|
+
setup_collaborator_object_instance_variables_for_new_card(card_template: card_template)
|
32
|
+
setup_simple_instance_variables_for_new_card
|
33
|
+
end
|
34
|
+
|
35
|
+
def setup_collaborator_object_instance_variables_for_new_card(card_template:)
|
36
|
+
@card_template = card_template
|
37
|
+
@deck = @note.deck
|
38
|
+
@collection = @deck.collection
|
39
|
+
end
|
40
|
+
|
41
|
+
def setup_simple_instance_variables_for_new_card
|
42
|
+
@id = milliseconds_since_epoch
|
43
|
+
@last_modified_timestamp = seconds_since_epoch
|
44
|
+
@usn = NEW_OBJECT_USN
|
45
|
+
%w[type queue due ivl factor reps lapses left odue odid flags].each do |instance_variable_name|
|
46
|
+
instance_variable_set "@#{instance_variable_name}", 0
|
47
|
+
end
|
48
|
+
@data = "{}"
|
49
|
+
end
|
50
|
+
|
51
|
+
def setup_instance_variables_from_existing(card_data:)
|
52
|
+
setup_collaborator_object_instance_variables_from_existing(card_data: card_data)
|
53
|
+
setup_simple_instance_variables_from_existing(card_data: card_data)
|
54
|
+
end
|
55
|
+
|
56
|
+
def setup_collaborator_object_instance_variables_from_existing(card_data:)
|
57
|
+
@collection = note.note_type.collection
|
58
|
+
@deck = collection.find_deck_by id: card_data["did"]
|
59
|
+
@card_template = note.note_type.card_templates.find do |card_template|
|
60
|
+
card_template.ordinal_number == card_data["ord"]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def setup_simple_instance_variables_from_existing(card_data:)
|
65
|
+
@last_modified_timestamp = card_data["mod"]
|
66
|
+
%w[id usn type queue due ivl factor reps lapses left odue odid flags data].each do |instance_variable_name|
|
67
|
+
instance_variable_set "@#{instance_variable_name}", card_data[instance_variable_name]
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
public
|
72
|
+
|
73
|
+
def save(note_exists_already: false) # :nodoc:
|
74
|
+
note_exists_already ? update_card_in_collection_anki21 : insert_new_card_in_collection_anki21
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def update_card_in_collection_anki21
|
80
|
+
statement = @collection.anki_package.prepare <<~SQL
|
81
|
+
update cards set nid = ?, did = ?, ord = ?, mod = ?, usn = ?, type = ?,
|
82
|
+
queue = ?, due = ?, ivl = ?, factor = ?, reps = ?, lapses = ?,
|
83
|
+
left = ?, odue = ?, odid = ?, flags = ?, data = ? where id = ?
|
84
|
+
SQL
|
85
|
+
statement.execute [@note.id, @deck.id, ordinal_number,
|
86
|
+
@last_modified_timestamp, @usn, @type, @queue,
|
87
|
+
@due, @ivl, @factor, @reps,
|
88
|
+
@lapses, @left, @odue, @odid, @flags, @data, @id]
|
89
|
+
end
|
90
|
+
|
91
|
+
def insert_new_card_in_collection_anki21
|
92
|
+
statement = @collection.anki_package.prepare <<~SQL
|
93
|
+
insert into cards (id, nid, did, ord,
|
94
|
+
mod, usn, type, queue,
|
95
|
+
due, ivl, factor, reps,
|
96
|
+
lapses, left, odue, odid, flags, data)
|
97
|
+
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
98
|
+
SQL
|
99
|
+
statement.execute [@id, @note.id, @deck.id, ordinal_number,
|
100
|
+
@last_modified_timestamp, @usn, @type, @queue,
|
101
|
+
@due, @ivl, @factor, @reps, @lapses, @left, @odue, @odid, @flags, @data]
|
102
|
+
end
|
103
|
+
|
104
|
+
def ordinal_number
|
105
|
+
@card_template&.ordinal_number
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnkiRecord
|
4
|
+
##
|
5
|
+
# Module with the Card class's attribute readers, writers, and accessors.
|
6
|
+
module CardAttributes
|
7
|
+
##
|
8
|
+
# The card's note object.
|
9
|
+
attr_reader :note
|
10
|
+
|
11
|
+
##
|
12
|
+
# The card's deck object.
|
13
|
+
attr_reader :deck
|
14
|
+
|
15
|
+
##
|
16
|
+
# The card's collection object.
|
17
|
+
attr_reader :collection
|
18
|
+
|
19
|
+
##
|
20
|
+
# The card's card template object.
|
21
|
+
attr_reader :card_template
|
22
|
+
|
23
|
+
##
|
24
|
+
# The card's id.
|
25
|
+
#
|
26
|
+
# This is also the number of milliseconds since the 1970 epoch at which the card was created.
|
27
|
+
attr_reader :id
|
28
|
+
|
29
|
+
##
|
30
|
+
# The number of seconds since the 1970 epoch at which the card was last modified.
|
31
|
+
attr_reader :last_modified_timestamp
|
32
|
+
|
33
|
+
##
|
34
|
+
# The card's update sequence number.
|
35
|
+
attr_reader :usn
|
36
|
+
|
37
|
+
attr_reader :type, :queue, :due, :ivl, :factor, :reps, :lapses, :left, :odue, :odid, :flags, :data
|
38
|
+
end
|
39
|
+
end
|
@@ -1,44 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
# TODO: All instance variables should at least be readable
|
3
|
+
require_relative "card_template_attributes"
|
6
4
|
|
7
5
|
module AnkiRecord
|
8
6
|
##
|
9
|
-
# CardTemplate represents a card template of an Anki note type
|
7
|
+
# CardTemplate represents a card template of an Anki note type.
|
10
8
|
class CardTemplate
|
11
|
-
|
12
|
-
# The name of this card template
|
13
|
-
attr_accessor :name
|
14
|
-
|
15
|
-
##
|
16
|
-
# The question format
|
17
|
-
attr_accessor :question_format
|
18
|
-
|
19
|
-
##
|
20
|
-
# The answer format
|
21
|
-
attr_accessor :answer_format
|
22
|
-
|
23
|
-
##
|
24
|
-
# The font style shown for this card template in the browser
|
25
|
-
attr_accessor :browser_font_style
|
26
|
-
|
27
|
-
##
|
28
|
-
# The font size used for this card template in the browser
|
29
|
-
attr_accessor :browser_font_size
|
30
|
-
|
31
|
-
##
|
32
|
-
# The note type that this card template belongs to
|
33
|
-
attr_reader :note_type
|
34
|
-
|
35
|
-
##
|
36
|
-
# 0 for the first card template of the note type, 1 for the second, etc.
|
37
|
-
attr_reader :ordinal_number
|
9
|
+
include CardTemplateAttributes
|
38
10
|
|
39
|
-
|
40
|
-
# Instantiates a new card template called +name+ for the given note type
|
41
|
-
#
|
11
|
+
# Instantiates a new card template with name +name+ for the note type +note_type+.
|
42
12
|
def initialize(note_type:, name: nil, args: nil)
|
43
13
|
raise ArgumentError unless (name && args.nil?) || (args && args["name"])
|
44
14
|
|
@@ -48,6 +18,21 @@ module AnkiRecord
|
|
48
18
|
else
|
49
19
|
setup_card_template_instance_variables(name: name)
|
50
20
|
end
|
21
|
+
|
22
|
+
@note_type.add_card_template self
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_h # :nodoc:
|
26
|
+
{
|
27
|
+
name: @name,
|
28
|
+
ord: @ordinal_number,
|
29
|
+
qfmt: @question_format, afmt: @answer_format,
|
30
|
+
bqfmt: @bqfmt,
|
31
|
+
bafmt: @bafmt,
|
32
|
+
did: @deck_id,
|
33
|
+
bfont: @browser_font_style,
|
34
|
+
bsize: @browser_font_size
|
35
|
+
}
|
51
36
|
end
|
52
37
|
|
53
38
|
private
|
@@ -66,7 +51,7 @@ module AnkiRecord
|
|
66
51
|
|
67
52
|
def setup_card_template_instance_variables(name:)
|
68
53
|
@name = name
|
69
|
-
@ordinal_number = @note_type.
|
54
|
+
@ordinal_number = @note_type.card_templates.length
|
70
55
|
@question_format = ""
|
71
56
|
@answer_format = ""
|
72
57
|
@bqfmt = ""
|
@@ -75,17 +60,5 @@ module AnkiRecord
|
|
75
60
|
@browser_font_style = ""
|
76
61
|
@browser_font_size = 0
|
77
62
|
end
|
78
|
-
|
79
|
-
public
|
80
|
-
|
81
|
-
##
|
82
|
-
# Returns the field names that are allowed in the answer format and question format
|
83
|
-
#
|
84
|
-
# These are the field_name values in {{field_name}} in those formats.
|
85
|
-
#
|
86
|
-
# They are equivalent to the names of the fields of the template's note type.
|
87
|
-
def allowed_field_names
|
88
|
-
@note_type.fields.map(&:name)
|
89
|
-
end
|
90
63
|
end
|
91
64
|
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnkiRecord
|
4
|
+
##
|
5
|
+
# Module with the CardTemplate class's attribute readers, writers, and accessors.
|
6
|
+
module CardTemplateAttributes
|
7
|
+
##
|
8
|
+
# The card template's name.
|
9
|
+
attr_accessor :name
|
10
|
+
|
11
|
+
##
|
12
|
+
# The card template's font style in the browser.
|
13
|
+
attr_accessor :browser_font_style
|
14
|
+
|
15
|
+
##
|
16
|
+
# The card template's font size used in the browser.
|
17
|
+
attr_accessor :browser_font_size
|
18
|
+
|
19
|
+
##
|
20
|
+
# The card template's question format.
|
21
|
+
attr_reader :question_format
|
22
|
+
|
23
|
+
##
|
24
|
+
# Sets the question format of the card template.
|
25
|
+
#
|
26
|
+
# Raises an ArgumentError if the specified format attempts to use invalid fields.
|
27
|
+
def question_format=(format)
|
28
|
+
fields_in_specified_format = format.scan(/{{.+?}}/).map do |capture|
|
29
|
+
capture.chomp("}}").reverse.chomp("{{").reverse
|
30
|
+
end
|
31
|
+
if fields_in_specified_format.any? do |field_name|
|
32
|
+
!note_type.allowed_card_template_question_format_field_names.include?(field_name)
|
33
|
+
end
|
34
|
+
raise ArgumentError, "You tried to use a field that the note type does not have."
|
35
|
+
end
|
36
|
+
|
37
|
+
@question_format = format
|
38
|
+
end
|
39
|
+
|
40
|
+
##
|
41
|
+
# The card template's answer format.
|
42
|
+
attr_reader :answer_format
|
43
|
+
|
44
|
+
##
|
45
|
+
# Sets the answer format of the card template.
|
46
|
+
#
|
47
|
+
# Raises an ArgumentError if the specified format attempts to use invalid fields.
|
48
|
+
def answer_format=(format)
|
49
|
+
fields_in_specified_format = format.scan(/{{.+?}}/).map do |capture|
|
50
|
+
capture.chomp("}}").reverse.chomp("{{").reverse
|
51
|
+
end
|
52
|
+
if fields_in_specified_format.any? do |field_name|
|
53
|
+
!note_type.allowed_card_template_answer_format_field_names.include?(field_name)
|
54
|
+
end
|
55
|
+
raise ArgumentError, "You tried to use a field that the note type does not have."
|
56
|
+
end
|
57
|
+
|
58
|
+
@answer_format = format
|
59
|
+
end
|
60
|
+
|
61
|
+
##
|
62
|
+
# The card template's note type object.
|
63
|
+
attr_reader :note_type
|
64
|
+
|
65
|
+
##
|
66
|
+
# 0 for the first card template of the note type, 1 for the second, etc.
|
67
|
+
attr_reader :ordinal_number
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,180 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
require_relative "../deck/deck"
|
6
|
+
require_relative "../deck_options_group/deck_options_group"
|
7
|
+
require_relative "../helpers/data_query_helper"
|
8
|
+
require_relative "../helpers/time_helper"
|
9
|
+
require_relative "../note_type/note_type"
|
10
|
+
require_relative "collection_attributes"
|
11
|
+
|
12
|
+
module AnkiRecord
|
13
|
+
##
|
14
|
+
# Collection represents the single record in the Anki collection.anki21 database's `col` table.
|
15
|
+
# The note types, decks, and deck options groups data are contained within this record.
|
16
|
+
class Collection
|
17
|
+
include AnkiRecord::DataQueryHelper
|
18
|
+
include AnkiRecord::TimeHelper
|
19
|
+
include AnkiRecord::CollectionAttributes
|
20
|
+
|
21
|
+
def initialize(anki_package:) # :nodoc:
|
22
|
+
setup_collection_instance_variables(anki_package: anki_package)
|
23
|
+
end
|
24
|
+
|
25
|
+
def add_note_type(note_type) # :nodoc:
|
26
|
+
raise ArgumentError unless note_type.instance_of?(AnkiRecord::NoteType)
|
27
|
+
|
28
|
+
existing_note_type = nil
|
29
|
+
@note_types.each do |nt|
|
30
|
+
existing_note_type = nt if nt.id == note_type.id
|
31
|
+
end
|
32
|
+
@note_types.delete(existing_note_type) if existing_note_type
|
33
|
+
|
34
|
+
@note_types << note_type
|
35
|
+
end
|
36
|
+
|
37
|
+
def add_deck(deck) # :nodoc:
|
38
|
+
raise ArgumentError unless deck.instance_of?(AnkiRecord::Deck)
|
39
|
+
|
40
|
+
@decks << deck
|
41
|
+
end
|
42
|
+
|
43
|
+
def add_deck_options_group(deck_options_group) # :nodoc:
|
44
|
+
raise ArgumentError unless deck_options_group.instance_of?(AnkiRecord::DeckOptionsGroup)
|
45
|
+
|
46
|
+
@deck_options_groups << deck_options_group
|
47
|
+
end
|
48
|
+
|
49
|
+
##
|
50
|
+
# Returns the collection's note type object found by either +name+ or +id+, or nil if it is not found.
|
51
|
+
def find_note_type_by(name: nil, id: nil)
|
52
|
+
if (id && name) || (id.nil? && name.nil?)
|
53
|
+
raise ArgumentError,
|
54
|
+
"You must pass either an id or name keyword argument."
|
55
|
+
end
|
56
|
+
|
57
|
+
name ? find_note_type_by_name(name: name) : find_note_type_by_id(id: id)
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def find_note_type_by_name(name:)
|
63
|
+
note_types.find { |note_type| note_type.name == name }
|
64
|
+
end
|
65
|
+
|
66
|
+
def find_note_type_by_id(id:)
|
67
|
+
note_types.find { |note_type| note_type.id == id }
|
68
|
+
end
|
69
|
+
|
70
|
+
public
|
71
|
+
|
72
|
+
##
|
73
|
+
# Returns the collection's deck object found by either +name+ or +id+, or nil if it is not found.
|
74
|
+
def find_deck_by(name: nil, id: nil)
|
75
|
+
if (id && name) || (id.nil? && name.nil?)
|
76
|
+
raise ArgumentError,
|
77
|
+
"You must pass either an id or name keyword argument."
|
78
|
+
end
|
79
|
+
|
80
|
+
name ? find_deck_by_name(name: name) : find_deck_by_id(id: id)
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def find_deck_by_name(name:)
|
86
|
+
decks.find { |deck| deck.name == name }
|
87
|
+
end
|
88
|
+
|
89
|
+
def find_deck_by_id(id:)
|
90
|
+
decks.find { |deck| deck.id == id }
|
91
|
+
end
|
92
|
+
|
93
|
+
public
|
94
|
+
|
95
|
+
##
|
96
|
+
# Returns the collection's deck options group object found by +id+, or nil if it is not found.
|
97
|
+
def find_deck_options_group_by(id:)
|
98
|
+
deck_options_groups.find { |deck_options_group| deck_options_group.id == id }
|
99
|
+
end
|
100
|
+
|
101
|
+
##
|
102
|
+
# Returns the collection's note object found by +id+, or nil if it is not found.
|
103
|
+
def find_note_by(id:)
|
104
|
+
note_cards_data = note_cards_data_for_note_id sql_able: anki_package, id: id
|
105
|
+
return nil unless note_cards_data
|
106
|
+
|
107
|
+
AnkiRecord::Note.new collection: self, data: note_cards_data
|
108
|
+
end
|
109
|
+
|
110
|
+
def decks_json # :nodoc:
|
111
|
+
JSON.parse(anki_package.prepare("select decks from col;").execute.first["decks"])
|
112
|
+
end
|
113
|
+
|
114
|
+
def models_json # :nodoc:
|
115
|
+
JSON.parse(anki_package.prepare("select models from col;").execute.first["models"])
|
116
|
+
end
|
117
|
+
|
118
|
+
def copy_over_existing(col_record:) # :nodoc:
|
119
|
+
@col_record = col_record
|
120
|
+
setup_simple_collaborator_objects
|
121
|
+
setup_custom_collaborator_objects
|
122
|
+
remove_instance_variable(:@col_record)
|
123
|
+
end
|
124
|
+
|
125
|
+
private
|
126
|
+
|
127
|
+
def setup_collection_instance_variables(anki_package:)
|
128
|
+
@anki_package = anki_package
|
129
|
+
setup_simple_collaborator_objects
|
130
|
+
setup_custom_collaborator_objects
|
131
|
+
remove_instance_variable(:@col_record)
|
132
|
+
end
|
133
|
+
|
134
|
+
def col_record
|
135
|
+
@col_record ||= @anki_package.prepare("select * from col").execute.first
|
136
|
+
end
|
137
|
+
|
138
|
+
# rubocop:disable Metrics/AbcSize
|
139
|
+
def setup_simple_collaborator_objects
|
140
|
+
@id = col_record["id"]
|
141
|
+
@created_at_timestamp = col_record["crt"]
|
142
|
+
@last_modified_timestamp = col_record["mod"]
|
143
|
+
@scm = col_record["scm"]
|
144
|
+
@ver = col_record["ver"]
|
145
|
+
@dty = col_record["dty"]
|
146
|
+
@usn = col_record["usn"]
|
147
|
+
@ls = col_record["ls"]
|
148
|
+
@configuration = JSON.parse(col_record["conf"])
|
149
|
+
@tags = JSON.parse(col_record["tags"])
|
150
|
+
end
|
151
|
+
# rubocop:enable Metrics/AbcSize
|
152
|
+
|
153
|
+
def setup_custom_collaborator_objects
|
154
|
+
setup_note_type_collaborators
|
155
|
+
setup_deck_options_groups_collaborators
|
156
|
+
setup_deck_collaborators
|
157
|
+
end
|
158
|
+
|
159
|
+
def setup_note_type_collaborators
|
160
|
+
@note_types = []
|
161
|
+
JSON.parse(col_record["models"]).values.map do |model_hash|
|
162
|
+
NoteType.new(collection: self, args: model_hash)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def setup_deck_collaborators
|
167
|
+
@decks = []
|
168
|
+
JSON.parse(col_record["decks"]).values.map do |deck_hash|
|
169
|
+
Deck.new(collection: self, args: deck_hash)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def setup_deck_options_groups_collaborators
|
174
|
+
@deck_options_groups = []
|
175
|
+
JSON.parse(col_record["dconf"]).values.map do |dconf_hash|
|
176
|
+
DeckOptionsGroup.new(collection: self, args: dconf_hash)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnkiRecord
|
4
|
+
##
|
5
|
+
# Module with the Collection class's attribute readers, writers, and accessors.
|
6
|
+
module CollectionAttributes
|
7
|
+
##
|
8
|
+
# The collection's Anki package object.
|
9
|
+
attr_reader :anki_package
|
10
|
+
|
11
|
+
##
|
12
|
+
# The collection's id, which is also the id of the col record in the collection.anki21 database (usually 1).
|
13
|
+
attr_reader :id
|
14
|
+
|
15
|
+
##
|
16
|
+
# The number of milliseconds since the 1970 epoch when the collection record was created.
|
17
|
+
attr_reader :created_at_timestamp
|
18
|
+
|
19
|
+
##
|
20
|
+
# The number of milliseconds since the 1970 epoch at which the collection record was last modified.
|
21
|
+
attr_reader :last_modified_timestamp
|
22
|
+
|
23
|
+
##
|
24
|
+
# The collection's note type objects as an array.
|
25
|
+
attr_reader :note_types
|
26
|
+
|
27
|
+
##
|
28
|
+
# The collection's deck objects as an array
|
29
|
+
attr_reader :decks
|
30
|
+
|
31
|
+
##
|
32
|
+
# The collection's deck option group objects as an array.
|
33
|
+
attr_reader :deck_options_groups
|
34
|
+
end
|
35
|
+
end
|