anki_record 0.2.0 → 0.3.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.
- checksums.yaml +4 -4
- data/.rubocop.yml +13 -2
- data/CHANGELOG.md +37 -9
- data/Gemfile +3 -1
- data/Gemfile.lock +10 -2
- data/README.md +120 -35
- data/anki_record.gemspec +1 -5
- data/lib/anki_record/anki_package/anki_package.rb +237 -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/card_template.rb +64 -0
- data/lib/anki_record/card_template/card_template_attributes.rb +69 -0
- data/lib/anki_record/collection/collection.rb +182 -0
- data/lib/anki_record/collection/collection_attributes.rb +35 -0
- data/lib/anki_record/database_setup_constants.rb +88 -0
- data/lib/anki_record/deck/deck.rb +99 -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} +12 -31
- data/lib/anki_record/deck_options_group/deck_options_group_attributes.rb +23 -0
- data/lib/anki_record/helpers/checksum_helper.rb +10 -11
- data/lib/anki_record/helpers/data_query_helper.rb +15 -0
- data/lib/anki_record/helpers/shared_constants_helper.rb +6 -6
- data/lib/anki_record/helpers/time_helper.rb +18 -13
- data/lib/anki_record/note/note.rb +178 -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 -16
- metadata +26 -16
- data/lib/anki_record/anki_package.rb +0 -194
- data/lib/anki_record/card.rb +0 -75
- data/lib/anki_record/card_template.rb +0 -105
- data/lib/anki_record/collection.rb +0 -105
- 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 -101
- data/lib/anki_record/note.rb +0 -135
- data/lib/anki_record/note_field.rb +0 -84
- data/lib/anki_record/note_type.rb +0 -233
@@ -0,0 +1,99 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "deck_attributes"
|
4
|
+
require_relative "deck_defaults"
|
5
|
+
require_relative "../helpers/shared_constants_helper"
|
6
|
+
require_relative "../helpers/time_helper"
|
7
|
+
|
8
|
+
module AnkiRecord
|
9
|
+
##
|
10
|
+
# Deck represents an Anki deck.
|
11
|
+
class Deck
|
12
|
+
include DeckAttributes
|
13
|
+
include DeckDefaults
|
14
|
+
include Helpers::SharedConstantsHelper
|
15
|
+
include Helpers::TimeHelper
|
16
|
+
|
17
|
+
##
|
18
|
+
# Instantiates a new Deck object belonging to +collection+ with name +name+.
|
19
|
+
def initialize(collection:, name: nil, args: nil)
|
20
|
+
raise ArgumentError unless (name && args.nil?) || (args && args["name"])
|
21
|
+
|
22
|
+
@collection = collection
|
23
|
+
if args
|
24
|
+
setup_deck_instance_variables_from_existing(args: args)
|
25
|
+
else
|
26
|
+
setup_deck_instance_variables(name: name)
|
27
|
+
end
|
28
|
+
|
29
|
+
@collection.add_deck self
|
30
|
+
save if args
|
31
|
+
end
|
32
|
+
|
33
|
+
##
|
34
|
+
# Saves the deck to the collection.anki21 database.
|
35
|
+
def save
|
36
|
+
collection_decks_hash = collection.decks_json
|
37
|
+
collection_decks_hash[@id] = to_h
|
38
|
+
sql = "update col set decks = ? where id = ?"
|
39
|
+
collection.anki_package.prepare(sql).execute([JSON.generate(collection_decks_hash), collection.id])
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_h # :nodoc:
|
43
|
+
{
|
44
|
+
id: @id, mod: @last_modified_timestamp, name: @name, usn: @usn,
|
45
|
+
lrnToday: @learn_today, revToday: @review_today, newToday: @new_today, timeToday: @time_today,
|
46
|
+
collapsed: @collapsed_in_main_window, browserCollapsed: @collapsed_in_browser,
|
47
|
+
desc: @description, dyn: @dyn, conf: @deck_options_group.id,
|
48
|
+
extendNew: @extend_new, extendRev: @extend_review
|
49
|
+
}
|
50
|
+
end
|
51
|
+
|
52
|
+
# :nodoc:
|
53
|
+
# :nocov:
|
54
|
+
def inspect
|
55
|
+
"#<AnkiRecord::Deck:#{object_id} id: #{id} name: #{name} description: #{description}>"
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
# rubocop:disable Metrics/MethodLength
|
61
|
+
# rubocop:disable Metrics/AbcSize
|
62
|
+
def setup_deck_instance_variables_from_existing(args:)
|
63
|
+
@id = args["id"]
|
64
|
+
@last_modified_timestamp = args["mod"]
|
65
|
+
@name = args["name"]
|
66
|
+
@usn = args["usn"]
|
67
|
+
@learn_today = args["lrnToday"]
|
68
|
+
@review_today = args["revToday"]
|
69
|
+
@new_today = args["newToday"]
|
70
|
+
@time_today = args["timeToday"]
|
71
|
+
@collapsed_in_main_window = args["collapsed"]
|
72
|
+
@collapsed_in_browser = args["browserCollapsed"]
|
73
|
+
@description = args["desc"]
|
74
|
+
@dyn = args["dyn"]
|
75
|
+
@deck_options_group = @collection.find_deck_options_group_by id: args["conf"]
|
76
|
+
@extend_new = args["extendNew"]
|
77
|
+
@extend_review = args["extendRev"]
|
78
|
+
end
|
79
|
+
# rubocop:enable Metrics/MethodLength
|
80
|
+
# rubocop:enable Metrics/AbcSize
|
81
|
+
|
82
|
+
# rubocop:disable Metrics/MethodLength
|
83
|
+
def setup_deck_instance_variables(name:)
|
84
|
+
@id = milliseconds_since_epoch
|
85
|
+
@last_modified_timestamp = seconds_since_epoch
|
86
|
+
@name = name
|
87
|
+
@usn = NEW_OBJECT_USN
|
88
|
+
@learn_today = @review_today = @new_today = @time_today = default_deck_today_array
|
89
|
+
@collapsed_in_main_window = default_collapsed
|
90
|
+
@collapsed_in_browser = default_collapsed
|
91
|
+
@description = ""
|
92
|
+
@dyn = NON_FILTERED_DECK_DYN
|
93
|
+
@deck_options_group = @collection.find_deck_options_group_by id: default_deck_options_group_id
|
94
|
+
@extend_new = 0
|
95
|
+
@extend_review = 0
|
96
|
+
end
|
97
|
+
# rubocop:enable Metrics/MethodLength
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnkiRecord
|
4
|
+
# Module with the Deck class's attribute readers, writers, and accessors.
|
5
|
+
module DeckAttributes
|
6
|
+
##
|
7
|
+
# The deck's collection object.
|
8
|
+
attr_reader :collection
|
9
|
+
|
10
|
+
##
|
11
|
+
# The deck's name.
|
12
|
+
attr_accessor :name
|
13
|
+
|
14
|
+
##
|
15
|
+
# The deck's description.
|
16
|
+
attr_accessor :description
|
17
|
+
|
18
|
+
##
|
19
|
+
# The deck's id.
|
20
|
+
attr_reader :id
|
21
|
+
|
22
|
+
##
|
23
|
+
# The number of seconds since the 1970 epoch when the deck was last modified.
|
24
|
+
attr_reader :last_modified_timestamp
|
25
|
+
|
26
|
+
##
|
27
|
+
# The deck's deck options group object.
|
28
|
+
attr_reader :deck_options_group
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnkiRecord
|
4
|
+
module DeckDefaults # :nodoc:
|
5
|
+
private
|
6
|
+
|
7
|
+
def default_deck_options_group_id
|
8
|
+
collection.deck_options_groups.min_by(&:id).id
|
9
|
+
end
|
10
|
+
|
11
|
+
def default_deck_today_array
|
12
|
+
[0, 0].freeze
|
13
|
+
end
|
14
|
+
|
15
|
+
def default_collapsed
|
16
|
+
false
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -1,37 +1,20 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
require_relative "
|
6
|
-
require_relative "helpers/time_helper"
|
3
|
+
require_relative "../helpers/shared_constants_helper"
|
4
|
+
require_relative "../helpers/time_helper"
|
5
|
+
require_relative "deck_options_group_attributes"
|
7
6
|
|
8
7
|
module AnkiRecord
|
9
8
|
##
|
10
|
-
#
|
9
|
+
# DeckOptionsGroup represents a set of options that can be applied to an Anki deck.
|
11
10
|
class DeckOptionsGroup
|
12
|
-
include
|
13
|
-
include
|
14
|
-
|
15
|
-
##
|
16
|
-
# The collection object that the deck options group belongs to
|
17
|
-
attr_reader :collection
|
18
|
-
|
19
|
-
##
|
20
|
-
# The name of the deck options group
|
21
|
-
attr_accessor :name
|
22
|
-
|
23
|
-
##
|
24
|
-
# The id of the deck options group
|
25
|
-
attr_reader :id
|
26
|
-
|
27
|
-
##
|
28
|
-
# The last time that this deck options group was modified in milliseconds since the 1970 epoch
|
29
|
-
attr_reader :last_modified_time
|
11
|
+
include DeckOptionsGroupAttributes
|
12
|
+
include Helpers::SharedConstantsHelper
|
13
|
+
include Helpers::TimeHelper
|
30
14
|
|
31
15
|
##
|
32
|
-
# Instantiates a new deck options group
|
16
|
+
# Instantiates a new deck options group belonging to +collection+ with name +name+.
|
33
17
|
def initialize(collection:, name: nil, args: nil)
|
34
|
-
# TODO: extract this check to a shared helper
|
35
18
|
raise ArgumentError unless (name && args.nil?) || (args && args["name"])
|
36
19
|
|
37
20
|
@collection = collection
|
@@ -41,6 +24,8 @@ module AnkiRecord
|
|
41
24
|
else
|
42
25
|
setup_deck_options_group_instance_variables(name: name)
|
43
26
|
end
|
27
|
+
|
28
|
+
@collection.add_deck_options_group self
|
44
29
|
end
|
45
30
|
|
46
31
|
private
|
@@ -49,7 +34,7 @@ module AnkiRecord
|
|
49
34
|
# rubocop:disable Metrics/AbcSize
|
50
35
|
def setup_deck_options_group_instance_variables_from_existing(args:)
|
51
36
|
@id = args["id"]
|
52
|
-
@
|
37
|
+
@last_modified_timestamp = args["mod"]
|
53
38
|
@name = args["name"]
|
54
39
|
@usn = args["usn"]
|
55
40
|
@max_taken = args["maxTaken"]
|
@@ -68,14 +53,10 @@ module AnkiRecord
|
|
68
53
|
@new_gather_priority = args["newGatherPriority"]
|
69
54
|
@bury_interday_learning = args["buryInterdayLearning"]
|
70
55
|
end
|
71
|
-
# rubocop:enable Metrics/AbcSize
|
72
|
-
# rubocop:enable Metrics/MethodLength
|
73
56
|
|
74
|
-
# rubocop:disable Metrics/MethodLength
|
75
|
-
# rubocop:disable Metrics/AbcSize
|
76
57
|
def setup_deck_options_group_instance_variables(name:)
|
77
58
|
@id = milliseconds_since_epoch
|
78
|
-
@
|
59
|
+
@last_modified_timestamp = seconds_since_epoch
|
79
60
|
@name = name
|
80
61
|
@usn = NEW_OBJECT_USN
|
81
62
|
@max_taken = 60
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnkiRecord
|
4
|
+
##
|
5
|
+
# Module with the Card class's attribute readers, writers, and accessors.
|
6
|
+
module DeckOptionsGroupAttributes
|
7
|
+
##
|
8
|
+
# The deck options group's collection object.
|
9
|
+
attr_reader :collection
|
10
|
+
|
11
|
+
##
|
12
|
+
# The deck option group's name.
|
13
|
+
attr_accessor :name
|
14
|
+
|
15
|
+
##
|
16
|
+
# The deck option group's id.
|
17
|
+
attr_reader :id
|
18
|
+
|
19
|
+
##
|
20
|
+
# The number of milliseconds since the 1970 epoch at which the deck options group was modified.
|
21
|
+
attr_reader :last_modified_timestamp
|
22
|
+
end
|
23
|
+
end
|
@@ -3,18 +3,17 @@
|
|
3
3
|
require "digest"
|
4
4
|
|
5
5
|
module AnkiRecord
|
6
|
-
|
7
|
-
# A module for the method that calculates the checksum value of notes.
|
8
|
-
#
|
9
|
-
# This checksum is used by Anki to detect duplicates.
|
10
|
-
module ChecksumHelper
|
6
|
+
module Helpers
|
11
7
|
##
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
|
16
|
-
|
17
|
-
|
8
|
+
# A module for the method that calculates the checksum value of notes.
|
9
|
+
#
|
10
|
+
# This checksum may be used by Anki to detect duplicates.
|
11
|
+
module ChecksumHelper
|
12
|
+
##
|
13
|
+
# Returns the integer representation of the first 8 characters of the SHA-1 digest of the +sfld+ argument
|
14
|
+
def checksum(sfld)
|
15
|
+
Digest::SHA1.hexdigest(sfld)[0...8].to_i(16).to_s
|
16
|
+
end
|
18
17
|
end
|
19
18
|
end
|
20
19
|
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnkiRecord
|
4
|
+
module Helpers
|
5
|
+
module DataQueryHelper # :nodoc:
|
6
|
+
def note_cards_data_for_note_id(sql_able:, id:)
|
7
|
+
note_data = sql_able.prepare("select * from notes where id = ?").execute([id]).first
|
8
|
+
return nil unless note_data
|
9
|
+
|
10
|
+
cards_data = sql_able.prepare("select * from cards where nid = ?").execute([id]).to_a
|
11
|
+
{ note_data: note_data, cards_data: cards_data }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -1,11 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module AnkiRecord
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
4
|
+
module Helpers
|
5
|
+
module SharedConstantsHelper # :nodoc:
|
6
|
+
NEW_OBJECT_USN = -1
|
7
|
+
NON_FILTERED_DECK_DYN = 0
|
8
|
+
private_constant :NEW_OBJECT_USN, :NON_FILTERED_DECK_DYN
|
9
|
+
end
|
10
10
|
end
|
11
11
|
end
|
@@ -3,21 +3,26 @@
|
|
3
3
|
require "date"
|
4
4
|
|
5
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
|
6
|
+
module Helpers
|
11
7
|
##
|
12
|
-
#
|
13
|
-
|
14
|
-
|
15
|
-
|
8
|
+
# Helper module to calculate integer time values since the 1970 epoch.
|
9
|
+
#
|
10
|
+
# Specifically, the time that has passed since 00:00:00 UTC Jan 1 1970.
|
11
|
+
module TimeHelper
|
12
|
+
##
|
13
|
+
# Returns approximately the number of milliseconds since the 1970 epoch.
|
14
|
+
# This is used for some of the primary key ids. To prevent violation of the
|
15
|
+
# uniqueness constraint, sleep is called for 1 millisecond.
|
16
|
+
def milliseconds_since_epoch
|
17
|
+
sleep 0.001
|
18
|
+
DateTime.now.strftime("%Q").to_i
|
19
|
+
end
|
16
20
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
+
##
|
22
|
+
# Returns approximately the number of seconds since the 1970 epoch.
|
23
|
+
def seconds_since_epoch
|
24
|
+
Time.now.to_i
|
25
|
+
end
|
21
26
|
end
|
22
27
|
end
|
23
28
|
end
|
@@ -0,0 +1,178 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "securerandom"
|
4
|
+
|
5
|
+
require_relative "../helpers/checksum_helper"
|
6
|
+
require_relative "../helpers/time_helper"
|
7
|
+
require_relative "note_attributes"
|
8
|
+
|
9
|
+
# rubocop:disable Metrics/ClassLength
|
10
|
+
module AnkiRecord
|
11
|
+
##
|
12
|
+
# Represents an Anki note.
|
13
|
+
class Note
|
14
|
+
include Helpers::ChecksumHelper
|
15
|
+
include NoteAttributes
|
16
|
+
include Helpers::TimeHelper
|
17
|
+
include Helpers::SharedConstantsHelper
|
18
|
+
|
19
|
+
##
|
20
|
+
# Instantiates a note of type +note_type+ and belonging to deck +deck+.
|
21
|
+
def initialize(note_type: nil, deck: nil, collection: nil, data: nil)
|
22
|
+
if note_type && deck
|
23
|
+
setup_instance_variables_for_new_note(note_type: note_type, deck: deck)
|
24
|
+
elsif collection && data
|
25
|
+
setup_instance_variables_from_existing(collection: collection,
|
26
|
+
note_data: data[:note_data], cards_data: data[:cards_data])
|
27
|
+
else
|
28
|
+
raise ArgumentError
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def setup_instance_variables_for_new_note(note_type:, deck:)
|
35
|
+
raise ArgumentError unless note_type.collection == deck.collection
|
36
|
+
|
37
|
+
setup_collaborator_object_instance_variables_for_new_note(note_type: note_type, deck: deck)
|
38
|
+
setup_simple_instance_variables_for_new_note
|
39
|
+
end
|
40
|
+
|
41
|
+
def setup_collaborator_object_instance_variables_for_new_note(note_type:, deck:)
|
42
|
+
@note_type = note_type
|
43
|
+
@deck = deck
|
44
|
+
@collection = deck.collection
|
45
|
+
@field_contents = setup_empty_field_contents_hash
|
46
|
+
@cards = @note_type.card_templates.map do |card_template|
|
47
|
+
Card.new(note: self, card_template: card_template)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def setup_simple_instance_variables_for_new_note
|
52
|
+
@id = milliseconds_since_epoch
|
53
|
+
@guid = globally_unique_id
|
54
|
+
@last_modified_timestamp = seconds_since_epoch
|
55
|
+
@usn = NEW_OBJECT_USN
|
56
|
+
@tags = []
|
57
|
+
@flags = 0
|
58
|
+
@data = ""
|
59
|
+
end
|
60
|
+
|
61
|
+
def setup_instance_variables_from_existing(collection:, note_data:, cards_data:)
|
62
|
+
setup_collaborator_object_instance_variables_from_existing(collection: collection, note_data: note_data,
|
63
|
+
cards_data: cards_data)
|
64
|
+
setup_simple_instance_variables_from_existing(note_data: note_data)
|
65
|
+
end
|
66
|
+
|
67
|
+
def setup_collaborator_object_instance_variables_from_existing(collection:, note_data:, cards_data:)
|
68
|
+
@collection = collection
|
69
|
+
@note_type = collection.find_note_type_by id: note_data["mid"]
|
70
|
+
@field_contents = setup_field_contents_hash_from_existing(note_data: note_data)
|
71
|
+
@cards = @note_type.card_templates.map.with_index do |_card_template, index|
|
72
|
+
Card.new(note: self, card_data: cards_data[index])
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def setup_field_contents_hash_from_existing(note_data:)
|
77
|
+
field_contents = setup_empty_field_contents_hash
|
78
|
+
snake_case_field_names_in_order = note_type.snake_case_field_names
|
79
|
+
note_data["flds"].split("\x1F").each_with_index do |fld, ordinal|
|
80
|
+
field_contents[snake_case_field_names_in_order[ordinal]] = fld
|
81
|
+
end
|
82
|
+
field_contents
|
83
|
+
end
|
84
|
+
|
85
|
+
def setup_simple_instance_variables_from_existing(note_data:)
|
86
|
+
@id = note_data["id"]
|
87
|
+
@guid = note_data["guid"]
|
88
|
+
@last_modified_timestamp = note_data["mod"]
|
89
|
+
@usn = note_data["usn"]
|
90
|
+
@tags = note_data["tags"].split
|
91
|
+
@flags = note_data["flags"]
|
92
|
+
@data = note_data["data"]
|
93
|
+
end
|
94
|
+
|
95
|
+
def setup_empty_field_contents_hash
|
96
|
+
field_contents = {}
|
97
|
+
note_type.snake_case_field_names.each { |field_name| field_contents[field_name] = "" }
|
98
|
+
field_contents
|
99
|
+
end
|
100
|
+
|
101
|
+
public
|
102
|
+
|
103
|
+
##
|
104
|
+
# Saves the note to the collection.anki21 database.
|
105
|
+
#
|
106
|
+
# This also saves the note's cards.
|
107
|
+
def save
|
108
|
+
collection.find_note_by(id: @id) ? update_note_in_collection_anki21 : insert_new_note_in_collection_anki21
|
109
|
+
true
|
110
|
+
end
|
111
|
+
|
112
|
+
private
|
113
|
+
|
114
|
+
def update_note_in_collection_anki21
|
115
|
+
statement = @collection.anki_package.prepare <<~SQL
|
116
|
+
update notes set guid = ?, mid = ?, mod = ?, usn = ?, tags = ?,
|
117
|
+
flds = ?, sfld = ?, csum = ?, flags = ?, data = ? where id = ?
|
118
|
+
SQL
|
119
|
+
statement.execute([@guid, note_type.id, @last_modified_timestamp,
|
120
|
+
@usn, @tags.join(" "), field_values_separated_by_us, sort_field_value,
|
121
|
+
checksum(sort_field_value), @flags, @data, @id])
|
122
|
+
cards.each { |card| card.save(note_exists_already: true) }
|
123
|
+
end
|
124
|
+
|
125
|
+
def insert_new_note_in_collection_anki21
|
126
|
+
statement = @collection.anki_package.prepare <<~SQL
|
127
|
+
insert into notes (id, guid, mid, mod, usn, tags, flds, sfld, csum, flags, data)
|
128
|
+
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
129
|
+
SQL
|
130
|
+
statement.execute([@id, @guid, note_type.id, @last_modified_timestamp,
|
131
|
+
@usn, @tags.join(" "), field_values_separated_by_us, sort_field_value,
|
132
|
+
checksum(sort_field_value), @flags, @data])
|
133
|
+
cards.each(&:save)
|
134
|
+
end
|
135
|
+
|
136
|
+
public
|
137
|
+
|
138
|
+
##
|
139
|
+
# Overrides BasicObject#method_missing and creates "ghost methods".
|
140
|
+
#
|
141
|
+
# The ghost methods are the setters and getters for the note field values.
|
142
|
+
def method_missing(method_name, field_content = nil)
|
143
|
+
raise NoMethodError, "##{method_name} is not defined or a ghost method" unless respond_to_missing? method_name
|
144
|
+
|
145
|
+
method_name = method_name.to_s
|
146
|
+
return @field_contents[method_name] unless method_name.end_with?("=")
|
147
|
+
|
148
|
+
@field_contents[method_name.chomp("=")] = field_content
|
149
|
+
end
|
150
|
+
|
151
|
+
##
|
152
|
+
# This allows #respond_to? to be accurate for the ghost methods created by #method_missing.
|
153
|
+
def respond_to_missing?(method_name, *)
|
154
|
+
method_name = method_name.to_s
|
155
|
+
if method_name.end_with?("=")
|
156
|
+
note_type.snake_case_field_names.include?(method_name.chomp("="))
|
157
|
+
else
|
158
|
+
note_type.snake_case_field_names.include?(method_name)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
private
|
163
|
+
|
164
|
+
def globally_unique_id
|
165
|
+
SecureRandom.uuid.slice(5...15)
|
166
|
+
end
|
167
|
+
|
168
|
+
def field_values_separated_by_us
|
169
|
+
# The ASCII control code represented by hexadecimal 1F is the Unit Separator (US)
|
170
|
+
note_type.snake_case_field_names.map { |field_name| @field_contents[field_name] }.join("\x1F")
|
171
|
+
end
|
172
|
+
|
173
|
+
def sort_field_value
|
174
|
+
@field_contents[note_type.snake_case_sort_field_name]
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
# rubocop:enable Metrics/ClassLength
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnkiRecord
|
4
|
+
# Module with the Note class's attribute readers, writers, and accessors.
|
5
|
+
module NoteAttributes
|
6
|
+
##
|
7
|
+
# The note's id.
|
8
|
+
attr_reader :id
|
9
|
+
|
10
|
+
##
|
11
|
+
# The note's globally unique id.
|
12
|
+
attr_reader :guid
|
13
|
+
|
14
|
+
##
|
15
|
+
# The number of seconds since the 1970 epoch at which the note was last modified.
|
16
|
+
attr_reader :last_modified_timestamp
|
17
|
+
|
18
|
+
##
|
19
|
+
# The note's update sequence number.
|
20
|
+
attr_reader :usn
|
21
|
+
|
22
|
+
##
|
23
|
+
# The note's tags, as an array. The tags are strings.
|
24
|
+
attr_reader :tags
|
25
|
+
|
26
|
+
##
|
27
|
+
# The note's field contents, as a hash.
|
28
|
+
attr_reader :field_contents
|
29
|
+
|
30
|
+
##
|
31
|
+
# The note's deck object.
|
32
|
+
#
|
33
|
+
# When the note is saved, the cards will belong to this deck.
|
34
|
+
attr_reader :deck
|
35
|
+
|
36
|
+
##
|
37
|
+
# The note's note type object.
|
38
|
+
attr_reader :note_type
|
39
|
+
|
40
|
+
##
|
41
|
+
# The note's collection object.
|
42
|
+
attr_reader :collection
|
43
|
+
|
44
|
+
##
|
45
|
+
# The note's card objects, as an array.
|
46
|
+
attr_reader :cards
|
47
|
+
|
48
|
+
##
|
49
|
+
# Corresponds to the flags column in the collection.anki21 notes table.
|
50
|
+
attr_reader :flags
|
51
|
+
|
52
|
+
##
|
53
|
+
# Corresponds to the data column in the collection.anki21 notes table.
|
54
|
+
attr_reader :data
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "note_field_attributes"
|
4
|
+
require_relative "note_field_defaults"
|
5
|
+
|
6
|
+
module AnkiRecord
|
7
|
+
##
|
8
|
+
# NoteField represents a field of an Anki note type.
|
9
|
+
class NoteField
|
10
|
+
include NoteFieldAttributes
|
11
|
+
include NoteFieldDefaults
|
12
|
+
|
13
|
+
##
|
14
|
+
# Instantiates a new field for the note type +note_type+ with name +name+.
|
15
|
+
def initialize(note_type:, name: nil, args: nil)
|
16
|
+
raise ArgumentError unless (name && args.nil?) || (args && args["name"])
|
17
|
+
|
18
|
+
@note_type = note_type
|
19
|
+
if args
|
20
|
+
setup_note_field_instance_variables_from_existing(args: args)
|
21
|
+
else
|
22
|
+
setup_note_field_instance_variables_for_new_field(name: name)
|
23
|
+
end
|
24
|
+
|
25
|
+
@note_type.add_note_field self
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_h # :nodoc:
|
29
|
+
{
|
30
|
+
name: @name,
|
31
|
+
ord: @ordinal_number,
|
32
|
+
sticky: @sticky,
|
33
|
+
rtl: @right_to_left,
|
34
|
+
font: @font_style,
|
35
|
+
size: @font_size,
|
36
|
+
description: @description
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def setup_note_field_instance_variables_from_existing(args:)
|
43
|
+
@name = args["name"]
|
44
|
+
@ordinal_number = args["ord"]
|
45
|
+
@sticky = args["sticky"]
|
46
|
+
@right_to_left = args["rtl"]
|
47
|
+
@font_style = args["font"]
|
48
|
+
@font_size = args["size"]
|
49
|
+
@description = args["description"]
|
50
|
+
end
|
51
|
+
|
52
|
+
def setup_note_field_instance_variables_for_new_field(name:)
|
53
|
+
@name = name
|
54
|
+
@ordinal_number = @note_type.note_fields.length
|
55
|
+
@sticky = false
|
56
|
+
@right_to_left = false
|
57
|
+
@font_style = default_field_font_style
|
58
|
+
@font_size = default_field_font_size
|
59
|
+
@description = default_field_description
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|