anki_record 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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