Style tweaks: use css snippets to tweak book styles (#3944)

Adds a new menu "Style tweaks", with a few CSS snippets
that can make some things better with some books.
pull/3946/head
poire-z 6 years ago committed by GitHub
parent a9905c5129
commit 5b7664b064
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -54,8 +54,8 @@ local function initDataDir()
local sub_data_dirs = {
"cache", "clipboard",
"data", "data/dict", "data/tessdata",
"history",
"ota", "screenshots", "settings",
"history", "ota",
"screenshots", "settings", "styletweaks",
}
for _, dir in ipairs(sub_data_dirs) do
local sub_data_dir = string.format("%s/%s", DataStorage:getDataDir(), dir)

@ -0,0 +1,514 @@
local Blitbuffer = require("ffi/blitbuffer")
local ButtonTable = require("ui/widget/buttontable")
local CenterContainer = require("ui/widget/container/centercontainer")
local CssTweaks = require("ui/data/css_tweaks")
local DataStorage = require("datastorage")
local Device = require("device")
local Event = require("ui/event")
local Font = require("ui/font")
local FrameContainer = require("ui/widget/container/framecontainer")
local Geom = require("ui/geometry")
local GestureRange = require("ui/gesturerange")
local InfoMessage = require("ui/widget/infomessage")
local InputContainer = require("ui/widget/container/inputcontainer")
local MovableContainer = require("ui/widget/container/movablecontainer")
local Size = require("ui/size")
local TextBoxWidget = require("ui/widget/textboxwidget")
local UIManager = require("ui/uimanager")
local VerticalGroup = require("ui/widget/verticalgroup")
local VerticalSpan = require("ui/widget/verticalspan")
local lfs = require("libs/libkoreader-lfs")
local logger = require("logger")
local util = require("util")
local _ = require("gettext")
local Screen = Device.screen
local T = require("ffi/util").template
-- Simple widget for showing tweak info
local TweakInfoWidget = InputContainer:new{
tweak = nil,
is_global_default = nil,
toggle_global_default_callback = function() end,
modal = true,
width = Screen:getWidth()*3/4,
}
function TweakInfoWidget:init()
local tweak = self.tweak
if Device:isTouchDevice() then
self.ges_events.TapClose = {
GestureRange:new{
ges = "tap",
range = Geom:new{
x = 0, y = 0,
w = Screen:getWidth(),
h = Screen:getHeight(),
}
}
}
end
if Device:hasKeys() then
self.key_events = {
Close = { {"Back"}, doc = "cancel" }
}
end
local content = VerticalGroup:new{
TextBoxWidget:new{
text = tweak.title,
bold = true,
face = Font:getFace("infofont"),
width = self.width,
},
VerticalSpan:new{
width = Size.padding.large,
},
}
if tweak.description then
table.insert(content,
TextBoxWidget:new{
text = tweak.description,
face = Font:getFace("smallinfofont"),
width = self.width,
}
)
table.insert(content, VerticalSpan:new{
width = Size.padding.large,
})
end
-- This css TextBoxWidget may make the widget overflow screen with
-- large css text. For now, we don't bother with the complicated
-- setup of a scrollable ScrollTextWidget.
local css = tweak.css
if not css and tweak.css_path then
css = ""
local f = io.open(tweak.css_path, "r")
if f then
css = f:read("*all")
f:close()
end
end
css = css:gsub("^%s+", ""):gsub("%s+$", "")
table.insert(content, FrameContainer:new{
bordersize = Size.border.thin,
padding = Size.padding.large,
TextBoxWidget:new{
text = css,
face = Font:getFace("infont", 16),
width = self.width - 2*Size.padding.large,
}
})
if self.is_global_default then
table.insert(content, VerticalSpan:new{
width = Size.padding.large,
})
table.insert(content,
TextBoxWidget:new{
text = _("This tweak is applied on all books."),
face = Font:getFace("smallinfofont"),
width = self.width,
}
)
end
content = FrameContainer:new{
bordersize = 0,
padding = Size.padding.large,
padding_top = Size.padding.default,
content,
}
local buttons = {
{
text = _("Close"),
callback = function()
UIManager:close(self)
end,
},
{
text = self.is_global_default and _("Don't use on all books") or _("Use on all books"),
callback = function()
self.toggle_global_default_callback()
UIManager:close(self)
end,
},
}
local button_table = ButtonTable:new{
width = content:getSize().w,
button_font_face = "cfont",
button_font_size = 20,
buttons = { buttons },
zero_sep = true,
show_parent = self,
}
self.movable = MovableContainer:new{
FrameContainer:new{
background = Blitbuffer.COLOR_WHITE,
radius = Size.radius.window,
margin = Size.margin.default,
padding = Size.padding.default,
padding_bottom = 0, -- no padding below buttontable
VerticalGroup:new{
align = "left",
content,
button_table,
}
}
}
self[1] = CenterContainer:new{
dimen = Screen:getSize(),
self.movable
}
end
function TweakInfoWidget:onShow()
UIManager:setDirty(self, function()
return "ui", self.movable.dimen
end)
end
function TweakInfoWidget:onCloseWidget()
UIManager:setDirty(nil, function()
return "ui", self.movable.dimen
end)
end
function TweakInfoWidget:onClose()
UIManager:close(self)
return true
end
function TweakInfoWidget:onTapClose(arg, ges)
if ges.pos:notIntersectWith(self.movable.dimen) then
self:onClose()
return true
end
return false
end
function TweakInfoWidget:onSelect()
if self.selected.x == 1 then
self:toggle_global_default_callback()
end
UIManager:close(self)
return true
end
-- Ordering function for tweaks when appened to css_test.
-- The order needs to be consistent for crengine's stylesheet change
-- detection code to not invalidate cache across loadings.
local function tweakOrdering(l, r)
if l.priority ~= r.priority then
-- lower priority first in the CSS text
return l.priority < r.priority
end
-- same priority: order by ids
return l.id < r.id
end
-- Reader component for managing tweaks. The aggregated css_text
-- is actually requested from us and applied by ReaderTypeset
local ReaderStyleTweak = InputContainer:new{
tweaks_by_id = nil,
tweaks_table = nil, -- sub-menu items
nb_enabled_tweaks = 0, -- for use by main menu item
css_text = nil, -- aggregated css text from tweaks individual css snippets
enabled = true, -- allows for toggling between selected tweaks / none
}
function ReaderStyleTweak:isTweakEnabled(tweak_id)
local g_enabled = false
local enabled = false
if self.global_tweaks[tweak_id] then
enabled = true
g_enabled = true
end
if self.doc_tweaks[tweak_id] == true then
enabled = true
elseif self.doc_tweaks[tweak_id] == false then
enabled = false
end
return enabled, g_enabled
end
function ReaderStyleTweak:nbTweaksEnabled(sub_item_table)
local nb_enabled = 0
local nb_found = 0
for _, item in ipairs(sub_item_table) do
if item.sub_item_table then
local sub_nb_enabled, sub_nb_found = self:nbTweaksEnabled(item.sub_item_table)
nb_enabled = nb_enabled + sub_nb_enabled
nb_found = nb_found + sub_nb_found
elseif item.tweak_id then
if self:isTweakEnabled(item.tweak_id) then
nb_enabled = nb_enabled + 1
end
nb_found = nb_found + 1
end
end
return nb_enabled, nb_found
end
-- Called by ReaderTypeset, returns the already built string
function ReaderStyleTweak:getCssText()
return self.css_text
end
-- Build css_text, and request ReaderTypeset to apply it if wanted
function ReaderStyleTweak:updateCssText(apply)
if self.enabled then
local tweaks = {}
for id, enabled in pairs(self.global_tweaks) do
-- there are only enabled tweaks in global_tweaks, but we don't
-- use them if they are disabled in doc_tweaks
if self.doc_tweaks[id] ~= false then
table.insert(tweaks, self.tweaks_by_id[id])
end
end
for id, enabled in pairs(self.doc_tweaks) do
-- there are enabled (true) and disabled (false) tweaks in doc_tweaks
if self.doc_tweaks[id] == true then
table.insert(tweaks, self.tweaks_by_id[id])
end
end
table.sort(tweaks, tweakOrdering)
self.nb_enabled_tweaks = 0
local css_snippets = {}
for _, tweak in ipairs(tweaks) do
self.nb_enabled_tweaks = self.nb_enabled_tweaks + 1
local css = tweak.css
if not css and tweak.css_path then
css = ""
local f = io.open(tweak.css_path, "r")
if f then
css = f:read("*all")
f:close()
end
-- We could store what's been read into tweak.css to avoid
-- re-reading it, but this will allow a user to experiment
-- wihout having to restart KOReader
end
css = css:gsub("^%s+", ""):gsub("%s+$", "")
table.insert(css_snippets, css)
end
self.css_text = table.concat(css_snippets, "\n")
logger.dbg("made tweak css:\n".. self.css_text .. "[END]")
else
self.css_text = nil
logger.dbg("made no tweak css (Style tweaks disabled)")
end
if apply then
self.ui:handleEvent(Event:new("ApplyStyleSheet"))
end
end
function ReaderStyleTweak:onReadSettings(config)
self.enabled = not (config:readSetting("style_tweaks_enabled") == false)
self.doc_tweaks = config:readSetting("style_tweaks") or {}
self.global_tweaks = G_reader_settings:readSetting("style_tweaks") or {}
self:updateCssText()
end
function ReaderStyleTweak:onSaveSettings()
if self.enabled then
self.ui.doc_settings:delSetting("style_tweaks_enabled")
else
self.ui.doc_settings:saveSetting("style_tweaks_enabled", false)
end
self.ui.doc_settings:saveSetting("style_tweaks", util.tableSize(self.doc_tweaks) > 0 and self.doc_tweaks or nil)
G_reader_settings:saveSetting("style_tweaks", util.tableSize(self.global_tweaks) > 0 and self.global_tweaks or nil)
end
function ReaderStyleTweak:init()
self.tweaks_by_id = {}
self.tweaks_table = {}
-- Add first item of sub-menu, that allows toggling between
-- enabled tweaks / none (without the need to disable each of
-- them)
table.insert(self.tweaks_table, {
text = _("Enable style tweaks (hold for info)"),
checked_func = function() return self.enabled end,
callback = function()
self.enabled = not self.enabled
self:updateCssText(true) -- apply it immediately
end,
hold_may_update_menu = true, -- just to keep menu opened
hold_callback = function()
UIManager:show(InfoMessage:new{
text = _([[
Style tweaks allow changing small parts of book styles (including the publisher/embedded styles) to make visual adjustments or disable unwanted publisher layout choices.
Some tweaks may be useful with some books, while resulting in undesirable effects with others.
You can enable individual tweaks on this book with a tap, or view more details about a tweak and enable it on all books with hold.]])
})
end,
separator = true,
})
-- Single function for use as enabled_func
local is_enabled = function() return self.enabled end
-- Generic function to recursively build a sub_item_table (as expected
-- by TouchMenu) from a table of tweak definitions (like CssTweaks from
-- css_tweaks.lua, or like the one we build from user styletweaks
-- directory files and sub-directories)
local addTweakMenuItem
addTweakMenuItem = function(menu, item)
if type(item) == "table" and #item > 0 then -- sub-menu
local sub_item_table = {}
for _, it in ipairs(item) do
addTweakMenuItem(sub_item_table, it) -- recurse
end
table.insert(menu, {
text_func = function()
local text = item.title or "### undefined submenu title ###"
local nb_enabled, nb_found = self:nbTweaksEnabled(sub_item_table) -- luacheck: no unused
-- We could add nb_enabled/nb_found, but that makes for
-- a busy/ugly menu
-- text = string.format("%s (%d/%d)", text, nb_enabled, nb_found)
if nb_enabled > 0 then
text = string.format("%s (%d)", text, nb_enabled)
end
return text
end,
enabled_func = is_enabled,
sub_item_table = sub_item_table,
})
elseif item.id then -- tweak menu item
-- Set a default priority of 0 if item doesn't have one
if not item.priority then item.priority = 0 end
self.tweaks_by_id[item.id] = item
table.insert(menu, {
tweak_id = item.id,
enabled_func = is_enabled,
checked_func = function() return self:isTweakEnabled(item.id) end,
-- text = item.title or "### undefined tweak title ###",
text_func = function()
local title = item.title or "### undefined tweak title ###"
if self.global_tweaks[item.id] then
title = title .. ""
end
return title
end,
hold_may_update_menu = true,
hold_callback = function(refresh_menu_func)
UIManager:show(TweakInfoWidget:new{
tweak = item,
is_global_default = self.global_tweaks[item.id],
toggle_global_default_callback = function()
if self.global_tweaks[item.id] then
self.global_tweaks[item.id] = nil
else
self.global_tweaks[item.id] = true
end
refresh_menu_func()
self:updateCssText(true) -- apply it immediately
end
})
end,
callback = function()
-- enable/disable only for this book
local enabled, g_enabled = self:isTweakEnabled(item.id)
if enabled then
if g_enabled then
-- if globaly enabled, mark it as disabled
-- for this document only
self.doc_tweaks[item.id] = false
else
self.doc_tweaks[item.id] = nil
end
else
self.doc_tweaks[item.id] = true
end
self:updateCssText(true) -- apply it immediately
end,
separator = item.separator,
})
else
table.insert(menu, {
text = item.if_empty_menu_title or _("This section is empty"),
enabled = false,
})
end
end
-- Add each of CssTweaks' top-level items as a sub-menu
for _, item in ipairs(CssTweaks) do
addTweakMenuItem(self.tweaks_table, item)
end
-- Users can put their own style tweaks as individual .css files into
-- koreader/styletweaks/ directory. These can be organized into
-- sub-directories that will show up as sub-menus.
local user_styletweaks_dir = DataStorage:getDataDir() .. "/styletweaks"
local user_tweaks_table = { title = _("User style tweaks") }
-- Build a tweak definition table from the content of a directory
local process_tweaks_dir
process_tweaks_dir = function(dir, item_table, if_empty_menu_title)
local file_list = {}
local dir_list = {}
if lfs.attributes(dir, "mode") == "directory" then
for f in lfs.dir(dir) do
if f ~= "." and f ~= ".." then
local mode = lfs.attributes(dir.."/"..f, "mode")
if mode == "directory" then
table.insert(dir_list, f)
elseif mode == "file" and string.match(f, "%.css$") then
table.insert(file_list, f)
end
end
end
end
table.sort(dir_list)
table.sort(file_list)
for __, subdir in ipairs(dir_list) do
local sub_item_table = { title = subdir:gsub("_", " ") }
process_tweaks_dir(dir.."/"..subdir, sub_item_table)
table.insert(item_table, sub_item_table)
end
for __, file in ipairs(file_list) do
local title = file:gsub("%.css$", ""):gsub("_", " ")
local filepath = dir.."/"..file
table.insert(item_table, {
title = title,
id = file, -- keep ".css" in id, to distinguish between koreader/user tweaks
description = T(_("User style tweak at %1"), filepath),
priority = 10, -- give user tweaks a higher priority
css_path = filepath,
})
end
if #item_table == 0 then
table.insert(item_table, {
if_empty_menu_title = if_empty_menu_title or _("No CSS tweak found in this directory"),
})
end
end
local if_empty_menu_title = _("Add your own tweaks in koreader/styletweaks/")
process_tweaks_dir(user_styletweaks_dir, user_tweaks_table, if_empty_menu_title)
self.tweaks_table[#self.tweaks_table].separator = true
addTweakMenuItem(self.tweaks_table, user_tweaks_table)
self.ui.menu:registerToMainMenu(self)
end
function ReaderStyleTweak:addToMainMenu(menu_items)
-- insert table to main reader menu
menu_items.style_tweaks = {
text_func = function()
if self.enabled and self.nb_enabled_tweaks > 0 then
return T(_("Style tweaks (%1)"), self.nb_enabled_tweaks)
else
return _("Style tweaks")
end
end,
sub_item_table = self.tweaks_table,
}
end
return ReaderStyleTweak

@ -19,10 +19,11 @@ end
function ReaderTypeset:onReadSettings(config)
self.css = config:readSetting("css") or G_reader_settings:readSetting("copt_css")
local tweaks_css = self.ui.styletweak:getCssText()
if self.css then
self.ui.document:setStyleSheet(self.css)
self.ui.document:setStyleSheet(self.css, tweaks_css)
else
self.ui.document:setStyleSheet(self.ui.document.default_css)
self.ui.document:setStyleSheet(self.ui.document.default_css, tweaks_css)
self.css = self.ui.document.default_css
end
@ -127,10 +128,18 @@ function ReaderTypeset:genStyleSheetMenu()
return style_table
end
function ReaderTypeset:onApplyStyleSheet()
local tweaks_css = self.ui.styletweak:getCssText()
self.ui.document:setStyleSheet(self.css, tweaks_css)
self.ui:handleEvent(Event:new("UpdatePos"))
return true
end
function ReaderTypeset:setStyleSheet(new_css)
if new_css ~= self.css then
self.css = new_css
self.ui.document:setStyleSheet(new_css)
local tweaks_css = self.ui.styletweak:getCssText()
self.ui.document:setStyleSheet(new_css, tweaks_css)
self.ui:handleEvent(Event:new("UpdatePos"))
end
end

@ -38,6 +38,7 @@ local ReaderPaging = require("apps/reader/modules/readerpaging")
local ReaderRolling = require("apps/reader/modules/readerrolling")
local ReaderSearch = require("apps/reader/modules/readersearch")
local ReaderStatus = require("apps/reader/modules/readerstatus")
local ReaderStyleTweak = require("apps/reader/modules/readerstyletweak")
local ReaderToc = require("apps/reader/modules/readertoc")
local ReaderTypeset = require("apps/reader/modules/readertypeset")
local ReaderView = require("apps/reader/modules/readerview")
@ -268,6 +269,12 @@ function ReaderUI:init()
self.document:render()
end)
-- styletweak controller (must be before typeset controller)
self:registerModule("styletweak", ReaderStyleTweak:new{
dialog = self.dialog,
view = self.view,
ui = self
})
-- typeset controller
self:registerModule("typeset", ReaderTypeset:new{
dialog = self.dialog,

@ -509,9 +509,11 @@ function CreDocument:setFontHinting(mode)
self._document:setIntProperty("font.hinting.mode", mode)
end
function CreDocument:setStyleSheet(new_css)
logger.dbg("CreDocument: set style sheet", new_css)
self._document:setStyleSheet(new_css)
function CreDocument:setStyleSheet(new_css_file, appended_css_content )
logger.dbg("CreDocument: set style sheet:",
new_css_file and new_css_file or "no file",
appended_css_content and "and appended content ("..#appended_css_content.." bytes)" or "(no appended content)")
self._document:setStyleSheet(new_css_file, appended_css_content)
end
function CreDocument:setEmbeddedStyleSheet(toggle)

@ -0,0 +1,158 @@
--[[--
CSS tweaks must have the following attributes:
- id: unique ID identifying this tweak, to be stored in settings
- title: menu item title (must not be too long)
- css: stylesheet text to append to external stylesheet
They may have the following optional attributes:
- description: text displayed when holding on menu item
- priority: higher numbers are appended after lower numbers
(if not specified, default to 0)
]]
local _ = require("gettext")
local CssTweaks = {
{
title = _("Page"),
{
id = "margin_body_0";
title = _("Ignore publisher page margins"),
description = _("Force page margins to be 0, and may allow KOReader's margin settings to work on books where they would not."),
css = [[body { margin: 0 !important; }]],
},
{
id = "margin_all_0";
title = _("Ignore all publisher margins"),
priority = 2,
css = [[* { margin: 0 !important; }]],
},
{
id = "titles_page-break-before_avoid ";
title = _("Avoid blank page on chapter start"),
css = [[h1, h2, h3, .title, .title1, .title2, .title3 { page-break-before: avoid !important; }]],
},
},
{
title = _("Text"),
{
id = "sub_sup_smaller";
title = _("Smaller sub- and superscript"),
description = _("Prevent sub- and superscript from affecting line-height."),
-- https://friendsofepub.github.io/eBookTricks/
-- https://github.com/koreader/koreader/issues/3923#issuecomment-386510294
css = [[
sup { font-size: 50%; vertical-align: super; }
sub { font-size: 50%; vertical-align: middle; }
]],
separator = true,
},
{
id = "lineheight_all_inherit";
title = _("Ignore publisher line heights"),
description = _("Disable line-height specified in embedded styles, and may allow KOReader's line spacing settings to work on books where they would not."),
css = [[* { line-height: inherit !important; }]],
},
{
id = "font_family_all_inherit";
title = _("Ignore publisher font families"),
description = _("Disable font-family specified in embedded styles."),
-- we have to use this trick, font-family handling by crengine is a bit complex
css = [[* { font-family: "NoSuchFont" !important; }]],
},
{
id = "font_size_all_inherit";
title = _("Ignore publisher font sizes"),
description = _("Disable font-size specified in embedded styles."),
css = [[* { font-size: inherit !important; }]],
separator = true,
},
{
title = _("Links color and weight"),
{
id = "a_black";
title = _("Links always black"),
css = [[a { color: black !important; }]],
},
{
id = "a_blue";
title = _("Links always blue"),
css = [[a { color: blue !important; }]],
separator = true,
},
{
id = "a_bold";
title = _("Links always bold"),
css = [[a { font-weight: bold !important; }]],
},
{
id = "a_not_bold";
title = _("Links never bold"),
css = [[a { font-weight: normal !important; }]],
},
},
},
{
title = _("Miscellaneous"),
{
id = "table_row_odd_even";
title = _("Alternate background color of table rows"),
css = [[
tr:nth-child(odd) { background-color: #EEE !important; }
tr:nth-child(even) { background-color: #CCC !important; }
]],
},
{
id = "table_force_border";
title = _("Show borders on all tables"),
css = [[
table, tcaption, tr, th, td { border: black solid 1px; border-collapse: collapse; }
]],
separator = true,
},
{
id = "image_full_width";
title = _("Make images full width"),
description = _("Useful for books containing only images, when they are smaller than your screen. May stretch images in some cases."),
-- This helped me once with a book. Will mess with aspect ratio
-- when images have a style="width: NNpx; heigh: NNpx"
css = [[
img {
text-align: center !important;
text-indent: 0px !important;
display: block !important;
width: 100% !important;
}
]],
},
},
{
title = _("Workarounds"),
{
id = "html_tags_fix";
title = _("Fix some HTML elements"),
description = _("Make some HTML elements (eg: <cite>) behave as they should (inline/block).\nThis may break past bookmarks and highlights."),
css = [[
cite { display: inline; font-style: italic; }
]],
},
{
id = "list_items_fix";
title = _("Fix some list items issues"),
description = _("Work around some crengine list items rendering issues."),
css = [[
li > p:first-child { display: inline !important; }
li > div:first-child { display: inline !important; }
]],
},
{
id = "border_all_none";
title = _("Remove all borders"),
description = _("Work around a crengine bug that makes a border drawn when {border: black solid 0px}."),
-- css = [[* { border-style: none !important; }]],
-- Better to keep the layout implied by width, just draw them in white
css = [[* { border-color: white !important; }]],
},
},
}
return CssTweaks

@ -23,6 +23,7 @@ local order = {
"page_overlap",
"switch_zoom_mode",
"set_render_style",
"style_tweaks",
"----------------------------",
"highlight_options",
"----------------------------",

Loading…
Cancel
Save