mirror of
https://github.com/koreader/koreader.git
synced 2025-12-13 20:36:53 +01:00
For consistency with BookMap and PageBrowser widgets where swipe south (the usual gesture to quick close) can't be used for closing and we had to use any multiswipe instead, allow any multiswipe to close these other fullscreen widgets too: Menu (ToC, Bookmarks), KeyValuePage, ImageViewer, BookStatusWidget, ReaderProgress, CalendarView.
664 lines
20 KiB
Lua
664 lines
20 KiB
Lua
local Blitbuffer = require("ffi/blitbuffer")
|
|
local Button = require("ui/widget/button")
|
|
local CenterContainer = require("ui/widget/container/centercontainer")
|
|
local CloseButton = require("ui/widget/closebutton")
|
|
local Device = require("device")
|
|
local Font = require("ui/font")
|
|
local FrameContainer = require("ui/widget/container/framecontainer")
|
|
local Geom = require("ui/geometry")
|
|
local GestureRange = require("ui/gesturerange")
|
|
local HorizontalGroup = require("ui/widget/horizontalgroup")
|
|
local HorizontalSpan = require("ui/widget/horizontalspan")
|
|
local ImageWidget = require("ui/widget/imagewidget")
|
|
local InputContainer = require("ui/widget/container/inputcontainer")
|
|
local InputDialog = require("ui/widget/inputdialog")
|
|
local InputText = require("ui/widget/inputtext")
|
|
local LeftContainer = require("ui/widget/container/leftcontainer")
|
|
local LineWidget = require("ui/widget/linewidget")
|
|
local OverlapGroup = require("ui/widget/overlapgroup")
|
|
local ProgressWidget = require("ui/widget/progresswidget")
|
|
local RenderImage = require("ui/renderimage")
|
|
local Size = require("ui/size")
|
|
local TextBoxWidget = require("ui/widget/textboxwidget")
|
|
local TextWidget = require("ui/widget/textwidget")
|
|
local ToggleSwitch = require("ui/widget/toggleswitch")
|
|
local UIManager = require("ui/uimanager")
|
|
local VerticalGroup = require("ui/widget/verticalgroup")
|
|
local VerticalSpan = require("ui/widget/verticalspan")
|
|
local util = require("util")
|
|
local _ = require("gettext")
|
|
local Screen = Device.screen
|
|
local T = require("ffi/util").template
|
|
|
|
local stats_book = {}
|
|
|
|
--[[
|
|
--Save into sdr folder addtional section
|
|
["summary"] = {
|
|
["rating"] = 5,
|
|
["note"] = "Some text",
|
|
["status"] = "Reading"
|
|
["modified"] = "24.01.2016"
|
|
},]]
|
|
local BookStatusWidget = InputContainer:new{
|
|
padding = Size.padding.fullscreen,
|
|
settings = nil,
|
|
thumbnail = nil,
|
|
props = nil,
|
|
star = {},
|
|
summary = {
|
|
rating = nil,
|
|
note = nil,
|
|
status = "",
|
|
modified = "",
|
|
},
|
|
}
|
|
|
|
function BookStatusWidget:init()
|
|
if self.settings then
|
|
-- What a blank, full summary table should look like
|
|
local new_summary = {
|
|
rating = nil,
|
|
note = nil,
|
|
status = "",
|
|
modified = "",
|
|
}
|
|
local summary = self.settings:readSetting("summary")
|
|
-- Check if the summary table we get is a full one, or a minimal one from CoverMenu...
|
|
if summary then
|
|
if summary.modified then
|
|
-- Complete, use it as-is
|
|
self.summary = summary
|
|
else
|
|
-- Incomplete, fill it up
|
|
self.summary = new_summary
|
|
util.tableMerge(self.summary, summary)
|
|
end
|
|
else
|
|
self.summary = new_summary
|
|
end
|
|
end
|
|
self.total_pages = self.ui.document:getPageCount()
|
|
stats_book = self:getStats()
|
|
|
|
self.small_font_face = Font:getFace("smallffont")
|
|
self.medium_font_face = Font:getFace("ffont")
|
|
self.large_font_face = Font:getFace("largeffont")
|
|
|
|
local button_enabled = true
|
|
if self.readonly then
|
|
button_enabled = false
|
|
end
|
|
|
|
self.star = Button:new{
|
|
icon = "star.empty",
|
|
bordersize = 0,
|
|
radius = 0,
|
|
margin = 0,
|
|
enabled = button_enabled,
|
|
show_parent = self,
|
|
readonly = self.readonly,
|
|
}
|
|
|
|
if Device:hasKeys() then
|
|
self.key_events = {
|
|
--don't get locked in on non touch devices
|
|
AnyKeyPressed = { { Device.input.group.Any },
|
|
seqtext = "any key", doc = "close dialog" }
|
|
}
|
|
end
|
|
if Device:isTouchDevice() then
|
|
self.ges_events.Swipe = {
|
|
GestureRange:new{
|
|
ges = "swipe",
|
|
range = function() return self.dimen end,
|
|
}
|
|
}
|
|
self.ges_events.MultiSwipe = {
|
|
GestureRange:new{
|
|
ges = "multiswipe",
|
|
range = function() return self.dimen end,
|
|
}
|
|
}
|
|
end
|
|
|
|
local screen_size = Screen:getSize()
|
|
self.covers_fullscreen = true -- hint for UIManager:_repaint()
|
|
self[1] = FrameContainer:new{
|
|
width = screen_size.w,
|
|
height = screen_size.h,
|
|
background = Blitbuffer.COLOR_WHITE,
|
|
bordersize = 0,
|
|
padding = 0,
|
|
self:getStatusContent(screen_size.w),
|
|
}
|
|
|
|
self.dithered = true
|
|
end
|
|
|
|
function BookStatusWidget:getStats()
|
|
return {}
|
|
end
|
|
|
|
function BookStatusWidget:getStatDays()
|
|
if stats_book.days then
|
|
return tostring(stats_book.days)
|
|
else
|
|
return _("N/A")
|
|
end
|
|
end
|
|
|
|
function BookStatusWidget:getStatHours()
|
|
if stats_book.time then
|
|
local user_duration_format = G_reader_settings:readSetting("duration_format", "classic")
|
|
return util.secondsToClockDuration(user_duration_format, stats_book.time, false)
|
|
else
|
|
return _("N/A")
|
|
end
|
|
end
|
|
|
|
function BookStatusWidget:getStatReadPages()
|
|
if stats_book.pages then
|
|
return string.format("%s/%s",stats_book.pages, self.total_pages)
|
|
else
|
|
return _("N/A")
|
|
end
|
|
end
|
|
|
|
function BookStatusWidget:getStatusContent(width)
|
|
local close_button = nil
|
|
local status_header = self:genHeader(_("Book Status"))
|
|
|
|
if self.readonly ~= true then
|
|
close_button = CloseButton:new{ window = self }
|
|
status_header = self:genHeader(_("Update Status"))
|
|
end
|
|
local content = VerticalGroup:new{
|
|
align = "left",
|
|
OverlapGroup:new{
|
|
dimen = Geom:new{ w = width, h = Size.item.height_default },
|
|
close_button,
|
|
},
|
|
self:genBookInfoGroup(),
|
|
self:genHeader(_("Statistics")),
|
|
self:genStatisticsGroup(width),
|
|
self:genHeader(_("Review")),
|
|
self:genSummaryGroup(width),
|
|
status_header,
|
|
self:generateSwitchGroup(width),
|
|
}
|
|
return content
|
|
end
|
|
|
|
function BookStatusWidget:genHeader(title)
|
|
local width, height = Screen:getWidth(), Size.item.height_default
|
|
|
|
local header_title = TextWidget:new{
|
|
text = title,
|
|
face = self.medium_font_face,
|
|
fgcolor = Blitbuffer.COLOR_WEB_GRAY,
|
|
}
|
|
|
|
local padding_span = HorizontalSpan:new{ width = self.padding }
|
|
local line_width = (width - header_title:getSize().w) / 2 - self.padding * 2
|
|
local line_container = LeftContainer:new{
|
|
dimen = Geom:new{ w = line_width, h = height },
|
|
LineWidget:new{
|
|
background = Blitbuffer.COLOR_LIGHT_GRAY,
|
|
dimen = Geom:new{
|
|
w = line_width,
|
|
h = Size.line.thick,
|
|
}
|
|
}
|
|
}
|
|
local span_top, span_bottom
|
|
if Screen:getScreenMode() == "landscape" then
|
|
span_top = VerticalSpan:new{ width = Size.span.horizontal_default }
|
|
span_bottom = VerticalSpan:new{ width = Size.span.horizontal_default }
|
|
else
|
|
span_top = VerticalSpan:new{ width = Size.item.height_default }
|
|
span_bottom = VerticalSpan:new{ width = Size.span.vertical_large }
|
|
end
|
|
|
|
return VerticalGroup:new{
|
|
span_top,
|
|
HorizontalGroup:new{
|
|
align = "center",
|
|
padding_span,
|
|
line_container,
|
|
padding_span,
|
|
header_title,
|
|
padding_span,
|
|
line_container,
|
|
padding_span,
|
|
},
|
|
span_bottom,
|
|
}
|
|
end
|
|
|
|
function BookStatusWidget:onChangeBookStatus(option_name, option_value)
|
|
self.summary.status = option_name[option_value]
|
|
self.summary.modified = os.date("%Y-%m-%d", os.time())
|
|
self:saveSummary()
|
|
return true
|
|
end
|
|
|
|
function BookStatusWidget:generateRateGroup(width, height, rating)
|
|
self.stars_container = CenterContainer:new{
|
|
dimen = Geom:new{ w = width, h = height },
|
|
}
|
|
|
|
self:setStar(rating)
|
|
return self.stars_container
|
|
end
|
|
|
|
function BookStatusWidget:setStar(num)
|
|
-- clear previous data
|
|
self.stars_container:clear()
|
|
|
|
local stars_group = HorizontalGroup:new{ align = "center" }
|
|
if num then
|
|
self.summary.rating = num
|
|
self:saveSummary()
|
|
|
|
for i = 1, num do
|
|
table.insert(stars_group, self.star:new{
|
|
icon = "star.full",
|
|
callback = function() self:setStar(i) end
|
|
})
|
|
end
|
|
else
|
|
num = 0
|
|
end
|
|
|
|
for i = num + 1, 5 do
|
|
table.insert(stars_group, self.star:new{ callback = function() self:setStar(i) end })
|
|
end
|
|
|
|
table.insert(self.stars_container, stars_group)
|
|
|
|
-- Individual stars are Button, w/ flash_ui, they'll have their own flash.
|
|
-- And we need to redraw the full widget, because we don't know the coordinates of stars_container :/.
|
|
UIManager:setDirty(self, "ui", nil, true)
|
|
return true
|
|
end
|
|
|
|
function BookStatusWidget:genBookInfoGroup()
|
|
local screen_width = Screen:getWidth()
|
|
local split_span_width = math.floor(screen_width * 0.05)
|
|
|
|
local img_width, img_height
|
|
if Screen:getScreenMode() == "landscape" then
|
|
img_width = Screen:scaleBySize(132)
|
|
img_height = Screen:scaleBySize(184)
|
|
else
|
|
img_width = Screen:scaleBySize(132 * 1.5)
|
|
img_height = Screen:scaleBySize(184 * 1.5)
|
|
end
|
|
|
|
local height = img_height
|
|
local width = screen_width - split_span_width - img_width
|
|
|
|
-- Get a chance to have title and authors rendered with alternate
|
|
-- glyphs for the book language
|
|
local lang = nil
|
|
if self.props.language and self.props.language ~= "" then
|
|
lang = self.props.language
|
|
end
|
|
-- title
|
|
local book_meta_info_group = VerticalGroup:new{
|
|
align = "center",
|
|
VerticalSpan:new{ width = height * 0.2 },
|
|
TextBoxWidget:new{
|
|
text = self.props.title,
|
|
lang = lang,
|
|
width = width,
|
|
face = self.medium_font_face,
|
|
alignment = "center",
|
|
},
|
|
|
|
}
|
|
-- author
|
|
local text_author = TextBoxWidget:new{
|
|
text = self.props.authors,
|
|
lang = lang,
|
|
face = self.small_font_face,
|
|
width = width,
|
|
alignment = "center",
|
|
}
|
|
table.insert(book_meta_info_group,
|
|
CenterContainer:new{
|
|
dimen = Geom:new{ w = width, h = text_author:getSize().h },
|
|
text_author
|
|
}
|
|
)
|
|
-- progress bar
|
|
local read_percentage = self.ui:getCurrentPage() / self.total_pages
|
|
local progress_bar = ProgressWidget:new{
|
|
width = math.floor(width * 0.7),
|
|
height = Screen:scaleBySize(10),
|
|
percentage = read_percentage,
|
|
ticks = nil,
|
|
last = nil,
|
|
}
|
|
table.insert(book_meta_info_group,
|
|
CenterContainer:new{
|
|
dimen = Geom:new{ w = width, h = progress_bar:getSize().h },
|
|
progress_bar
|
|
}
|
|
)
|
|
-- complete text
|
|
local text_complete = TextWidget:new{
|
|
text = T(_("%1% Completed"),
|
|
string.format("%1.f", read_percentage * 100)),
|
|
face = self.small_font_face,
|
|
}
|
|
table.insert(book_meta_info_group,
|
|
CenterContainer:new{
|
|
dimen = Geom:new{ w = width, h = text_complete:getSize().h },
|
|
text_complete
|
|
}
|
|
)
|
|
-- rating
|
|
table.insert(book_meta_info_group,
|
|
VerticalSpan:new{ width = Screen:scaleBySize(30) })
|
|
local rateHeight = Screen:scaleBySize(60)
|
|
table.insert(book_meta_info_group,
|
|
self:generateRateGroup(screen_width, rateHeight, self.summary.rating))
|
|
|
|
-- build the final group
|
|
local book_info_group = HorizontalGroup:new{
|
|
align = "top",
|
|
HorizontalSpan:new{ width = split_span_width }
|
|
}
|
|
-- thumbnail
|
|
if self.thumbnail then
|
|
-- Much like BookInfoManager, honor AR here
|
|
local cbb_w, cbb_h = self.thumbnail:getWidth(), self.thumbnail:getHeight()
|
|
if cbb_w > img_width or cbb_h > img_height then
|
|
local scale_factor = math.min(img_width / cbb_w, img_height / cbb_h)
|
|
cbb_w = math.min(math.floor(cbb_w * scale_factor)+1, img_width)
|
|
cbb_h = math.min(math.floor(cbb_h * scale_factor)+1, img_height)
|
|
self.thumbnail = RenderImage:scaleBlitBuffer(self.thumbnail, cbb_w, cbb_h, true)
|
|
end
|
|
|
|
table.insert(book_info_group, ImageWidget:new{
|
|
image = self.thumbnail,
|
|
width = cbb_w,
|
|
height = cbb_h,
|
|
})
|
|
-- dereference thumbnail since we let imagewidget manages its lifecycle
|
|
self.thumbnail = nil
|
|
end
|
|
|
|
table.insert(book_info_group, CenterContainer:new{
|
|
dimen = Geom:new{ w = width, h = height },
|
|
book_meta_info_group,
|
|
})
|
|
|
|
return CenterContainer:new{
|
|
dimen = Geom:new{ w = screen_width, h = img_height },
|
|
book_info_group,
|
|
}
|
|
end
|
|
|
|
function BookStatusWidget:genStatisticsGroup(width)
|
|
local height = Screen:scaleBySize(60)
|
|
local statistics_container = CenterContainer:new{
|
|
dimen = Geom:new{ w = width, h = height },
|
|
}
|
|
|
|
local statistics_group = VerticalGroup:new{ align = "left" }
|
|
|
|
local tile_width = width / 3
|
|
local tile_height = height / 2
|
|
|
|
local titles_group = HorizontalGroup:new{
|
|
align = "center",
|
|
CenterContainer:new{
|
|
dimen = Geom:new{ w = tile_width, h = tile_height },
|
|
TextWidget:new{
|
|
text = _("Days"),
|
|
face = self.small_font_face,
|
|
},
|
|
},
|
|
CenterContainer:new{
|
|
dimen = Geom:new{ w = tile_width, h = tile_height },
|
|
TextWidget:new{
|
|
text = _("Time"),
|
|
face = self.small_font_face,
|
|
},
|
|
},
|
|
CenterContainer:new{
|
|
dimen = Geom:new{ w = tile_width, h = tile_height },
|
|
TextWidget:new{
|
|
text = _("Read pages"),
|
|
face = self.small_font_face,
|
|
}
|
|
}
|
|
}
|
|
|
|
local data_group = HorizontalGroup:new{
|
|
align = "center",
|
|
CenterContainer:new{
|
|
dimen = Geom:new{ w = tile_width, h = tile_height },
|
|
TextWidget:new{
|
|
text = self:getStatDays(),
|
|
face = self.medium_font_face,
|
|
},
|
|
},
|
|
CenterContainer:new{
|
|
dimen = Geom:new{ w = tile_width, h = tile_height },
|
|
TextWidget:new{
|
|
text = self:getStatHours(),
|
|
face = self.medium_font_face,
|
|
},
|
|
},
|
|
CenterContainer:new{
|
|
dimen = Geom:new{ w = tile_width, h = tile_height },
|
|
TextWidget:new{
|
|
text = self:getStatReadPages(),
|
|
face = self.medium_font_face,
|
|
}
|
|
}
|
|
}
|
|
|
|
table.insert(statistics_group, titles_group)
|
|
table.insert(statistics_group, data_group)
|
|
|
|
table.insert(statistics_container, statistics_group)
|
|
return statistics_container
|
|
end
|
|
|
|
function BookStatusWidget:genSummaryGroup(width)
|
|
local height
|
|
if Screen:getScreenMode() == "landscape" then
|
|
height = Screen:scaleBySize(80)
|
|
else
|
|
height = Screen:scaleBySize(160)
|
|
end
|
|
|
|
local text_padding = Size.padding.default
|
|
self.input_note = InputText:new{
|
|
text = self.summary.note,
|
|
face = self.medium_font_face,
|
|
width = width - self.padding * 3,
|
|
height = math.floor(height * 0.75),
|
|
scroll = true,
|
|
bordersize = Size.border.default,
|
|
focused = false,
|
|
padding = text_padding,
|
|
parent = self,
|
|
readonly = self.readonly,
|
|
hint = _("A few words about the book"),
|
|
}
|
|
|
|
return VerticalGroup:new{
|
|
VerticalSpan:new{ width = Size.span.vertical_large },
|
|
CenterContainer:new{
|
|
dimen = Geom:new{ w = width, h = height },
|
|
self.input_note
|
|
}
|
|
}
|
|
end
|
|
|
|
function BookStatusWidget:onUpdateNote()
|
|
self.summary.note = self.input_note:getText()
|
|
self:saveSummary()
|
|
return true
|
|
end
|
|
|
|
function BookStatusWidget:saveSummary()
|
|
if self.summary then
|
|
self.settings:saveSetting("summary", self.summary)
|
|
self.settings:flush()
|
|
end
|
|
end
|
|
|
|
function BookStatusWidget:generateSwitchGroup(width)
|
|
local height
|
|
if Screen:getScreenMode() == "landscape" then
|
|
-- landscape mode
|
|
height = Screen:scaleBySize(60)
|
|
else
|
|
-- portrait mode
|
|
height = Screen:scaleBySize(105)
|
|
end
|
|
|
|
local args = { "complete", "reading", "abandoned" }
|
|
|
|
local current_status = self.summary.status
|
|
local position = 2
|
|
for k, v in pairs(args) do
|
|
if v == current_status then
|
|
position = k
|
|
end
|
|
end
|
|
|
|
local config = {
|
|
event = "ChangeBookStatus",
|
|
default_value = 2,
|
|
args = args,
|
|
default_arg = "reading",
|
|
toggle = { _("Finished"), _("Reading"), _("On hold") },
|
|
values = { 1, 2, 3 },
|
|
name = "book_status",
|
|
alternate = false,
|
|
enabled = true,
|
|
}
|
|
|
|
if self.readonly then
|
|
config.enable = false
|
|
end
|
|
|
|
local switch = ToggleSwitch:new{
|
|
width = math.floor(width * 0.6),
|
|
default_value = config.default_value,
|
|
name = config.name,
|
|
name_text = config.name_text,
|
|
event = config.event,
|
|
toggle = config.toggle,
|
|
args = config.args,
|
|
alternate = config.alternate,
|
|
default_arg = config.default_arg,
|
|
values = config.values,
|
|
enabled = config.enable,
|
|
config = self,
|
|
readonly = self.readonly,
|
|
}
|
|
switch:setPosition(position)
|
|
|
|
return VerticalGroup:new{
|
|
VerticalSpan:new{ width = Screen:scaleBySize(10) },
|
|
CenterContainer:new{
|
|
ignore = "height",
|
|
dimen = Geom:new{ w = width, h = height },
|
|
switch,
|
|
}
|
|
}
|
|
end
|
|
|
|
function BookStatusWidget:onConfigChoose(values, name, event, args, position)
|
|
UIManager:tickAfterNext(function()
|
|
if values then
|
|
self:onChangeBookStatus(args, position)
|
|
end
|
|
UIManager:setDirty(nil, "ui", nil, true)
|
|
end)
|
|
end
|
|
|
|
|
|
function BookStatusWidget:onAnyKeyPressed()
|
|
return self:onClose()
|
|
end
|
|
|
|
function BookStatusWidget:onSwipe(arg, ges_ev)
|
|
if ges_ev.direction == "south" then
|
|
-- Allow easier closing with swipe down
|
|
self:onClose()
|
|
elseif ges_ev.direction == "east" or ges_ev.direction == "west" or ges_ev.direction == "north" then
|
|
-- no use for now
|
|
do end -- luacheck: ignore 541
|
|
else -- diagonal swipe
|
|
-- trigger full refresh
|
|
UIManager:setDirty(nil, "full", nil, true)
|
|
-- a long diagonal swipe may also be used for taking a screenshot,
|
|
-- so let it propagate
|
|
return false
|
|
end
|
|
end
|
|
|
|
function BookStatusWidget:onMultiSwipe(arg, ges_ev)
|
|
-- For consistency with other fullscreen widgets where swipe south can't be
|
|
-- used to close and where we then allow any multiswipe to close, allow any
|
|
-- multiswipe to close this widget too.
|
|
self:onClose()
|
|
return true
|
|
end
|
|
|
|
function BookStatusWidget:onClose()
|
|
self:saveSummary()
|
|
-- NOTE: Flash on close to avoid ghosting, since we show an image.
|
|
UIManager:close(self, "flashpartial")
|
|
return true
|
|
end
|
|
|
|
function BookStatusWidget:onSwitchFocus(inputbox)
|
|
self.note_dialog = InputDialog:new{
|
|
title = _("Review"),
|
|
input = self.input_note:getText(),
|
|
input_hint = "",
|
|
input_type = "text",
|
|
scroll = true,
|
|
allow_newline = true,
|
|
text_height = Screen:scaleBySize(150),
|
|
buttons = {
|
|
{
|
|
{
|
|
text = _("Cancel"),
|
|
callback = function()
|
|
self:closeInputDialog()
|
|
end,
|
|
},
|
|
{
|
|
text = _("Save review"),
|
|
is_enter_default = true,
|
|
callback = function()
|
|
self.input_note:setText(self.note_dialog:getInputText())
|
|
self:closeInputDialog()
|
|
self:onUpdateNote()
|
|
end,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
UIManager:show(self.note_dialog)
|
|
self.note_dialog:onShowKeyboard()
|
|
end
|
|
|
|
function BookStatusWidget:closeInputDialog()
|
|
UIManager:close(self.note_dialog)
|
|
end
|
|
|
|
return BookStatusWidget
|