anki_record 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pry"
4
+ require "json"
5
+
6
+ require_relative "deck"
7
+ require_relative "deck_options_group"
8
+ require_relative "helpers/time_helper"
9
+ require_relative "note_type"
10
+
11
+ module AnkiRecord
12
+ ##
13
+ # Collection represents the single record in the Anki database `col` table
14
+ class Collection
15
+ include AnkiRecord::TimeHelper
16
+
17
+ ##
18
+ # An array of the collection's note type objects
19
+ attr_reader :note_types
20
+
21
+ ##
22
+ # An array of the collection's deck objects
23
+ attr_reader :decks
24
+
25
+ ##
26
+ # Instantiates the collection object for the +anki_package+
27
+ def initialize(anki_package:)
28
+ setup_collection_instance_variables(anki_package: anki_package)
29
+ end
30
+
31
+ private
32
+
33
+ # rubocop:disable Metrics/MethodLength
34
+ # rubocop:disable Metrics/AbcSize
35
+ def setup_collection_instance_variables(anki_package:)
36
+ @anki_package = anki_package
37
+ @id = col_record["id"]
38
+ @crt = col_record["crt"]
39
+ @last_modified_time = (mod = col_record["mod"]).zero? ? milliseconds_since_epoch : mod
40
+ @scm = col_record["scm"]
41
+ @ver = col_record["ver"]
42
+ @dty = col_record["dty"]
43
+ @usn = col_record["usn"]
44
+ @ls = col_record["ls"]
45
+ @configuration = JSON.parse(col_record["conf"])
46
+ @note_types = JSON.parse(col_record["models"]).values.map do |model_hash|
47
+ NoteType.new(collection: self, args: model_hash)
48
+ end
49
+ @decks = JSON.parse(col_record["decks"]).values.map do |deck_hash|
50
+ Deck.new(collection: self, args: deck_hash)
51
+ end
52
+ @deck_option_groups = JSON.parse(col_record["dconf"]).values.map do |dconf_hash|
53
+ DeckOptionsGroup.new(collection: self, args: dconf_hash)
54
+ end
55
+ @tags = JSON.parse(col_record["tags"])
56
+ remove_instance_variable(:@col_record)
57
+ end
58
+ # rubocop:enable Metrics/AbcSize
59
+ # rubocop:enable Metrics/MethodLength
60
+
61
+ def col_record
62
+ @col_record ||= @anki_package.execute("select * from col;").first
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,77 @@
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
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnkiRecord
4
+ ##
5
+ # This is the SQL insert statement (as a Ruby string/here document) for the
6
+ # default col record in the collection.anki21 database exported from a new Anki 2.1.54 profile
7
+ CLEAN_COLLECTION_21_RECORD = <<~SQL
8
+ 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}}','{}');
9
+ SQL
10
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnkiRecord
4
+ ##
5
+ # This is the SQL insert statement (as a Ruby string/here document) for the
6
+ # default col record in the collection.anki2 database exported from a new Anki 2.1.54 profile
7
+ CLEAN_COLLECTION_2_RECORD = <<~SQL
8
+ 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}}','{}');
9
+ SQL
10
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pry"
4
+
5
+ require_relative "helpers/shared_constants_helper"
6
+ require_relative "helpers/time_helper"
7
+
8
+ # TODO: All instance variables should at least be readable
9
+
10
+ module AnkiRecord
11
+ ##
12
+ # Deck represents an Anki deck
13
+ class Deck
14
+ include SharedConstantsHelper
15
+ include TimeHelper
16
+
17
+ DEFAULT_DECK_TODAY_ARRAY = [0, 0].freeze
18
+ DEFAULT_COLLAPSED = false
19
+
20
+ private_constant :DEFAULT_DECK_TODAY_ARRAY, :DEFAULT_COLLAPSED
21
+
22
+ ##
23
+ # The name of the deck
24
+ attr_accessor :name
25
+
26
+ ##
27
+ # The description of the deck
28
+ attr_accessor :description
29
+
30
+ ##
31
+ # One of many attributes that is currently read-only and needs to be documented.
32
+ attr_reader :collection, :id, :last_modified_time, :deck_options_group_id
33
+
34
+ ##
35
+ # Instantiate a new Deck
36
+ def initialize(collection:, name: nil, args: nil)
37
+ raise ArgumentError unless (name && args.nil?) || (args && args["name"])
38
+
39
+ @collection = collection
40
+ if args
41
+ setup_deck_instance_variables_from_existing(args: args)
42
+ else
43
+ setup_deck_instance_variables(name: name)
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ # rubocop:disable Metrics/MethodLength
50
+ # rubocop:disable Metrics/AbcSize
51
+ def setup_deck_instance_variables_from_existing(args:)
52
+ @id = args["id"]
53
+ @last_modified_time = args["mod"]
54
+ @name = args["name"]
55
+ @usn = args["usn"]
56
+ @learn_today = args["lrnToday"]
57
+ @review_today = args["revToday"]
58
+ @new_today = args["newToday"]
59
+ @time_today = args["timeToday"]
60
+ @collapsed_in_main_window = args["collapsed"]
61
+ @collapsed_in_browser = args["browserCollapsed"]
62
+ @description = args["desc"]
63
+ @dyn = args["dyn"]
64
+ @deck_options_group_id = args["conf"]
65
+ @extend_new = args["extendNew"]
66
+ @extend_review = args["extendRev"]
67
+ end
68
+ # rubocop:enable Metrics/MethodLength
69
+ # rubocop:enable Metrics/AbcSize
70
+
71
+ # rubocop:disable Metrics/MethodLength
72
+ def setup_deck_instance_variables(name:)
73
+ @id = milliseconds_since_epoch
74
+ @last_modified_time = seconds_since_epoch
75
+ @name = name
76
+ @usn = NEW_OBJECT_USN
77
+ @learn_today = @review_today = @new_today = @time_today = DEFAULT_DECK_TODAY_ARRAY
78
+ @collapsed_in_main_window = DEFAULT_COLLAPSED
79
+ @collapsed_in_browser = DEFAULT_COLLAPSED
80
+ @description = ""
81
+ @dyn = NON_FILTERED_DECK_DYN
82
+ @deck_options_group_id = nil # TODO
83
+ @extend_new = 0
84
+ @extend_review = 0
85
+ end
86
+ # rubocop:enable Metrics/MethodLength
87
+ end
88
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pry"
4
+
5
+ require_relative "helpers/shared_constants_helper"
6
+ require_relative "helpers/time_helper"
7
+
8
+ module AnkiRecord
9
+ ##
10
+ # Represents the set of options that can be applied to a deck
11
+ class DeckOptionsGroup
12
+ include SharedConstantsHelper
13
+ include TimeHelper
14
+
15
+ ##
16
+ # The name of the options group
17
+ attr_accessor :name
18
+
19
+ ##
20
+ # One of many attributes that is currently read-only and needs to be documented.
21
+ attr_reader :collection, :id, :last_modified_time, :usn, :max_taken, :auto_play, :timer, :replay_question,
22
+ :new_options, :review_options, :lapse_options, :dyn, :new_mix, :new_per_day_minimum,
23
+ :interday_learning_mix, :review_order, :new_sort_order, :new_gather_priority, :bury_interday_learning
24
+
25
+ ##
26
+ # Instantiates a new deck options group called +name+ with defaults
27
+ def initialize(collection:, name: nil, args: nil)
28
+ # TODO: extract this check to a shared helper
29
+ raise ArgumentError unless (name && args.nil?) || (args && args["name"])
30
+
31
+ @collection = collection
32
+
33
+ if args
34
+ setup_deck_options_group_instance_variables_from_existing(args: args)
35
+ else
36
+ setup_deck_options_group_instance_variables(name: name)
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ # rubocop:disable Metrics/MethodLength
43
+ # rubocop:disable Metrics/AbcSize
44
+ def setup_deck_options_group_instance_variables_from_existing(args:)
45
+ @id = args["id"]
46
+ @last_modified_time = args["mod"]
47
+ @name = args["name"]
48
+ @usn = args["usn"]
49
+ @max_taken = args["maxTaken"]
50
+ @auto_play = args["autoplay"]
51
+ @timer = args["timer"]
52
+ @replay_question = args["true"]
53
+ @new_options = args["new"]
54
+ @review_options = args["rev"]
55
+ @lapse_options = args["lapse"]
56
+ @dyn = args["dyn"]
57
+ @new_mix = args["newMix"]
58
+ @new_per_day_minimum = args["newPerDayMinimum"]
59
+ @interday_learning_mix = args["interdayLearningMix"]
60
+ @review_order = args["reviewOrder"]
61
+ @new_sort_order = args["new_sort_order"]
62
+ @new_gather_priority = args["newGatherPriority"]
63
+ @bury_interday_learning = args["buryInterdayLearning"]
64
+ end
65
+ # rubocop:enable Metrics/AbcSize
66
+ # rubocop:enable Metrics/MethodLength
67
+
68
+ # rubocop:disable Metrics/MethodLength
69
+ # rubocop:disable Metrics/AbcSize
70
+ def setup_deck_options_group_instance_variables(name:)
71
+ @id = milliseconds_since_epoch
72
+ @last_modified_time = seconds_since_epoch
73
+ @name = name
74
+ @usn = NEW_OBJECT_USN
75
+ @max_taken = 60
76
+ @auto_play = true
77
+ @timer = 0
78
+ @replay_question = true
79
+ @new_options = { bury: false, delays: [1.0, 10.0], initialFactor: 2500, ints: [1, 4, 0], order: 1, perDay: 20 }
80
+ @review_options = { bury: false, ease4: 1.3, ivlFct: 1.0, maxIvl: 36_500, perDay: 200, hardFactor: 1.2 }
81
+ @lapse_options = { delays: [10.0], leechAction: 1, leechFails: 8, minInt: 1, mult: 0.0 }
82
+ @dyn = NON_FILTERED_DECK_DYN
83
+ @new_mix = 0
84
+ @new_per_day_minimum = 0
85
+ @interday_learning_mix = 0
86
+ @review_order = 0
87
+ @new_sort_order = 0
88
+ @new_gather_priority = 0
89
+ @bury_interday_learning = false
90
+ end
91
+ # rubocop:enable Metrics/AbcSize
92
+ # rubocop:enable Metrics/MethodLength
93
+ end
94
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnkiRecord
4
+ ##
5
+ # Helper module to hold the constants used by multiple classes
6
+ module SharedConstantsHelper
7
+ NEW_OBJECT_USN = -1
8
+ NON_FILTERED_DECK_DYN = 0
9
+ private_constant :NEW_OBJECT_USN, :NON_FILTERED_DECK_DYN
10
+ end
11
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+
5
+ module AnkiRecord
6
+ ##
7
+ # Helper module to calculate integer time values since the 1970 epoch
8
+ #
9
+ # Specifically, the time that has passed since 00:00:00 UTC Jan 1 1970
10
+ module TimeHelper
11
+ ##
12
+ # Return the number of milliseconds since the 1970 epoch
13
+ def milliseconds_since_epoch
14
+ DateTime.now.strftime("%Q").to_i
15
+ end
16
+
17
+ ##
18
+ # Return the number of seconds since the 1970 epoch
19
+ def seconds_since_epoch
20
+ Time.now.to_i
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ # TODO: raise exceptions on invalid assignment to prevent downstream errors and specs for the writeable attributes
4
+ # TODO: All instance variables should at least be readable
5
+
6
+ require "pry"
7
+
8
+ module AnkiRecord
9
+ ##
10
+ # NoteField represents a field of an Anki note type
11
+ class NoteField
12
+ DEFAULT_FIELD_FONT_STYLE = "Arial"
13
+ DEFAULT_FIELD_FONT_SIZE = 20
14
+ DEFAULT_FIELD_DESCRIPTION = ""
15
+ private_constant :DEFAULT_FIELD_FONT_STYLE, :DEFAULT_FIELD_FONT_SIZE, :DEFAULT_FIELD_DESCRIPTION
16
+
17
+ ##
18
+ # The name of the note field
19
+ attr_accessor :name
20
+
21
+ ##
22
+ # One of many attributes that is readable and writeable but needs to be documented
23
+ attr_accessor :sticky, :right_to_left, :font_style, :font_size, :description
24
+
25
+ ##
26
+ # One of many attributes that is currently read-only and needs to be documented.
27
+ attr_reader :note_type, :ordinal_number
28
+
29
+ ##
30
+ # Instantiates a new field for the given note type
31
+ def initialize(note_type:, name: nil, args: nil)
32
+ raise ArgumentError unless (name && args.nil?) || (args && args["name"])
33
+
34
+ @note_type = note_type
35
+ if args
36
+ setup_note_field_instance_variables_from_existing(args: args)
37
+ else
38
+ setup_note_field_instance_variables(name: name)
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def setup_note_field_instance_variables_from_existing(args:)
45
+ @name = args["name"]
46
+ @ordinal_number = args["ord"]
47
+ @sticky = args["sticky"]
48
+ @right_to_left = args["rtl"]
49
+ @font_style = args["font"]
50
+ @font_size = args["size"]
51
+ end
52
+
53
+ def setup_note_field_instance_variables(name:)
54
+ @name = name
55
+ @ordinal_number = @note_type.fields.length
56
+ @sticky = false
57
+ @right_to_left = false
58
+ @font_style = DEFAULT_FIELD_FONT_STYLE
59
+ @font_size = DEFAULT_FIELD_FONT_SIZE
60
+ @description = DEFAULT_FIELD_DESCRIPTION
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pry"
4
+
5
+ require_relative "card_template"
6
+ require_relative "helpers/shared_constants_helper"
7
+ require_relative "helpers/time_helper"
8
+ require_relative "note_field"
9
+
10
+ # TODO: All instance variables should at least be readable
11
+
12
+ module AnkiRecord
13
+ ##
14
+ # NoteType represents an Anki note type (also called a model)
15
+ class NoteType
16
+ include AnkiRecord::SharedConstantsHelper
17
+ include AnkiRecord::TimeHelper
18
+ NEW_NOTE_TYPE_SORT_FIELD = 0
19
+ private_constant :NEW_NOTE_TYPE_SORT_FIELD
20
+
21
+ ##
22
+ # The name of this note type
23
+ attr_accessor :name
24
+
25
+ ##
26
+ # true if the note type is a cloze note type and false if it is not
27
+ attr_accessor :cloze
28
+
29
+ ##
30
+ # One of many attributes that is readable and writeable but needs to be documented
31
+ attr_accessor :css, :latex_preamble, :latex_postamble, :latex_svg
32
+
33
+ ##
34
+ # One of many attributes that is currently read-only and needs to be documented.
35
+ attr_reader :id, :templates, :fields, :deck_id
36
+
37
+ ##
38
+ # Instantiates a new note type
39
+ def initialize(collection:, name: nil, cloze: false, args: nil)
40
+ raise ArgumentError unless (name && args.nil?) || (args && args["name"])
41
+
42
+ @collection = collection
43
+
44
+ if args
45
+ setup_note_type_instance_variables_from_existing(args: args)
46
+ else
47
+ setup_note_type_instance_variables(
48
+ name: name, cloze: cloze
49
+ )
50
+ end
51
+ end
52
+
53
+ ##
54
+ # Create a new field and adds it to this note type's fields
55
+ #
56
+ # The field is an instance of AnkiRecord::NoteField
57
+ def new_note_field(name:)
58
+ # TODO: Check if name already used by a field in this note type
59
+ @fields << AnkiRecord::NoteField.new(note_type: self, name: name)
60
+ end
61
+
62
+ ##
63
+ # Create a new card template and adds it to this note type's templates
64
+ #
65
+ # The card template is an instance of AnkiRecord::CardTemplate
66
+ def new_card_template(name:)
67
+ # TODO: Check if name already used by a template in this note type
68
+ @templates << AnkiRecord::CardTemplate.new(note_type: self, name: name)
69
+ end
70
+
71
+ private
72
+
73
+ # rubocop:disable Metrics/MethodLength
74
+ # rubocop:disable Metrics/AbcSize
75
+ def setup_note_type_instance_variables_from_existing(args:)
76
+ @id = args["id"]
77
+ @name = args["name"]
78
+ @cloze = args["type"] == 1
79
+ @last_modified_time = args["mod"]
80
+ @usn = args["usn"]
81
+ @sort_field = args["sortf"]
82
+ @deck_id = args["did"]
83
+ @fields = args["flds"].map { |fld| NoteField.new(note_type: self, args: fld) }
84
+ @templates = args["tmpls"].map { |tmpl| CardTemplate.new(note_type: self, args: tmpl) }
85
+ @css = args["css"]
86
+ @latex_preamble = args["latexPre"]
87
+ @latex_postamble = args["latexPost"]
88
+ @latex_svg = args["latexsvg"]
89
+ @req = args["req"]
90
+ @tags = args["tags"]
91
+ @vers = args["vers"]
92
+ end
93
+ # rubocop:enable Metrics/MethodLength
94
+ # rubocop:enable Metrics/AbcSize
95
+
96
+ # rubocop:disable Metrics/MethodLength
97
+ def setup_note_type_instance_variables(name:, cloze:)
98
+ @id = milliseconds_since_epoch
99
+ @name = name
100
+ @cloze = cloze
101
+ @last_modified_time = seconds_since_epoch
102
+ @usn = NEW_OBJECT_USN
103
+ @sort_field = NEW_NOTE_TYPE_SORT_FIELD
104
+ @deck_id = nil
105
+ @fields = []
106
+ @templates = []
107
+ @css = default_css
108
+ @latex_preamble = default_latex_preamble
109
+ @latex_postamble = default_latex_postamble
110
+ @latex_svg = false
111
+ @req = nil
112
+ @tags = []
113
+ @vers = []
114
+ end
115
+ # rubocop:enable Metrics/MethodLength
116
+
117
+ # TODO: use constant here
118
+ def default_css
119
+ <<-CSS
120
+ .card {
121
+ color: black;
122
+ background-color: transparent;
123
+ text-align: center;
124
+ }
125
+ CSS
126
+ end
127
+
128
+ # TODO: use constant here
129
+ def default_latex_preamble
130
+ <<-LATEX_PRE
131
+ \\documentclass[12pt]{article}
132
+ \\special{papersize=3in,5in}
133
+ \\usepackage{amssymb,amsmath}
134
+ \\pagestyle{empty}
135
+ \\setlength{\\parindent}{0in}
136
+ \\begin{document}
137
+ LATEX_PRE
138
+ end
139
+
140
+ # TODO: use constant here
141
+ def default_latex_postamble
142
+ <<-LATEX_POST
143
+ \\end{document}
144
+ LATEX_POST
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnkiRecord
4
+ VERSION = "0.1.1" # :nodoc:
5
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+ require "sqlite3"
5
+ require "zip"
6
+
7
+ require_relative "anki_record/anki_package"
8
+ require_relative "anki_record/version"
9
+
10
+ ##
11
+ # This module is the namespace for all AnkiRecord classes:
12
+ # - AnkiPackage
13
+ # - CardTemplate
14
+ # - Collection
15
+ # - DeckOptionsGroup
16
+ # - Deck
17
+ # - NoteField
18
+ # - NoteType
19
+ #
20
+ # And modules:
21
+ # - SharedConstantsHelper
22
+ # - TimeHelper
23
+ module AnkiRecord
24
+ class Error < StandardError; end # :nodoc:
25
+ end