2
0
mirror of https://github.com/koreader/koreader synced 2024-11-10 01:10:34 +00:00
koreader/plugins/autowarmth.koplugin/main.lua

1057 lines
43 KiB
Lua
Raw Normal View History

--[[--
Plugin for setting screen warmth based on the sun position and/or a time schedule
@module koplugin.autowarmth
--]]--
local ConfirmBox = require("ui/widget/confirmbox")
local Device = require("device")
local DateTimeWidget = require("ui/widget/datetimewidget")
local DoubleSpinWidget = require("/ui/widget/doublespinwidget")
local DeviceListener = require("device/devicelistener")
local Dispatcher = require("dispatcher")
local Event = require("ui/event")
local FFIUtil = require("ffi/util")
local Font = require("ui/font")
local InfoMessage = require("ui/widget/infomessage")
local InputDialog = require("ui/widget/inputdialog")
local Notification = require("ui/widget/notification")
local SpinWidget = require("ui/widget/spinwidget")
local SunTime = require("suntime")
local TextWidget = require("ui/widget/textwidget")
local UIManager = require("ui/uimanager")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local logger = require("logger")
local _ = require("gettext")
local C_ = _.pgettext
local Powerd = Device.powerd
local T = FFIUtil.template
local Screen = require("device").screen
local util = require("util")
local activate_sun = 1
local activate_schedule = 2
local activate_closer_noon = 3
local activate_closer_midnight = 4
local midnight_index = 11
local device_max_warmth = Device:hasNaturalLight() and Powerd.fl_warmth_max or 100
local device_warmth_fit_scale = device_max_warmth / 100
local function frac(x)
return x - math.floor(x)
end
Clarify our OOP semantics across the codebase (#9586) Basically: * Use `extend` for class definitions * Use `new` for object instantiations That includes some minor code cleanups along the way: * Updated `Widget`'s docs to make the semantics clearer. * Removed `should_restrict_JIT` (it's been dead code since https://github.com/koreader/android-luajit-launcher/pull/283) * Minor refactoring of LuaSettings/LuaData/LuaDefaults/DocSettings to behave (mostly, they are instantiated via `open` instead of `new`) like everything else and handle inheritance properly (i.e., DocSettings is now a proper LuaSettings subclass). * Default to `WidgetContainer` instead of `InputContainer` for stuff that doesn't actually setup key/gesture events. * Ditto for explicit `*Listener` only classes, make sure they're based on `EventListener` instead of something uselessly fancier. * Unless absolutely necessary, do not store references in class objects, ever; only values. Instead, always store references in instances, to avoid both sneaky inheritance issues, and sneaky GC pinning of stale references. * ReaderUI: Fix one such issue with its `active_widgets` array, with critical implications, as it essentially pinned *all* of ReaderUI's modules, including their reference to the `Document` instance (i.e., that was a big-ass leak). * Terminal: Make sure the shell is killed on plugin teardown. * InputText: Fix Home/End/Del physical keys to behave sensibly. * InputContainer/WidgetContainer: If necessary, compute self.dimen at paintTo time (previously, only InputContainers did, which might have had something to do with random widgets unconcerned about input using it as a baseclass instead of WidgetContainer...). * OverlapGroup: Compute self.dimen at *init* time, because for some reason it needs to do that, but do it directly in OverlapGroup instead of going through a weird WidgetContainer method that it was the sole user of. * ReaderCropping: Under no circumstances should a Document instance member (here, self.bbox) risk being `nil`ed! * Kobo: Minor code cleanups.
2022-10-06 00:14:48 +00:00
local AutoWarmth = WidgetContainer:extend{
name = "autowarmth",
Clarify our OOP semantics across the codebase (#9586) Basically: * Use `extend` for class definitions * Use `new` for object instantiations That includes some minor code cleanups along the way: * Updated `Widget`'s docs to make the semantics clearer. * Removed `should_restrict_JIT` (it's been dead code since https://github.com/koreader/android-luajit-launcher/pull/283) * Minor refactoring of LuaSettings/LuaData/LuaDefaults/DocSettings to behave (mostly, they are instantiated via `open` instead of `new`) like everything else and handle inheritance properly (i.e., DocSettings is now a proper LuaSettings subclass). * Default to `WidgetContainer` instead of `InputContainer` for stuff that doesn't actually setup key/gesture events. * Ditto for explicit `*Listener` only classes, make sure they're based on `EventListener` instead of something uselessly fancier. * Unless absolutely necessary, do not store references in class objects, ever; only values. Instead, always store references in instances, to avoid both sneaky inheritance issues, and sneaky GC pinning of stale references. * ReaderUI: Fix one such issue with its `active_widgets` array, with critical implications, as it essentially pinned *all* of ReaderUI's modules, including their reference to the `Document` instance (i.e., that was a big-ass leak). * Terminal: Make sure the shell is killed on plugin teardown. * InputText: Fix Home/End/Del physical keys to behave sensibly. * InputContainer/WidgetContainer: If necessary, compute self.dimen at paintTo time (previously, only InputContainers did, which might have had something to do with random widgets unconcerned about input using it as a baseclass instead of WidgetContainer...). * OverlapGroup: Compute self.dimen at *init* time, because for some reason it needs to do that, but do it directly in OverlapGroup instead of going through a weird WidgetContainer method that it was the sole user of. * ReaderCropping: Under no circumstances should a Document instance member (here, self.bbox) risk being `nil`ed! * Kobo: Minor code cleanups.
2022-10-06 00:14:48 +00:00
sched_times_s = nil, -- array
sched_warmths = nil, -- array
fl_turned_off = nil -- true/false if autowarmth has toggled the frontlight
}
-- get timezone offset in hours (including dst)
function AutoWarmth:getTimezoneOffset()
local utcdate = os.date("!*t")
local localdate = os.date("*t")
return os.difftime(os.time(localdate), os.time(utcdate))/3600
end
function AutoWarmth:init()
self:onDispatcherRegisterActions()
self.ui.menu:registerToMainMenu(self)
self.easy_mode = G_reader_settings:nilOrTrue("autowarmth_easy_mode")
self.activate = G_reader_settings:readSetting("autowarmth_activate") or 0
self.location = G_reader_settings:readSetting("autowarmth_location") or "Geysir"
self.latitude = G_reader_settings:readSetting("autowarmth_latitude") or 64.31 --great Geysir in Iceland
self.longitude = G_reader_settings:readSetting("autowarmth_longitude") or -20.30
self.altitude = G_reader_settings:readSetting("autowarmth_altitude") or 200
self.timezone = G_reader_settings:readSetting("autowarmth_timezone") or 0
self.scheduler_times = G_reader_settings:readSetting("autowarmth_scheduler_times")
or {0.0, 5.5, 6.0, 6.5, 7.0, 13.0, 21.5, 22.0, 22.5, 23.0, 24.0}
self.warmth = G_reader_settings:readSetting("autowarmth_warmth")
or { 90, 90, 80, 60, 20, 20, 20, 60, 80, 90, 90}
self.fl_off_during_day = G_reader_settings:readSetting("autowarmth_fl_off_during_day")
self.control_warmth = G_reader_settings:nilOrTrue("autowarmth_control_warmth")
self.control_nightmode = G_reader_settings:nilOrTrue("autowarmth_control_nightmode")
if not self.control_warmth and not self.control_nightmode then
logger.dbg("AutoWarmth: autowarmth_control_warmth and autowarmth_control_nightmode are both false, set them to true")
self.control_warmth = true
self.control_nightmode = true
end
-- schedule recalculation shortly afer midnight
self:scheduleMidnightUpdate()
end
function AutoWarmth:onDispatcherRegisterActions()
Dispatcher:registerAction("show_ephemeris",
{category="none", event="ShowEphemeris", title=_("Show ephemeris"), general=true})
Dispatcher:registerAction("auto_warmth_off",
{category="none", event="AutoWarmthOff", title=_("Auto warmth off"), screen=true})
Dispatcher:registerAction("auto_warmth_cycle_trough",
{category="none", event="AutoWarmthMode", title=_("Auto warmth cycle through modes"), screen=true})
end
function AutoWarmth:onShowEphemeris()
self:showTimesInfo(_("Information about the sun in"), true, activate_sun, false)
end
function AutoWarmth:onAutoWarmthOff()
self.activate = 0
G_reader_settings:saveSetting("autowarmth_activate", self.activate)
Notification:notify(_("Auto warmth turned off"))
self:scheduleMidnightUpdate()
end
function AutoWarmth:onAutoWarmthMode()
if self.activate > 0 then
self.activate = self.activate - 1
else
self.activate = activate_closer_midnight
end
local notify_text
if self.activate == 0 then
notify_text = _("Auto warmth turned off")
elseif self.activate == activate_sun then
notify_text = _("Auto warmth use sun position")
elseif self.activate == activate_schedule then
notify_text = _("Auto warmth use schedule")
elseif self.activate == activate_closer_midnight then
notify_text = _("Auto warmth use whatever is closer to midnight")
elseif self.activate == activate_closer_noon then
notify_text = _("Auto warmth use whatever is closer to noon")
end
G_reader_settings:saveSetting("autowarmth_activate", self.activate)
Notification:notify(notify_text)
self:scheduleMidnightUpdate()
end
function AutoWarmth:leavePowerSavingState(from_resume)
logger.dbg("AutoWarmth: onResume/onLeaveStandby")
local resume_date = os.date("*t")
-- check if resume and suspend are done on the same day
if resume_date.day == SunTime.date.day and resume_date.month == SunTime.date.month
and resume_date.year == SunTime.date.year then
local now_s = SunTime:getTimeInSec(resume_date)
self:scheduleNextWarmthChange(now_s, self.sched_warmth_index > 1 and self.sched_warmth_index - 1 or 1, from_resume)
-- Reschedule 1sec after midnight
UIManager:scheduleIn(24*3600 + 1 - now_s, self.scheduleMidnightUpdate, self)
else
self:scheduleMidnightUpdate(from_resume) -- resume is on the other day, do all calcs again
end
end
function AutoWarmth:_onResume()
self:leavePowerSavingState(true)
end
function AutoWarmth:_onLeaveStandby()
self:leavePowerSavingState(false)
end
function AutoWarmth:_onSuspend()
UIManager:unschedule(self.scheduleMidnightUpdate)
UIManager:unschedule(self.setWarmth)
end
AutoWarmth._onEnterStandby = AutoWarmth._onSuspend
function AutoWarmth:setEventHandlers()
self.onResume = self._onResume
self.onSuspend = self._onSuspend
self.onEnterStandby = self._onEnterStandby
self.onLeaveStandby = self._onLeaveStandby
end
function AutoWarmth:clearEventHandlers()
self.onResume = nil
self.onSuspend = nil
self.onEnterStandby = nil
self.onLeaveStandby = nil
end
-- from_resume ... true if called from onResume
function AutoWarmth:scheduleMidnightUpdate(from_resume)
logger.dbg("AutoWarmth: scheduleMidnightUpdate")
-- first unschedule all old functions
UIManager:unschedule(self.scheduleMidnightUpdate)
UIManager:unschedule(AutoWarmth.setWarmth)
SunTime:setPosition(self.location, self.latitude, self.longitude, self.timezone, self.altitude, true)
SunTime:setAdvanced()
SunTime:setDate() -- today
SunTime:calculateTimes() -- calculates times in hours
self.sched_times_s = {}
self.sched_warmths = {}
self.fl_turned_off = nil
local function prepareSchedule(times_h, index1, index2)
local time1_h = times_h[index1]
if not time1_h then return end
local time1_s = SunTime:getTimeInSec(time1_h)
self.sched_times_s[#self.sched_times_s + 1] = time1_s
self.sched_warmths[#self.sched_warmths + 1] = self.warmth[index1]
local time2_h = times_h[index2]
if not time2_h then return end -- to near to the pole
local warmth_diff = math.min(self.warmth[index2], 100) - math.min(self.warmth[index1], 100)
local time_diff_s = SunTime:getTimeInSec(time2_h) - time1_s
if warmth_diff ~= 0 and time_diff_s > 0 then
local delta_t = time_diff_s / math.abs(warmth_diff) -- cannot be inf, no problem
local delta_w = warmth_diff > 0 and 1 or -1
for i = 1, math.abs(warmth_diff) - 1 do
local next_warmth = math.min(self.warmth[index1], 100) + delta_w * i
-- only apply warmth for steps the hardware has (e.g. Tolino has 0-10 hw steps
-- which map to warmth 0, 10, 20, 30 ... 100)
if frac(next_warmth * device_warmth_fit_scale) == 0 then
table.insert(self.sched_times_s, time1_s + delta_t * i)
table.insert(self.sched_warmths, next_warmth)
end
end
end
end
if self.activate == activate_sun then
self.current_times_h = {unpack(SunTime.times, 1, midnight_index)}
elseif self.activate == activate_schedule then
self.current_times_h = {unpack(self.scheduler_times, 1, midnight_index)}
else
self.current_times_h = {unpack(SunTime.times, 1, midnight_index)}
if self.activate == activate_closer_noon then
for i = 1, midnight_index do
if not self.current_times_h[i] then
self.current_times_h[i] = self.scheduler_times[i]
elseif self.scheduler_times[i] and
math.abs(self.current_times_h[i]%24 - 12) > math.abs(self.scheduler_times[i]%24 - 12) then
self.current_times_h[i] = self.scheduler_times[i]
end
end
else -- activate_closer_midnight
for i = 1, midnight_index do
if not self.current_times_h[i] then
self.current_times_h[i] = self.scheduler_times[i]
elseif self.scheduler_times[i] and
math.abs(self.current_times_h[i]%24 - 12) < math.abs(self.scheduler_times[i]%24 - 12) then
self.current_times_h[i] = self.scheduler_times[i]
end
end
end
end
if self.easy_mode then
self.current_times_h[1] = nil
self.current_times_h[2] = nil
self.current_times_h[3] = nil
self.current_times_h[6] = nil
self.current_times_h[9] = nil
self.current_times_h[10] = nil
self.current_times_h[11] = nil
end
-- here are dragons
local i = 1
-- find first valid entry
while not self.current_times_h[i] and i <= midnight_index do
i = i + 1
end
local next
while i <= midnight_index do
next = i + 1
-- find next valid entry
while not self.current_times_h[next] and next <= midnight_index do
next = next + 1
end
prepareSchedule(self.current_times_h, i, next)
i = next
end
local now_s = SunTime:getTimeInSec()
-- Reschedule 1sec after midnight
UIManager:scheduleIn(24*3600 + 1 - now_s, self.scheduleMidnightUpdate, self)
-- set event handlers
if self.activate ~= 0 then
-- Schedule the first warmth change
self:scheduleNextWarmthChange(now_s, 1, from_resume)
self:setEventHandlers()
else
self:clearEventHandlers()
end
end
-- schedules the next warmth change
-- search_pos ... start searching from that index
-- from_resume ... true if first call after resume
function AutoWarmth:scheduleNextWarmthChange(time_s, search_pos, from_resume)
logger.dbg("AutoWarmth: scheduleNextWarmthChange")
UIManager:unschedule(AutoWarmth.setWarmth)
if self.activate == 0 or #self.sched_warmths == 0 or search_pos > #self.sched_warmths then
return
end
self.sched_warmth_index = search_pos or 1
-- `actual_warmth` is the value which should be applied now.
-- `next_warmth` is valid `delay_s` seconds after now for resume on some devices (KA1)
-- Most of the times this will be the same as `actual_warmth`.
-- We need both, as we could have a very rapid change in warmth (depending on user settings)
-- or by chance a change in warmth very shortly after (a few ms) resume time.
local delay_s = 1.5
-- Use the last warmth value, so that we have a valid value when resuming after 24:00 but
-- before true midnight. OK, this value is actually not quite the right one, as it is calculated
-- for the current day (and not the previous one), but this is for a corner case
-- and the error is small.
local actual_warmth = self.sched_warmths[self.sched_warmth_index or #self.sched_warmths]
local next_warmth = actual_warmth
for i = self.sched_warmth_index, #self.sched_warmths do
if self.sched_times_s[i] <= time_s then
actual_warmth = self.sched_warmths[i]
local j = i
while from_resume and j <= #self.sched_warmths and self.sched_times_s[j] <= time_s + delay_s do
-- Most times only one iteration through this loop
next_warmth = self.sched_warmths[j]
j = j + 1
end
else
self.sched_warmth_index = i
break
end
end
-- update current warmth immediately
self:setWarmth(actual_warmth, false) -- no setWarmth rescheduling, don't force warmth
if self.sched_warmth_index <= #self.sched_warmths then
local next_sched_time_s = self.sched_times_s[self.sched_warmth_index] - time_s
if next_sched_time_s > 0 then
-- This setWarmth will call scheduleNextWarmthChange which will schedule setWarmth again.
UIManager:scheduleIn(next_sched_time_s, self.setWarmth, self, self.sched_warmths[self.sched_warmth_index], true)
elseif self.sched_warmth_index < #self.sched_warmths then
-- If this really happens under strange conditions, wait until the next full minute to
-- minimize wakeups from standby.
UIManager:scheduleIn(61 - tonumber(os.date("%S")),
self.setWarmth, self, self.sched_warmths[self.sched_warmth_index], true)
else
logger.dbg("AutoWarmth: schedule is over, waiting for midnight update")
end
end
if from_resume then
-- On some strange devices like KA1 setWarmth doesn't work right after a resume so
-- schedule setting of another valid warmth (=`next_warmth`) again (one time).
-- On sane devices this schedule does no harm.
-- see https://github.com/koreader/koreader/issues/8363
UIManager:scheduleIn(delay_s, self.setWarmth, self, next_warmth, false) -- no setWarmth rescheduling, force warmth
end
-- Check if AutoWarmth shall toggle frontlight daytime and twilight
if self.fl_off_during_day then
if time_s >= self.current_times_h[5]*3600 and time_s < self.current_times_h[7]*3600 then
-- during daytime (depending on choosens activation: SunTime, fixed Schedule, closer...
-- turn on frontlight off once, user can override this selection by a gesture
if Powerd:isFrontlightOn() then
if self.fl_turned_off ~= true then -- can be false or nil
Powerd:turnOffFrontlight()
UIManager:broadcastEvent(Event:new("FrontlightTurnedOff"))
end
end
self.fl_turned_off = true
else
-- outside of selected daytime, turn on frontlight once, user can override this selection by a gesture
if Powerd:isFrontlightOff() then
if self.fl_turned_off ~= false then -- can be true or nil
Powerd:turnOnFrontlight()
end
end
self.fl_turned_off = false
end
end
end
-- Set warmth and schedule the next warmth change
function AutoWarmth:setWarmth(val, schedule_next, force_warmth)
if val then
if self.control_nightmode then
DeviceListener:onSetNightMode(val > 100)
end
if Device:hasNaturalLight() and self.control_warmth then
val = math.min(val, 100) -- "mask" night mode
Powerd:setWarmth(val, force_warmth)
end
end
if schedule_next then
local now_s = SunTime:getTimeInSec()
self:scheduleNextWarmthChange(now_s, self.sched_warmth_index, false)
end
end
function AutoWarmth:hoursToClock(hours)
if hours then
hours = hours % 24 * 3600 + 0.01 -- round up, due to reduced precision in settings.reader.lua
end
return util.secondsToClock(hours, self.easy_mode)
end
function AutoWarmth:addToMainMenu(menu_items)
menu_items.autowarmth = {
text = Device:hasNaturalLight() and _("Auto warmth and night mode") or _("Auto night mode"),
checked_func = function() return self.activate ~= 0 end,
sub_item_table_func = function()
return self:getSubMenuItems()
end,
}
end
local function tidy_menu(menu, request)
for i = #menu, 1, -1 do
if menu[i].mode ~=nil then
if menu[i].mode ~= request then
table.remove(menu,i)
else
menu[i].mode = nil
end
end
end
return menu
end
function AutoWarmth:updateItems(touchmenu_instance)
touchmenu_instance:updateItems()
UIManager:broadcastEvent(Event:new("UpdateFooter", self.view and self.view.footer_visible or false))
end
local about_text = _([[Set the frontlight warmth (if available) and night mode based on a time schedule or the sun's position.
There are three types of twilight:
Civil: You can read a newspaper
Nautical: You can see the first stars
Astronomical: It is really dark
Custom warmth values can be set for every kind of twilight and sunrise, noon, sunset and midnight.
The screen warmth is continuously adjusted to the current time.
To use the sun's position, a geographical location must be entered. The calculations are very precise, with a deviation less than minute and a half.]])
function AutoWarmth:getSubMenuItems()
return {
{
text = Device:hasNaturalLight() and _("About auto warmth and night mode") or _("About auto night mode"),
callback = function()
UIManager:show(InfoMessage:new{
text = about_text,
width = math.floor(Screen:getWidth() * 0.9),
})
end,
keep_menu_open = true,
separator = true,
},
{
text = _("Activate"),
checked_func = function()
return self.activate ~= 0
end,
sub_item_table = self:getActivateMenu(),
},
{
text = _("Expert mode"),
checked_func = function()
return not self.easy_mode
end,
help_text = _("In the expert mode, different types of twilight can be used in addition to civil twilight."),
callback = function(touchmenu_instance)
self.easy_mode = not self.easy_mode
G_reader_settings:saveSetting("autowarmth_easy_mode", self.easy_mode)
self:scheduleMidnightUpdate()
if touchmenu_instance then
touchmenu_instance.item_table = self:getSubMenuItems()
self:updateItems(touchmenu_instance)
end
end,
keep_menu_open = true,
},
{
text = _("Location settings"),
sub_item_table = self:getLocationMenu(),
},
{
text = _("Fixed schedule settings"),
enabled_func = function()
return self.activate ~= activate_sun and self.activate ~=0
end,
sub_item_table = self:getScheduleMenu(),
},
{
enabled_func = function()
return self.activate ~=0
end,
text = Device:hasNaturalLight() and _("Warmth and night mode settings") or _("Night mode settings"),
sub_item_table = self:getWarmthMenu(),
},
{
checked_func = function()
return self.fl_off_during_day
end,
text = _("Frontlight off during the day"),
callback = function(touchmenu_instance)
self.fl_off_during_day = not self.fl_off_during_day
G_reader_settings:saveSetting("autowarmth_fl_off_during_day", self.fl_off_during_day)
self:scheduleMidnightUpdate()
-- Turn the fl on during day if necessary;
-- no need to turn it off as this is done trough `scheduleMidnightUpdate()`
if not self.fl_off_during_day and Powerd:isFrontlightOff() then
Powerd:turnOnFrontlight()
end
if touchmenu_instance then
self:updateItems(touchmenu_instance)
end
end,
keep_menu_open = true,
separator = true,
},
self:getTimesMenu(_("Currently active parameters")),
self:getTimesMenu(_("Sun position information for"), true, activate_sun),
self:getTimesMenu(_("Fixed schedule information"), false, activate_schedule),
}
end
function AutoWarmth:getActivateMenu()
local function getActivateMenuEntry(text, help_text, activator)
return {
text = text,
help_text = help_text,
checked_func = function() return self.activate == activator end,
callback = function()
self.activate = self.activate ~= activator and activator or 0
G_reader_settings:saveSetting("autowarmth_activate", self.activate)
self:scheduleMidnightUpdate()
UIManager:broadcastEvent(Event:new("UpdateFooter", self.view and self.view.footer_visible or false))
end,
}
end
return {
getActivateMenuEntry(_("According to the sun's position"),
_("Only use the times calculated from the position of the sun."),
activate_sun),
getActivateMenuEntry(_("According to the fixed schedule"),
_("Only use the times from the fixed schedule."),
activate_schedule),
getActivateMenuEntry(_("Whatever is closer to noon"),
_("Use the times from the sun position or schedule that are closer to noon."),
activate_closer_noon),
getActivateMenuEntry(_("Whatever is closer to midnight"),
_("Use the times from the sun position or schedule that are closer to midnight."),
activate_closer_midnight),
}
end
function AutoWarmth:getLocationMenu()
return {{
text_func = function()
if self.location ~= "" then
return T(_("Location: %1"), self.location)
else
return _("Location")
end
end,
callback = function(touchmenu_instance)
local location_name_dialog
location_name_dialog = InputDialog:new{
title = _("Location name"),
input = self.location,
buttons = {
{
{
text = _("Cancel"),
id = "close",
callback = function()
UIManager:close(location_name_dialog)
end,
},
{
text = _("OK"),
callback = function()
self.location = location_name_dialog:getInputText()
G_reader_settings:saveSetting("autowarmth_location",
self.location)
UIManager:close(location_name_dialog)
if touchmenu_instance then self:updateItems(touchmenu_instance) end
end,
},
}
},
}
UIManager:show(location_name_dialog)
location_name_dialog:onShowKeyboard()
end,
keep_menu_open = true,
},
{
text_func = function()
return T(_("Coordinates: (%1°, %2°)"), self.latitude, self.longitude)
end,
callback = function(touchmenu_instance)
local location_widget = DoubleSpinWidget:new{
title_text = _("Set location"),
info_text = _("Enter decimal degrees, northern hemisphere and eastern length are '+'."),
left_text = _("Latitude"),
left_value = self.latitude,
left_min = -90,
left_max = 90,
left_step = 0.1,
left_hold_step = 5,
left_precision = "%0.2f",
right_text = _("Longitude"),
right_value = self.longitude,
right_min = -180,
right_max = 180,
right_step = 0.1,
right_hold_step = 5,
right_precision = "%0.2f",
unit = "°",
callback = function(lat, long)
self.latitude = lat
self.longitude = long
self.timezone = self:getTimezoneOffset() -- use timezone of device
G_reader_settings:saveSetting("autowarmth_latitude", self.latitude)
G_reader_settings:saveSetting("autowarmth_longitude", self.longitude)
G_reader_settings:saveSetting("autowarmth_timezone", self.timezone)
self:scheduleMidnightUpdate()
if touchmenu_instance then self:updateItems(touchmenu_instance) end
end,
}
UIManager:show(location_widget)
end,
keep_menu_open = true,
},
{
text_func = function()
return T(_("Altitude: %1 m"), self.altitude)
end,
callback = function(touchmenu_instance)
UIManager:show(SpinWidget:new{
title_text = _("Altitude"),
info_text = _("Enter the altitude in meters above sea level."),
value = self.altitude,
value_min = -100,
value_max = 15000, -- intercontinental flight
wrap = false,
value_step = 10,
value_hold_step = 100,
unit = C_("Length", "m"),
ok_text = _("Set"),
callback = function(spin)
self.altitude = spin.value
G_reader_settings:saveSetting("autowarmth_altitude", self.altitude)
self:scheduleMidnightUpdate()
if touchmenu_instance then self:updateItems(touchmenu_instance) end
end,
extra_text = _("Default"),
extra_callback = function()
self.altitude = 200
G_reader_settings:saveSetting("autowarmth_altitude", self.altitude)
self:scheduleMidnightUpdate()
if touchmenu_instance then self:updateItems(touchmenu_instance) end
end,
})
end,
keep_menu_open = true,
}}
end
function AutoWarmth:getScheduleMenu()
local function store_times(touchmenu_instance, new_time, num)
self.scheduler_times[num] = new_time
G_reader_settings:saveSetting("autowarmth_scheduler_times", self.scheduler_times)
self:scheduleMidnightUpdate()
if touchmenu_instance then self:updateItems(touchmenu_instance) end
end
-- mode == nil ... show always
-- == true ... easy mode
-- == false ... expert mode
local function getScheduleMenuEntry(text, num, mode)
return {
mode = mode,
text_func = function()
2022-09-16 18:23:41 +00:00
return T(_("%1: %2"), text,
self:hoursToClock(self.scheduler_times[num]))
end,
checked_func = function()
return self.scheduler_times[num] ~= nil
end,
callback = function(touchmenu_instance)
local hh = 12
local mm = 0
if self.scheduler_times[num] then
hh = math.floor(self.scheduler_times[num])
mm = math.floor(frac(self.scheduler_times[num]) * 60 + 0.5)
end
UIManager:show(DateTimeWidget:new{
title_text = _("Set time"),
info_text = _("Enter time in hours and minutes."),
hour = hh,
hour_min = -1,
hour_max = 24,
min = mm,
ok_text = _("Set time"),
callback = function(time)
local new_time = time.hour + time.min / 60
local function get_valid_time(n, dir)
for i = n+dir, dir > 0 and midnight_index or 1, dir do
if self.scheduler_times[i] then
return self.scheduler_times[i]
end
end
return dir > 0 and 0 or 26
end
if num > 1 and new_time < get_valid_time(num, -1) then
UIManager:show(ConfirmBox:new{
text = _("This time is before the previous time.\nAdjust the previous time?"),
ok_callback = function()
for i = num - 1, 1, -1 do
if self.scheduler_times[i] then
if new_time < self.scheduler_times[i] then
self.scheduler_times[i] = new_time
else
break
end
end
end
store_times(touchmenu_instance, new_time, num)
end,
})
elseif num < 10 and new_time > get_valid_time(num, 1) then
UIManager:show(ConfirmBox:new{
text = _("This time is after the subsequent time.\nAdjust the subsequent time?"),
ok_callback = function()
for i = num + 1, midnight_index - 1 do
if self.scheduler_times[i] then
if new_time > self.scheduler_times[i] then
self.scheduler_times[i] = new_time
else
break
end
end
end
store_times(touchmenu_instance, new_time, num)
end,
})
else
store_times(touchmenu_instance, new_time, num)
end
end,
extra_text = _("Invalidate"),
extra_callback = function()
store_times(touchmenu_instance, nil, num)
end,
})
end,
keep_menu_open = true,
}
end
local retval = {
getScheduleMenuEntry(_("Solar midnight (previous day)"), 1, false),
getScheduleMenuEntry(_("Astronomical dawn"), 2, false),
getScheduleMenuEntry(_("Nautical dawn"), 3, false),
getScheduleMenuEntry(_("Civil dawn"), 4),
getScheduleMenuEntry(_("Sunrise"), 5),
getScheduleMenuEntry(_("Solar noon"), 6, false),
getScheduleMenuEntry(_("Sunset"), 7),
getScheduleMenuEntry(_("Civil dusk"), 8),
getScheduleMenuEntry(_("Nautical dusk"), 9, false),
getScheduleMenuEntry(_("Astronomical dusk"), 10, false),
getScheduleMenuEntry(_("Solar midnight"), 11, false),
}
return tidy_menu(retval, self.easy_mode)
end
function AutoWarmth:getWarmthMenu()
-- mode == nil ... show always
-- == true ... easy mode
-- == false ... expert mode
local function getWarmthMenuEntry(text, num, mode)
return {
mode = mode,
text_func = function()
if Device:hasNaturalLight() and self.control_warmth then
if self.warmth[num] <= 100 then
return T(_("%1: %2 %"), text, self.warmth[num])
else
return T(_("%1: 100 % %2"), text, self.control_nightmode and "+ ☾" or "")
end
else
if self.warmth[num] <= 100 then
return T(_("%1: ☼"), text)
else
return T(_("%1: ☾"), text)
end
end
end,
callback = function(touchmenu_instance)
if Device:hasNaturalLight() and self.control_warmth then
UIManager:show(SpinWidget:new{
title_text = text,
info_text = _("Enter percentage of warmth."),
value = self.warmth[num],
value_min = 0,
value_max = 100,
wrap = false,
value_step = math.floor(100 / device_max_warmth),
value_hold_step = 10,
unit = "%",
ok_text = _("Set"),
callback = function(spin)
self.warmth[num] = spin.value
self.warmth[#self.warmth - num + 1] = spin.value
G_reader_settings:saveSetting("autowarmth_warmth", self.warmth)
self:scheduleMidnightUpdate()
if touchmenu_instance then self:updateItems(touchmenu_instance) end
end,
extra_text = self.control_nightmode and _("Use night mode"),
extra_callback = self.control_nightmode and function()
self.warmth[num] = 110
self.warmth[#self.warmth - num + 1] = 110
G_reader_settings:saveSetting("autowarmth_warmth", self.warmth)
self:scheduleMidnightUpdate()
if touchmenu_instance then self:updateItems(touchmenu_instance) end
end,
})
else
UIManager:show(ConfirmBox:new{
text = _("Night mode"),
ok_text = _("Set"),
ok_callback = function()
self.warmth[num] = 110
self.warmth[#self.warmth - num + 1] = 110
G_reader_settings:saveSetting("autowarmth_warmth", self.warmth)
self:scheduleMidnightUpdate()
if touchmenu_instance then self:updateItems(touchmenu_instance) end
end,
cancel_text = _("Unset"),
cancel_callback = function()
self.warmth[num] = 0
self.warmth[#self.warmth - num + 1] = 0
G_reader_settings:saveSetting("autowarmth_warmth", self.warmth)
self:scheduleMidnightUpdate()
if touchmenu_instance then self:updateItems(touchmenu_instance) end
end,
other_buttons = {{
{
text = _("Cancel"),
}
}},
})
end
end,
keep_menu_open = true,
}
end
local retval = {
{
text_func = function()
if Device:hasNaturalLight() then
return T(_("Control: %1%2%3"), self.control_warmth and _("warmth") or "",
self.control_warmth and self.control_nightmode and string.format(" %s ", _("and")) or "",
self.control_nightmode and _("night mode") or "")
else
return _("Control: night mode")
end
end,
enabled_func = function()
return Device:hasNaturalLight()
end,
callback = function(touchmenu_instance)
if self.control_warmth and self.control_nightmode then
self.control_nightmode = false
G_reader_settings:makeFalse("autowarmth_control_nightmode")
elseif self.control_warmth and not self.control_nightmode then
self.control_warmth = false
self.control_nightmode = true
G_reader_settings:makeFalse("autowarmth_control_warmth")
G_reader_settings:makeTrue("autowarmth_control_nightmode")
else
self.control_warmth = true
self.control_nightmode = true
G_reader_settings:makeTrue("autowarmth_control_warmth")
G_reader_settings:makeTrue("autowarmth_control_nightmode")
end
self:scheduleMidnightUpdate()
if touchmenu_instance then self:updateItems(touchmenu_instance) end
end,
keep_menu_open = true,
separator = true,
},
{
text = Device:hasNaturalLight() and _("Set warmth and night mode for:") or _("Set night mode for:"),
enabled_func = function() return false end,
},
getWarmthMenuEntry(_("Solar noon"), 6, false),
getWarmthMenuEntry(_("Sunset and sunrise"), 5),
getWarmthMenuEntry(_("Darkest time of civil twilight"), 4),
getWarmthMenuEntry(_("Darkest time of nautical twilight"), 3, false),
getWarmthMenuEntry(_("Darkest time of astronomical twilight"), 2, false),
getWarmthMenuEntry(_("Solar midnight"), 1, false),
}
return tidy_menu(retval, self.easy_mode)
end
-- title
-- location: add a location string
-- activator: nil .. current_times_h,
-- activate_sun .. sun times
-- activate_schedule .. scheduler times
-- request_easy: true if easy_mode should be used
function AutoWarmth:showTimesInfo(title, location, activator, request_easy)
local times
if not activator then
times = self.current_times_h
elseif activator == activate_sun then
times = SunTime.times
elseif activator == activate_schedule then
times = self.scheduler_times
end
-- text to show
-- t .. times
-- num .. index in times
local function info_line(indent, text, t, num, face, easy)
-- get width of space
local unit = " "
local tmp = TextWidget:new{
text = unit,
face = face,
}
local space_w = tmp:getSize().w
-- get width of text
unit = text
tmp = TextWidget:new{
text = unit,
face = face,
}
local text_w = tmp:getSize().w
tmp:free()
-- width of text in spaces
local str_len = math.floor(text_w / space_w + 0.5)
local tab_width = 18 - indent
local retval = string.rep(" ", indent) .. text .. string.rep(" ", tab_width - str_len)
.. self:hoursToClock(t[num])
if easy then
if t[num] and self.current_times_h[num] and self.current_times_h[num] ~= t[num] then
return text .. "\n"
else
return ""
end
end
if not t[num] then -- entry deactivated
return retval .. "\n"
elseif Device:hasNaturalLight() and self.control_warmth then
if self.current_times_h[num] == t[num] then
if self.warmth[num] <= 100 then
return retval .. " (💡" .. self.warmth[num] .."%)\n"
else
return retval .. " (💡100%" .. (self.control_nightmode and " + ☾" or "") .. ")\n"
end
else
return retval .. "\n"
end
else
if self.current_times_h[num] == t[num] then
if self.warmth[num] <= 100 then
return retval .. " (☼)\n"
else
return retval .. " (☾)\n"
end
else
return retval .. "\n"
end
end
end
local location_string = ""
if location then
location_string = " " .. self:getLocationString()
end
local function add_line(text, easy)
return easy and "" or (" " .. text .. "\n")
end
local face = Font:getFace("scfont")
UIManager:show(InfoMessage:new{
face = face,
width = math.floor(Screen:getWidth() * (self.easy_mode and 0.75 or 0.90)),
text = title .. location_string .. ":\n\n" ..
info_line(0, _("Solar midnight:"), times, 1, face, request_easy) ..
add_line(_("Dawn"), request_easy) ..
info_line(4, _("Astronomic:"), times, 2, face, request_easy) ..
info_line(4, _("Nautical:"), times, 3, face, request_easy)..
info_line(request_easy and 0 or 4,
request_easy and _("Twilight:") or _("Civil:"), times, 4, face) ..
add_line(_("Dawn"), request_easy) ..
info_line(0, _("Sunrise:"), times, 5, face) ..
"\n" ..
info_line(0, _("Solar noon:"), times, 6, face, request_easy) ..
add_line("", request_easy) ..
info_line(0, _("Sunset:"), times, 7, face) ..
add_line(_("Dusk"), request_easy) ..
info_line(request_easy and 0 or 4,
request_easy and _("Twilight:") or _("Civil:"), times, 8, face) ..
info_line(4, _("Nautical:"), times, 9, face, request_easy) ..
info_line(4, _("Astronomic:"), times, 10, face, request_easy) ..
add_line(_("Dusk"), request_easy) ..
info_line(0, _("Solar midnight:"), times, midnight_index, face, request_easy)
})
end
-- title
-- location: add a location string
-- activator: nil .. current_times_h,
-- activate_sun .. sun times
-- activate_schedule .. scheduler times
function AutoWarmth:getTimesMenu(title, location, activator)
return {
enabled_func = function()
-- always show sun position times so you can see ephemeris
return self.activate ~= 0 and (self.activate ~= activate_sun or activator == nil)
or activator == activate_sun
end,
text_func = function()
if location then
return title .. " " .. self:getLocationString()
end
return title
end,
callback = function()
self:showTimesInfo(title, location, activator, self.easy_mode)
end,
keep_menu_open = true,
}
end
function AutoWarmth:getLocationString()
if self.location ~= "" then
return self.location
else
return string.format("(%.2f°,%.2f°)", self.latitude, self.longitude)
end
end
return AutoWarmth