From cafbf36bb270d3ebfaa0676701c9f1dee17ed3e1 Mon Sep 17 00:00:00 2001 From: weijiuqiao <59040746+weijiuqiao@users.noreply.github.com> Date: Mon, 13 Jun 2022 03:34:17 +0800 Subject: [PATCH] Vocabulary builder: store word context, other tweaks and fixes (#9195) Add a copy button and save word context (off by default), shown via the three-dot menu of word entries. Also some db refactoring and minor UI improvements: - a dedicated book title table in order to shrink db size by storing references to title names instead of repeated actual strings, - alignment of different forms of the "more" button and possible clipped words in translations. - fix plugin name so it can be disabled --- .../apps/reader/modules/readerdictionary.lua | 12 +- .../apps/reader/modules/readerhighlight.lua | 23 ++ .../ui/elements/filemanager_menu_order.lua | 2 +- frontend/ui/elements/reader_menu_order.lua | 2 +- plugins/vocabbuilder.koplugin/_meta.lua | 2 +- plugins/vocabbuilder.koplugin/db.lua | 81 ++++- plugins/vocabbuilder.koplugin/main.lua | 338 +++++++++++++----- 7 files changed, 339 insertions(+), 121 deletions(-) diff --git a/frontend/apps/reader/modules/readerdictionary.lua b/frontend/apps/reader/modules/readerdictionary.lua index 349612417..70a876b2c 100644 --- a/frontend/apps/reader/modules/readerdictionary.lua +++ b/frontend/apps/reader/modules/readerdictionary.lua @@ -888,13 +888,13 @@ function ReaderDictionary:stardictLookup(word, dict_names, fuzzy_search, boxes, return end - local book_title = self.ui.doc_settings and self.ui.doc_settings:readSetting("doc_props").title or _("Dictionary lookup") - if book_title == "" then -- no or empty metadata title - if self.ui.document and self.ui.document.file then - local directory, filename = util.splitFilePathName(self.ui.document.file) -- luacheck: no unused - book_title = util.splitFileNameSuffix(filename) - end + local book_title = self.ui.doc_settings and self.ui.doc_settings:readSetting("doc_props").title or _("Dictionary lookup") + if book_title == "" then -- no or empty metadata title + if self.ui.document and self.ui.document.file then + local directory, filename = util.splitFilePathName(self.ui.document.file) -- luacheck: no unused + book_title = util.splitFileNameSuffix(filename) end + end -- Event for plugin to catch lookup with book title self.ui:handleEvent(Event:new("WordLookedUp", word, book_title)) diff --git a/frontend/apps/reader/modules/readerhighlight.lua b/frontend/apps/reader/modules/readerhighlight.lua index 1f06ce560..6fb04fe4a 100644 --- a/frontend/apps/reader/modules/readerhighlight.lua +++ b/frontend/apps/reader/modules/readerhighlight.lua @@ -1230,6 +1230,29 @@ dbg:guard(ReaderHighlight, "lookup", "lookup must not be called with nil selected_text!") end) +function ReaderHighlight:getSelectedWordContext(nb_words) + if not self.ui.rolling or not self.selected_text then return nil end + local pos_start = self.selected_text.pos0 + local pos_end = self.selected_text.pos1 + + for i=0, nb_words do + local ok, start = pcall(self.ui.document.getPrevVisibleWordStart, self.ui.document, pos_start) + if ok then pos_start = start + else break end + end + + for i=0, nb_words do + local ok, ending = pcall(self.ui.document.getNextVisibleWordEnd, self.ui.document, pos_end) + if ok then pos_end = ending + else break end + end + + local ok_prev, prev = pcall(self.ui.document.getTextFromXPointers, self.ui.document, pos_start, self.selected_text.pos0) + local ok_next, next = pcall(self.ui.document.getTextFromXPointers, self.ui.document, self.selected_text.pos1, pos_end) + + return ok_prev and prev, ok_next and next +end + function ReaderHighlight:viewSelectionHTML(debug_view, no_css_files_buttons) if self.ui.document.info.has_pages then return diff --git a/frontend/ui/elements/filemanager_menu_order.lua b/frontend/ui/elements/filemanager_menu_order.lua index dfc751b81..1758cf67e 100644 --- a/frontend/ui/elements/filemanager_menu_order.lua +++ b/frontend/ui/elements/filemanager_menu_order.lua @@ -140,7 +140,7 @@ local order = { search = { "dictionary_lookup", "dictionary_lookup_history", - "vocabulary_builder", + "vocabbuilder", "dictionary_settings", "----------------------------", "wikipedia_lookup", diff --git a/frontend/ui/elements/reader_menu_order.lua b/frontend/ui/elements/reader_menu_order.lua index 239ccdb39..3f3cced0c 100644 --- a/frontend/ui/elements/reader_menu_order.lua +++ b/frontend/ui/elements/reader_menu_order.lua @@ -188,7 +188,7 @@ local order = { search = { "dictionary_lookup", "dictionary_lookup_history", - "vocabulary_builder", + "vocabbuilder", "dictionary_settings", "----------------------------", "wikipedia_lookup", diff --git a/plugins/vocabbuilder.koplugin/_meta.lua b/plugins/vocabbuilder.koplugin/_meta.lua index d75754252..3b3184a5e 100644 --- a/plugins/vocabbuilder.koplugin/_meta.lua +++ b/plugins/vocabbuilder.koplugin/_meta.lua @@ -1,6 +1,6 @@ local _ = require("gettext") return { - name = "vocabulary_builder", + name = "vocabbuilder", fullname = _("Vocabulary builder"), description = _([[This plugin processes dictionary word lookups and uses spaced repetition to help you remember new words.]]), } diff --git a/plugins/vocabbuilder.koplugin/db.lua b/plugins/vocabbuilder.koplugin/db.lua index 041594ddc..bd97639d1 100644 --- a/plugins/vocabbuilder.koplugin/db.lua +++ b/plugins/vocabbuilder.koplugin/db.lua @@ -5,19 +5,27 @@ local LuaData = require("luadata") local db_location = DataStorage:getSettingsDir() .. "/vocabulary_builder.sqlite3" -local DB_SCHEMA_VERSION = 20220522 +local DB_SCHEMA_VERSION = 20220608 local VOCABULARY_DB_SCHEMA = [[ -- To store looked up words CREATE TABLE IF NOT EXISTS "vocabulary" ( "word" TEXT NOT NULL UNIQUE, - "book_title" TEXT DEFAULT '', + "title_id" INTEGER, "create_time" INTEGER NOT NULL, "review_time" INTEGER, "due_time" INTEGER NOT NULL, "review_count" INTEGER NOT NULL DEFAULT 0, + "prev_context" TEXT, + "next_context" TEXT, PRIMARY KEY("word") ); + CREATE TABLE IF NOT EXISTS "title" ( + "id" INTEGER NOT NULL UNIQUE, + "name" TEXT UNIQUE, + PRIMARY KEY("id" AUTOINCREMENT) + ); CREATE INDEX IF NOT EXISTS due_time_index ON vocabulary(due_time); + CREATE INDEX IF NOT EXISTS title_name_index ON title(name); ]] local VocabularyBuilder = { @@ -62,7 +70,22 @@ function VocabularyBuilder:createDB() if db_version < DB_SCHEMA_VERSION then if db_version == 0 then self:insertLookupData(db_conn) + elseif db_version < 20220608 then + db_conn:exec([[ ALTER TABLE vocabulary ADD prev_context TEXT; + ALTER TABLE vocabulary ADD next_context TEXT; + ALTER TABLE vocabulary ADD title_id INTEGER; + + INSERT INTO title (name) + SELECT DISTINCT book_title FROM vocabulary; + + UPDATE vocabulary SET title_id = ( + SELECT id FROM title WHERE name = book_title + ); + + ALTER TABLE vocabulary DROP book_title;]]) end + + db_conn:exec("CREATE INDEX IF NOT EXISTS title_id_index ON vocabulary(title_id);") -- Update version db_conn:exec(string.format("PRAGMA user_version=%d;", DB_SCHEMA_VERSION)) @@ -76,21 +99,29 @@ function VocabularyBuilder:insertLookupData(db_conn) local lookup_history = LuaData:open(file_path, { name = "LookupHistory" }) if lookup_history:has("lookup_history") then local lookup_history_table = lookup_history:readSetting("lookup_history") - local words = {} + local book_titles = {} + local stmt = db_conn:prepare("INSERT INTO title (name) values (?);") + for i = #lookup_history_table, 1, -1 do + local book_title = lookup_history_table[i].book_title or "" + if not book_titles[book_title] then + stmt:bind(book_title) + stmt:step() + stmt:clearbind():reset() + book_titles[book_title] = true + end + end + local words = {} + local insert_sql = [[INSERT OR REPLACE INTO vocabulary + (word, title_id, create_time, due_time) values + (?, (SELECT id FROM title WHERE name = ?), ?, ?);]] + stmt = db_conn:prepare(insert_sql) for i = #lookup_history_table, 1, -1 do local value = lookup_history_table[i] if not words[value.word] then - local insert_sql = [[INSERT OR REPLACE INTO vocabulary - (word, book_title, create_time, due_time) values - (?, ?, ?, ?); - ]] - local stmt = db_conn:prepare(insert_sql) - stmt:bind(value.word, value.book_title or "", value.time, value.time + 5*60) stmt:step() stmt:clearbind():reset() - words[value.word] = true end end @@ -100,7 +131,7 @@ end function VocabularyBuilder:_select_items(items, start_idx) local conn = SQ3.open(db_location) - local sql = string.format("SELECT * FROM vocabulary ORDER BY due_time limit %d OFFSET %d;", 32, start_idx-1) + local sql = string.format("SELECT * FROM vocabulary LEFT JOIN title ON title_id = title.id ORDER BY due_time limit %d OFFSET %d;", 32, start_idx-1) local results = conn:exec(sql) conn:close() @@ -113,11 +144,13 @@ function VocabularyBuilder:_select_items(items, start_idx) if item and not item.word then item.word = results.word[i] item.review_count = math.max(0, math.min(8, tonumber(results.review_count[i]))) - item.book_title = results.book_title[i] or "" + item.book_title = results.name[i] or "" item.create_time = tonumber( results.create_time[i]) item.review_time = nil --use this field to flag change item.due_time = tonumber(results.due_time[i]) item.is_dim = tonumber(results.due_time[i]) > current_time + item.prev_context = results.prev_context[i] + item.next_context = results.next_context[i] item.got_it_callback = function(item_input) VocabularyBuilder:gotOrForgot(item_input, true) end @@ -156,7 +189,7 @@ function VocabularyBuilder:gotOrForgot(item, isGot) local due_time local target_count = math.min(math.max(item.review_count + (isGot and 1 or -1), 0), 8) - if target_count == 0 then + if not isGot or target_count == 0 then due_time = current_time + 5 * 60 elseif target_count == 1 then due_time = current_time + 30 * 60 @@ -198,18 +231,28 @@ function VocabularyBuilder:batchUpdateItems(items) stmt:clearbind():reset() end end + + conn:exec("DELETE FROM title WHERE NOT EXISTS( SELECT title_id FROM vocabulary WHERE id = title_id );") conn:close() end function VocabularyBuilder:insertOrUpdate(entry) local conn = SQ3.open(db_location) - local stmt = conn:prepare([[INSERT INTO vocabulary (word, book_title, create_time, due_time) - VALUES (?, ?, ?, ?) - ON CONFLICT(word) DO UPDATE SET book_title = excluded.book_title, + local stmt = conn:prepare("INSERT OR IGNORE INTO title (name) VALUES (?);") + stmt:bind(entry.book_title) + stmt:step() + stmt:clearbind():reset() + + stmt = conn:prepare([[INSERT INTO vocabulary (word, title_id, create_time, due_time, prev_context, next_context) + VALUES (?, (SELECT id FROM title WHERE name = ?), ?, ?, ?, ?) + ON CONFLICT(word) DO UPDATE SET title_id = excluded.title_id, create_time = excluded.create_time, review_count = MAX(review_count-1, 0), - due_time = ?;]]) - stmt:bind(entry.word, entry.book_title, entry.time, entry.time+300, entry.time+300) + due_time = ?, + prev_context = ifnull(excluded.prev_context, prev_context), + next_context = ifnull(excluded.next_context, next_context);]]); + stmt:bind(entry.word, entry.book_title, entry.time, entry.time+300, + entry.prev_context, entry.next_context, entry.time+300) stmt:step() stmt:clearbind():reset() self.count = tonumber(conn:rowexec("SELECT count(0) from vocabulary;")) @@ -236,7 +279,7 @@ end function VocabularyBuilder:purge() local conn = SQ3.open(db_location) - conn:exec("DELETE FROM vocabulary;") + conn:exec("DELETE FROM vocabulary; DELETE FROM title;") self.count = 0 conn:close() end diff --git a/plugins/vocabbuilder.koplugin/main.lua b/plugins/vocabbuilder.koplugin/main.lua index cd5a858c1..eea11d05c 100644 --- a/plugins/vocabbuilder.koplugin/main.lua +++ b/plugins/vocabbuilder.koplugin/main.lua @@ -21,10 +21,13 @@ local Geom = require("ui/geometry") local GestureRange = require("ui/gesturerange") local HorizontalGroup = require("ui/widget/horizontalgroup") local HorizontalSpan = require("ui/widget/horizontalspan") +local IconButton = require("ui/widget/iconbutton") +local IconWidget = require("ui/widget/iconwidget") local InputContainer = require("ui/widget/container/inputcontainer") local LeftContainer = require("ui/widget/container/leftcontainer") local LineWidget = require("ui/widget/linewidget") local MovableContainer = require("ui/widget/container/movablecontainer") +local Notification = require("ui/widget/notification") local RightContainer = require("ui/widget/container/rightcontainer") local OverlapGroup = require("ui/widget/overlapgroup") local Screen = Device.screen @@ -53,7 +56,7 @@ local settings = G_reader_settings:readSetting("vocabulary_builder", {enabled = Menu dialogue widget --]]-- local MenuDialog = FocusManager:new{ - padding = Size.padding.fullscreen, + padding = Size.padding.large, is_edit_mode = false, edit_callback = nil, tap_close_callback = nil, @@ -67,7 +70,7 @@ function MenuDialog:init() self.key_events.Close = { { Device.input.group.Back }, doc = "close dialog" } end if Device:isTouchDevice() then - self.ges_events.TapClose = { + self.ges_events.Tap = { GestureRange:new { ges = "tap", range = Geom:new { @@ -80,11 +83,24 @@ function MenuDialog:init() } end - local switch_ratio = 0.61 local size = Screen:getSize() local width = math.floor(size.w * 0.8) + + -- Switch text translations could be long + local temp_text_widget = TextWidget:new{ + text = _("Accept new words"), + face = Font:getFace("xx_smallinfofont") + } + local switch_guide_width = temp_text_widget:getSize().w + temp_text_widget:setText(_("Save context")) + switch_guide_width = math.max(switch_guide_width, temp_text_widget:getSize().w) + switch_guide_width = math.min(math.max(switch_guide_width, math.ceil(width*0.39)), math.ceil(width*0.61)) + temp_text_widget:free() + + local switch_width = width - switch_guide_width - Size.padding.fullscreen - Size.padding.default + local switch = ToggleSwitch:new{ - width = math.floor(width * switch_ratio), + width = switch_width, default_value = 2, name = "vocabulary_builder", name_text = nil, --_("Accept new words"), @@ -101,6 +117,23 @@ function MenuDialog:init() switch:setPosition(settings.enabled and 2 or 1) self:mergeLayoutInVertical(switch) + self.context_switch = ToggleSwitch:new{ + width = switch_width, + default_value = 1, + name_text = nil, + event = "ChangeContextStatus", + args = {"off", "on"}, + default_arg = "off", + toggle = { _("off"), _("on") }, + values = {1, 2}, + alternate = false, + enabled = settings.enabled, + config = self, + readonly = self.readonly, + } + self.context_switch:setPosition(settings.with_context and 2 or 1) + self:mergeLayoutInVertical(self.context_switch) + local edit_button = { text = self.is_edit_mode and _("Resume") or _("Quick deletion"), callback = function() @@ -151,15 +184,15 @@ function MenuDialog:init() self:mergeLayoutInVertical(buttons) self.covers_fullscreen = true - local switch_guide_width = math.ceil(math.max(5, width * (1-switch_ratio) - Size.padding.fullscreen)) self[1] = CenterContainer:new{ dimen = size, FrameContainer:new{ - padding = self.padding, + padding = Size.padding.default, + padding_top = Size.padding.large, + padding_bottom = 0, background = Blitbuffer.COLOR_WHITE, bordersize = Size.border.window, radius = Size.radius.window, - padding_bottom = Size.padding.button, VerticalGroup:new{ HorizontalGroup:new{ RightContainer:new{ @@ -173,6 +206,19 @@ function MenuDialog:init() HorizontalSpan:new{width = Size.padding.fullscreen}, switch, }, + VerticalSpan:new{ width = Size.padding.default}, + HorizontalGroup:new{ + RightContainer:new{ + dimen = Geom:new{w = switch_guide_width, h = switch:getSize().h}, + TextWidget:new{ + text = _("Save context"), + face = Font:getFace("xx_smallinfofont"), + max_width = switch_guide_width + } + }, + HorizontalSpan:new{width = Size.padding.fullscreen}, + self.context_switch, + }, VerticalSpan:new{ width = Size.padding.large}, LineWidget:new{ background = Blitbuffer.COLOR_GRAY, @@ -201,7 +247,15 @@ function MenuDialog:onCloseWidget() end) end -function MenuDialog:onTapClose() +function MenuDialog:onTap(_, ges) + if ges.pos:notIntersectWith(self[1][1].dimen) then + -- Tap outside closes widget + self:onClose() + return true + end +end + +function MenuDialog:onClose() UIManager:close(self) if self.tap_close_callback then self.tap_close_callback() @@ -209,20 +263,25 @@ function MenuDialog:onTapClose() return true end -function MenuDialog:onClose() - self:onTapClose() - return true +function MenuDialog:onChangeContextStatus(args, position) + settings.with_context = position == 2 + G_reader_settings:saveSetting("vocabulary_builder", settings) end function MenuDialog:onChangeEnableStatus(args, position) settings.enabled = position == 2 + self.context_switch.enabled = position == 2 G_reader_settings:saveSetting("vocabulary_builder", settings) end function MenuDialog:onConfigChoose(values, name, event, args, position) UIManager:tickAfterNext(function() if values then - self:onChangeEnableStatus(args, position) + if event == "ChangeEnableStatus" then + self:onChangeEnableStatus(args, position) + elseif event == "ChangeContextStatus" then + self:onChangeContextStatus(args, position) + end end UIManager:setDirty(nil, "ui", nil, true) end) @@ -243,14 +302,14 @@ local WordInfoDialog = InputContainer:new{ reset_callback = nil, dismissable = true, -- set to false if any button callback is required } - +local word_info_dialog_width function WordInfoDialog:init() if self.dismissable then if Device:hasKeys() then self.key_events.Close = { { Device.input.group.Back }, doc = "close dialog" } end if Device:isTouchDevice() then - self.ges_events.TapClose = { + self.ges_events.Tap = { GestureRange:new { ges = "tap", range = Geom:new { @@ -264,7 +323,18 @@ function WordInfoDialog:init() end end - local width = math.floor(math.min(Screen:getWidth(), Screen:getHeight()) * 0.61) + if not word_info_dialog_width then + local temp_text = TextWidget:new{ + text = self.dates, + padding = Size.padding.fullscreen, + face = Font:getFace("cfont", 14) + } + local dates_width = temp_text:getSize().w + temp_text:free() + local screen_width = math.min(Screen:getWidth(), Screen:getHeight()) + word_info_dialog_width = math.floor(math.max(screen_width * 0.6, math.min(screen_width * 0.8, dates_width))) + end + local width = word_info_dialog_width local reset_button = { text = _("Reset progress"), callback = function() @@ -285,6 +355,18 @@ function WordInfoDialog:init() buttons = {{reset_button, remove_button}}, show_parent = self } + + local copy_button = Button:new{ + text = "", -- copy in nerdfont, + callback = function() + Device.input.setClipboardText(self.title) + UIManager:show(Notification:new{ + text = _("Word copied to clipboard."), + }) + end, + bordersize = 0, + } + local has_context = self.prev_context or self.next_context self[1] = CenterContainer:new{ dimen = Screen:getSize(), MovableContainer:new{ @@ -292,31 +374,46 @@ function WordInfoDialog:init() VerticalGroup:new{ align = "center", FrameContainer:new{ - padding =self.padding, + padding = self.padding, + padding_top = Size.padding.buttontable, + padding_bottom = Size.padding.buttontable, margin = self.margin, bordersize = 0, VerticalGroup:new { align = "left", - TextWidget:new{ - text = self.title, - width = width, - face = word_face, - bold = true, - alignment = self.title_align or "left", + HorizontalGroup:new{ + TextWidget:new{ + text = self.title, + max_width = width - copy_button:getSize().w - Size.padding.default, + face = word_face, + bold = true, + alignment = self.title_align or "left", + }, + HorizontalSpan:new{ width=Size.padding.default }, + copy_button, }, TextBoxWidget:new{ text = self.book_title, width = width, - face = subtitle_italic_face, - fgcolor = subtitle_color, + face = Font:getFace("NotoSans-Italic.ttf", 15), alignment = self.title_align or "left", }, VerticalSpan:new{width= Size.padding.default}, + has_context and + TextBoxWidget:new{ + text = "..." .. self.prev_context:gsub("\n", " ") .. "【" ..self.title.."】" .. self.next_context:gsub("\n", " ") .. "...", + width = width, + face = Font:getFace("smallffont"), + alignment = self.title_align or "left", + } + or VerticalSpan:new{ width = Size.padding.default }, + VerticalSpan:new{ width = has_context and Size.padding.default or 0}, TextBoxWidget:new{ text = self.dates, width = width, - face = subtitle_face, + face = Font:getFace("cfont", 14), alignment = self.title_align or "left", + fgcolor = dim_color }, } @@ -325,7 +422,7 @@ function WordInfoDialog:init() background = Blitbuffer.COLOR_GRAY, dimen = Geom:new{ w = width + self.padding + self.margin, - h = Screen:scaleBySize(2), + h = Screen:scaleBySize(1), } }, focus_button @@ -333,8 +430,7 @@ function WordInfoDialog:init() background = Blitbuffer.COLOR_WHITE, bordersize = Size.border.window, radius = Size.radius.window, - padding = Size.padding.button, - padding_bottom = 0, + padding = 0 } } } @@ -360,7 +456,7 @@ function WordInfoDialog:onCloseWidget() end) end -function WordInfoDialog:onTapClose() +function WordInfoDialog:onClose() UIManager:close(self) if self.tap_close_callback then self.tap_close_callback() @@ -368,9 +464,12 @@ function WordInfoDialog:onTapClose() return true end -function WordInfoDialog:onClose() - self:onTapClose() - return true +function WordInfoDialog:onTap(_, ges) + if ges.pos:notIntersectWith(self[1][1].dimen) then + -- Tap outside closes widget + self:onClose() + return true + end end function WordInfoDialog:paintTo(...) @@ -381,7 +480,6 @@ end -- values useful for item cells -local review_button_width = math.ceil(math.min(Screen:scaleBySize(95), Screen:getWidth()/6)) local ellipsis_button_width = Screen:scaleBySize(34) local star_width = Screen:scaleBySize(25) @@ -399,6 +497,7 @@ local VocabItemWidget = InputContainer:new{ face = Font:getFace("smallinfofont"), width = nil, height = nil, + review_button_width = nil, show_parent = nil, item = nil, forgot_button = nil, @@ -450,21 +549,9 @@ end function VocabItemWidget:initItemWidget() for i = 1, #self.layout do self.layout[i] = nil end - - local word_widget = Button:new{ - text = self.item.word, - bordersize = 0, - callback = function() self:onTap() end - } - if self.item.is_dim then - word_widget.label_widget.fgcolor = dim_color - end - - table.insert(self.layout, word_widget) - - if not self.show_parent.is_edit_mode and self.item.review_count < 5 then + if not self.show_parent.is_edit_mode and self.item.review_count < 6 then self.more_button = Button:new{ - text = "⋮", + text = (self.item.prev_context or self.item.next_context) and "⋯" or "⋮", padding = Size.padding.button, callback = function() self:showMore() end, width = ellipsis_button_width, @@ -472,13 +559,11 @@ function VocabItemWidget:initItemWidget() show_parent = self } else - self.more_button = Button:new{ + self.more_button = IconButton:new{ icon = "exit", - icon_width = star_width, - icon_height = star_width, - bordersize = 0, - radius = 0, - padding = (ellipsis_button_width - star_width)/2, + width = star_width, + height = star_width, + padding = math.floor((ellipsis_button_width - star_width)/2) + Size.padding.button, callback = function() self:remover() end, @@ -489,18 +574,17 @@ function VocabItemWidget:initItemWidget() local right_side_width local right_widget if not self.show_parent.is_edit_mode and self.item.due_time <= os.time() then - right_side_width = review_button_width * 2 + Size.padding.large * 2 + ellipsis_button_width - + self.has_review_buttons = true + right_side_width = self.review_button_width * 2 + Size.padding.large * 2 + ellipsis_button_width self.forgot_button = Button:new{ text = _("Forgot"), - width = review_button_width, - max_width = review_button_width, + width = self.review_button_width, + max_width = self.review_button_width, radius = Size.radius.button, callback = function() self:onForgot() end, show_parent = self, - -- no_focus = true } self.got_it_button = Button:new{ @@ -509,12 +593,10 @@ function VocabItemWidget:initItemWidget() callback = function() self:onGotIt() end, - width = review_button_width, - max_width = review_button_width, + width = self.review_button_width, + max_width = self.review_button_width, show_parent = self, - -- no_focus = true } - right_widget = HorizontalGroup:new{ dimen = Geom:new{ w = 0, h = self.height }, self.margin_span, @@ -527,18 +609,14 @@ function VocabItemWidget:initItemWidget() table.insert(self.layout, self.got_it_button) table.insert(self.layout, self.more_button) else - local star = Button:new{ + self.has_review_buttons = false + local star = IconWidget:new{ icon = "check", - icon_width = star_width, - icon_height = star_width, - bordersize = 0, - radius = 0, - margin = 0, - show_parent = self, - enabled = false, - no_focus = true, + width = star_width, + height = star_width, + dim = true } - right_side_width = Size.padding.large * 3 + self.item.review_count * (star:getSize().w) + right_side_width = Size.padding.large * 4 + self.item.review_count * (star:getSize().w) if self.item.review_count > 0 then right_widget = HorizontalGroup:new { @@ -567,6 +645,20 @@ function VocabItemWidget:initItemWidget() fgcolor = subtitle_color } + local word_widget = Button:new{ + text = self.item.word, + bordersize = 0, + callback = function() self.item.callback(self.item) end, + padding = 0, + max_width = math.ceil(math.max(5,text_max_width - Size.padding.fullscreen)) + } + + if self.item.is_dim then + word_widget.label_widget.fgcolor = dim_color + end + + table.insert(self.layout, 1, word_widget) + self[1] = FrameContainer:new{ padding = 0, bordersize = 0, @@ -670,6 +762,8 @@ function VocabItemWidget:showMore() book_title = self.item.book_title, dates = _("Added on") .. " " .. os.date("%Y-%m-%d", self.item.create_time) .. " | " .. _("Review scheduled at") .. " " .. os.date("%Y-%m-%d %H:%M", self.item.due_time), + prev_context = self.item.prev_context, + next_context = self.item.next_context, remove_callback = function() self:remover() end, @@ -683,17 +777,48 @@ function VocabItemWidget:showMore() end function VocabItemWidget:onTap(_, ges) - if self.item.callback then - self.item.callback(self.item) + if self.has_review_buttons then + if ges.pos.x > self.forgot_button.dimen.x and ges.pos.x < self.forgot_button.dimen.x + self.forgot_button.dimen.w then + self:onForgot() + elseif ges.pos.x > self.got_it_button.dimen.x and ges.pos.x < self.got_it_button.dimen.x + self.got_it_button.dimen.w then + self:onGotIt() + elseif ges.pos.x > self.more_button.dimen.x and ges.pos.x < self.more_button.dimen.x + self.more_button.dimen.w then + if self.item.review_count < 6 then + self:showMore() + else + self:remover() + end + elseif self.item.callback then + self.item.callback(self.item) + end + else + if BD.mirroredUILayout() then + if ges.pos.x > self.more_button.dimen.x and ges.pos.x < self.more_button.dimen.x + self.more_button.dimen.w * 2 then + if self.show_parent.is_edit_mode or self.item.review_count >= 6 then + self:remover() + else + self:showMore() + end + elseif self.item.callback then + self.item.callback(self.item) + end + else + if ges.pos.x > self.more_button.dimen.x - self.more_button.dimen.w and ges.pos.x < self.more_button.dimen.x + self.more_button.dimen.w then + if self.show_parent.is_edit_mode or self.item.review_count >= 6 then + self:remover() + else + self:showMore() + end + elseif self.item.callback then + self.item.callback(self.item) + end + end end - return true end -function VocabItemWidget:onHold() - if self.item.callback then - self.item.callback(self.item) - end +function VocabItemWidget:onHold(_, ges) + self:onTap(_, ges) return true end @@ -755,6 +880,12 @@ function VocabularyBuilderWidget:init() range = self.dimen, } } + self.ges_events.MultiSwipe = { + GestureRange:new{ + ges = "multiswipe", + range = function() return self.dimen end, + } + } end local padding = Size.padding.large self.width_widget = self.dimen.w - 2 * padding @@ -871,6 +1002,16 @@ function VocabularyBuilderWidget:init() self:setupItemHeight() self.main_content = VerticalGroup:new{} + -- calculate item's review button width once + local temp_button = Button:new{ + text = _("Got it"), + padding_h = Size.padding.large + } + self.review_button_width = temp_button:getSize().w + temp_button:setText(_("Forgot")) + self.review_button_width = math.min(math.max(self.review_button_width, temp_button:getSize().w), Screen:getWidth()/4) + temp_button:free() + self:_populateItems() local frame_content = FrameContainer:new{ @@ -907,22 +1048,19 @@ function VocabularyBuilderWidget:setupItemHeight() self.items_per_page = math.floor(content_height / line_height) self.item_margin = self.item_margin + math.floor((content_height - self.items_per_page * line_height ) / self.items_per_page ) self.pages = math.ceil(DB:selectCount() / self.items_per_page) + self.show_page = math.min(self.pages, self.show_page) end function VocabularyBuilderWidget:nextPage() - local new_page = math.min(self.show_page+1, self.pages) - if new_page > self.show_page then - self.show_page = new_page - self:_populateItems() - end + local new_page = self.show_page == self.pages and 1 or self.show_page + 1 + self.show_page = new_page + self:_populateItems() end function VocabularyBuilderWidget:prevPage() - local new_page = math.max(self.show_page-1, 1) - if new_page < self.show_page then - self.show_page = new_page - self:_populateItems() - end + local new_page = self.show_page == 1 and self.pages or self.show_page - 1 + self.show_page = new_page + self:_populateItems() end function VocabularyBuilderWidget:goToPage(page) @@ -968,6 +1106,7 @@ function VocabularyBuilderWidget:_populateItems() local item = VocabItemWidget:new{ height = self.item_height, width = self.item_width, + review_button_width = self.review_button_width, item = self.item_table[idx], index = idx, show_parent = self, @@ -1094,6 +1233,14 @@ function VocabularyBuilderWidget:onSwipe(arg, ges_ev) end end +function VocabularyBuilderWidget:onMultiSwipe(arg, ges_ev) + -- For consistency with other fullscreen widgets where swipe south can't be + -- used to close and where we then allow any multiswipe to close, allow any + -- multiswipe to close this widget too. + self:onClose() + return true +end + function VocabularyBuilderWidget:onClose() DB:batchUpdateItems(self.item_table) UIManager:close(self) @@ -1124,9 +1271,8 @@ function VocabBuilder:init() end function VocabBuilder:addToMainMenu(menu_items) - menu_items.vocabulary_builder = { + menu_items.vocabbuilder = { text = _("Vocabulary builder"), - keep_menu_open = true, callback = function() local vocab_items = {} for i = 1, DB:selectCount() do @@ -1198,11 +1344,17 @@ end function VocabBuilder:onWordLookedUp(word, title) if not settings.enabled then return end if self.builder_widget and self.builder_widget.current_lookup_word == word then return true end - + local prev_context + local next_context + if settings.with_context then + prev_context, next_context = self.ui.highlight:getSelectedWordContext(15) + end DB:insertOrUpdate({ book_title = title, time = os.time(), - word = word + word = word, + prev_context = prev_context, + next_context = next_context }) return true end