diff --git a/frontend/optmath.lua b/frontend/optmath.lua index 178ebdb0d..7a22612a3 100644 --- a/frontend/optmath.lua +++ b/frontend/optmath.lua @@ -3,6 +3,7 @@ Simple math helper functions ]] local bit = require("bit") +local dbg = require("dbg") local Math = {} @@ -102,4 +103,26 @@ function Math.tmax(tab, func) return tmin_max(tab, func, "max") end +--[[-- +Restricts a value within an interval. + +@number value +@number min +@number max +@treturn number value clamped to the interval [min,max] +]] +function Math.clamp(value, min, max) + if value <= min then + return min + elseif value >= max then + return max + end + return value +end +dbg:guard(Math, "minmax", + function(value, min, max) + assert(min ~= nil and max ~= nil, "Math.clamp: min " .. min .. " and max " .. nil .. " must not be nil") + assert(min < max, "Math.clamp: min .. " .. min .. " must be less than max " .. max) + end) + return Math diff --git a/frontend/ui/widget/datetimewidget.lua b/frontend/ui/widget/datetimewidget.lua index fe89d4f43..fbc570ed9 100644 --- a/frontend/ui/widget/datetimewidget.lua +++ b/frontend/ui/widget/datetimewidget.lua @@ -19,6 +19,7 @@ local VerticalSpan = require("ui/widget/verticalspan") local WidgetContainer = require("ui/widget/container/widgetcontainer") local _ = require("gettext") local Screen = Device.screen +local T = require("ffi/util").template local DateTimeWidget = InputContainer:new{ title_face = Font:getFace("x_smalltfont"), @@ -64,33 +65,34 @@ function DateTimeWidget:init() end -- Actually the widget layout - self:update() + self:layout() end -function DateTimeWidget:update() - local year_widget = NumberPickerWidget:new{ +local year_widget, month_hour_widget, day_min_widget +function DateTimeWidget:layout() + year_widget = NumberPickerWidget:new{ show_parent = self, value = self.year, value_min = 2021, value_max = 2041, value_step = 1, - value_hold_step = 4, + value_hold_step = self.year_hold_step or 4, } - local month_hour_widget = NumberPickerWidget:new{ + month_hour_widget = NumberPickerWidget:new{ show_parent = self, value = self.is_date and self.month or self.hour, - value_min = self.is_date and 1 or 0, - value_max = self.is_date and 12 or self.hour_max, + value_min = self.hour_min or self.month_min or (self.is_date and 1 or 0), + value_max = self.hour_max or self.month_max or (self.is_date and 12 or 24), value_step = 1, - value_hold_step = 3, + value_hold_step = self.hour_hold_step or self.month_hold_step or 3, } - local day_min_widget = NumberPickerWidget:new{ + day_min_widget = NumberPickerWidget:new{ show_parent = self, value = self.is_date and self.day or self.min, - value_min = self.is_date and 1 or 0, - value_max = self.is_date and 31 or 59, + value_min = self.min_min or self.day_min or (self.is_date and 1 or 0), + value_max = self.min_max or self.day_max or (self.is_date and 31 or 59), value_step = 1, - value_hold_step = self.is_date and 5 or 10, + value_hold_step = self.day_hold_step or self.min_hold_step or (self.is_date and 5 or 10), date_month_hour = month_hour_widget, date_year = year_widget, } @@ -146,49 +148,59 @@ function DateTimeWidget:update() else date_info = VerticalSpan:new{ width = 0 } end - local buttons = { - { - { - text = self.cancel_text, - callback = function() - self:onClose() - end, - }, + + local buttons = {} + if self.default_value then + table.insert(buttons, { { - text = self.ok_text, + text = self.default_text or T(_("Default value: %1"), self.default_value), callback = function() - if self.callback then - self.year = year_widget:getValue() - if self.is_date then - self.month = month_hour_widget:getValue() - self.day = day_min_widget:getValue() - else - self.hour = month_hour_widget:getValue() - self.min = day_min_widget:getValue() - end - self:callback(self) + if self.default_callback then + self.default_callback(year_widget:getValue(), month_hour_widget:getValue(), + day_min_widget:getValue()) + end + if not self.keep_shown_on_apply then -- assume extra wants it same as ok + self:onClose() end - self:onClose() end, }, - } - } + }) + end if self.extra_text then - table.insert(buttons,{ + table.insert(buttons, { { text = self.extra_text, callback = function() - if self.extra_callback then - self.extra_callback(year_widget:getValue(), month_hour_widget:getValue(), - day_min_widget:getValue()) - end - if not self.keep_shown_on_apply then -- assume extra wants it same as ok - self:onClose() - end + self.extra_callback(self) end, }, }) end + table.insert(buttons, { + { + text = self.cancel_text, + callback = function() + self:onClose() + end, + }, + { + text = self.ok_text, + callback = function() + if self.callback then + self.year = year_widget:getValue() + if self.is_date then + self.month = month_hour_widget:getValue() + self.day = day_min_widget:getValue() + else + self.hour = month_hour_widget:getValue() + self.min = day_min_widget:getValue() + end + self:callback(self) + end + self:onClose() + end, + }, + }) local ok_cancel_buttons = ButtonTable:new{ width = self.width - 2*Size.padding.default, @@ -242,6 +254,15 @@ function DateTimeWidget:update() end) end +function DateTimeWidget:update(left, mid, right) + year_widget.value = left + year_widget:update() + month_hour_widget.value = mid + month_hour_widget:update() + day_min_widget.value = right + day_min_widget:update() +end + function DateTimeWidget:onCloseWidget() UIManager:setDirty(nil, function() return "ui", self.date_frame.dimen diff --git a/frontend/util.lua b/frontend/util.lua index 51ff1e46d..7f81916f0 100644 --- a/frontend/util.lua +++ b/frontend/util.lua @@ -111,7 +111,7 @@ Source: https://gist.github. ---- @int seconds number of seconds ---- @bool withoutSeconds if true 00:00, if false 00:00:00 ---- @treturn string clock string in the form of 00:00 or 00:00:00 -function util.secondsToClock(seconds, withoutSeconds) +function util.secondsToClock(seconds, withoutSeconds, withDays) seconds = tonumber(seconds) if not seconds then if withoutSeconds then @@ -127,7 +127,14 @@ function util.secondsToClock(seconds, withoutSeconds) end else local round = withoutSeconds and require("optmath").round or passthrough - local hours = string.format("%02d", seconds / 3600) + local days = "0" + local hours + if withDays then + days = string.format("%d", seconds / (24*3600)) -- implicit math.floor for string.format + hours = string.format("%02d", (seconds / 3600) % 24) + else + hours = string.format("%02d", seconds / 3600) + end local mins = string.format("%02d", round(seconds % 3600 / 60)) if withoutSeconds then if mins == "60" then @@ -135,10 +142,10 @@ function util.secondsToClock(seconds, withoutSeconds) mins = string.format("%02d", 0) hours = string.format("%02d", hours + 1) end - return hours .. ":" .. mins + return (days ~= "0" and (days .. "d") or "") .. hours .. ":" .. mins else local secs = string.format("%02d", seconds % 60) - return hours .. ":" .. mins .. ":" .. secs + return (days ~= "0" and (days .. "d") or "") .. hours .. ":" .. mins .. ":" .. secs end end end @@ -147,8 +154,9 @@ end ---- @int seconds number of seconds ---- @bool withoutSeconds if true 1h30', if false 1h30'10'' ---- @bool hmsFormat, if true format 1h30m10s +---- @bool withDays, if true format 1d12h30m10s ---- @treturn string clock string in the form of 1h30'10'' or 1h30m10s -function util.secondsToHClock(seconds, withoutSeconds, hmsFormat) +function util.secondsToHClock(seconds, withoutSeconds, hmsFormat, withDays) local SECONDS_SYMBOL = "\"" seconds = tonumber(seconds) if seconds == 0 then @@ -189,7 +197,7 @@ function util.secondsToHClock(seconds, withoutSeconds, hmsFormat) end end else - local time_string = util.secondsToClock(seconds, withoutSeconds) + local time_string = util.secondsToClock(seconds, withoutSeconds, withDays) if withoutSeconds then time_string = time_string .. ":" end @@ -198,15 +206,15 @@ function util.secondsToHClock(seconds, withoutSeconds, hmsFormat) time_string = time_string:gsub(":", _("h"), 1) -- @translators This is the 'm' for minute, like in 1h30m30s. This is a duration. time_string = time_string:gsub(":", _("m"), 1) - -- @translators This is the 's' for second, like in 1h30m30s. This is a duration. - time_string = time_string:gsub("00" .. _("h"), "") -- delete leading "00h" + time_string = time_string:gsub("^00" .. _("h"), "") -- delete leading "00h" time_string = time_string:gsub("^0", "") -- delete leading "0" + -- @translators This is the 's' for second, like in 1h30m30s. This is a duration. return withoutSeconds and time_string or (time_string .. _("s")) else -- @translators This is the 'h' for hour, like in 1h30m30s. This is a duration. time_string = time_string:gsub(":", _("h"), 1) time_string = time_string:gsub(":", "'", 1) - time_string = time_string:gsub("00" .. _("h"), "") -- delete leading "00h" + time_string = time_string:gsub("^00" .. _("h"), "") -- delete leading "00h" time_string = time_string:gsub("^0", "") -- delete leading "0" return withoutSeconds and time_string or (time_string .. SECONDS_SYMBOL) end @@ -218,13 +226,14 @@ end ---- @string Either "modern" for 1h30'10" or "classic" for 1:30:10 ---- @bool withoutSeconds if true 1h30' or 1h30m, if false 1h30'10" or 1h30m10s ---- @bool hmsFormat, modern format only, if true format 1h30m or 1h30m10s +---- @bool withDays, if hours>=24 include days in clock string 1d12h10m10s ---- @treturn string clock string in the specific format of 1h30', 1h30'10" resp. 1h30m, 1h30m10s -function util.secondsToClockDuration(format, seconds, withoutSeconds, hmsFormat) +function util.secondsToClockDuration(format, seconds, withoutSeconds, hmsFormat, withDays) if format == "modern" then - return util.secondsToHClock(seconds, withoutSeconds, hmsFormat) + return util.secondsToHClock(seconds, withoutSeconds, hmsFormat, withDays) else -- Assume "classic" to give safe default - return util.secondsToClock(seconds, withoutSeconds) + return util.secondsToClock(seconds, withoutSeconds, withDays) end end diff --git a/plugins/autosuspend.koplugin/main.lua b/plugins/autosuspend.koplugin/main.lua index d2963583e..915f6c3e1 100644 --- a/plugins/autosuspend.koplugin/main.lua +++ b/plugins/autosuspend.koplugin/main.lua @@ -17,10 +17,11 @@ local WidgetContainer = require("ui/widget/container/widgetcontainer") local logger = require("logger") local util = require("util") local _ = require("gettext") +local Math = require("optmath") local T = require("ffi/util").template -local default_autoshutdown_timeout_seconds = 3*24*60*60 -local default_auto_suspend_timeout_seconds = 60*60 +local default_autoshutdown_timeout_seconds = 3*24*60*60 -- three days +local default_auto_suspend_timeout_seconds = 15*60 -- 15 minutes local AutoSuspend = WidgetContainer:new{ name = "autosuspend", @@ -107,8 +108,10 @@ end function AutoSuspend:init() logger.dbg("AutoSuspend: init") if Device:isPocketBook() and not Device:canSuspend() then return end - self.autoshutdown_timeout_seconds = G_reader_settings:readSetting("autoshutdown_timeout_seconds") or default_autoshutdown_timeout_seconds - self.auto_suspend_timeout_seconds = G_reader_settings:readSetting("auto_suspend_timeout_seconds") or default_auto_suspend_timeout_seconds + self.autoshutdown_timeout_seconds = G_reader_settings:readSetting("autoshutdown_timeout_seconds", + default_autoshutdown_timeout_seconds) + self.auto_suspend_timeout_seconds = G_reader_settings:readSetting("auto_suspend_timeout_seconds", + default_auto_suspend_timeout_seconds) UIManager.event_hook:registerWidget("InputEvent", self) -- We need an instance-specific function reference to schedule, because in some rare cases, @@ -169,87 +172,121 @@ function AutoSuspend:onPreventStandby() self.standby_prevented = true end +function AutoSuspend:setSuspendShutdownTimes(touchmenu_instance, title, info, setting, + default_value, range, is_day_hour) + -- Attention if is_day_hour then time.hour stands for days and time.min for hours + + local InfoMessage = require("ui/widget/infomessage") + local DateTimeWidget = require("ui/widget/datetimewidget") + + local setting_val = self[setting] > 0 and self[setting] or default_value + + local left_val = is_day_hour and math.floor(setting_val / (24*3600)) + or math.floor(setting_val / 3600) + local right_val = is_day_hour and math.floor(setting_val / 3600) % 24 + or math.floor((setting_val / 60) % 60) + local time_spinner + time_spinner = DateTimeWidget:new { + is_date = false, + hour = left_val, + min = right_val, + hour_hold_step = 5, + min_hold_step = 10, + hour_max = is_day_hour and math.floor(range[2] / (24*3600)) or math.floor(range[2] / 3600), + min_max = is_day_hour and 23 or 59, + ok_text = _("Set timeout"), + title_text = title, + info_text = info, + callback = function(time) + self[setting] = is_day_hour and (time.hour * 24 * 3600 + time.min * 3600) + or (time.hour * 3600 + time.min * 60) + self[setting] = Math.clamp(self[setting], range[1], range[2]) + G_reader_settings:saveSetting(setting, self[setting]) + self:_unschedule() + self:_start() + if touchmenu_instance then touchmenu_instance:updateItems() end + local time_string = util.secondsToClockDuration("modern", self[setting], true, true, true) + time_string = time_string:gsub("00m","") + UIManager:show(InfoMessage:new{ + text = T(_("%1: %2"), title, time_string), + timeout = 3, + }) + end, + default_value = util.secondsToClockDuration("modern", default_value, true, true, true):gsub("00m$",""), + default_callback = function() + local hour = is_day_hour and math.floor(default_value / (24*3600)) + or math.floor(default_value / 3600) + local min = is_day_hour and math.floor(default_value / 3600) % 24 + or math.floor(default_value / 60) % 60 + time_spinner:update(nil, hour, min) + end, + extra_text = _("Disable"), + extra_callback = function(_self) + self[setting] = -1 -- disable with a negative time/number + self:_unschedule() + if touchmenu_instance then touchmenu_instance:updateItems() end + UIManager:show(InfoMessage:new{ + text = T(_("%1: disabled"), title), + timeout = 3, + }) + _self:onClose() + end, + keep_shown_on_apply = true, + } + UIManager:show(time_spinner) +end + function AutoSuspend:addToMainMenu(menu_items) menu_items.autosuspend = { sorting_hint = "device", + checked_func = function() + return self:_enabled() + end, text_func = function() - if self.auto_suspend_timeout_seconds then - local duration_format = G_reader_settings:readSetting("duration_format", "classic") - return T(_("Autosuspend timeout: %1"), - util.secondsToClockDuration(duration_format, self.auto_suspend_timeout_seconds, true)) + if self.auto_suspend_timeout_seconds and self.auto_suspend_timeout_seconds > 0 then + local time_string = util.secondsToClockDuration("modern", + self.auto_suspend_timeout_seconds, true, true, true):gsub("00m$","") + return T(_("Autosuspend timeout: %1"), time_string) else return _("Autosuspend timeout") end end, keep_menu_open = true, callback = function(touchmenu_instance) - local InfoMessage = require("ui/widget/infomessage") - local SpinWidget = require("ui/widget/spinwidget") - local autosuspend_spin = SpinWidget:new { - value = self.auto_suspend_timeout_seconds / 60, - value_min = 5, - value_max = 240, - value_hold_step = 15, - ok_text = _("Set timeout"), - title_text = _("Timeout in minutes"), - callback = function(autosuspend_spin) - self.auto_suspend_timeout_seconds = autosuspend_spin.value * 60 - G_reader_settings:saveSetting("auto_suspend_timeout_seconds", self.auto_suspend_timeout_seconds) - UIManager:show(InfoMessage:new{ - text = T(_("The system will automatically suspend after %1 minutes of inactivity."), - string.format("%.2f", self.auto_suspend_timeout_seconds / 60)), - timeout = 3, - }) - self:_unschedule() - self:_start() - if touchmenu_instance then touchmenu_instance:updateItems() end - end - } - UIManager:show(autosuspend_spin) + -- 60 sec (1') is the minimum and 24*3600 sec (1day) is the maximum suspend time. + -- A suspend time of one day seems to be excessive. + -- But or battery testing it might give some sense. + self:setSuspendShutdownTimes(touchmenu_instance, + _("Timeout for autosuspend"), _("Enter time in hours and minutes."), + "auto_suspend_timeout_seconds", default_auto_suspend_timeout_seconds, + {60, 24*3600}, false) end, } if not (Device:canPowerOff() or Device:isEmulator()) then return end menu_items.autoshutdown = { sorting_hint = "device", + checked_func = function() + return self:_enabledShutdown() + end, text_func = function() - if self.autoshutdown_timeout_seconds then - local duration_format = G_reader_settings:readSetting("duration_format", "classic") - return T(_("Autoshutdown timeout: %1"), - util.secondsToClockDuration(duration_format, self.autoshutdown_timeout_seconds, true)) + if self.autoshutdown_timeout_seconds and self.autoshutdown_timeout_seconds > 0 then + local time_string = util.secondsToClockDuration("modern", + self.autoshutdown_timeout_seconds, true, true, true):gsub("00m$","") + return T(_("Autoshutdown timeout: %1"), time_string) else return _("Autoshutdown timeout") end end, keep_menu_open = true, callback = function(touchmenu_instance) - local InfoMessage = require("ui/widget/infomessage") - local SpinWidget = require("ui/widget/spinwidget") - local autosuspend_spin = SpinWidget:new { - value = self.autoshutdown_timeout_seconds / 60 / 60, - -- About a minute, good for testing and battery life fanatics. - -- Just high enough to avoid an instant shutdown death scenario. - value_min = 0.017, - -- More than three weeks seems a bit excessive if you want to enable authoshutdown, - -- even if the battery can last up to three months. - value_max = 28*24, - value_hold_step = 24, - precision = "%.2f", - ok_text = _("Set timeout"), - title_text = _("Timeout in hours"), - callback = function(autosuspend_spin) - self.autoshutdown_timeout_seconds = math.floor(autosuspend_spin.value * 60 * 60) - G_reader_settings:saveSetting("autoshutdown_timeout_seconds", self.autoshutdown_timeout_seconds) - UIManager:show(InfoMessage:new{ - text = T(_("The system will automatically shut down after %1 hours of inactivity."), - string.format("%.2f", self.autoshutdown_timeout_seconds / 60 / 60)), - timeout = 3, - }) - self:_unschedule() - self:_start() - if touchmenu_instance then touchmenu_instance:updateItems() end - end - } - UIManager:show(autosuspend_spin) + -- 5*60 sec (5') is the minimum and 28*24*3600 (28days) is the maximum shutdown time. + -- Minimum time has to be big enough, to avoid start-stop death scenarious. + -- Maximum more than four weeks seems a bit excessive if you want to enable authoshutdown, + -- even if the battery can last up to three months. + self:setSuspendShutdownTimes(touchmenu_instance, + _("Timeout for autoshutdown"), _("Enter time in days and hours."), + "autoshutdown_timeout_seconds", default_autoshutdown_timeout_seconds, + {5*60, 28*24*3600}, true) end, } end diff --git a/spec/unit/optmath_spec.lua b/spec/unit/optmath_spec.lua index d7f316cce..a9aab5cce 100644 --- a/spec/unit/optmath_spec.lua +++ b/spec/unit/optmath_spec.lua @@ -58,4 +58,16 @@ describe("Math module", function() assert.are.same("even", Math.oddEven(0)) end) + describe("minmax", function() + it("should return a value within a range", function() + assert.are.same(-1, Math.clamp(-2, -1, 10)) + assert.are.same(-1, Math.clamp(-1, -1, 10)) + assert.are.same(0, Math.clamp(0, -1, 10)) + assert.are.same(5, Math.clamp(5, -1, 10)) + assert.are.same(9, Math.clamp(9, -1, 10)) + assert.are.same(10, Math.clamp(10, -1, 10)) + assert.are.same(10, Math.clamp(11, -1, 10)) + end) + end) + end)