local BD = require("ui/bidi") local Blitbuffer = require("ffi/blitbuffer") local ButtonDialogTitle = require("ui/widget/buttondialogtitle") local BookStatusWidget = require("ui/widget/bookstatuswidget") local BottomContainer = require("ui/widget/container/bottomcontainer") local Device = require("device") local DocSettings = require("docsettings") local DocumentRegistry = require("document/documentregistry") local Font = require("ui/font") local Geom = require("ui/geometry") local InfoMessage = require("ui/widget/infomessage") local ImageWidget = require("ui/widget/imagewidget") local Math = require("optmath") local OverlapGroup = require("ui/widget/overlapgroup") local ScreenSaverWidget = require("ui/widget/screensaverwidget") local SpinWidget = require("ui/widget/spinwidget") local TextBoxWidget = require("ui/widget/textboxwidget") local TopContainer = require("ui/widget/container/topcontainer") local UIManager = require("ui/uimanager") local ffiUtil = require("ffi/util") local lfs = require("libs/libkoreader-lfs") local logger = require("logger") local util = require("util") local _ = require("gettext") local Screen = Device.screen local T = ffiUtil.template -- Default settings if G_reader_settings:hasNot("screensaver_show_message") then G_reader_settings:makeFalse("screensaver_show_message") end if G_reader_settings:hasNot("screensaver_type") then G_reader_settings:saveSetting("screensaver_type", "disable") G_reader_settings:makeTrue("screensaver_show_message") end if G_reader_settings:hasNot("screensaver_img_background") then G_reader_settings:saveSetting("screensaver_img_background", "black") end if G_reader_settings:hasNot("screensaver_msg_background") then G_reader_settings:saveSetting("screensaver_msg_background", "none") end if G_reader_settings:hasNot("screensaver_message_position") then G_reader_settings:saveSetting("screensaver_message_position", "middle") end if G_reader_settings:hasNot("screensaver_stretch_images") then G_reader_settings:makeFalse("screensaver_stretch_images") end if G_reader_settings:hasNot("screensaver_delay") then G_reader_settings:saveSetting("screensaver_delay", "disable") end if G_reader_settings:hasNot("screensaver_hide_fallback_msg") then G_reader_settings:makeFalse("screensaver_hide_fallback_msg") end local Screensaver = { screensaver_provider = { jpg = true, jpeg = true, png = true, gif = true, tif = true, tiff = true, }, default_screensaver_message = _("Sleeping"), } -- Remind emulator users that Power is bound to F2 if Device:isEmulator() then Screensaver.default_screensaver_message = Screensaver.default_screensaver_message .. "\n" .. _("(Press F2 to resume)") end function Screensaver:_getRandomImage(dir) if not dir then return nil end if string.sub(dir, string.len(dir)) ~= "/" then dir = dir .. "/" end local pics = {} local i = 0 math.randomseed(os.time()) local ok, iter, dir_obj = pcall(lfs.dir, dir) if ok then for f in iter, dir_obj do -- Always ignore macOS resource forks, too. if lfs.attributes(dir .. f, "mode") == "file" and not util.stringStartsWith(f, "._") then local extension = string.lower(string.match(f, ".+%.([^.]+)") or "") if self.screensaver_provider[extension] then i = i + 1 pics[i] = f end end end if i == 0 then return nil end else return nil end return dir .. pics[math.random(i)] end -- This is implemented by the Statistics plugin function Screensaver:getAvgTimePerPage() return end function Screensaver:_calcAverageTimeForPages(pages) local sec = _("N/A") local average_time_per_page = self:getAvgTimePerPage() -- Compare average_time_per_page against itself to make sure it's not nan if average_time_per_page and average_time_per_page == average_time_per_page and pages then local user_duration_format = G_reader_settings:readSetting("duration_format", "classic") sec = util.secondsToClockDuration(user_duration_format, pages * average_time_per_page, true) end return sec end function Screensaver:expandSpecial(message, fallback) -- Expand special character sequences in given message. -- %p percentage read -- %c current page -- %t total pages -- %T document title -- %A document authors -- %S document series -- %h time left in chapter -- %H time left in document if G_reader_settings:hasNot("lastfile") then return fallback end local ret = message local lastfile = G_reader_settings:readSetting("lastfile") local totalpages = 0 local percent = 0 local currentpage = 0 local title = _("N/A") local authors = _("N/A") local series = _("N/A") local time_left_chapter = _("N/A") local time_left_document = _("N/A") local ReaderUI = require("apps/reader/readerui") local ui = ReaderUI:_getRunningInstance() if ui and ui.document then -- If we have a ReaderUI instance, use it. local doc = ui.document currentpage = ui.view.state.page or currentpage totalpages = doc:getPageCount() or totalpages percent = Math.round((currentpage * 100) / totalpages) local props = doc:getProps() if props then title = props.title and props.title ~= "" and props.title or title authors = props.authors and props.authors ~= "" and props.authors or authors series = props.series and props.series ~= "" and props.series or series end time_left_chapter = self:_calcAverageTimeForPages(ui.toc:getChapterPagesLeft(currentpage) or doc:getTotalPagesLeft(currentpage)) time_left_document = self:_calcAverageTimeForPages(doc:getTotalPagesLeft(currentpage)) elseif DocSettings:hasSidecarFile(lastfile) then -- If there's no ReaderUI instance, but the file has sidecar data, use that local docinfo = DocSettings:open(lastfile) totalpages = docinfo.data.doc_pages or totalpages percent = docinfo.data.percent_finished or percent currentpage = Math.round(percent * totalpages) percent = Math.round(percent * 100) if docinfo.data.doc_props then title = docinfo.data.doc_props.title and docinfo.data.doc_props.title ~= "" and docinfo.data.doc_props.title or title authors = docinfo.data.doc_props.authors and docinfo.data.doc_props.authors ~= "" and docinfo.data.doc_props.authors or authors series = docinfo.data.doc_props.series and docinfo.data.doc_props.series ~= "" and docinfo.data.doc_props.series or series end -- Unable to set time_left_chapter and time_left_document without ReaderUI, so leave N/A end local replace = { ["%c"] = currentpage, ["%t"] = totalpages, ["%p"] = percent, ["%T"] = title, ["%A"] = authors, ["%S"] = series, ["%h"] = time_left_chapter, ["%H"] = time_left_document, } ret = ret:gsub("(%%%a)", replace) return ret end local function addOverlayMessage(widget, text) local FrameContainer = require("ui/widget/container/framecontainer") local RightContainer = require("ui/widget/container/rightcontainer") local Size = require("ui/size") local TextWidget = require("ui/widget/textwidget") local face = Font:getFace("infofont") local screen_w, screen_h = Screen:getWidth(), Screen:getHeight() local textw = TextWidget:new{ text = text, face = face, } -- Don't make our message reach full screen width if textw:getWidth() > screen_w * 0.9 then -- Text too wide: use TextBoxWidget for multi lines display textw = TextBoxWidget:new{ text = text, face = face, width = math.floor(screen_w * 0.9) } end textw = FrameContainer:new{ background = Blitbuffer.COLOR_WHITE, bordersize = Size.border.default, margin = 0, textw, } textw = RightContainer:new{ dimen = { w = screen_w, h = textw:getSize().h, }, textw, } widget = OverlapGroup:new{ dimen = { h = screen_w, w = screen_h, }, widget, textw, } return widget end function Screensaver:chooseFolder() local buttons = {} table.insert(buttons, { { text = _("Choose screensaver folder"), callback = function() UIManager:close(self.choose_dialog) require("ui/downloadmgr"):new{ onConfirm = function(path) logger.dbg("set screensaver directory to", path) G_reader_settings:saveSetting("screensaver_dir", path) UIManager:show(InfoMessage:new{ text = T(_("Screensaver folder set to:\n%1"), BD.dirpath(path)), timeout = 3, }) end, }:chooseDir() end, } }) table.insert(buttons, { { text = _("Close"), callback = function() UIManager:close(self.choose_dialog) end, } }) local screensaver_dir = G_reader_settings:readSetting("screensaver_dir") or _("N/A") self.choose_dialog = ButtonDialogTitle:new{ title = T(_("Current screensaver image folder:\n%1"), BD.dirpath(screensaver_dir)), buttons = buttons } UIManager:show(self.choose_dialog) end function Screensaver:chooseFile(document_cover) local text = document_cover and _("Choose document cover") or _("Choose screensaver image") local buttons = {} table.insert(buttons, { { text = text, callback = function() UIManager:close(self.choose_dialog) local PathChooser = require("ui/widget/pathchooser") local path_chooser = PathChooser:new{ select_directory = false, select_file = true, file_filter = function(filename) local suffix = util.getFileNameSuffix(filename) if document_cover and DocumentRegistry:hasProvider(filename) then return true elseif self.screensaver_provider[suffix] then return true end end, detailed_file_info = true, path = self.root_path, onConfirm = function(file_path) if document_cover then G_reader_settings:saveSetting("screensaver_document_cover", file_path) UIManager:show(InfoMessage:new{ text = T(_("Screensaver document cover set to:\n%1"), BD.filepath(file_path)), timeout = 3, }) else G_reader_settings:saveSetting("screensaver_image", file_path) UIManager:show(InfoMessage:new{ text = T(_("Screensaver image set to:\n%1"), BD.filepath(file_path)), timeout = 3, }) end end } UIManager:show(path_chooser) end, } }) table.insert(buttons, { { text = _("Close"), callback = function() UIManager:close(self.choose_dialog) end, } }) local screensaver_image = G_reader_settings:readSetting("screensaver_image") or _("N/A") local screensaver_document_cover = G_reader_settings:readSetting("screensaver_document_cover") or _("N/A") local title = document_cover and T(_("Current screensaver document cover:\n%1"), BD.filepath(screensaver_document_cover)) or T(_("Current screensaver image:\n%1"), BD.filepath(screensaver_image)) self.choose_dialog = ButtonDialogTitle:new{ title = title, buttons = buttons } UIManager:show(self.choose_dialog) end function Screensaver:isExcluded() local ReaderUI = require("apps/reader/readerui") local ui = ReaderUI:_getRunningInstance() if ui and ui.doc_settings then local doc_settings = ui.doc_settings return doc_settings:isTrue("exclude_screensaver") else if G_reader_settings:hasNot("lastfile") then return false end local lastfile = G_reader_settings:readSetting("lastfile") if DocSettings:hasSidecarFile(lastfile) then local doc_settings = DocSettings:open(lastfile) return doc_settings:isTrue("exclude_screensaver") else -- No DocSetting, not excluded return false end end end function Screensaver:setMessage() local InputDialog = require("ui/widget/inputdialog") local screensaver_message = G_reader_settings:readSetting("screensaver_message") or self.default_screensaver_message self.input_dialog = InputDialog:new{ title = "Screensaver message", description = _("Enter the message to be displayed by the screensaver. The following escape sequences can be used:\n %p percentage read\n %c current page number\n %t total number of pages\n %T title\n %A authors\n %S series\n %h time left in chapter\n %H time left in document"), input = screensaver_message, buttons = { { { text = _("Cancel"), id = "close", callback = function() UIManager:close(self.input_dialog) end, }, { text = _("Set message"), is_enter_default = true, callback = function() G_reader_settings:saveSetting("screensaver_message", self.input_dialog:getInputText()) UIManager:close(self.input_dialog) end, }, }, }, } UIManager:show(self.input_dialog) self.input_dialog:onShowKeyboard() end function Screensaver:setStretchLimit(touchmenu_instance) UIManager:show(SpinWidget:new{ value = G_reader_settings:readSetting("screensaver_stretch_limit_percentage", 8), value_min = 0, value_max = 25, default_value = 8, unit = "%", title_text = _("Set maximum stretch limit"), ok_text = _("Set"), ok_always_enabled = true, callback = function(spin) G_reader_settings:saveSetting("screensaver_stretch_limit_percentage", spin.value) G_reader_settings:makeTrue("screensaver_stretch_images") if touchmenu_instance then touchmenu_instance:updateItems() end end, extra_text = _("Disable stretch"), extra_callback = function() G_reader_settings:makeFalse("screensaver_stretch_images") if touchmenu_instance then touchmenu_instance:updateItems() end end, option_text = _("Full stretch"), option_callback = function() G_reader_settings:makeTrue("screensaver_stretch_images") G_reader_settings:delSetting("screensaver_stretch_limit_percentage") if touchmenu_instance then touchmenu_instance:updateItems() end end, }) end -- When called after setup(), may not match the saved settings, because it accounts for fallbacks that might have kicked in. function Screensaver:getMode() return self.screensaver_type end function Screensaver:modeExpectsPortrait() return self.screensaver_type ~= "message" and self.screensaver_type ~= "disable" and self.screensaver_type ~= "readingprogress" and self.screensaver_type ~= "bookstatus" end function Screensaver:modeIsImage() return self.screensaver_type == "cover" or self.screensaver_type == "random_image" or self.screensaver_type == "image_file" end function Screensaver:withBackground() return self.screensaver_background ~= "none" end function Screensaver:setup(event, fallback_message) self.show_message = G_reader_settings:isTrue("screensaver_show_message") self.screensaver_type = G_reader_settings:readSetting("screensaver_type") local screensaver_img_background = G_reader_settings:readSetting("screensaver_img_background") local screensaver_msg_background = G_reader_settings:readSetting("screensaver_msg_background") -- These 2 (optional) parameters are to support poweroff and reboot actions on Kobo (c.f., UIManager) self.prefix = event and event .. "_" or "" -- "", "poweroff_" or "reboot_" self.fallback_message = fallback_message self.overlay_message = nil if G_reader_settings:has(self.prefix .. "screensaver_type") then self.screensaver_type = G_reader_settings:readSetting(self.prefix .. "screensaver_type") else if event and G_reader_settings:isFalse("screensaver_hide_fallback_msg") then -- Display the provided fallback_message over the screensaver, -- so the user can distinguish between suspend (no overlay), -- and reboot/poweroff (overlaid message). self.overlay_message = self.fallback_message end end -- Reset state self.lastfile = nil self.image = nil self.image_file = nil -- Check lastfile and setup the requested mode's resources, or a fallback mode if the required resources are unavailable. local ReaderUI = require("apps/reader/readerui") local ui = ReaderUI:_getRunningInstance() self.lastfile = G_reader_settings:readSetting("lastfile") if self.screensaver_type == "document_cover" then -- Set lastfile to the document of which we want to show the cover. self.lastfile = G_reader_settings:readSetting("screensaver_document_cover") self.screensaver_type = "cover" end if self.screensaver_type == "cover" then self.lastfile = self.lastfile ~= nil and self.lastfile or G_reader_settings:readSetting("lastfile") local excluded if DocSettings:hasSidecarFile(self.lastfile) then local doc_settings if ui and ui.doc_settings then doc_settings = ui.doc_settings else doc_settings = DocSettings:open(self.lastfile) end excluded = doc_settings:isTrue("exclude_screensaver") else -- No DocSetting, not excluded excluded = false end if not excluded then if self.lastfile and lfs.attributes(self.lastfile, "mode") == "file" then if ui and ui.document then local doc = ui.document self.image = doc:getCoverPageImage() else local doc = DocumentRegistry:openDocument(self.lastfile) if doc.loadDocument then -- CreDocument doc:loadDocument(false) -- load only metadata end self.image = doc:getCoverPageImage() doc:close() end if self.image == nil then self.screensaver_type = "random_image" end else self.screensaver_type = "random_image" end else -- Fallback to random images if this book cover is excluded self.screensaver_type = "random_image" end end if self.screensaver_type == "bookstatus" then if self.lastfile and lfs.attributes(self.lastfile, "mode") == "file" then if not ui then self.screensaver_type = "disable" self.show_message = true end else self.screensaver_type = "disable" self.show_message = true end end if self.screensaver_type == "random_image" then local screensaver_dir = G_reader_settings:readSetting(self.prefix .. "screensaver_dir") or G_reader_settings:readSetting("screensaver_dir") self.image_file = self:_getRandomImage(screensaver_dir) if self.image_file == nil then self.screensaver_type = "disable" self.show_message = true end end if self.screensaver_type == "image_file" then self.image_file = G_reader_settings:readSetting(self.prefix .. "screensaver_image") or G_reader_settings:readSetting("screensaver_image") if self.image_file == nil or lfs.attributes(self.image_file, "mode") ~= "file" then self.screensaver_type = "disable" self.show_message = true end end if self.screensaver_type == "readingprogress" then -- This is implemented by the Statistics plugin if Screensaver.getReaderProgress == nil then self.screensaver_type = "disable" self.show_message = true end end -- Use the right background setting depending on the effective mode, now that fallbacks have kicked in. if self:modeIsImage() then self.screensaver_background = screensaver_img_background else self.screensaver_background = screensaver_msg_background end end function Screensaver:show() if self.screensaver_widget then UIManager:close(self.screensaver_widget) self.screensaver_widget = nil end -- In as-is mode with no message and no overlay, we've got nothing to show :) if self.screensaver_type == "disable" and not self.show_message and not self.overlay_message then return end -- We mostly always suspend in Portrait/Inverted Portrait mode... -- ... except when we just show an InfoMessage or when the screensaver -- is disabled, as it plays badly with Landscape mode (c.f., #4098 and #5290). -- We also exclude full-screen widgets that work fine in Landscape mode, -- like ReadingProgress and BookStatus (c.f., #5724) if self:modeExpectsPortrait() then Device.orig_rotation_mode = Screen:getRotationMode() -- Leave Portrait & Inverted Portrait alone, that works just fine. if bit.band(Device.orig_rotation_mode, 1) == 1 then -- i.e., only switch to Portrait if we're currently in *any* Landscape orientation (odd number) Screen:setRotationMode(Screen.ORIENTATION_PORTRAIT) else Device.orig_rotation_mode = nil end -- On eInk, if we're using a screensaver mode that shows an image, -- flash the screen to white first, to eliminate ghosting. if Device:hasEinkScreen() and self:modeIsImage() then if self:withBackground() then Screen:clear() end Screen:refreshFull() -- On Kobo, on sunxi SoCs with a recent kernel, wait a tiny bit more to avoid weird refresh glitches... if Device:isKobo() and Device:isSunxi() then ffiUtil.usleep(150 * 1000) end end else -- nil it, in case user switched ScreenSaver modes during our lifetime. Device.orig_rotation_mode = nil end -- Build the main widget for the effective mode, all the sanity checks were handled in setup local widget = nil if self.screensaver_type == "cover" then widget = ImageWidget:new{ image = self.image, image_disposable = true, width = Screen:getWidth(), height = Screen:getHeight(), scale_factor = G_reader_settings:isFalse("screensaver_stretch_images") and 0 or nil, stretch_limit_percentage = G_reader_settings:readSetting("screensaver_stretch_limit_percentage"), } elseif self.screensaver_type == "bookstatus" then local ReaderUI = require("apps/reader/readerui") local ui = ReaderUI:_getRunningInstance() local doc = ui.document local doc_settings = ui.doc_settings widget = BookStatusWidget:new{ thumbnail = doc:getCoverPageImage(), props = doc:getProps(), document = doc, settings = doc_settings, ui = ui, readonly = true, } elseif self.screensaver_type == "random_image" or self.screensaver_type == "image_file" then widget = ImageWidget:new{ file = self.image_file, file_do_cache = false, alpha = true, width = Screen:getWidth(), height = Screen:getHeight(), scale_factor = G_reader_settings:isFalse("screensaver_stretch_images") and 0 or nil, stretch_limit_percentage = G_reader_settings:readSetting("screensaver_stretch_limit_percentage"), } elseif self.screensaver_type == "readingprogress" then widget = Screensaver.getReaderProgress() end -- Assume that we'll be covering the full-screen by default (either because of a widget, or a background fill). local covers_fullscreen = true -- Speaking of, set that background fill up... local background if self.screensaver_background == "black" then background = Blitbuffer.COLOR_BLACK elseif self.screensaver_background == "white" then background = Blitbuffer.COLOR_WHITE elseif self.screensaver_background == "none" then background = nil end if self.show_message then -- Handle user settings & fallbacks, with that prefix mess on top... local screensaver_message if G_reader_settings:has(self.prefix .. "screensaver_message") then screensaver_message = G_reader_settings:readSetting(self.prefix .. "screensaver_message") else if G_reader_settings:has("screensaver_message") then -- We prefer the global user setting to the event's fallback message. screensaver_message = G_reader_settings:readSetting("screensaver_message") else screensaver_message = self.fallback_message or self.default_screensaver_message end end -- NOTE: Only attempt to expand if there are special characters in the message. if screensaver_message:find("%%") then screensaver_message = self:expandSpecial(screensaver_message, self.fallback_message or self.default_screensaver_message) end local message_pos if G_reader_settings:has(self.prefix .. "screensaver_message_position") then message_pos = G_reader_settings:readSetting(self.prefix .. "screensaver_message_position") else message_pos = G_reader_settings:readSetting("screensaver_message_position") end -- The only case where we *won't* cover the full-screen is when we only display a message and no background. if widget == nil and self.screensaver_background == "none" then covers_fullscreen = false end local message_widget if message_pos == "middle" then message_widget = InfoMessage:new{ text = screensaver_message, readonly = true, dismissable = false, } else local face = Font:getFace("infofont") local container if message_pos == "bottom" then container = BottomContainer else container = TopContainer end local screen_w, screen_h = Screen:getWidth(), Screen:getHeight() message_widget = container:new{ dimen = Geom:new{ w = screen_w, h = screen_h, }, TextBoxWidget:new{ text = screensaver_message, face = face, width = screen_w, alignment = "center", } } end -- No overlay needed as we just displayed *a* message (not necessarily the event's, though). self.overlay_message = nil -- Check if message_widget should be overlaid on another widget if message_widget then if widget then -- We have a Screensaver widget -- Show message_widget on top of previously created widget widget = OverlapGroup:new{ dimen = { w = Screen:getWidth(), h = Screen:getHeight(), }, widget, message_widget, } else -- No prevously created widget, so just show message widget widget = message_widget end end end if self.overlay_message then widget = addOverlayMessage(widget, self.overlay_message) end if widget then self.screensaver_widget = ScreenSaverWidget:new{ widget = widget, background = background, covers_fullscreen = covers_fullscreen, } self.screensaver_widget.modal = true self.screensaver_widget.dithered = true UIManager:show(self.screensaver_widget, "full") end end function Screensaver:close_widget() logger.dbg("close screensaver") if self.screensaver_widget then UIManager:close(self.screensaver_widget) self.screensaver_widget = nil end if self.delayed_close then self.delayed_close = nil end end function Screensaver:close() if self.screensaver_widget == nil then return end local screensaver_delay = G_reader_settings:readSetting("screensaver_delay") local screensaver_delay_number = tonumber(screensaver_delay) if screensaver_delay_number then UIManager:scheduleIn(screensaver_delay_number, self.close_widget, self) self.delayed_close = true elseif screensaver_delay == "disable" then self:close_widget() elseif screensaver_delay == "gesture" then if self.screensaver_widget then self.screensaver_widget:showWaitForGestureMessage() end else logger.dbg("tap to exit from screensaver") end end return Screensaver