├── README.md ├── 2-progress-bar-twins.lua ├── 2-mini-receipt-frankenpatch.lua └── 2-cvs-receipt-frankenpatch.lua /README.md: -------------------------------------------------------------------------------- 1 | # koreader-frankenpatches 2 | "frankenpatches" because every little coding experiment i take on is a frankenstein mix of borrowed code, gpt code and some of my own. :D 3 | 4 | # cvs receipt 5 | ![cvs receipt](https://github.com/user-attachments/assets/ec9cebc3-1a03-4bb7-ad8d-b1f25fc5a6da) 6 | 7 | a little box that you can summon with a corner tap (or any other gesture) from reader view. shows your chapter progress, book progress and time left in each. i made this because i wanted to keep my reader view squeaky clean but also easily see how far into the book i am. 8 | 9 | INSTALLATION: drop the .lua into the koreader/patches folder. set up a gesture to trigger the 'cvs receipt' action in 'general' category. 10 | 11 | # mini receipt 12 | Reader_gerald durrell - my family and other animals epub_p_12_2025-12-18_174637 13 | 14 | made this because at one point i found cvs receipt to be a little too big and cluttered. 15 | 16 | INSTALLATION: drop the .lua into the koreader/patches folder. set up a gesture to trigger the 'cvs receipt' action in 'general' category. 17 | 18 | # progress bar twins 19 | ![twins](https://github.com/user-attachments/assets/d4bd1992-8928-40b1-b30c-6fda269f5115) 20 | 21 | two progress bars side by side. you can choose what progress (chapter or book) to show on each. you can even join them together and mirror one of them so that they essentially look like one regular progress bar that fills from the middle towards the edges. 22 | 23 | INSTALLATION: drop the .lua into the koreader/patches folder. you'll find instructions to customise the patch inside the file. 24 | 25 | 26 | -------------------------------------------------------------------------------- /2-progress-bar-twins.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | i found that chapter ticks on a regular progress bar wasn't for me. i wanted a 4 | separate progress bar for chapter that also didn't clutter up the ui. so i made this. 5 | 6 | this patch joins two half-sized progress bars end to end 7 | to make them look like one regular progress bar. 8 | 9 | you can choose what progress (chapter/book) to show on either side and there's 10 | also an option to 'mirror' the progress bars. for eg., chapter progress on both sides 11 | plus mirrored = ON and gap = 0 will essentially give you one regular size progress bar 12 | that fills from the centre towards the edges. 13 | 14 | you should probably disable the default koreader progress bar and definitely 15 | uncheck 'auto refresh items' from status bar settings for this to work properly. 16 | 17 | this patch works really well on my kindle 4 and kindle basic 2019. 18 | 19 | you'll find instructions to configure this patch if you scroll down. 20 | 21 | happy reading! =) 22 | 23 | CREDITS: some outline code for this was borrowed from a user patch made by 24 | joshua cantara. (https://github.com/joshuacant/KOReader.patches) 25 | 26 | ]]-- 27 | 28 | local Blitbuffer = require("ffi/blitbuffer") 29 | local Device = require("device") 30 | local Size = require("ui/size") 31 | local Screen = Device.screen 32 | local ReaderView = require("apps/reader/modules/readerview") 33 | local _ReaderView_paintTo_orig = ReaderView.paintTo 34 | local screen_width = Screen:getWidth() 35 | local screen_height = Screen:getHeight() 36 | local ProgressWidget = require("ui/widget/progresswidget") 37 | local UIManager = require("ui/uimanager") 38 | 39 | ReaderView.paintTo = function(self, bb, x, y) 40 | _ReaderView_paintTo_orig(self, bb, x, y) 41 | if self.render_mode ~= nil then return end -- Show only for epub-likes and never on pdf-likes 42 | 43 | -- book info 44 | local pageno = self.state.page or 1 -- Current page 45 | local pages = self.ui.doc_settings.data.doc_pages or 1 46 | local pages_left_book = pages - pageno 47 | -- chapter info 48 | local pages_chapter = self.ui.toc:getChapterPageCount(pageno) or pages 49 | local pages_left = self.ui.toc:getChapterPagesLeft(pageno) or self.ui.document:getTotalPagesLeft(pageno) 50 | local pages_done = self.ui.toc:getChapterPagesDone(pageno) or 0 51 | pages_done = pages_done + 1 -- This +1 is to include the page you're looking at. 52 | local BOOK_MARGIN = self.document:getPageMargins().left 53 | local CHAPTER = 0 54 | local BOOK = 1 55 | local ON = true 56 | local OFF = false 57 | 58 | 59 | 60 | ------------------------------------------------------- 61 | -- adjust the values below to configure progress bars. 62 | ------------------------------------------------------- 63 | 64 | local prog_bar_height = 2 -- progress bar height. 65 | local bottom_padding = 35 -- space b/w progress bars and bottom edge. 66 | local margin = BOOK_MARGIN -- use BOOK_MARGIN or any numeric value. 67 | local gap = 0 -- gap between progress bars. 68 | local radius = 0 -- make the ends a little round. 69 | local mirrored = ON -- mirrored progress bars ON or OFF. 70 | local left_bar_type = CHAPTER -- set this to CHAPTER or BOOK 71 | local right_bar_type = CHAPTER -- set this to CHAPTER or BOOK 72 | 73 | ------------------------------------------------------ 74 | -- you don't have to change anything below this line. 75 | ------------------------------------------------------ 76 | 77 | 78 | 79 | screen_width = Screen:getWidth() 80 | screen_height = Screen:getHeight() 81 | 82 | local chapter_percentage = pages_done/pages_chapter 83 | local inv_chapter_percentage = pages_left/pages_chapter 84 | local book_percentage = pageno/pages 85 | local inv_book_percentage = pages_left_book/pages 86 | 87 | local left_bar_percentage 88 | local left_bar_inv_percentage 89 | local left_bar_fill_color 90 | local left_bar_bg_color 91 | local right_bar_percentage 92 | 93 | local pblack = Blitbuffer.COLOR_GRAY_4 94 | local pgray = Blitbuffer.COLOR_GRAY 95 | 96 | local prog_bar_width = (screen_width - gap - margin*2) / 2 97 | local prog_bar_y = screen_height - prog_bar_height - bottom_padding 98 | 99 | -- left bar 100 | 101 | if left_bar_type == 0 then 102 | left_bar_percentage = chapter_percentage 103 | left_bar_inv_percentage = inv_chapter_percentage 104 | else 105 | left_bar_percentage = book_percentage 106 | left_bar_inv_percentage = inv_book_percentage 107 | end 108 | 109 | if mirrored then 110 | left_bar_percentage = left_bar_inv_percentage 111 | left_bar_fill_color = pgray 112 | left_bar_bg_color = pblack 113 | else 114 | left_bar_fill_color = pblack 115 | left_bar_bg_color = pgray 116 | end 117 | 118 | local left_bar = ProgressWidget:new{ 119 | width = prog_bar_width, 120 | height = prog_bar_height, 121 | percentage = left_bar_percentage, 122 | margin_v = 0, 123 | margin_h = 0, 124 | radius = radius, 125 | bordersize = 0, 126 | fillcolor = left_bar_fill_color, 127 | bgcolor = left_bar_bg_color, 128 | } 129 | 130 | -- right bar 131 | 132 | if right_bar_type == 0 then 133 | right_bar_percentage = chapter_percentage 134 | else 135 | right_bar_percentage = book_percentage 136 | end 137 | 138 | local right_bar = ProgressWidget:new{ 139 | width = prog_bar_width, 140 | height = prog_bar_height, 141 | percentage = right_bar_percentage, 142 | margin_v = 0, 143 | margin_h = 0, 144 | radius = radius, 145 | bordersize = 0, 146 | fillcolor = pblack, 147 | bgcolor = pgray, 148 | } 149 | local right_bar_x = Screen:getWidth()/ 2 + gap / 2 150 | 151 | left_bar:paintTo(bb, margin, prog_bar_y) 152 | right_bar:paintTo(bb, right_bar_x, prog_bar_y) 153 | 154 | end 155 | -------------------------------------------------------------------------------- /2-mini-receipt-frankenpatch.lua: -------------------------------------------------------------------------------- 1 | --[[ mini receipt v1.0 #6 ]] 2 | -- help text 3 | 4 | local Blitbuffer = require("ffi/blitbuffer") 5 | local CenterContainer = require("ui/widget/container/centercontainer") 6 | local datetime = require("datetime") 7 | local DataStorage = require("datastorage") 8 | local db_location = DataStorage:getSettingsDir() .. "/statistics.sqlite3" 9 | local Device = require("device") 10 | local Dispatcher = require("dispatcher") 11 | local Event = require("ui/event") 12 | local FileManager = require("apps/filemanager/filemanager") 13 | local Font = require("ui/font") 14 | local FrameContainer = require("ui/widget/container/framecontainer") 15 | local Geom = require("ui/geometry") 16 | local GestureRange = require("ui/gesturerange") 17 | local HorizontalGroup = require("ui/widget/horizontalgroup") 18 | local HorizontalSpan = require("ui/widget/horizontalspan") 19 | local InputContainer = require("ui/widget/container/inputcontainer") 20 | local LineWidget = require("ui/widget/linewidget") 21 | local ReaderFooter = require("apps/reader/modules/readerfooter") 22 | local ReaderUI = require("apps/reader/readerui") 23 | local ReaderView = require("apps/reader/modules/readerview") 24 | local Screen = Device.screen 25 | local Size = require("ui/size") 26 | local SQ3 = require("lua-ljsqlite3/init") 27 | local T = require("ffi/util").template 28 | local TextWidget = require("ui/widget/textwidget") 29 | local TextBoxWidget = require("ui/widget/textboxwidget") 30 | local UIManager = require("ui/uimanager") 31 | local util = require("util") 32 | local VerticalGroup = require("ui/widget/verticalgroup") 33 | local VerticalSpan = require("ui/widget/verticalspan") 34 | local Widget = require("ui/widget/widget") 35 | local _ = require("gettext") 36 | local N_ = _.ngettext 37 | 38 | -- SWITCHES 39 | local bookCompleted = false 40 | local showBookCompleteWindow, altFontEnabled, brtAuthorsEnabled = false, false, false 41 | if G_reader_settings and G_reader_settings.isTrue then 42 | showBookCompleteWindow = G_reader_settings:isTrue("cvs_rct_book_complete_window") 43 | altFontEnabled = G_reader_settings:isTrue("cvs_rct_altFont") 44 | brtAuthorsEnabled = G_reader_settings:isTrue("cvs_rct_brtAuthors") 45 | end 46 | 47 | -- ADD TO MENU 48 | 49 | local cvsMenu = { 50 | text = _("mini receipt"), 51 | sorting_hint = "tools", 52 | sub_item_table = { 53 | { 54 | text = _("show 'book complete' window"), 55 | help_text = _("a little window that pops up when you flip past the last page of a book. shows time read, starting date and highlight count for current book.\n\n(set Menu>gear icon>Document>End of document action to 'Do nothing' for best results)."), 56 | checked_func = function() 57 | return G_reader_settings:isTrue("cvs_rct_book_complete_window") 58 | end, 59 | callback = function() 60 | G_reader_settings:flipNilOrFalse("cvs_rct_book_complete_window") 61 | showBookCompleteWindow = G_reader_settings:isTrue("cvs_rct_book_complete_window") 62 | end, 63 | }, 64 | { 65 | text = _("show author(s) in 'books read today' window."), 66 | help_text = _("shows or hides authors in 'book complete' window."), 67 | checked_func = function() 68 | return G_reader_settings:isTrue("cvs_rct_brtAuthors") 69 | end, 70 | callback = function() 71 | G_reader_settings:flipNilOrFalse("cvs_rct_brtAuthors") 72 | brtAuthorsEnabled = G_reader_settings:isTrue("cvs_rct_brtAuthors") 73 | end, 74 | }, 75 | { 76 | text = _("serif font"), 77 | help_text = _("toggles between NotoSans and NotoSerif fonts."), 78 | checked_func = function() 79 | return G_reader_settings:isTrue("cvs_rct_altFont") 80 | end, 81 | callback = function() 82 | G_reader_settings:flipNilOrFalse("cvs_rct_altFont") 83 | altFontEnabled = G_reader_settings:isTrue("cvs_rct_altFont") 84 | end, 85 | }, 86 | }, 87 | } 88 | 89 | if not ReaderFooter._cvs_receipt_hooked then 90 | local orig_ReaderFooter_addToMainMenu_cvs = ReaderFooter.addToMainMenu 91 | ReaderFooter.addToMainMenu = function(self, menu_items) 92 | if orig_ReaderFooter_addToMainMenu_cvs then 93 | orig_ReaderFooter_addToMainMenu_cvs(self, menu_items) 94 | end 95 | menu_items.cvs_rct_menu = cvsMenu 96 | end 97 | ReaderFooter._cvs_receipt_hooked = true 98 | end 99 | 100 | local quicklookwindow = 101 | InputContainer:extend { 102 | modal = true, 103 | name = "quick_look_window" 104 | } 105 | 106 | function quicklookwindow:init() 107 | 108 | local ReaderStatistics = self.ui.statistics 109 | local statsEnabled = ReaderStatistics and ReaderStatistics.settings and ReaderStatistics.settings.is_enabled 110 | local ReaderToc = self.ui.toc 111 | 112 | -- BOOK INFO 113 | 114 | local book_title = "" 115 | local book_author = "" 116 | if self.ui.doc_props then 117 | book_title = self.ui.doc_props.display_title or "" 118 | book_author = self.ui.doc_props.authors or "" 119 | if book_author:find("\n") then -- Show first author if multiple authors 120 | book_author = T(_("%1 et al."), util.splitToArray(book_author, "\n")[1] .. ",") 121 | end 122 | end 123 | 124 | -- PAGE COUNT AND BOOK PERCENTAGE 125 | 126 | local book_page = 0 127 | local book_total = 0 128 | local book_left = 0 129 | local book_percentage = 0 130 | if self.ui.document then 131 | book_page = self.state.page or 1 -- Current page 132 | book_total = self.ui.doc_settings.data.doc_pages or 1 133 | book_left = book_total - book_page 134 | book_percentage = (book_page / book_total) * 100 -- Format like %.1f in header_string below 135 | end 136 | 137 | -- CHAPTER INFO 138 | 139 | local chapter_title = "" 140 | local chapter_total = 0 141 | local chapter_left = 0 142 | local chapter_page = 0 143 | if ReaderToc then 144 | chapter_title = ReaderToc:getTocTitleByPage(book_page) or "" -- Chapter name 145 | chapter_page = ReaderToc:getChapterPagesDone(book_page) or 0 146 | chapter_page = chapter_page + 1 -- This +1 is to include the page you're looking at 147 | chapter_total = ReaderToc:getChapterPageCount(book_page) or book_total 148 | chapter_left = ReaderToc:getChapterPagesLeft(book_page) or book_left 149 | end 150 | 151 | -- BOOK PAGE TURNS (cuz everything gets reassigned with stable pages), 152 | 153 | local book_pageturn = book_page 154 | local book_pageturn_total = book_total 155 | local book_pageturn_left = book_left 156 | 157 | -- STABLE PAGES 158 | 159 | if self.ui.pagemap and self.ui.pagemap:wantsPageLabels() then 160 | book_page = self.ui.pagemap:getCurrentPageLabel(true) -- these two are strings. 161 | book_total = self.ui.pagemap:getLastPageLabel(true) 162 | end 163 | 164 | -- CLOCK 165 | 166 | local current_time = datetime.secondsToHour(os.time(), G_reader_settings:isTrue("twelve_hour_clock")) or "" 167 | 168 | -- BATTERY 169 | 170 | local battery = "" 171 | if Device:hasBattery() then 172 | local power_dev = Device:getPowerDevice() 173 | local batt_lvl = power_dev:getCapacity() or 0 174 | local is_charging = power_dev:isCharging() or false 175 | local batt_prefix = power_dev:getBatterySymbol(power_dev:isCharged(), is_charging, batt_lvl) or "" 176 | battery = batt_prefix .. batt_lvl .. "%" 177 | end 178 | 179 | local screen_width = Screen:getWidth() 180 | local screen_height = Screen:getHeight() 181 | local w_width = math.floor(screen_width / 2) 182 | if screen_width > screen_height then 183 | w_width = math.floor(w_width * screen_height / screen_width) 184 | end 185 | 186 | -- FONT AND PADDING 187 | local mainFontFace = altFontEnabled and "NotoSerif" or "NotoSans" 188 | local w_font = { 189 | face = { 190 | reg = mainFontFace .. "-Regular.ttf", 191 | bold = mainFontFace .. "-Bold.ttf", 192 | it = mainFontFace .. "-Italic.ttf", 193 | boldit = mainFontFace .. "-BoldItalic.ttf" 194 | }, 195 | size = {big = 25, med = 18, small = 15, tiny = 13}, 196 | color = { 197 | black = Blitbuffer.COLOR_BLACK, 198 | darkGray = Blitbuffer.COLOR_GRAY_1, 199 | lightGray = Blitbuffer.COLOR_GRAY_4 200 | } 201 | } 202 | 203 | local w_padding = { 204 | internal = Screen:scaleBySize(7), 205 | external = Screen:scaleBySize(16) 206 | } -- ext: between frame and widgets, int: verticalspace bw widgets (12) 207 | 208 | -- HELPER FUNCTIONS 209 | 210 | local function secsToTimestring(secs, isCompact) -- seconds to 'x hrs y mins' format 211 | local timestring = "" 212 | 213 | local h = math.floor(secs / 3600) 214 | local m = math.floor((secs % 3600) / 60) 215 | 216 | if (h == 0) and (m < 1) then m = 1 end 217 | 218 | local h_str, m_str = "", "" 219 | if isCompact then 220 | h_str = T(_("%1h"), h) 221 | m_str = T(_("%1m"), m) 222 | else 223 | h_str = T(N_("1 hr", "%1 hrs", h), h) 224 | m_str = T(N_("1 min", "%1 mins", m), m) 225 | end 226 | 227 | if h >= 1 then timestring = timestring .. h_str .. " " end 228 | if m >= 1 then timestring = timestring .. m_str .. " " end 229 | timestring = timestring:sub(1, -2) -- remove the last space 230 | 231 | 232 | return timestring 233 | end 234 | 235 | local function vertical_spacing(h) -- vertical space eq. to h*w_padding.internal 236 | if h == nil then 237 | h = 1 238 | end 239 | local s = VerticalSpan:new {width = math.floor(w_padding.internal * h)} 240 | return s 241 | end 242 | 243 | local function textt(txt, tfont, tsize, tclr, tpadding) -- creates TextWidget 244 | if not tclr then tclr = w_font.color.black end 245 | 246 | local w = 247 | TextWidget:new { 248 | text = txt, 249 | face = Font:getFace(tfont, tsize), 250 | fgcolor = tclr, 251 | bold = false, 252 | padding = tpadding or Screen:scaleBySize(2) 253 | } 254 | return w 255 | end 256 | 257 | local function getWidth(text, face, size) -- text width 258 | local t = textt(text, face, size) 259 | local width = t:getSize().w 260 | t:free() 261 | return width 262 | end 263 | 264 | local function textboxx(txt, tfont, tsize, tclr, twidth, tbold, alignmt) -- creates TextBoxWidget 265 | if not tclr then tclr = w_font.color.black end 266 | if not tbold then tbold = false end 267 | if not alignmt then alignmt = "center" end 268 | local w = 269 | TextBoxWidget:new { 270 | text = txt, 271 | face = Font:getFace(tfont, tsize), 272 | fgcolor = tclr, 273 | bold = tbold, 274 | width = twidth, 275 | alignment = alignmt, 276 | line_height = line_ht 277 | } 278 | return w 279 | end 280 | 281 | -- CURRENT TIMESTAMP / MIDNIGHT TIMESTAMP 282 | 283 | local secsInOneDay = 3600 * 24 284 | local ts_now = os.time() 285 | local t = os.date("*t", ts_now) 286 | t.hour = 0 287 | t.min = 0 288 | t.sec = 0 289 | local ts_midnight_today = os.time(t) 290 | 291 | --============================================ 292 | --'QUICK LOOK' WINDOW 293 | --============================================ 294 | 295 | local function buildQuickLookWindow() 296 | 297 | -- FILL ALL THE VARIABLES 298 | 299 | -- we manually calculate chapter page, chapter total and chapter left in terms of pageturns. 300 | -- this is because we want progress % to update after every PAGETURN (because that feels more 301 | -- organic) as opposed to having it update every STABLEPAGE (one single stablepage might spread 302 | -- across multiple pageturns). 303 | 304 | local chapter_pgturn, chapter_pgturn_left, chapter_pgturn_total = 0, 0, 0 305 | local nextChapterTickPgturn, previousChapterTickPgturn = 0, 0 306 | if self.ui.pagemap and self.ui.pagemap:wantsPageLabels() and ReaderToc then -- if stable pages are ON and toc is available 307 | nextChapterTickPgturn = ReaderToc:getNextChapter(book_pageturn) or (book_pageturn_total + 1) 308 | previousChapterTickPgturn = ReaderToc:getPreviousChapter(book_pageturn) or 1 309 | if book_pageturn == 1 or ReaderToc:isChapterStart(book_pageturn) then previousChapterTickPgturn = book_pageturn end 310 | chapter_pgturn = book_pageturn - previousChapterTickPgturn +1 311 | chapter_pgturn_total = nextChapterTickPgturn - previousChapterTickPgturn 312 | chapter_pgturn_left = nextChapterTickPgturn - book_pageturn - 1 313 | else 314 | chapter_pgturn = chapter_page 315 | chapter_pgturn_total = chapter_total 316 | chapter_pgturn_left = chapter_left 317 | end 318 | 319 | -- progress percentages 320 | 321 | local prog_pct_book = math.floor((book_pageturn / book_pageturn_total) * 100) 322 | local prog_pct_chapter = math.floor((chapter_pgturn / chapter_pgturn_total) * 100) 323 | 324 | -- time read today / pages read today 325 | 326 | local timeReadToday, pagesReadToday = 0, 0 327 | local timeReadToday_str, pagesReadToday_str = "", "" 328 | if statsEnabled and self.ui.document and not bookCompleted then 329 | timeReadToday, pagesReadToday = ReaderStatistics:getTodayBookStats() -- stats for today across all books 330 | timeReadToday_str = secsToTimestring(timeReadToday, true) 331 | pagesReadToday_str = T(N_("1 pg", "%1 pgs", pagesReadToday), pagesReadToday) 332 | end 333 | 334 | -- time left in chapter / book 335 | 336 | local function timeLeft_secs(pages) 337 | local avgTimePerPgturn = 0 338 | if statsEnabled then 339 | avgTimePerPgturn = ReaderStatistics.avg_time 340 | end 341 | local total_secs = avgTimePerPgturn * pages 342 | return total_secs 343 | end 344 | 345 | local timeLeft_book = "calc. time left" 346 | local timeLeft_chapter = timeLeft_book 347 | if ReaderStatistics.avg_time and ReaderStatistics.avg_time > 0 then 348 | timeLeft_book = secsToTimestring(timeLeft_secs(book_pageturn_left + 1), true) .. " left" -- +1 to include current page when calc. time left 349 | timeLeft_chapter = secsToTimestring(timeLeft_secs(chapter_pgturn_left + 1), true) .. " left" 350 | end 351 | 352 | -- BUILD WIDGETS 353 | 354 | local lineClearance = Screen:scaleBySize(10) 355 | local widgetClearance = vertical_spacing(0.3) 356 | local titleFontSize = w_font.size.small + 1 357 | 358 | -- vertical line helper function 359 | 360 | local vertical_line = function(height) 361 | local line = 362 | LineWidget:new{ 363 | background = Blitbuffer.COLOR_GRAY, 364 | dimen = 365 | Geom:new{ 366 | w = Screen:scaleBySize(1), 367 | h = height, 368 | }, 369 | } 370 | line = 371 | HorizontalGroup:new{ 372 | HorizontalSpan:new{width = lineClearance}, 373 | line, 374 | HorizontalSpan:new{width = lineClearance}, 375 | } 376 | return line 377 | end 378 | 379 | -- book box 380 | 381 | local function buildBookBox() 382 | local wid = w_width * 0.7 383 | local t = book_title .. " - " .. book_author 384 | t = string.lower(t) 385 | local t_widget = textboxx(t, w_font.face.it, titleFontSize, w_font.color.black, wid, nil, "left") 386 | 387 | local pXofY = "page " .. book_page .. " of " .. book_total 388 | local pXofY_widget = textboxx(pXofY, w_font.face.reg, w_font.size.small, w_font.color.black, wid, nil, "left") 389 | 390 | local tleft_font = w_font.face.bold 391 | local tleft_text = timeLeft_book 392 | if book_pageturn == book_pageturn_total then 393 | tleft_font = w_font.face.boldit 394 | tleft_text = "fin." 395 | end 396 | if not statsEnabled then tleft_text = "--" end 397 | local bookTLeft_widget = textboxx(tleft_text, tleft_font, w_font.size.small, w_font.color.black, wid, nil,"left") 398 | 399 | return VerticalGroup:new{ 400 | t_widget, 401 | widgetClearance, 402 | pXofY_widget, 403 | bookTLeft_widget 404 | } 405 | end 406 | 407 | local bookBox = buildBookBox() 408 | local bookBox_dimen = bookBox:getSize() 409 | 410 | -- top separator 411 | 412 | local top_separator = vertical_line(bookBox_dimen.h) 413 | 414 | -- book percentage box 415 | 416 | local function buildBookPctBox() 417 | local wid = w_width * 0.3 418 | local pct = prog_pct_book 419 | if pct > 0 and pct < 10 then pct = "0" .. pct end 420 | local pct_textsize = (prog_pct_book < 100) and 55 or 45 421 | 422 | local pctBox = textt(pct, w_font.face.reg, pct_textsize, w_font.color.black, 0) 423 | pctBox = 424 | HorizontalGroup:new{ 425 | HorizontalSpan:new{width = Screen:scaleBySize(3)}, 426 | pctBox 427 | } 428 | local pctBox_dimen = pctBox:getSize() 429 | 430 | local pctSymbol = textt("%", w_font.face.reg, w_font.size.small, w_font.color.black, 0) 431 | local pctSymbol_dimen = pctSymbol:getSize() 432 | 433 | local remaining = wid - pctBox_dimen.w - pctSymbol_dimen.w 434 | 435 | return HorizontalGroup:new{ 436 | HorizontalSpan:new{width = remaining / 2}, 437 | pctBox, 438 | VerticalGroup:new{ 439 | VerticalSpan:new{width = pctBox_dimen.h - pctSymbol_dimen.h * 2}, 440 | pctSymbol, 441 | }, 442 | HorizontalSpan:new{width = remaining / 2}, 443 | } 444 | end 445 | local bookPctBox = buildBookPctBox() 446 | 447 | -- chapter box 448 | 449 | local function buildChapterBox() 450 | local wid = w_width * 0.7 451 | 452 | local t = "CHAPTER: " .. string.lower(chapter_title) 453 | local t_widget = textboxx(t, w_font.face.it, titleFontSize, w_font.color.black, wid, nil, "left") 454 | 455 | local pXofY = "page " .. chapter_page .. " of " .. chapter_total 456 | pXofY = T(_("%1 (%2%)"), pXofY, prog_pct_chapter) 457 | local pXofY_widget = textboxx(pXofY, w_font.face.reg, w_font.size.small, w_font.color.black, wid, nil, "left") 458 | 459 | local tleft_font = w_font.face.bold 460 | local tleft_text = timeLeft_chapter 461 | if chapter_pgturn == chapter_pgturn_total then 462 | tleft_font = w_font.face.boldit 463 | tleft_text = "fin." 464 | end 465 | if not statsEnabled then tleft_text = "--" end 466 | local chapterTLeft_widget = textboxx(tleft_text, tleft_font, w_font.size.small, w_font.color.black, wid, nil,"left") 467 | 468 | return VerticalGroup:new{ 469 | t_widget, 470 | widgetClearance, 471 | pXofY_widget, 472 | chapterTLeft_widget 473 | } 474 | end 475 | local chapterBox = buildChapterBox() 476 | local chapterBox_dimen = chapterBox:getSize() 477 | 478 | local bottom_separator = vertical_line(chapterBox_dimen.h) 479 | 480 | -- time read today box 481 | 482 | local function buildTimeReadTodayBox() 483 | local wid = w_width * 0.3 484 | local t = T(_("today:\n%1\n%2"), pagesReadToday_str, timeReadToday_str) 485 | if pagesReadToday == 0 then 486 | t = "today:\nnope. :(" 487 | end 488 | if not statsEnabled then 489 | t = "today:\n--" 490 | end 491 | local t_widget = textboxx(t, w_font.face.reg, w_font.size.small, w_font.color.lightGray, wid, nil, "left") 492 | 493 | return VerticalGroup:new{ 494 | t_widget, 495 | VerticalSpan:new{width = chapterBox_dimen.h - t_widget:getSize().h } 496 | } 497 | end 498 | local timeReadTodayBox = buildTimeReadTodayBox() 499 | 500 | local topHalf = 501 | HorizontalGroup:new{ 502 | bookPctBox, 503 | top_separator, 504 | bookBox, 505 | } 506 | 507 | local horSeparator = 508 | VerticalGroup:new{ 509 | VerticalSpan:new{width = lineClearance}, 510 | LineWidget:new{ 511 | background = Blitbuffer.COLOR_GRAY, 512 | dimen = 513 | Geom:new{ 514 | w = w_width + Screen:scaleBySize(1) + lineClearance * 2, 515 | h = Screen:scaleBySize(1), 516 | }, 517 | }, 518 | VerticalSpan:new{width = lineClearance}, 519 | } 520 | 521 | local bottomHalf = 522 | HorizontalGroup:new{ 523 | chapterBox, 524 | bottom_separator, 525 | timeReadTodayBox, 526 | } 527 | 528 | local quickLookWindow = 529 | VerticalGroup:new{ 530 | topHalf, 531 | horSeparator, 532 | bottomHalf 533 | } 534 | 535 | local final_frame = 536 | FrameContainer:new{ 537 | radius = Screen:scaleBySize(22), 538 | bordersize = Screen:scaleBySize(2), 539 | padding = w_padding.external, 540 | background = Blitbuffer.COLOR_WHITE, 541 | quickLookWindow 542 | } 543 | 544 | return final_frame 545 | end 546 | 547 | --============================================ 548 | --'BOOK COMPLETE' WINDOW 549 | --============================================ 550 | 551 | local function buildBookCompleteWindow() 552 | local id_book = 0 553 | if statsEnabled then id_book = ReaderStatistics.id_curr_book or 0 end 554 | 555 | -- book start date 556 | 557 | -- stats plugin returns book start date as a a poorly formatted string, 558 | -- so we grab the book start timestamp directly from the sql instead. 559 | 560 | local ts_bookStart = 0 561 | if bookCompleted and statsEnabled then 562 | local conn = SQ3.open(db_location) 563 | local sql_stmt_bookStartTimestamp = 564 | [[ 565 | SELECT min(start_time) 566 | FROM page_stat 567 | WHERE id_book = %d; 568 | ]] 569 | ts_bookStart = conn:rowexec(string.format(sql_stmt_bookStartTimestamp, id_book)) or ts_now 570 | conn:close() 571 | end 572 | 573 | -- seconds since 12am for book start time and current time 574 | 575 | local t_bookStart = os.date("*t", tonumber(ts_bookStart)) 576 | t_bookStart.hour = 0 577 | t_bookStart.min = 0 578 | t_bookStart.sec = 0 579 | local ts_midnight_bookstartDay = os.time(t_bookStart) 580 | local secsSinceMidnight_now = ts_now - ts_midnight_today 581 | local secsSinceMidnight_bookstart = ts_bookStart - ts_midnight_bookstartDay 582 | 583 | local daysAgo = (ts_now - ts_bookStart) / secsInOneDay 584 | if secsSinceMidnight_now < secsSinceMidnight_bookstart then 585 | daysAgo = daysAgo + 1 586 | end 587 | local daysAgoTxt = "" 588 | if not statsEnabled then 589 | daysAgoTxt = "--" 590 | elseif daysAgo == 0 then 591 | daysAgoTxt = "started today" 592 | elseif daysAgo == 1 then 593 | daysAgoTxt = "started yesterday" 594 | else 595 | daysAgoTxt = string.format("started %i days ago", daysAgo) 596 | end 597 | 598 | local bookStartDate = "" 599 | if statsEnabled then 600 | bookStartDate = os.date("%d-%m-%Y", tonumber(ts_bookStart)) 601 | end 602 | 603 | local startedOn_str = string.format("%s (%s)", daysAgoTxt, bookStartDate) -- "started x days ago (dd--mm--yyyy)" 604 | 605 | -- BOOK READ TIME / HIGHLIGHT COUNT 606 | 607 | local bookReadTime, bookPagesRead, highlightCount = 0, 0, 0 -- bookreadtime is from FIRST OPEN till NOW 608 | if statsEnabled and bookCompleted then 609 | local pages_placeholder, time_placeholder = ReaderStatistics:getPageTimeTotalStats(ReaderStatistics.id_curr_book) 610 | bookReadTime = time_placeholder or 0 611 | bookPagesRead = pages_placeholder or 0 612 | local ok, stats = pcall(ReaderStatistics.getCurrentStat, ReaderStatistics) -- using pcall to defend against some 613 | if ok and stats and stats[15] then -- unexpected crashes when launching bc window. 614 | highlightCount = tonumber(stats[15][2]) or 0 615 | end 616 | end 617 | 618 | local bookReadTime_string = "" 619 | local bookCompleteStats = "--" 620 | local highlightCount_str = "" 621 | if statsEnabled then 622 | highlightCount_str = T(N_("1 highlight", "%1 highlights", highlightCount), highlightCount) 623 | bookReadTime_string = string.format("read for %s", secsToTimestring(bookReadTime)) 624 | bookCompleteStats = string.format("%s\n%s\n%s", bookReadTime_string, startedOn_str, highlightCount_str) 625 | end 626 | 627 | -- WINDOW WIDTH 628 | 629 | local bcWidgetWidth = 0 630 | if not statsEnabled then 631 | bcWidgetWidth = getWidth("book complete!", w_font.face.boldit, w_font.size.med) 632 | else 633 | local wid1 = getWidth(startedOn_str, w_font.face.it, w_font.size.small) 634 | local wid2 = getWidth(bookReadTime_string, w_font.face.it, w_font.size.small) 635 | bcWidgetWidth = math.max(wid1, wid2) 636 | end 637 | 638 | local bookCompleteWindow = {} 639 | if bookCompleted then 640 | bookCompleteWindow = 641 | VerticalGroup:new { 642 | textboxx("book complete!", w_font.face.boldit, w_font.size.med, w_font.color.black, bcWidgetWidth), 643 | vertical_spacing(0.5), 644 | textboxx(bookCompleteStats, w_font.face.it, w_font.size.small, w_font.color.black, bcWidgetWidth) 645 | } 646 | end 647 | 648 | local final_frame = 649 | FrameContainer:new { 650 | radius = Screen:scaleBySize(10), 651 | bordersize = Screen:scaleBySize(2), 652 | padding = w_padding.external, 653 | padding_top = math.floor(w_padding.external * 0.5), 654 | padding_bottom = math.floor(w_padding.external * 0.9), 655 | background = Blitbuffer.COLOR_WHITE, 656 | bookCompleteWindow 657 | } 658 | 659 | return final_frame 660 | end 661 | 662 | --============================================ 663 | --'BOOKS READ TODAY' WINDOW 664 | --============================================ 665 | 666 | local function buildBooksReadTodayWindow() 667 | local booksReadToday = {} 668 | if not self.ui.document and statsEnabled then 669 | local conn = SQ3.open(db_location) 670 | local sql_stmt_booksReadToday = 671 | [[ 672 | SELECT book_tbl.title AS title, 673 | count(distinct page_stat_tbl.page), 674 | sum(page_stat_tbl.duration), 675 | book_tbl.id 676 | FROM page_stat AS page_stat_tbl, book AS book_tbl 677 | WHERE page_stat_tbl.id_book=book_tbl.id AND page_stat_tbl.start_time BETWEEN %d AND %d 678 | GROUP BY book_tbl.id 679 | ORDER BY book_tbl.last_open DESC; 680 | ]] 681 | booksReadToday = conn:exec(string.format(sql_stmt_booksReadToday, ts_midnight_today + 1, ts_now)) 682 | conn:close() 683 | end 684 | 685 | if statsEnabled and booksReadToday then 686 | for i = 1, #booksReadToday[1] do 687 | local p = tonumber(booksReadToday[2][i]) -- pages read today 688 | local p_str = T(N_("1 pg", "%1 pgs", p), p) 689 | local d = secsToTimestring(tonumber(booksReadToday[3][i])) -- time read 690 | if brtAuthorsEnabled then -- if authors enabled, then replaces "title" in [1][i] with "title - author(s)" 691 | local bookId = booksReadToday[4][i] -- grab book id 692 | local auth = "" 693 | local auth_str = "" 694 | if ReaderStatistics:getBookStat(bookId) then 695 | auth = ReaderStatistics:getBookStat(bookId)[2][2] or "" 696 | if auth:find("\n") then 697 | auth = string.gsub(auth, "[\n]", ", ") 698 | end 699 | if auth ~= "N/A" then auth_str = string.format( " - %s", string.lower(auth)) end 700 | booksReadToday[1][i] = booksReadToday[1][i] .. auth_str -- append author(s) to book title 701 | end 702 | end 703 | booksReadToday[4][i] = string.format("%s · %s", p_str, d) -- replace book id in [4][i] with book stats string 704 | end 705 | end 706 | 707 | -- WINDOW WIDTH 708 | local brtWindowTitle = textt("books read today", w_font.face.boldit, w_font.size.med, w_font.color.black, 0) 709 | local brtWindowWidth = brtWindowTitle:getSize().w 710 | local brtWindowWidth_max = math.floor(screen_width / 2) 711 | if screen_width > screen_height then 712 | brtWindowWidth_max = math.floor(brtWindowWidth_max * screen_height / screen_width) 713 | end 714 | if statsEnabled and booksReadToday then 715 | local maxTitleWidth = 0 -- max width of book title string 716 | local maxStatWidth = 0 -- max width of book stats string 717 | for i = 1, #booksReadToday[1] do 718 | local w_title = getWidth(booksReadToday[1][i], w_font.face.reg, w_font.size.small) 719 | if w_title > maxTitleWidth then 720 | maxTitleWidth = w_title 721 | end 722 | local w_stats = getWidth(booksReadToday[4][i], w_font.face.it, w_font.size.small) 723 | if w_stats > maxStatWidth then 724 | maxStatWidth = w_stats 725 | end 726 | end 727 | if maxTitleWidth > brtWindowWidth then 728 | brtWindowWidth = math.min((maxTitleWidth), brtWindowWidth_max) 729 | end 730 | if maxStatWidth > brtWindowWidth then 731 | brtWindowWidth = maxStatWidth -- max window width is disregarded here because 732 | end -- we want stats to stay within one line. 733 | end 734 | 735 | -- HELPERS 736 | 737 | local function booksReadTodayEntry(brt_bookTitle, brtStats_str) 738 | local titleText = brt_bookTitle --string.format("%s", brt_bookTitle) 739 | local title = textboxx(titleText, w_font.face.reg, w_font.size.small, w_font.color.black, brtWindowWidth) 740 | 741 | local brtStats = textt(brtStats_str, w_font.face.it, w_font.size.small, w_font.color.lightGray, 0) 742 | 743 | local w = 744 | VerticalGroup:new { 745 | title, 746 | brtStats 747 | } 748 | return w 749 | end 750 | 751 | -- WINDOW CREATION 752 | 753 | local booksReadTodayWindow = {} 754 | booksReadTodayWindow = 755 | VerticalGroup:new { 756 | brtWindowTitle, 757 | vertical_spacing(0.7) 758 | } 759 | if statsEnabled and booksReadToday then 760 | local brt_separator = textboxx("-", w_font.face.it, w_font.size.small, w_font.color.black, brtWindowWidth) 761 | brt_separator.forced_height = brtWindowTitle:getSize().h 762 | for i = 1, #booksReadToday[1] do 763 | local t = string.lower(booksReadToday[1][i]) -- book title 764 | local statsStr = booksReadToday[4][i] 765 | booksReadTodayWindow[#booksReadTodayWindow + 1] = booksReadTodayEntry(t, statsStr) 766 | booksReadTodayWindow[#booksReadTodayWindow + 1] = vertical_spacing() 767 | end 768 | table.remove(booksReadTodayWindow) -- removes trailing separator 769 | elseif not statsEnabled then 770 | booksReadTodayWindow[#booksReadTodayWindow + 1] = 771 | textboxx("--", w_font.face.it, w_font.size.small, w_font.color.black, brtWindowWidth) 772 | else -- if no books read yet 773 | booksReadTodayWindow[#booksReadTodayWindow + 1] = 774 | textboxx("nope. :(", w_font.face.it, w_font.size.small, w_font.color.black, brtWindowWidth) 775 | end 776 | 777 | local final_frame = 778 | FrameContainer:new { 779 | radius = Screen:scaleBySize(10), 780 | bordersize = Screen:scaleBySize(2), 781 | padding = w_padding.external, 782 | padding_top = math.floor(w_padding.external / 1.5), 783 | background = Blitbuffer.COLOR_WHITE, 784 | booksReadTodayWindow 785 | } 786 | 787 | return final_frame 788 | end 789 | 790 | --==================//////////==================-- 791 | 792 | local WindowToBeDisplayed = nil 793 | if not self.ui.document then 794 | WindowToBeDisplayed = buildBooksReadTodayWindow() 795 | frameRadius = Screen:scaleBySize(10) 796 | padding_top = math.floor(w_padding.external * 0.2) 797 | elseif bookCompleted then 798 | WindowToBeDisplayed = buildBookCompleteWindow() 799 | frameRadius = Screen:scaleBySize(10) 800 | elseif not bookCompleted then 801 | WindowToBeDisplayed = buildQuickLookWindow() 802 | end 803 | 804 | self[1] = 805 | CenterContainer:new { 806 | dimen = Screen:getSize(), 807 | VerticalGroup:new { 808 | WindowToBeDisplayed 809 | } 810 | } 811 | 812 | -- taps and keypresses 813 | 814 | if Device:hasKeys() then 815 | self.key_events.AnyKeyPressed = {{Device.input.group.Any}} 816 | end 817 | if Device:isTouchDevice() then 818 | self.ges_events.Swipe = { 819 | GestureRange:new { 820 | ges = "swipe", 821 | range = function() 822 | return self.dimen 823 | end 824 | } 825 | } 826 | self.ges_events.Tap = { 827 | GestureRange:new { 828 | ges = "tap", 829 | range = function() 830 | return self.dimen 831 | end 832 | } 833 | } 834 | self.ges_events.MultiSwipe = { 835 | GestureRange:new { 836 | ges = "multiswipe", 837 | range = function() 838 | return self.dimen 839 | end 840 | } 841 | } 842 | end 843 | end 844 | 845 | function quicklookwindow:onTap() 846 | UIManager:close(self) 847 | end 848 | 849 | function quicklookwindow:onSwipe(arg, ges_ev) 850 | if ges_ev.direction == "south" then 851 | -- Allow easier closing with swipe up/down 852 | self:onClose() 853 | elseif ges_ev.direction == "east" or ges_ev.direction == "west" or ges_ev.direction == "north" then 854 | -- -- no use for now 855 | -- do end -- luacheck: ignore 541 856 | self:onClose() 857 | else -- diagonal swipe 858 | self:onClose() 859 | end 860 | end 861 | 862 | function quicklookwindow:onClose() 863 | UIManager:close(self) 864 | return true 865 | end 866 | 867 | quicklookwindow.onAnyKeyPressed = quicklookwindow.onClose 868 | quicklookwindow.onMultiSwipe = quicklookwindow.onClose 869 | 870 | function quicklookwindow:onShow() 871 | UIManager:setDirty( 872 | self, 873 | function() 874 | return "ui", self[1][1][1].dimen 875 | end 876 | ) 877 | return true 878 | end 879 | 880 | function quicklookwindow:onCloseWidget() 881 | if self[1] and self[1][1] and self[1][1][1] then 882 | UIManager:setDirty( 883 | nil, 884 | function() 885 | return "ui", self[1][1][1].dimen 886 | end 887 | ) 888 | end 889 | end 890 | 891 | -- ADD TO DISPATCHER 892 | 893 | Dispatcher:registerAction( 894 | "quicklookbox_action", 895 | { 896 | category = "none", 897 | event = "QuickLook", 898 | title = _("mini receipt"), 899 | general = true 900 | } 901 | ) 902 | 903 | function ReaderUI:onQuickLook() 904 | if self.statistics then 905 | self.statistics:insertDB() 906 | end 907 | 908 | bookCompleted = false 909 | 910 | local widget = 911 | quicklookwindow:new { 912 | ui = self, 913 | document = self.document, 914 | state = self.view and self.view.state 915 | } 916 | 917 | UIManager:show(widget, "ui", widget.dimen) 918 | end 919 | 920 | function ReaderUI:onEndOfBook() 921 | if self.statistics then 922 | self.statistics:insertDB() 923 | end 924 | 925 | bookCompleted = true 926 | 927 | if showBookCompleteWindow then 928 | local widget = 929 | quicklookwindow:new { 930 | ui = self, 931 | document = self.document, 932 | state = self.view and self.view.state 933 | } 934 | 935 | UIManager:show(widget, "ui", widget.dimen) 936 | end 937 | end 938 | 939 | function FileManager:onQuickLook() 940 | if self.statistics then 941 | self.statistics:insertDB() 942 | end 943 | 944 | local widget = 945 | quicklookwindow:new { 946 | ui = self, 947 | document = self.document, 948 | state = self.view and self.view.state 949 | } 950 | 951 | UIManager:show(widget, "ui", widget.dimen) 952 | end 953 | 954 | -------------------------------------------------------------------------------- /2-cvs-receipt-frankenpatch.lua: -------------------------------------------------------------------------------- 1 | --[[ cvs receipt v2.3 #11 - public ]] 2 | -- infinite loop protection in addToMainMenu 3 | 4 | local Blitbuffer = require("ffi/blitbuffer") 5 | local bookCompleted = false 6 | local CenterContainer = require("ui/widget/container/centercontainer") 7 | local datetime = require("datetime") 8 | local DataStorage = require("datastorage") 9 | local db_location = DataStorage:getSettingsDir() .. "/statistics.sqlite3" 10 | local Device = require("device") 11 | local Dispatcher = require("dispatcher") 12 | local Event = require("ui/event") 13 | local FileManager = require("apps/filemanager/filemanager") 14 | local Font = require("ui/font") 15 | local FrameContainer = require("ui/widget/container/framecontainer") 16 | local Geom = require("ui/geometry") 17 | local GestureRange = require("ui/gesturerange") 18 | local HorizontalGroup = require("ui/widget/horizontalgroup") 19 | local HorizontalSpan = require("ui/widget/horizontalspan") 20 | local InputContainer = require("ui/widget/container/inputcontainer") 21 | local LeftContainer = require("ui/widget/container/leftcontainer") 22 | local Math = require("optmath") 23 | local ProgressWidget = require("ui/widget/progresswidget") 24 | local ReaderFooter = require("apps/reader/modules/readerfooter") 25 | local ReaderUI = require("apps/reader/readerui") 26 | local ReaderView = require("apps/reader/modules/readerview") 27 | local Screen = Device.screen 28 | local Size = require("ui/size") 29 | local SQ3 = require("lua-ljsqlite3/init") 30 | local T = require("ffi/util").template 31 | local TextWidget = require("ui/widget/textwidget") 32 | local TextBoxWidget = require("ui/widget/textboxwidget") 33 | local UIManager = require("ui/uimanager") 34 | local userpatch = require("userpatch") 35 | local util = require("util") 36 | local VerticalGroup = require("ui/widget/verticalgroup") 37 | local VerticalSpan = require("ui/widget/verticalspan") 38 | local Widget = require("ui/widget/widget") 39 | local _ = require("gettext") 40 | local N_ = _.ngettext 41 | local WidgetContainer = require("ui/widget/container/widgetcontainer") 42 | 43 | -- SWITCHES 44 | 45 | local buttonProgressBarEnabled, showBookCompleteWindow, altFontEnabled, brtAuthorsEnabled = false, false, false, false 46 | if G_reader_settings and G_reader_settings.isTrue then 47 | buttonProgressBarEnabled = G_reader_settings:isTrue("cvs_rct_button_progbars") 48 | showBookCompleteWindow = G_reader_settings:isTrue("cvs_rct_book_complete_window") 49 | altFontEnabled = G_reader_settings:isTrue("cvs_rct_altFont") 50 | brtAuthorsEnabled = G_reader_settings:isTrue("cvs_rct_brtAuthors") 51 | end 52 | 53 | -- ADD TO MENU 54 | 55 | local cvsMenu = { 56 | text = _("cvs receipt"), 57 | sorting_hint = "tools", 58 | sub_item_table = { 59 | { 60 | text = _("button style progress bars"), 61 | help_text = _("button style progress bars inspired by old kindle firmwares. when unchecked, the receipt goes back to regular progress bars."), 62 | checked_func = function() 63 | return G_reader_settings:isTrue("cvs_rct_button_progbars") 64 | end, 65 | callback = function() 66 | G_reader_settings:flipNilOrFalse("cvs_rct_button_progbars") 67 | buttonProgressBarEnabled = G_reader_settings:isTrue("cvs_rct_button_progbars") 68 | end, 69 | }, 70 | { 71 | text = _("'book complete' window"), 72 | help_text = _("a little window that pops up when you flip past the last page of a book. shows time read, starting date and highlight count for current book.\n\n(set Menu>gear icon>End of document action to 'Do nothing' for best results)."), 73 | checked_func = function() 74 | return G_reader_settings:isTrue("cvs_rct_book_complete_window") 75 | end, 76 | callback = function() 77 | G_reader_settings:flipNilOrFalse("cvs_rct_book_complete_window") 78 | showBookCompleteWindow = G_reader_settings:isTrue("cvs_rct_book_complete_window") 79 | end, 80 | }, 81 | { 82 | text = _("show author(s) in 'books read today' window."), 83 | help_text = _("uncheck this if for a cleaner, more minimal looking 'books read today' window."), 84 | checked_func = function() 85 | return G_reader_settings:isTrue("cvs_rct_brtAuthors") 86 | end, 87 | callback = function() 88 | G_reader_settings:flipNilOrFalse("cvs_rct_brtAuthors") 89 | brtAuthorsEnabled = G_reader_settings:isTrue("cvs_rct_brtAuthors") 90 | end, 91 | }, 92 | { 93 | text = _("alt-font"), 94 | help_text = _("monospace font (or any other font you've added via the lua file)."), 95 | checked_func = function() 96 | return G_reader_settings:isTrue("cvs_rct_altFont") 97 | end, 98 | callback = function() 99 | G_reader_settings:flipNilOrFalse("cvs_rct_altFont") 100 | altFontEnabled = G_reader_settings:isTrue("cvs_rct_altFont") 101 | end, 102 | }, 103 | }, 104 | } 105 | 106 | if not ReaderFooter._cvs_receipt_hooked then 107 | local orig_ReaderFooter_addToMainMenu_cvs = ReaderFooter.addToMainMenu 108 | ReaderFooter.addToMainMenu = function(self, menu_items) 109 | if orig_ReaderFooter_addToMainMenu_cvs then 110 | orig_ReaderFooter_addToMainMenu_cvs(self, menu_items) 111 | end 112 | menu_items.cvs_rct_menu = cvsMenu 113 | end 114 | ReaderFooter._cvs_receipt_hooked = true 115 | end 116 | 117 | local quicklookwindow = 118 | InputContainer:extend { 119 | modal = true, 120 | name = "quick_look_window" 121 | } 122 | 123 | function quicklookwindow:init() 124 | 125 | local ReaderStatistics = self.ui.statistics 126 | local statsEnabled = ReaderStatistics and ReaderStatistics.settings and ReaderStatistics.settings.is_enabled 127 | local ReaderToc = self.ui.toc 128 | 129 | -- BOOK INFO 130 | 131 | local book_title = "" 132 | local book_author = "" 133 | if self.ui.doc_props then 134 | book_title = self.ui.doc_props.display_title or "" 135 | book_author = self.ui.doc_props.authors or "" 136 | if book_author:find("\n") then -- Show first author if multiple authors 137 | book_author = T(_("%1 et al."), util.splitToArray(book_author, "\n")[1] .. ",") 138 | end 139 | end 140 | 141 | -- PAGE COUNT AND BOOK PERCENTAGE 142 | 143 | local book_page = 0 144 | local book_total = 0 145 | local book_left = 0 146 | local book_percentage = 0 147 | if self.ui.document then 148 | book_page = self.state.page or 1 -- Current page 149 | book_total = self.ui.doc_settings.data.doc_pages or 1 150 | book_left = book_total - book_page 151 | book_percentage = (book_page / book_total) * 100 -- Format like %.1f in header_string below 152 | end 153 | 154 | -- CHAPTER INFO 155 | 156 | local chapter_title = "" 157 | local chapter_total = 0 158 | local chapter_left = 0 159 | local chapter_page = 0 160 | if ReaderToc then 161 | chapter_title = ReaderToc:getTocTitleByPage(book_page) or "" -- Chapter name 162 | chapter_page = ReaderToc:getChapterPagesDone(book_page) or 0 163 | chapter_page = chapter_page + 1 -- This +1 is to include the page you're looking at 164 | chapter_total = ReaderToc:getChapterPageCount(book_page) or book_total 165 | chapter_left = ReaderToc:getChapterPagesLeft(book_page) or book_left 166 | end 167 | 168 | -- BOOK PAGE TURNS (cuz everything gets reassigned with stable pages), 169 | 170 | local book_pageturn = book_page 171 | local book_pageturn_total = book_total 172 | local book_pageturn_left = book_left 173 | 174 | -- STABLE PAGES 175 | 176 | if self.ui.pagemap and self.ui.pagemap:wantsPageLabels() then 177 | book_page = self.ui.pagemap:getCurrentPageLabel(true) -- these two are strings. 178 | book_total = self.ui.pagemap:getLastPageLabel(true) 179 | end 180 | 181 | -- CLOCK 182 | 183 | local current_time = datetime.secondsToHour(os.time(), G_reader_settings:isTrue("twelve_hour_clock")) or "" 184 | 185 | -- BATTERY 186 | 187 | local battery = "" 188 | if Device:hasBattery() then 189 | local power_dev = Device:getPowerDevice() 190 | local batt_lvl = power_dev:getCapacity() or 0 191 | local is_charging = power_dev:isCharging() or false 192 | local batt_prefix = power_dev:getBatterySymbol(power_dev:isCharged(), is_charging, batt_lvl) or "" 193 | battery = batt_prefix .. batt_lvl .. "%" 194 | end 195 | 196 | local screen_width = Screen:getWidth() 197 | local screen_height = Screen:getHeight() 198 | local w_width = math.floor(screen_width / 2) 199 | if screen_width > screen_height then 200 | w_width = math.floor(w_width * screen_height / screen_width) 201 | end 202 | 203 | -- FONT AND PADDING 204 | 205 | local w_font = { 206 | face = { 207 | reg = "NotoSans-Regular.ttf", 208 | bold = "NotoSans-Bold.ttf", 209 | it = "NotoSans-Italic.ttf", 210 | boldit = "NotoSans-BoldItalic.ttf" 211 | }, 212 | size = {big = 25, med = 18, small = 15, tiny = 13}, 213 | color = { 214 | black = Blitbuffer.COLOR_BLACK, 215 | darkGray = Blitbuffer.COLOR_GRAY_1, 216 | lightGray = Blitbuffer.COLOR_GRAY_4 217 | } 218 | } 219 | local fontSizeCorrection = -1 220 | local monospaceFont = "DroidSansMono.ttf" 221 | if altFontEnabled then -- monospace 222 | w_font.face = { 223 | reg = monospaceFont, 224 | bold = monospaceFont, 225 | it = monospaceFont, 226 | boldit = monospaceFont 227 | } 228 | for key, value in pairs(w_font.size) do 229 | w_font.size[key] = value + fontSizeCorrection 230 | end 231 | end 232 | 233 | local w_padding = { 234 | internal = buttonProgressBarEnabled and Screen:scaleBySize(7) or Screen:scaleBySize(12), 235 | external = Screen:scaleBySize(20) 236 | } -- ext: between frame and widgets, int: verticalspace bw widgets (12) 237 | 238 | -- HELPER FUNCTIONS 239 | 240 | local function secsToTimestring(secs) -- seconds to 'x hrs y mins' format 241 | local timestring = "" 242 | 243 | local h = math.floor(secs / 3600) 244 | local m = math.floor((secs % 3600) / 60) 245 | local h_str = T(N_("1 hr", "%1 hrs", h), h) 246 | local m_str = T(N_("1 min", "%1 mins", m), m) 247 | 248 | if h == 0 and m < 1 then 249 | return "less than a minute" 250 | else 251 | if h >= 1 then timestring = timestring .. h_str .. " " end 252 | if m >= 1 then timestring = timestring .. m_str .. " " end 253 | timestring = timestring:sub(1, -2) -- remove the last space 254 | end 255 | 256 | return timestring 257 | end 258 | 259 | local function vertical_spacing(h) -- vertical space eq. to h*w_padding.internal 260 | if h == nil then 261 | h = 1 262 | end 263 | local s = VerticalSpan:new {width = math.floor(w_padding.internal * h)} 264 | return s 265 | end 266 | 267 | local function textt(txt, tfont, tsize, tclr, tpadding) -- creates TextWidget 268 | if not tclr then tclr = w_font.color.black end 269 | 270 | local w = 271 | TextWidget:new { 272 | text = txt, 273 | face = Font:getFace(tfont, tsize), 274 | fgcolor = tclr, 275 | bold = false, 276 | padding = tpadding or Screen:scaleBySize(2) 277 | } 278 | return w 279 | end 280 | 281 | local function getWidth(text, face, size) -- text width 282 | local t = textt(text, face, size) 283 | local width = t:getSize().w 284 | t:free() 285 | return width 286 | end 287 | 288 | local function textboxx(txt, tfont, tsize, tclr, twidth, tbold, alignmt, justif) -- creates TextBoxWidget 289 | if not tclr then tclr = w_font.color.black end 290 | if not tbold then tbold = false end 291 | if not justif then justif = false end 292 | if not alignmt then alignmt = "center" end 293 | local w = 294 | TextBoxWidget:new { 295 | text = txt, 296 | face = Font:getFace(tfont, tsize), 297 | fgcolor = tclr, 298 | bold = tbold, 299 | width = twidth, 300 | alignment = alignmt, 301 | justified = justif, 302 | padding = 0 303 | } 304 | return w 305 | end 306 | 307 | -- CURRENT TIMESTAMP / MIDNIGHT TIMESTAMP 308 | 309 | local secsInOneDay = 3600 * 24 310 | local ts_now = os.time() 311 | local t = os.date("*t", ts_now) 312 | t.hour = 0 313 | t.min = 0 314 | t.sec = 0 315 | local ts_midnight_today = os.time(t) 316 | 317 | --============================================ 318 | --'QUICK LOOK' WINDOW 319 | --============================================ 320 | 321 | local function buildQuickLookWindow() 322 | 323 | -- we manually calculate chapter page, chapter total and chapter left in terms of pageturns. 324 | -- this is because we want progress % to update after every PAGETURN (because that feels more 325 | -- organic) as opposed to having it update every STABLEPAGE (one single stablepage might spread 326 | -- across multiple pageturns). 327 | 328 | local chapter_pgturn, chapter_pgturn_left, chapter_pgturn_total = 0, 0, 0 329 | local nextChapterTickPgturn, previousChapterTickPgturn = 0, 0 330 | if self.ui.pagemap and self.ui.pagemap:wantsPageLabels() and ReaderToc then -- if stable pages are ON and toc is available 331 | nextChapterTickPgturn = ReaderToc:getNextChapter(book_pageturn) or (book_pageturn_total + 1) 332 | previousChapterTickPgturn = ReaderToc:getPreviousChapter(book_pageturn) or 1 333 | if book_pageturn == 1 or ReaderToc:isChapterStart(book_pageturn) then previousChapterTickPgturn = book_pageturn end 334 | chapter_pgturn = book_pageturn - previousChapterTickPgturn +1 335 | chapter_pgturn_total = nextChapterTickPgturn - previousChapterTickPgturn 336 | chapter_pgturn_left = nextChapterTickPgturn - book_pageturn - 1 337 | else 338 | chapter_pgturn = chapter_page 339 | chapter_pgturn_total = chapter_total 340 | chapter_pgturn_left = chapter_left 341 | end 342 | 343 | --=== QUICK LOOK WINDOW WIDGETS ===-- 344 | 345 | local function boxtype(book_or_ch) 346 | local widget = textt(book_or_ch, w_font.face.bold, w_font.size.big) 347 | return widget 348 | end 349 | 350 | function itemname(book_or_ch_name) 351 | local t = string.lower(book_or_ch_name) 352 | local widget = textboxx(t, w_font.face.reg, w_font.size.med, w_font.color.black, w_width, false, "left") 353 | return widget 354 | end 355 | 356 | -- PROGRESS MODULE 357 | 358 | function progressmodule(pgturn, pgturn_total, st_pageno, st_pagetotal) -- last two args rep. stable pages. 359 | if st_pageno == nil then st_pageno = pgturn end -- fallback to page turns if st. pgs. off 360 | if st_pagetotal == nil then st_pagetotal = pgturn_total end 361 | 362 | local prog_pct = pgturn / pgturn_total 363 | local progressbarwidth = math.floor(w_width) 364 | local prog_bar 365 | 366 | local function normal_prog_bar(pct) 367 | local p = 368 | ProgressWidget:new { 369 | width = progressbarwidth, 370 | height = Screen:scaleBySize(2), 371 | percentage = pct, 372 | margin_v = 0, 373 | margin_h = 0, 374 | radius = 0, 375 | bordersize = 0, 376 | fillcolor = w_font.color.black, 377 | bgcolor = Blitbuffer.COLOR_GRAY 378 | } 379 | return p 380 | end 381 | 382 | local function buttonProgressBar(pct) 383 | local buttonFontFace = altFontEnabled and "DroidSansMono.ttf" or "NotoSans-Regular.ttf" 384 | 385 | -- helper: get buttonString 386 | 387 | local getButtonString = function(bChar, wid, bsize) 388 | local charWidth = getWidth(bChar, buttonFontFace, bsize) 389 | local maxCharNum = math.floor(wid / charWidth) 390 | local bString = string.rep(bChar, maxCharNum) 391 | return bString 392 | end 393 | 394 | local buttonFontSize_unread = 22 395 | local buttonFontSize_read = buttonFontSize_unread 396 | local buttonChar_unread = "·" 397 | local buttonChar_read = "•" 398 | 399 | local increment = 4 -- % increment for button progress bar. < 2 makes the bar fill up before touching 100 400 | local readWidth = (math.floor(pct * 100 / increment)) * increment / 100 * progressbarwidth 401 | 402 | -- read segment 403 | 404 | local readSegmentString = getButtonString(buttonChar_read, readWidth, buttonFontSize_read) 405 | local readButtonSegment = textt(readSegmentString, buttonFontFace, buttonFontSize_read, nil, 0) 406 | 407 | local readSegmentHeight = readButtonSegment:getSize().h 408 | local paddingFix = altFontEnabled and Screen:scaleBySize(math.floor(0.05 * readSegmentHeight)) or 0 409 | local container_height = readSegmentHeight - paddingFix 410 | readButtonSegment.forced_height = container_height 411 | 412 | -- unread segment 413 | 414 | local unreadWidth = progressbarwidth - readButtonSegment:getSize().w 415 | 416 | local unreadButtonSegment 417 | if altFontEnabled then 418 | local unreadButtonString = getButtonString(buttonChar_unread, unreadWidth, buttonFontSize_unread) 419 | unreadButtonSegment = textt(unreadButtonString, buttonFontFace, buttonFontSize_unread, nil, 0) 420 | 421 | unreadButtonSegment.forced_height = container_height 422 | else 423 | local dotSeparation = 424 | HorizontalSpan:new {width = ((getWidth(" ", buttonFontFace, buttonFontSize_unread)) / 3)} 425 | local urButton = textt(buttonChar_unread, buttonFontFace, buttonFontSize_unread, nil, 0) 426 | 427 | urButton.forced_height = container_height 428 | 429 | local repeatingUnit = 430 | HorizontalGroup:new { 431 | dotSeparation, 432 | urButton 433 | } 434 | 435 | local repeatingUnit_reqNum = math.floor(unreadWidth / repeatingUnit:getSize().w) 436 | unreadButtonSegment = HorizontalGroup:new {} 437 | for i = 1, repeatingUnit_reqNum do 438 | table.insert(unreadButtonSegment, repeatingUnit) 439 | end 440 | end 441 | 442 | local rem = 443 | HorizontalSpan:new { 444 | width = progressbarwidth - readButtonSegment:getSize().w - unreadButtonSegment:getSize().w 445 | } 446 | 447 | local container = 448 | LeftContainer:new { 449 | dimen = Geom:new { 450 | w = progressbarwidth, 451 | h = Screen:scaleBySize(26) 452 | }, 453 | HorizontalGroup:new { 454 | align = "center", 455 | readButtonSegment, 456 | unreadButtonSegment, 457 | rem 458 | } 459 | } 460 | 461 | return container 462 | end 463 | 464 | prog_bar = buttonProgressBarEnabled and buttonProgressBar(prog_pct) or normal_prog_bar(prog_pct) 465 | 466 | local pgXofY_txt = T(_("page %1 of %2"), st_pageno, st_pagetotal) 467 | local pageXofY = textt(pgXofY_txt, w_font.face.reg, w_font.size.small, w_font.color.darkGray) 468 | 469 | local percentage_display_txt = string.format("%i%%", prog_pct * 100) 470 | local percentage_display = 471 | textt(percentage_display_txt, w_font.face.reg, w_font.size.small, w_font.color.darkGray) 472 | 473 | local progressModule = 474 | VerticalGroup:new { 475 | prog_bar, 476 | HorizontalGroup:new { 477 | pageXofY, 478 | HorizontalSpan:new {width = w_width - pageXofY:getSize().w - percentage_display:getSize().w}, 479 | percentage_display 480 | } 481 | } 482 | return progressModule 483 | end 484 | 485 | -- TIME READ TODAY / PAGES READ TODAY 486 | 487 | local timeReadToday, pagesReadToday = 0, 0 488 | local timeReadToday_str, pagesReadToday_str = "", "" 489 | if self.ui.document and not bookCompleted then 490 | timeReadToday, pagesReadToday = ReaderStatistics:getTodayBookStats() -- stats for today across all books 491 | timeReadToday_str = string.format("%s read today", secsToTimestring(timeReadToday)) 492 | pagesReadToday_str = T(N_("1 pg", "%1 pgs", pagesReadToday), pagesReadToday) 493 | end 494 | 495 | local time_read_today_box = function() 496 | local t = string.format("%s · %s", pagesReadToday_str, timeReadToday_str) 497 | local widget = textt(t, w_font.face.it, w_font.size.small, w_font.color.darkGray) 498 | 499 | if not statsEnabled or timeReadToday < 60 then -- if time read < 1 min, hide time_read_today_box 500 | return nil 501 | end 502 | local trt_wid = widget:getSize().w 503 | if trt_wid > w_width then w_width = trt_wid + Screen:scaleBySize(10) end 504 | return widget 505 | end 506 | 507 | -- TIME LEFT IN CHAPTER/BOOK 508 | 509 | local function timeLeft_secs(pages) 510 | local avgTimePerPgturn = 0 511 | if statsEnabled then 512 | avgTimePerPgturn = ReaderStatistics.avg_time 513 | end 514 | local total_secs = avgTimePerPgturn * pages 515 | return total_secs 516 | end 517 | 518 | local book_timeLeft = "calculating time" 519 | local chapter_timeLeft = "calculating time" 520 | if ReaderStatistics.avg_time and ReaderStatistics.avg_time > 0 then 521 | book_timeLeft = secsToTimestring(timeLeft_secs(book_pageturn_left + 1)) -- +1 to include current page when calc. time left 522 | chapter_timeLeft = secsToTimestring(timeLeft_secs(chapter_pgturn_left + 1)) 523 | end 524 | 525 | function time_left_display(timeleftstring, book_or_ch) 526 | local tldfont = w_font.face.boldit 527 | if not statsEnabled or timeReadToday < 60 then 528 | tldfont = w_font.face.it 529 | end 530 | 531 | displayText = string.format("%s left in %s", timeleftstring, book_or_ch) 532 | if not statsEnabled then 533 | displayText = string.format("-- left in %s", book_or_ch) 534 | end 535 | 536 | local tldWidth = getWidth(displayText, tldfont, w_font.size.small, w_font) 537 | if tldWidth > w_width then 538 | w_width = tldWidth + Screen:scaleBySize(10) 539 | end -- monospace fonts take up more space 540 | 541 | local widget = textt(displayText, tldfont, w_font.size.small, w_font.color.darkGray) 542 | 543 | return widget 544 | end 545 | 546 | local batt_pct_box = 547 | textboxx(battery, w_font.face.reg, w_font.size.small, w_font.color.black, w_width / 2, false, "left") 548 | 549 | local glyph_clock = "⌚" 550 | local time_box_txt = string.format("%s%s", glyph_clock, current_time) 551 | local time_box = 552 | textboxx(time_box_txt, w_font.face.reg, w_font.size.small, w_font.color.black, w_width / 2, false, "right") 553 | 554 | local bottom_bar = function() 555 | local widget = 556 | HorizontalGroup:new { 557 | batt_pct_box, 558 | time_box 559 | } 560 | return widget 561 | end 562 | 563 | local bookboxtitle = string.format("%s - %s", book_title, book_author) 564 | 565 | local tleftc = time_left_display(chapter_timeLeft, "chapter") 566 | local tleftb = time_left_display(book_timeLeft, "book") 567 | local trtbox = time_read_today_box() 568 | local progModule_book = progressmodule(book_pageturn, book_pageturn_total, book_page, book_total) 569 | local progModule_ch = progressmodule(chapter_pgturn, chapter_pgturn_total, chapter_page, chapter_total) 570 | 571 | 572 | local quickLookWindow = 573 | VerticalGroup:new { 574 | boxtype("chapter"), --1 575 | vertical_spacing(), --2 576 | itemname(chapter_title),--3 577 | progModule_ch, --4 578 | vertical_spacing(), --5 579 | tleftc, --6 580 | vertical_spacing(), --7 581 | boxtype("book"), --8 582 | vertical_spacing(), --9 583 | itemname(bookboxtitle), --10 584 | progModule_book, --11 585 | vertical_spacing(), --12 586 | tleftb, --13 587 | vertical_spacing(1.2), --14 588 | bottom_bar() --15 589 | } 590 | 591 | if trtbox then 592 | table.insert(quickLookWindow, 14, trtbox) 593 | end 594 | if not buttonProgressBarEnabled then 595 | table.insert(quickLookWindow, 11, vertical_spacing()) 596 | table.insert(quickLookWindow, 4, vertical_spacing()) 597 | end 598 | 599 | return quickLookWindow 600 | end 601 | 602 | --============================================ 603 | --'BOOK COMPLETE' WINDOW 604 | --============================================ 605 | 606 | local function buildBookCompleteWindow() 607 | local id_book = 0 608 | if statsEnabled then id_book = ReaderStatistics.id_curr_book or 0 end 609 | 610 | -- book start date 611 | 612 | -- stats plugin returns book start date as a a poorly formatted string, 613 | -- so we grab the book start timestamp directly from the sql instead. 614 | 615 | local ts_bookStart = 0 616 | if bookCompleted and statsEnabled then 617 | local conn = SQ3.open(db_location) 618 | local sql_stmt_bookStartTimestamp = 619 | [[ 620 | SELECT min(start_time) 621 | FROM page_stat 622 | WHERE id_book = %d; 623 | ]] 624 | ts_bookStart = conn:rowexec(string.format(sql_stmt_bookStartTimestamp, id_book)) or ts_now 625 | conn:close() 626 | end 627 | 628 | -- seconds since 12am for book start time and current time 629 | 630 | local t_bookStart = os.date("*t", tonumber(ts_bookStart)) 631 | t_bookStart.hour = 0 632 | t_bookStart.min = 0 633 | t_bookStart.sec = 0 634 | local ts_midnight_bookstartDay = os.time(t_bookStart) 635 | local secsSinceMidnight_now = ts_now - ts_midnight_today 636 | local secsSinceMidnight_bookstart = ts_bookStart - ts_midnight_bookstartDay 637 | 638 | local daysAgo = (ts_now - ts_bookStart) / secsInOneDay 639 | if secsSinceMidnight_now < secsSinceMidnight_bookstart then 640 | daysAgo = daysAgo + 1 641 | end 642 | local daysAgoTxt = "" 643 | if not statsEnabled then 644 | daysAgoTxt = "--" 645 | elseif daysAgo == 0 then 646 | daysAgoTxt = "started today" 647 | elseif daysAgo == 1 then 648 | daysAgoTxt = "started yesterday" 649 | else 650 | daysAgoTxt = string.format("started %i days ago", daysAgo) 651 | end 652 | 653 | local bookStartDate = "" 654 | if statsEnabled then 655 | bookStartDate = os.date("%d-%m-%Y", tonumber(ts_bookStart)) 656 | end 657 | 658 | local startedOn_str = string.format("%s (%s)", daysAgoTxt, bookStartDate) -- "started x days ago (dd--mm--yyyy)" 659 | 660 | -- BOOK READ TIME / HIGHLIGHT COUNT 661 | 662 | local bookReadTime, bookPagesRead, highlightCount = 0, 0, 0 -- bookreadtime is from FIRST OPEN till NOW 663 | if statsEnabled and bookCompleted then 664 | local pages_placeholder, time_placeholder = ReaderStatistics:getPageTimeTotalStats(ReaderStatistics.id_curr_book) 665 | bookReadTime = time_placeholder or 0 666 | bookPagesRead = pages_placeholder or 0 667 | local ok, stats = pcall(ReaderStatistics.getCurrentStat, ReaderStatistics) -- using pcall to defend against some 668 | if ok and stats and stats[15] then -- unexpected crashes when launching bc window. 669 | highlightCount = tonumber(stats[15][2]) or 0 670 | end 671 | end 672 | 673 | local bookReadTime_string = "" 674 | local bookCompleteStats = "--" 675 | local highlightCount_str = "" 676 | if statsEnabled then 677 | highlightCount_str = T(N_("1 highlight", "%1 highlights", highlightCount), highlightCount) 678 | bookReadTime_string = string.format("read for %s", secsToTimestring(bookReadTime)) 679 | bookCompleteStats = string.format("%s\n%s\n%s", bookReadTime_string, startedOn_str, highlightCount_str) 680 | end 681 | 682 | -- WINDOW WIDTH 683 | 684 | local bcWidgetWidth = 0 685 | if not statsEnabled then 686 | bcWidgetWidth = getWidth("book complete!", w_font.face.boldit, w_font.size.med) 687 | else 688 | local wid1 = getWidth(startedOn_str, w_font.face.it, w_font.size.small) 689 | local wid2 = getWidth(bookReadTime_string, w_font.face.it, w_font.size.small) 690 | bcWidgetWidth = math.max(wid1, wid2) 691 | end 692 | 693 | local bookCompleteWindow = {} 694 | if bookCompleted then 695 | bookCompleteWindow = 696 | VerticalGroup:new { 697 | textboxx("book complete!", w_font.face.boldit, w_font.size.med, w_font.color.black, bcWidgetWidth), 698 | vertical_spacing(0.5), 699 | textboxx(bookCompleteStats, w_font.face.it, w_font.size.small, w_font.color.black, bcWidgetWidth) 700 | } 701 | end 702 | 703 | return bookCompleteWindow 704 | end 705 | 706 | --============================================ 707 | --'BOOKS READ TODAY' WINDOW 708 | --============================================ 709 | 710 | local function buildBooksReadTodayWindow() 711 | local booksReadToday = {} 712 | if not self.ui.document and statsEnabled then 713 | local conn = SQ3.open(db_location) 714 | local sql_stmt_booksReadToday = 715 | [[ 716 | SELECT book_tbl.title AS title, 717 | count(distinct page_stat_tbl.page), 718 | sum(page_stat_tbl.duration), 719 | book_tbl.id 720 | FROM page_stat AS page_stat_tbl, book AS book_tbl 721 | WHERE page_stat_tbl.id_book=book_tbl.id AND page_stat_tbl.start_time BETWEEN %d AND %d 722 | GROUP BY book_tbl.id 723 | ORDER BY book_tbl.last_open DESC; 724 | ]] 725 | booksReadToday = conn:exec(string.format(sql_stmt_booksReadToday, ts_midnight_today + 1, ts_now)) 726 | conn:close() 727 | end 728 | 729 | if statsEnabled and booksReadToday then 730 | for i = 1, #booksReadToday[1] do 731 | local p = tonumber(booksReadToday[2][i]) -- pages read today 732 | local p_str = T(N_("1 pg", "%1 pgs", p), p) 733 | local d = secsToTimestring(tonumber(booksReadToday[3][i])) -- time read 734 | if brtAuthorsEnabled then -- if authors enabled, then replaces "title" in [1][i] with "title - author(s)" 735 | local bookId = booksReadToday[4][i] -- grab book id 736 | local auth = "" 737 | local auth_str = "" 738 | if ReaderStatistics:getBookStat(bookId) then 739 | auth = ReaderStatistics:getBookStat(bookId)[2][2] or "" 740 | if auth:find("\n") then 741 | auth = string.gsub(auth, "[\n]", ", ") 742 | end 743 | if auth ~= "N/A" then auth_str = string.format( " - %s", string.lower(auth)) end 744 | booksReadToday[1][i] = booksReadToday[1][i] .. auth_str -- append author(s) to book title 745 | end 746 | end 747 | booksReadToday[4][i] = string.format("%s · %s", p_str, d) -- replace book id in [4][i] with book stats string 748 | end 749 | end 750 | 751 | -- WINDOW WIDTH 752 | local brtWindowTitle = textt("books read today", w_font.face.boldit, w_font.size.med, w_font.color.black, 0) 753 | local brtWindowWidth = brtWindowTitle:getSize().w 754 | local brtWindowWidth_max = math.floor(screen_width / 2) 755 | if screen_width > screen_height then 756 | brtWindowWidth_max = math.floor(brtWindowWidth_max * screen_height / screen_width) 757 | end 758 | if statsEnabled and booksReadToday then 759 | local maxTitleWidth = 0 -- max width of book title string 760 | local maxStatWidth = 0 -- max width of book stats string 761 | for i = 1, #booksReadToday[1] do 762 | local w_title = getWidth(booksReadToday[1][i], w_font.face.it, w_font.size.small) 763 | if w_title > maxTitleWidth then 764 | maxTitleWidth = w_title 765 | end 766 | local w_stats = getWidth(booksReadToday[4][i], w_font.face.it, w_font.size.small) 767 | if w_stats > maxStatWidth then 768 | maxStatWidth = w_stats 769 | end 770 | end 771 | if maxTitleWidth > brtWindowWidth then 772 | brtWindowWidth = math.min((maxTitleWidth), brtWindowWidth_max) 773 | end 774 | if maxStatWidth > brtWindowWidth then 775 | brtWindowWidth = maxStatWidth -- max window width is disregarded here because 776 | end -- we want stats to stay within one line. 777 | end 778 | 779 | -- HELPERS 780 | 781 | local function booksReadTodayEntry(brt_bookTitle, brtStats_str) 782 | local titleText = brt_bookTitle --string.format("%s", brt_bookTitle) 783 | local title = textboxx(titleText, w_font.face.it, w_font.size.small, w_font.color.black, brtWindowWidth) 784 | 785 | local brtStats = textt(brtStats_str, w_font.face.it, w_font.size.small, w_font.color.lightGray, 0) 786 | 787 | local w = 788 | VerticalGroup:new { 789 | title, 790 | brtStats 791 | } 792 | return w 793 | end 794 | 795 | -- WINDOW CREATION 796 | 797 | local booksReadTodayWindow = {} 798 | booksReadTodayWindow = 799 | VerticalGroup:new { 800 | brtWindowTitle, 801 | vertical_spacing() 802 | } 803 | if statsEnabled and booksReadToday then 804 | local brt_separator = textboxx("-", w_font.face.it, w_font.size.small, w_font.color.black, brtWindowWidth) 805 | brt_separator.forced_height = brtWindowTitle:getSize().h 806 | for i = 1, #booksReadToday[1] do 807 | local t = string.lower(booksReadToday[1][i]) -- book title 808 | local statsStr = booksReadToday[4][i] 809 | booksReadTodayWindow[#booksReadTodayWindow + 1] = booksReadTodayEntry(t, statsStr) 810 | booksReadTodayWindow[#booksReadTodayWindow + 1] = brt_separator 811 | end 812 | table.remove(booksReadTodayWindow) -- removes trailing separator 813 | elseif not statsEnabled then 814 | booksReadTodayWindow[#booksReadTodayWindow + 1] = 815 | textboxx("--", w_font.face.it, w_font.size.small, w_font.color.black, brtWindowWidth) 816 | else -- if no books read yet 817 | booksReadTodayWindow[#booksReadTodayWindow + 1] = 818 | textboxx("nope. :(", w_font.face.it, w_font.size.small, w_font.color.black, brtWindowWidth) 819 | end 820 | 821 | return booksReadTodayWindow 822 | end 823 | 824 | --==================//////////==================-- 825 | 826 | local frameRadius = Screen:scaleBySize(22) 827 | local framePadding = w_padding.external 828 | 829 | local WindowToBeDisplayed = nil 830 | if not self.ui.document then 831 | WindowToBeDisplayed = buildBooksReadTodayWindow() 832 | frameRadius = Screen:scaleBySize(10) 833 | elseif bookCompleted then 834 | WindowToBeDisplayed = buildBookCompleteWindow() 835 | frameRadius = Screen:scaleBySize(10) 836 | elseif not bookCompleted then 837 | WindowToBeDisplayed = buildQuickLookWindow() 838 | end 839 | 840 | local final_frame = 841 | FrameContainer:new { 842 | radius = frameRadius, 843 | bordersize = Screen:scaleBySize(2), 844 | padding = framePadding, 845 | padding_top = math.floor(w_padding.external / 2.1), 846 | padding_bottom = math.floor(w_padding.external / 1.1), 847 | background = Blitbuffer.COLOR_WHITE, 848 | WindowToBeDisplayed 849 | } 850 | 851 | self[1] = 852 | CenterContainer:new { 853 | dimen = Screen:getSize(), 854 | VerticalGroup:new { 855 | final_frame 856 | } 857 | } 858 | 859 | -- taps and keypresses 860 | 861 | if Device:hasKeys() then 862 | self.key_events.AnyKeyPressed = {{Device.input.group.Any}} 863 | end 864 | if Device:isTouchDevice() then 865 | self.ges_events.Swipe = { 866 | GestureRange:new { 867 | ges = "swipe", 868 | range = function() 869 | return self.dimen 870 | end 871 | } 872 | } 873 | self.ges_events.Tap = { 874 | GestureRange:new { 875 | ges = "tap", 876 | range = function() 877 | return self.dimen 878 | end 879 | } 880 | } 881 | self.ges_events.MultiSwipe = { 882 | GestureRange:new { 883 | ges = "multiswipe", 884 | range = function() 885 | return self.dimen 886 | end 887 | } 888 | } 889 | end 890 | end 891 | 892 | function quicklookwindow:onTap() 893 | UIManager:close(self) 894 | end 895 | 896 | function quicklookwindow:onSwipe(arg, ges_ev) 897 | if ges_ev.direction == "south" then 898 | -- Allow easier closing with swipe up/down 899 | self:onClose() 900 | elseif ges_ev.direction == "east" or ges_ev.direction == "west" or ges_ev.direction == "north" then 901 | -- -- no use for now 902 | -- do end -- luacheck: ignore 541 903 | self:onClose() 904 | else -- diagonal swipe 905 | self:onClose() 906 | end 907 | end 908 | 909 | function quicklookwindow:onClose() 910 | UIManager:close(self) 911 | return true 912 | end 913 | 914 | quicklookwindow.onAnyKeyPressed = quicklookwindow.onClose 915 | quicklookwindow.onMultiSwipe = quicklookwindow.onClose 916 | 917 | function quicklookwindow:onShow() 918 | UIManager:setDirty( 919 | self, 920 | function() 921 | return "ui", self[1][1][1].dimen 922 | end 923 | ) 924 | return true 925 | end 926 | 927 | function quicklookwindow:onCloseWidget() 928 | if self[1] and self[1][1] and self[1][1][1] then 929 | UIManager:setDirty( 930 | nil, 931 | function() 932 | return "ui", self[1][1][1].dimen 933 | end 934 | ) 935 | end 936 | end 937 | 938 | -- ADD TO DISPATCHER 939 | 940 | Dispatcher:registerAction( 941 | "quicklookbox_action", 942 | { 943 | category = "none", 944 | event = "QuickLook", 945 | title = _("cvs receipt"), 946 | general = true 947 | } 948 | ) 949 | 950 | function ReaderUI:onQuickLook() 951 | if self.statistics then 952 | self.statistics:insertDB() 953 | end 954 | 955 | bookCompleted = false 956 | 957 | local widget = 958 | quicklookwindow:new { 959 | ui = self, 960 | document = self.document, 961 | state = self.view and self.view.state 962 | } 963 | 964 | UIManager:show(widget, "ui", widget.dimen) 965 | end 966 | 967 | function ReaderUI:onEndOfBook() 968 | if self.statistics then 969 | self.statistics:insertDB() 970 | end 971 | 972 | bookCompleted = true 973 | 974 | if showBookCompleteWindow then 975 | local widget = 976 | quicklookwindow:new { 977 | ui = self, 978 | document = self.document, 979 | state = self.view and self.view.state 980 | } 981 | 982 | UIManager:show(widget, "ui", widget.dimen) 983 | end 984 | end 985 | 986 | function FileManager:onQuickLook() 987 | if self.statistics then 988 | self.statistics:insertDB() 989 | end 990 | 991 | local widget = 992 | quicklookwindow:new { 993 | ui = self, 994 | document = self.document, 995 | state = self.view and self.view.state 996 | } 997 | 998 | UIManager:show(widget, "ui", widget.dimen) 999 | end 1000 | 1001 | --------------------------------------------------------------------------------