From 30915546f04e7cc19dbbc48a2de775225db7480d Mon Sep 17 00:00:00 2001 From: weijiuqiao <59040746+weijiuqiao@users.noreply.github.com> Date: Sun, 31 Jul 2022 15:02:09 +0800 Subject: [PATCH] vocabbuilder.koplugin: always show more button, add book filtering (#9393) Always show more button instead of delete regardless of review count per #9383. Adds book filtering per #9256 (from menu or by swiping up). --- frontend/ui/widget/sortwidget.lua | 5 +- plugins/vocabbuilder.koplugin/db.lua | 92 +++++++---- plugins/vocabbuilder.koplugin/main.lua | 214 +++++++++++++++++-------- 3 files changed, 216 insertions(+), 95 deletions(-) diff --git a/frontend/ui/widget/sortwidget.lua b/frontend/ui/widget/sortwidget.lua index ef47500ce..42fea682a 100644 --- a/frontend/ui/widget/sortwidget.lua +++ b/frontend/ui/widget/sortwidget.lua @@ -93,10 +93,12 @@ function SortItemWidget:init() end function SortItemWidget:onTap(_, ges) - if self.item.checked_func and ges.pos:intersectWith(self.checkmark_widget.dimen) then + if self.item.checked_func and ( self.show_parent.sort_disabled or ges.pos:intersectWith(self.checkmark_widget.dimen) ) then if self.item.callback then self.item:callback() end + elseif self.show_parent.sort_disabled then + return true elseif self.show_parent.marked == self.index then self.show_parent.marked = 0 else @@ -123,6 +125,7 @@ local SortWidget = FocusManager:new{ -- table of items to sort item_table = nil, -- mandatory (array) callback = nil, + sort_disabled = false } function SortWidget:init() diff --git a/plugins/vocabbuilder.koplugin/db.lua b/plugins/vocabbuilder.koplugin/db.lua index bd97639d1..b93a005eb 100644 --- a/plugins/vocabbuilder.koplugin/db.lua +++ b/plugins/vocabbuilder.koplugin/db.lua @@ -5,7 +5,7 @@ local LuaData = require("luadata") local db_location = DataStorage:getSettingsDir() .. "/vocabulary_builder.sqlite3" -local DB_SCHEMA_VERSION = 20220608 +local DB_SCHEMA_VERSION = 20220730 local VOCABULARY_DB_SCHEMA = [[ -- To store looked up words CREATE TABLE IF NOT EXISTS "vocabulary" ( @@ -22,34 +22,25 @@ local VOCABULARY_DB_SCHEMA = [[ CREATE TABLE IF NOT EXISTS "title" ( "id" INTEGER NOT NULL UNIQUE, "name" TEXT UNIQUE, - PRIMARY KEY("id" AUTOINCREMENT) + "filter" INTEGER NOT NULL DEFAULT 1, + PRIMARY KEY("id") ); 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 = { - count = 0, -} +local VocabularyBuilder = {} function VocabularyBuilder:init() VocabularyBuilder:createDB() end -function VocabularyBuilder:hasItems() - if self.count > 0 then - return true - end - self.count = self:selectCount() - return self.count > 0 -end - function VocabularyBuilder:selectCount(conn) if conn then - return tonumber(conn:rowexec("SELECT count(0) FROM vocabulary;")) + return tonumber(conn:rowexec("SELECT count(0) FROM vocabulary INNER JOIN title ON filter=true AND title_id=id;")) else local db_conn = SQ3.open(db_location) - local count = tonumber(db_conn:rowexec("SELECT count(0) FROM vocabulary;")) + local count = tonumber(db_conn:rowexec("SELECT count(0) FROM vocabulary INNER JOIN title ON filter=true AND title_id=id;")) db_conn:close() return count end @@ -67,10 +58,12 @@ function VocabularyBuilder:createDB() db_conn:exec(VOCABULARY_DB_SCHEMA) -- Check version local db_version = tonumber(db_conn:rowexec("PRAGMA user_version;")) - if db_version < DB_SCHEMA_VERSION then - if db_version == 0 then - self:insertLookupData(db_conn) - elseif db_version < 20220608 then + if db_version == 0 then + self:insertLookupData(db_conn) + -- Update version + db_conn:exec(string.format("PRAGMA user_version=%d;", DB_SCHEMA_VERSION)) + elseif db_version < DB_SCHEMA_VERSION then + if 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; @@ -84,6 +77,9 @@ function VocabularyBuilder:createDB() ALTER TABLE vocabulary DROP book_title;]]) end + if db_version < 20220730 then + db_conn:exec("ALTER TABLE title ADD filter INTEGER NOT NULL DEFAULT 1;") + end db_conn:exec("CREATE INDEX IF NOT EXISTS title_id_index ON vocabulary(title_id);") -- Update version @@ -113,13 +109,13 @@ function VocabularyBuilder:insertLookupData(db_conn) local words = {} local insert_sql = [[INSERT OR REPLACE INTO vocabulary - (word, title_id, create_time, due_time) values - (?, (SELECT id FROM title WHERE name = ?), ?, ?);]] + (word, title_id, create_time, due_time, review_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 - stmt:bind(value.word, value.book_title or "", value.time, value.time + 5*60) + stmt:bind(value.word, value.book_title or "", value.time, value.time + 5*60, value.time) stmt:step() stmt:clearbind():reset() words[value.word] = true @@ -131,7 +127,7 @@ end function VocabularyBuilder:_select_items(items, start_idx) local conn = SQ3.open(db_location) - 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 sql = string.format("SELECT * FROM vocabulary INNER JOIN title ON title_id = title.id AND filter = true ORDER BY due_time limit %d OFFSET %d;", 32, start_idx-1) local results = conn:exec(sql) conn:close() @@ -143,7 +139,7 @@ function VocabularyBuilder:_select_items(items, start_idx) local item = items[start_idx+i-1] 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.review_count = math.max(0, tonumber(results.review_count[i])) 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 @@ -188,7 +184,7 @@ function VocabularyBuilder:gotOrForgot(item, isGot) local current_time = os.time() local due_time - local target_count = math.min(math.max(item.review_count + (isGot and 1 or -1), 0), 8) + local target_count = math.max(item.review_count + (isGot and 1 or -1), 0) if not isGot or target_count == 0 then due_time = current_time + 5 * 60 elseif target_count == 1 then @@ -243,22 +239,58 @@ function VocabularyBuilder:insertOrUpdate(entry) 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 = ?), ?, ?, ?, ?) + stmt = conn:prepare([[INSERT INTO vocabulary (word, title_id, create_time, due_time, review_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 = ?, 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, + stmt:bind(entry.word, entry.book_title, entry.time, entry.time+300, entry.time, entry.prev_context, entry.next_context, entry.time+300) stmt:step() stmt:clearbind():reset() - self.count = tonumber(conn:rowexec("SELECT count(0) from vocabulary;")) conn:close() end +function VocabularyBuilder:toggleBookFilter(ids) + local id_string = "" + for key, _ in pairs(ids) do + id_string = id_string .. (id_string == "" and "" or ",") .. key + end + local conn = SQ3.open(db_location) + conn:exec("UPDATE title SET filter = (filter | 1) - (filter & 1) WHERE id in ("..id_string..");") + conn:close() +end + +function VocabularyBuilder:selectBooks() + local conn = SQ3.open(db_location) + local sql = string.format("SELECT * FROM title") + + local results = conn:exec(sql) + conn:close() + + local items = {} + if not results then return items end + + for i = 1, #results.id do + table.insert(items, { + id = tonumber(results.id[i]), + name = results.name[i], + filter = tonumber(results.filter[i]) ~= 0 + }) + end + return items +end + +function VocabularyBuilder:hasFilteredBook() + local conn = SQ3.open(db_location) + local has_filter = tonumber(conn:rowexec("SELECT count(0) FROM title WHERE filter = false limit 1;")) + conn:close() + return has_filter ~= 0 +end + function VocabularyBuilder:remove(item) local conn = SQ3.open(db_location) local stmt = conn:prepare("DELETE FROM vocabulary WHERE word = ? ;") @@ -266,7 +298,6 @@ function VocabularyBuilder:remove(item) stmt:step() stmt:clearbind():reset() - self.count = self.count - 1 conn:close() end @@ -280,7 +311,6 @@ end function VocabularyBuilder:purge() local conn = SQ3.open(db_location) 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 0ea8673d1..e8bf0de9e 100644 --- a/plugins/vocabbuilder.koplugin/main.lua +++ b/plugins/vocabbuilder.koplugin/main.lua @@ -32,6 +32,7 @@ local RightContainer = require("ui/widget/container/rightcontainer") local OverlapGroup = require("ui/widget/overlapgroup") local Screen = Device.screen local Size = require("ui/size") +local SortWidget = require("ui/widget/sortwidget") local TextWidget = require("ui/widget/textwidget") local TextBoxWidget = require("ui/widget/textboxwidget") local TitleBar = require("ui/widget/titlebar") @@ -52,6 +53,45 @@ local subtitle_color = Blitbuffer.COLOR_DARK_GRAY local dim_color = Blitbuffer.Color8(0x22) local settings = G_reader_settings:readSetting("vocabulary_builder", {enabled = true}) + +local function onShowFilter(widget) + local sort_items = {} + local book_data = DB:selectBooks() + local toggled = {} + for _, ifo in pairs(book_data) do + table.insert(sort_items, { + text = ifo.name or "", + callback = function() + ifo.filter = not ifo.filter + if toggled[ifo.id] then + toggled[ifo.id] = nil + else + toggled[ifo.id] = true + end + end, + checked_func = function() + return ifo.filter + end, + ifo = ifo, + }) + end + + local sort_widget = SortWidget:new{ + title = _("Filter words from books"), + item_table = sort_items, + sort_disabled = true, + callback = function() + if #toggled then + DB:toggleBookFilter(toggled) + widget:reloadItems() + end + + UIManager:setDirty(nil, "ui") + end + } + UIManager:show(sort_widget) +end + --[[-- Menu dialogue widget --]]-- @@ -134,6 +174,14 @@ function MenuDialog:init() self.context_switch:setPosition(settings.with_context and 2 or 1) self:mergeLayoutInVertical(self.context_switch) + local filter_button = { + text = _("Filter books"), + callback = function() + self:onClose() + onShowFilter(self.show_parent) + end + } + local edit_button = { text = self.is_edit_mode and _("Resume") or _("Quick deletion"), callback = function() @@ -175,6 +223,7 @@ function MenuDialog:init() local buttons = ButtonTable:new{ width = width, buttons = { + {filter_button}, {edit_button}, {reset_button}, {clean_button} @@ -287,7 +336,6 @@ function MenuDialog:onConfigChoose(values, name, event, args, position) end) end - --[[-- Individual word info dialogue widget --]]-- @@ -549,7 +597,7 @@ end function VocabItemWidget:initItemWidget() for i = 1, #self.layout do self.layout[i] = nil end - if not self.show_parent.is_edit_mode and self.item.review_count < 6 then + if not self.show_parent.is_edit_mode then self.more_button = Button:new{ text = (self.item.prev_context or self.item.next_context) and "⋯" or "⋮", padding = Size.padding.button, @@ -616,9 +664,32 @@ function VocabItemWidget:initItemWidget() height = star_width, dim = true } - right_side_width = Size.padding.large * 4 + self.item.review_count * (star:getSize().w) - if self.item.review_count > 0 then + if self.item.review_count > 6 then + right_side_width = Size.padding.large * 4 + 9 * (star:getSize().w) + right_widget = HorizontalGroup:new { + dimen = Geom:new{w=0, h = self.height} + } + for i=1, 6, 1 do + table.insert(right_widget, star) + end + table.insert(right_widget, + TextWidget:new { + text = " + ", + face = word_face, + fgcolor = Blitbuffer.COLOR_DARK_GRAY + } + ) + table.insert(right_widget, star) + table.insert(right_widget, + TextWidget:new { + text = "× " .. self.item.review_count-6, + face = word_face, + fgcolor = Blitbuffer.COLOR_DARK_GRAY + } + ) + elseif self.item.review_count > 0 then + right_side_width = Size.padding.large * 4 + self.item.review_count * (star:getSize().w) right_widget = HorizontalGroup:new { dimen = Geom:new{w=0, h = self.height} } @@ -627,6 +698,7 @@ function VocabItemWidget:initItemWidget() end else star:free() + right_side_width = Size.padding.large * 4 right_widget = HorizontalGroup:new{ dimen = Geom:new{w=0, h = self.height}, HorizontalSpan:new {width = Size.padding.default } @@ -711,7 +783,6 @@ function VocabItemWidget:initItemWidget() end function VocabItemWidget:getTimeSinceDue() - if self.item.review_count >= 8 then return "" end local elapsed = os.time() - self.item.due_time local abs = math.abs(elapsed) @@ -781,18 +852,14 @@ function VocabItemWidget:onTap(_, ges) 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 + self:showMore() 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 + if self.show_parent.is_edit_mode then self:remover() else self:showMore() @@ -802,7 +869,7 @@ function VocabItemWidget:onTap(_, ges) 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 + if self.show_parent.is_edit_mode then self:remover() else self:showMore() @@ -1045,8 +1112,8 @@ function VocabularyBuilderWidget:setupItemHeight() local content_height = self.dimen.h - self.title_bar:getHeight() - self.footer_height - Size.padding.large 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) + self.pages = math.ceil(#self.item_table / self.items_per_page) + self.show_page = math.max(1, math.min(self.pages, self.show_page)) end function VocabularyBuilderWidget:nextPage() @@ -1115,6 +1182,9 @@ function VocabularyBuilderWidget:_populateItems() item ) end + if #self.main_content == 0 then + table.insert(self.main_content, HorizontalSpan:new{width = self.item_width}) + end self.footer_page:setText(T(_("Page %1 of %2"), self.show_page, self.pages), self.footer_center_width) if self.pages > 1 then self.footer_page:enable() @@ -1122,11 +1192,17 @@ function VocabularyBuilderWidget:_populateItems() self.footer_page:disableWithoutDimming() end if self.pages == 0 then - self.footer_page:setText(_("No items"), self.footer_center_width) + local has_filtered_book = DB:hasFilteredBook() + self.footer_page:setText(has_filtered_book and _("Filter in effect") or _("No items"), self.footer_center_width) self.footer_first_up:hide() self.footer_last_down:hide() self.footer_left:hide() self.footer_right:hide() + elseif self.footer_left.hidden then + self.footer_first_up:show() + self.footer_last_down:show() + self.footer_left:show() + self.footer_right:show() end local chevron_first = "chevron.first" local chevron_last = "chevron.last" @@ -1193,9 +1269,17 @@ function VocabularyBuilderWidget:showMenu() reset_callback = function() self:resetItems() end, + show_parent = self }) end +function VocabularyBuilderWidget:reloadItems() + DB:batchUpdateItems(self.item_table) + self.item_table = self.reload_items_callback() + self.pages = math.ceil(#self.item_table / self.items_per_page) + self:goToPage(1) +end + function VocabularyBuilderWidget:onShow() UIManager:setDirty(self, "flashui") end @@ -1220,8 +1304,8 @@ function VocabularyBuilderWidget:onSwipe(arg, ges_ev) -- Allow easier closing with swipe down self:onClose() elseif direction == "north" then - -- no use for now - do end -- luacheck: ignore 541 + -- open filter + onShowFilter(self) else -- diagonal swipe -- trigger full refresh UIManager:setDirty(nil, "full") @@ -1272,65 +1356,69 @@ function VocabBuilder:addToMainMenu(menu_items) menu_items.vocabbuilder = { text = _("Vocabulary builder"), callback = function() - local vocab_items = {} - for i = 1, DB:selectCount() do - table.insert(vocab_items, { - callback = function(item) - -- custom button table - local tweak_buttons_func - if item.due_time <= os.time() then - tweak_buttons_func = function(buttons) - local tweaked_button_count = 0 - local early_break - for j = 1, #buttons do - for k = 1, #buttons[j] do - if buttons[j][k].id == "highlight" and not buttons[j][k].enabled then - buttons[j][k] = { - id = "got_it", - text = _("Got it"), - callback = function() - self.builder_widget:gotItFromDict(item.word) - UIManager:sendEvent(Event:new("Close")) + local reload_items = function() + local vocab_items = {} + for i = 1, DB:selectCount() do + table.insert(vocab_items, { + callback = function(item) + -- custom button table + local tweak_buttons_func + if item.due_time <= os.time() then + tweak_buttons_func = function(buttons) + local tweaked_button_count = 0 + local early_break + for j = 1, #buttons do + for k = 1, #buttons[j] do + if buttons[j][k].id == "highlight" and not buttons[j][k].enabled then + buttons[j][k] = { + id = "got_it", + text = _("Got it"), + callback = function() + self.builder_widget:gotItFromDict(item.word) + UIManager:sendEvent(Event:new("Close")) + end + } + if tweaked_button_count == 1 then + early_break = true + break end - } - if tweaked_button_count == 1 then - early_break = true - break - end - tweaked_button_count = tweaked_button_count + 1 - elseif buttons[j][k].id == "search" and not buttons[j][k].enabled then - buttons[j][k] = { - id = "forgot", - text = _("Forgot"), - callback = function() - self.builder_widget:forgotFromDict(item.word) - UIManager:sendEvent(Event:new("Close")) + tweaked_button_count = tweaked_button_count + 1 + elseif buttons[j][k].id == "search" and not buttons[j][k].enabled then + buttons[j][k] = { + id = "forgot", + text = _("Forgot"), + callback = function() + self.builder_widget:forgotFromDict(item.word) + UIManager:sendEvent(Event:new("Close")) + end + } + if tweaked_button_count == 1 then + early_break = true + break end - } - if tweaked_button_count == 1 then - early_break = true - break + tweaked_button_count = tweaked_button_count + 1 end - tweaked_button_count = tweaked_button_count + 1 end + if early_break then break end end - if early_break then break end end end - end - self.builder_widget.current_lookup_word = item.word - self.ui:handleEvent(Event:new("LookupWord", item.word, true, nil, nil, nil, tweak_buttons_func)) - end - }) + self.builder_widget.current_lookup_word = item.word + self.ui:handleEvent(Event:new("LookupWord", item.word, true, nil, nil, nil, tweak_buttons_func)) + end + }) + end + return vocab_items end self.builder_widget = VocabularyBuilderWidget:new{ title = _("Vocabulary builder"), - item_table = vocab_items, + item_table = reload_items(), select_items_callback = function(items, start_idx, end_idx) DB:select_items(items, start_idx, end_idx) - end + end, + reload_items_callback = reload_items } UIManager:show(self.builder_widget) @@ -1344,7 +1432,7 @@ function VocabBuilder:onWordLookedUp(word, title) 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 + if settings.with_context and self.ui.highlight then prev_context, next_context = self.ui.highlight:getSelectedWordContext(15) end DB:insertOrUpdate({