diff --git a/Makefile b/Makefile
index c9c252089..baef42281 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-PHONY = all android-ndk android-sdk base clean distclean doc fetchthirdparty po pot re static-check
+PHONY = all android-ndk android-sdk base clean distclean doc fetchthirdparty re static-check
SOUND = $(INSTALL_DIR)/%
# koreader-base directory
@@ -81,6 +81,7 @@ ev_replay.py
help
history
l10n/templates
+l10n/*/*.po
ota
resources/fonts*
resources/icons/src*
@@ -104,6 +105,7 @@ endef
define UPDATE_PATH_EXCLUDES +=
dummy-test-file*
file.sdr*
+i18n-test
readerbookmark.*
readerhighlight.*
testdata
@@ -134,7 +136,7 @@ $(foreach a,$1,'$(if $(filter --%,$a),$a,$(abspath $a))') $(or $2,koreader)
$(call release_excludes,$(or $2,koreader)/)
endef
-all: base
+all: base mo
install -d $(INSTALL_DIR)/koreader
rm -f $(INSTALL_DIR)/koreader/git-rev; echo "$(VERSION)" > $(INSTALL_DIR)/koreader/git-rev
ifdef ANDROID
@@ -190,7 +192,7 @@ else
git submodule update --jobs 3 --init --recursive
endif
-clean: base-clean
+clean: base-clean mo-clean
rm -rf $(INSTALL_DIR)
distclean: clean base-distclean
@@ -209,31 +211,14 @@ ifneq (,$(wildcard make/$(TARGET).mk))
include make/$(TARGET).mk
endif
+include make/gettext.mk
+
android-ndk:
$(MAKE) -C $(KOR_BASE)/toolchain $(ANDROID_NDK_HOME)
android-sdk:
$(MAKE) -C $(KOR_BASE)/toolchain $(ANDROID_HOME)
-# for gettext
-DOMAIN=koreader
-TEMPLATE_DIR=l10n/templates
-XGETTEXT_BIN=xgettext
-
-pot: po
- mkdir -p $(TEMPLATE_DIR)
- $(XGETTEXT_BIN) --from-code=utf-8 \
- --keyword=C_:1c,2 --keyword=N_:1,2 --keyword=NC_:1c,2,3 \
- --add-comments=@translators \
- reader.lua `find frontend -iname "*.lua" | sort` \
- `find plugins -iname "*.lua" | sort` \
- `find tools -iname "*.lua" | sort` \
- -o $(TEMPLATE_DIR)/$(DOMAIN).pot
-
-po:
- git submodule update --remote l10n
-
-
static-check:
@if which luacheck > /dev/null; then \
luacheck -q {reader,setupkoenv,datastorage}.lua frontend plugins spec; \
diff --git a/base b/base
index ca3f8fcda..63f95f0f2 160000
--- a/base
+++ b/base
@@ -1 +1 @@
-Subproject commit ca3f8fcda4da00b13e737d8e0c2f6baf003cc1aa
+Subproject commit 63f95f0f2a8d87ce2b0c1a32e9f9774e2d80b556
diff --git a/frontend/apps/reader/modules/readerdictionary.lua b/frontend/apps/reader/modules/readerdictionary.lua
index bc379ddbe..b3bbd1769 100644
--- a/frontend/apps/reader/modules/readerdictionary.lua
+++ b/frontend/apps/reader/modules/readerdictionary.lua
@@ -14,6 +14,7 @@ local KeyValuePage = require("ui/widget/keyvaluepage")
local LuaData = require("luadata")
local MultiConfirmBox = require("ui/widget/multiconfirmbox")
local NetworkMgr = require("ui/network/manager")
+local Presets = require("ui/presets")
local SortWidget = require("ui/widget/sortwidget")
local Trapper = require("ui/trapper")
local UIManager = require("ui/uimanager")
@@ -165,6 +166,17 @@ function ReaderDictionary:init()
if not lookup_history then
lookup_history = LuaData:open(DataStorage:getSettingsDir() .. "/lookup_history.lua", "LookupHistory")
end
+
+ self.preset_obj = {
+ presets = G_reader_settings:readSetting("dict_presets", {}),
+ cycle_index = G_reader_settings:readSetting("dict_presets_cycle_index"),
+ dispatcher_name = "load_dictionary_preset",
+ saveCycleIndex = function(this)
+ G_reader_settings:saveSetting("dict_presets_cycle_index", this.cycle_index)
+ end,
+ buildPreset = function() return self:buildPreset() end,
+ loadPreset = function(preset) self:loadPreset(preset) end,
+ }
end
function ReaderDictionary:registerKeyEvents()
@@ -281,9 +293,18 @@ function ReaderDictionary:addToMainMenu(menu_items)
end)
end,
},
+ {
+ text = _("Dictionary presets"),
+ help_text = _("This feature allows you to organize dictionaries into presets (for example, by language). You can quickly switch between these presets to change which dictionaries are used for lookups.\n\nNote: presets only store dictionaries, no other settings."),
+ sub_item_table_func = function()
+ return Presets.genPresetMenuItemTable(self.preset_obj, _("Create new preset from enabled dictionaries"),
+ function() return self.enabled_dict_names and #self.enabled_dict_names > 0 end)
+ end,
+ },
{
text = _("Download dictionaries"),
sub_item_table_func = function() return self:_genDownloadDictionariesMenu() end,
+ separator = true,
},
{
text_func = function()
@@ -313,32 +334,40 @@ function ReaderDictionary:addToMainMenu(menu_items)
separator = true,
},
{
- text = _("Enable dictionary lookup history"),
+ text = _("Dictionary lookup history"),
checked_func = function()
return not self.disable_lookup_history
end,
- callback = function()
- self.disable_lookup_history = not self.disable_lookup_history
- G_reader_settings:saveSetting("disable_lookup_history", self.disable_lookup_history)
- end,
- },
- {
- text = _("Clean dictionary lookup history"),
- enabled_func = function()
- return lookup_history:has("lookup_history")
- end,
- keep_menu_open = true,
- callback = function(touchmenu_instance)
- UIManager:show(ConfirmBox:new{
- text = _("Clean dictionary lookup history?"),
- ok_text = _("Clean"),
- ok_callback = function()
- -- empty data table to replace current one
- lookup_history:reset{}
- touchmenu_instance:updateItems()
+ sub_item_table = {
+ {
+ text = _("Enable dictionary lookup history"),
+ checked_func = function()
+ return not self.disable_lookup_history
end,
- })
- end,
+ callback = function()
+ self.disable_lookup_history = not self.disable_lookup_history
+ G_reader_settings:saveSetting("disable_lookup_history", self.disable_lookup_history)
+ end,
+ },
+ {
+ text = _("Clean dictionary lookup history"),
+ enabled_func = function()
+ return lookup_history:has("lookup_history")
+ end,
+ keep_menu_open = true,
+ callback = function(touchmenu_instance)
+ UIManager:show(ConfirmBox:new{
+ text = _("Clean dictionary lookup history?"),
+ ok_text = _("Clean"),
+ ok_callback = function()
+ -- empty data table to replace current one
+ lookup_history:reset{}
+ touchmenu_instance:updateItems()
+ end,
+ })
+ end,
+ },
+ },
separator = true,
},
{ -- setting used by dictquicklookup
@@ -385,7 +414,7 @@ function ReaderDictionary:addToMainMenu(menu_items)
}
}
if not is_docless then
- table.insert(menu_items.dictionary_settings.sub_item_table, 3, {
+ table.insert(menu_items.dictionary_settings.sub_item_table, 2, {
keep_menu_open = true,
text = _("Set dictionary priority for this book"),
help_text = _("This feature enables you to specify dictionary priorities on a per-book basis. Results from higher-priority dictionaries will be displayed first when looking up words. Only dictionaries that are currently active can be selected and prioritized."),
@@ -859,31 +888,70 @@ function ReaderDictionary:dismissLookupInfo()
end
function ReaderDictionary:onShowDictionaryLookup()
+ local buttons = {}
+ local preset_names = Presets.getPresets(self.preset_obj)
+ if preset_names and #preset_names > 0 then
+ table.insert(buttons, {
+ {
+ text = _("Search with preset"),
+ callback = function()
+ local text = self.dictionary_lookup_dialog:getInputText()
+ if text == "" or text:match("^%s*$") then return end
+ local current_dict_state = self:buildPreset()
+ local button_dialog, dialog_buttons = nil, {} -- CI won't like it if we call it buttons :( so dialog_buttons
+ for _, preset_name in ipairs(preset_names) do
+ table.insert(dialog_buttons, {
+ {
+ align = "left",
+ text = preset_name,
+ callback = function()
+ self:loadPreset(self.preset_obj.presets[preset_name], true)
+ UIManager:close(button_dialog)
+ UIManager:close(self.dictionary_lookup_dialog)
+ self:onLookupWord(text, true, nil, nil, nil,
+ function()
+ self:loadPreset(current_dict_state, true)
+ end
+ )
+ end,
+ }
+ })
+ end
+ button_dialog = ButtonDialog:new{
+ buttons = dialog_buttons,
+ shrink_unneeded_width = true,
+ }
+ UIManager:show(button_dialog)
+ end,
+ }
+ })
+ end
+
+ table.insert(buttons, {
+ {
+ text = _("Cancel"),
+ id = "close",
+ callback = function()
+ UIManager:close(self.dictionary_lookup_dialog)
+ end,
+ },
+ {
+ text = _("Search dictionary"),
+ is_enter_default = true,
+ callback = function()
+ if self.dictionary_lookup_dialog:getInputText() == "" then return end
+ UIManager:close(self.dictionary_lookup_dialog)
+ -- Trust that input text does not need any cleaning (allows querying for "-suffix")
+ self:onLookupWord(self.dictionary_lookup_dialog:getInputText(), true)
+ end,
+ },
+ })
+
self.dictionary_lookup_dialog = InputDialog:new{
title = _("Enter a word or phrase to look up"),
input = "",
input_type = "text",
- buttons = {
- {
- {
- text = _("Cancel"),
- id = "close",
- callback = function()
- UIManager:close(self.dictionary_lookup_dialog)
- end,
- },
- {
- text = _("Search dictionary"),
- is_enter_default = true,
- callback = function()
- if self.dictionary_lookup_dialog:getInputText() == "" then return end
- UIManager:close(self.dictionary_lookup_dialog)
- -- Trust that input text does not need any cleaning (allows querying for "-suffix")
- self:onLookupWord(self.dictionary_lookup_dialog:getInputText(), true)
- end,
- },
- }
- },
+ buttons = buttons,
}
UIManager:show(self.dictionary_lookup_dialog)
self.dictionary_lookup_dialog:onShowKeyboard()
@@ -1459,4 +1527,64 @@ The current default (★) is enabled.]])
})
end
+function ReaderDictionary:buildPreset()
+ local preset = { enabled_dict_names = {} } -- Only store the names of enabled dictionaries.
+ for _, name in ipairs(self.enabled_dict_names) do
+ preset.enabled_dict_names[name] = true
+ end
+ return preset
+end
+
+function ReaderDictionary:loadPreset(preset, skip_notification)
+ if not preset.enabled_dict_names then return end
+ -- build a list of currently available dictionary names for validation
+ local available_dict_names = {}
+ for _, ifo in ipairs(available_ifos) do
+ available_dict_names[ifo.name] = true
+ end
+ -- Only enable dictionaries from the preset that are still available, and re-build self.dicts_disabled
+ -- to make sure dicts added after the creation of the preset, are disabled as well.
+ local dicts_disabled, valid_enabled_names = {}, {}
+ for _, ifo in ipairs(available_ifos) do
+ if preset.enabled_dict_names[ifo.name] then
+ table.insert(valid_enabled_names, ifo.name)
+ else
+ dicts_disabled[ifo.file] = true
+ end
+ end
+ -- update both settings and save
+ self.dicts_disabled = dicts_disabled
+ self.enabled_dict_names = valid_enabled_names
+ G_reader_settings:saveSetting("dicts_disabled", self.dicts_disabled)
+ self:onSaveSettings()
+ self:updateSdcvDictNamesOptions()
+ -- Show a message if any dictionaries from the preset are missing.
+ if not skip_notification and util.tableSize(preset.enabled_dict_names) > #valid_enabled_names then
+ local missing_dicts = {}
+ for preset_name, _ in pairs(preset.enabled_dict_names) do
+ if not available_dict_names[preset_name] then
+ table.insert(missing_dicts, preset_name)
+ end
+ end
+ UIManager:show(InfoMessage:new{
+ text = _("Some dictionaries from this preset have been deleted or are no longer available:") .. "\n\n• " .. table.concat(missing_dicts, "\n• "),
+ })
+ end
+end
+
+function ReaderDictionary:onCycleDictionaryPresets()
+ return Presets.cycleThroughPresets(self.preset_obj, true)
+end
+
+function ReaderDictionary:onLoadDictionaryPreset(preset_name)
+ return Presets.onLoadPreset(self.preset_obj, preset_name, true)
+end
+
+function ReaderDictionary.getPresets() -- for Dispatcher
+ local dict_config = {
+ presets = G_reader_settings:readSetting("dict_presets", {})
+ }
+ return Presets.getPresets(dict_config)
+end
+
return ReaderDictionary
diff --git a/frontend/apps/reader/modules/readerfooter.lua b/frontend/apps/reader/modules/readerfooter.lua
index b52f3ba0f..a653f5e5a 100644
--- a/frontend/apps/reader/modules/readerfooter.lua
+++ b/frontend/apps/reader/modules/readerfooter.lua
@@ -9,12 +9,10 @@ local FrameContainer = require("ui/widget/container/framecontainer")
local Geom = require("ui/geometry")
local HorizontalGroup = require("ui/widget/horizontalgroup")
local HorizontalSpan = require("ui/widget/horizontalspan")
-local InfoMessage = require("ui/widget/infomessage")
-local InputDialog = require("ui/widget/inputdialog")
local LeftContainer = require("ui/widget/container/leftcontainer")
local LineWidget = require("ui/widget/linewidget")
-local MultiConfirmBox = require("ui/widget/multiconfirmbox")
local MultiInputDialog = require("ui/widget/multiinputdialog")
+local Presets = require("ui/presets")
local ProgressWidget = require("ui/widget/progresswidget")
local RightContainer = require("ui/widget/container/rightcontainer")
local Size = require("ui/size")
@@ -25,7 +23,6 @@ local VerticalGroup = require("ui/widget/verticalgroup")
local VerticalSpan = require("ui/widget/verticalspan")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local datetime = require("datetime")
-local ffiUtil = require("ffi/util")
local logger = require("logger")
local util = require("util")
local T = require("ffi/util").template
@@ -665,6 +662,13 @@ function ReaderFooter:init()
self.custom_text = G_reader_settings:readSetting("reader_footer_custom_text", "KOReader")
self.custom_text_repetitions =
tonumber(G_reader_settings:readSetting("reader_footer_custom_text_repetitions", "1"))
+
+ self.preset_obj = {
+ presets = G_reader_settings:readSetting("footer_presets", {}),
+ dispatcher_name = "load_footer_preset",
+ buildPreset = function() return self:buildPreset() end,
+ loadPreset = function(preset) self:loadPreset(preset) end,
+ }
end
function ReaderFooter:set_custom_text(touchmenu_instance)
@@ -1710,7 +1714,7 @@ With this feature enabled, the current page is factored in, resulting in the cou
text = _("Status bar presets"),
separator = true,
sub_item_table_func = function()
- return self:genPresetMenuItemTable()
+ return Presets.genPresetMenuItemTable(self.preset_obj, nil, nil)
end,
})
table.insert(sub_items, {
@@ -1930,91 +1934,6 @@ function ReaderFooter:genAlignmentMenuItems(value)
}
end
-function ReaderFooter:genPresetMenuItemTable()
- local footer_presets = G_reader_settings:readSetting("footer_presets", {})
- local items = {
- {
- text = _("Create new preset from current settings"),
- keep_menu_open = true,
- callback = function(touchmenu_instance)
- self:createPresetFromCurrentSettings(touchmenu_instance)
- end,
- separator = true,
- },
- }
- for preset_name in ffiUtil.orderedPairs(footer_presets) do
- table.insert(items, {
- text = preset_name,
- keep_menu_open = true,
- callback = function()
- self:loadPreset(footer_presets[preset_name])
- end,
- hold_callback = function(touchmenu_instance, item)
- UIManager:show(MultiConfirmBox:new{
- text = T(_("What would you like to do with preset '%1'?"), preset_name),
- choice1_text = _("Delete"),
- choice1_callback = function()
- footer_presets[preset_name] = nil
- UIManager:broadcastEvent(Event:new("DispatcherActionValueChanged",
- { name = "load_footer_preset", old_value = preset_name, new_value = nil }))
- table.remove(touchmenu_instance.item_table, item.idx)
- touchmenu_instance:updateItems()
- end,
- choice2_text = _("Update"),
- choice2_callback = function()
- footer_presets[preset_name] = self:buildPreset()
- UIManager:show(InfoMessage:new{
- text = T(_("Preset '%1' was updated with current settings"), preset_name),
- timeout = 2,
- })
- end,
- })
- end,
- })
- end
- return items
-end
-
-function ReaderFooter:createPresetFromCurrentSettings(touchmenu_instance)
- local input_dialog
- input_dialog = InputDialog:new{
- title = _("Enter preset name"),
- buttons = {
- {
- {
- text = _("Cancel"),
- id = "close",
- callback = function()
- UIManager:close(input_dialog)
- end,
- },
- {
- text = _("Save"),
- is_enter_default = true,
- callback = function()
- local preset_name = input_dialog:getInputText()
- if preset_name == "" or preset_name:match("^%s*$") then return end
- local footer_presets = G_reader_settings:readSetting("footer_presets")
- if footer_presets[preset_name] then
- UIManager:show(InfoMessage:new{
- text = T(_("A preset named '%1' already exists. Please choose a different name."), preset_name),
- timeout = 2,
- })
- else
- footer_presets[preset_name] = self:buildPreset()
- UIManager:close(input_dialog)
- touchmenu_instance.item_table = self:genPresetMenuItemTable()
- touchmenu_instance:updateItems()
- end
- end,
- },
- },
- },
- }
- UIManager:show(input_dialog)
- input_dialog:onShowKeyboard()
-end
-
function ReaderFooter:buildPreset()
return {
footer = util.tableDeepCopy(self.settings),
@@ -2048,25 +1967,14 @@ function ReaderFooter:loadPreset(preset)
end
function ReaderFooter:onLoadFooterPreset(preset_name)
- local footer_presets = G_reader_settings:readSetting("footer_presets")
- if footer_presets and footer_presets[preset_name] then
- self:loadPreset(footer_presets[preset_name])
- end
- return true
+ return Presets.onLoadPreset(self.preset_obj, preset_name, true)
end
function ReaderFooter.getPresets() -- for Dispatcher
- local footer_presets = G_reader_settings:readSetting("footer_presets")
- local actions = {}
- if footer_presets and next(footer_presets) then
- for preset_name in pairs(footer_presets) do
- table.insert(actions, preset_name)
- end
- if #actions > 1 then
- table.sort(actions)
- end
- end
- return actions, actions
+ local footer_config = {
+ presets = G_reader_settings:readSetting("footer_presets", {})
+ }
+ return Presets.getPresets(footer_config)
end
function ReaderFooter:addAdditionalFooterContent(content_func)
diff --git a/frontend/apps/reader/modules/readerhighlight.lua b/frontend/apps/reader/modules/readerhighlight.lua
index db6531115..ea4059870 100644
--- a/frontend/apps/reader/modules/readerhighlight.lua
+++ b/frontend/apps/reader/modules/readerhighlight.lua
@@ -75,6 +75,9 @@ function ReaderHighlight:init()
callback = function()
this:startSelection(index)
this:onClose()
+ if not Device:isTouchDevice() then
+ self:onStartHighlightIndicator()
+ end
end,
}
end,
@@ -245,20 +248,18 @@ function ReaderHighlight:registerKeyEvents()
self.key_events.RightHighlightIndicator = { { "Right" }, event = "MoveHighlightIndicator", args = {1, 0} }
self.key_events.HighlightPress = { { "Press" } }
end
- if Device:hasKeyboard() then
+ if Device:hasScreenKB() or Device:hasKeyboard() then
+ local modifier = Device:hasScreenKB() and "ScreenKB" or "Shift"
-- Used for text selection with dpad/keys
local QUICK_INDICATOR_MOVE = true
- self.key_events.QuickUpHighlightIndicator = { { "Shift", "Up" }, event = "MoveHighlightIndicator", args = {0, -1, QUICK_INDICATOR_MOVE} }
- self.key_events.QuickDownHighlightIndicator = { { "Shift", "Down" }, event = "MoveHighlightIndicator", args = {0, 1, QUICK_INDICATOR_MOVE} }
- self.key_events.QuickLeftHighlightIndicator = { { "Shift", "Left" }, event = "MoveHighlightIndicator", args = {-1, 0, QUICK_INDICATOR_MOVE} }
- self.key_events.QuickRightHighlightIndicator = { { "Shift", "Right" }, event = "MoveHighlightIndicator", args = {1, 0, QUICK_INDICATOR_MOVE} }
- self.key_events.StartHighlightIndicator = { { "H" } }
- elseif Device:hasScreenKB() then
- local QUICK_INDICATOR_MOVE = true
- self.key_events.QuickUpHighlightIndicator = { { "ScreenKB", "Up" }, event = "MoveHighlightIndicator", args = {0, -1, QUICK_INDICATOR_MOVE} }
- self.key_events.QuickDownHighlightIndicator = { { "ScreenKB", "Down" }, event = "MoveHighlightIndicator", args = {0, 1, QUICK_INDICATOR_MOVE} }
- self.key_events.QuickLeftHighlightIndicator = { { "ScreenKB", "Left" }, event = "MoveHighlightIndicator", args = {-1, 0, QUICK_INDICATOR_MOVE} }
- self.key_events.QuickRightHighlightIndicator = { { "ScreenKB", "Right" }, event = "MoveHighlightIndicator", args = {1, 0, QUICK_INDICATOR_MOVE} }
+ self.key_events.QuickUpHighlightIndicator = { { modifier, "Up" }, event = "MoveHighlightIndicator", args = {0, -1, QUICK_INDICATOR_MOVE} }
+ self.key_events.QuickDownHighlightIndicator = { { modifier, "Down" }, event = "MoveHighlightIndicator", args = {0, 1, QUICK_INDICATOR_MOVE} }
+ self.key_events.QuickLeftHighlightIndicator = { { modifier, "Left" }, event = "MoveHighlightIndicator", args = {-1, 0, QUICK_INDICATOR_MOVE} }
+ self.key_events.QuickRightHighlightIndicator = { { modifier, "Right" }, event = "MoveHighlightIndicator", args = {1, 0, QUICK_INDICATOR_MOVE} }
+ self.key_events.HighlightModifierPress = { { modifier, "Press" } }
+ if Device:hasKeyboard() then
+ self.key_events.StartHighlightIndicator = { { "H" } }
+ end
end
end
@@ -2673,15 +2674,22 @@ end
-- dpad/keys support
-function ReaderHighlight:onHighlightPress()
+function ReaderHighlight:onHighlightPress(skip_tap_check)
if not self._current_indicator_pos then return false end
if self._start_indicator_highlight then
self:onHoldRelease(nil, self:_createHighlightGesture("hold_release"))
self:onStopHighlightIndicator()
return true
end
+ -- Check if we're in select mode (or extending an existing highlight)
+ if self.select_mode and self.highlight_idx then
+ self:onHold(nil, self:_createHighlightGesture("hold"))
+ self:onHoldRelease(nil, self:_createHighlightGesture("hold_release"))
+ self:onStopHighlightIndicator()
+ return true
+ end
-- Attempt to open an existing highlight
- if self:onTap(nil, self:_createHighlightGesture("tap")) then
+ if not skip_tap_check and self:onTap(nil, self:_createHighlightGesture("tap")) then
self:onStopHighlightIndicator(true) -- need_clear_selection=true
return true
end
@@ -2764,6 +2772,19 @@ function ReaderHighlight:onHighlightPress()
return true
end
+function ReaderHighlight:onHighlightModifierPress()
+ if not self._current_indicator_pos then return false end -- let event propagate to hotkeys
+ if not self._start_indicator_highlight then
+ self:onHighlightPress(true)
+ return true -- don't trigger hotkeys during text selection
+ end
+ -- Simulate very long-long press by setting the long hold flag. This will trigger the long-press dialog.
+ self.long_hold_reached = true
+ self:onHoldRelease(nil, self:_createHighlightGesture("hold_release"))
+ self:onStopHighlightIndicator()
+ return true
+end
+
function ReaderHighlight:onStartHighlightIndicator()
-- disable long-press icon (poke-ball), as it is triggered constantly due to NT devices needing a workaround for text selection to work.
self.long_hold_reached_action = function() end
@@ -2786,6 +2807,16 @@ function ReaderHighlight:onStartHighlightIndicator()
end
function ReaderHighlight:onStopHighlightIndicator(need_clear_selection)
+ -- If we're in select mode and user presses back, end the selection
+ if self.select_mode and self.highlight_idx then
+ self.select_mode = false
+ if self.ui.annotation.annotations[self.highlight_idx].is_tmp then
+ self:deleteHighlight(self.highlight_idx) -- temporary highlight, delete it
+ else
+ UIManager:setDirty(self.dialog, "ui", self.view.flipping:getRefreshRegion())
+ end
+ self.highlight_idx = nil
+ end
if self._current_indicator_pos then
local rect = self._current_indicator_pos
self._previous_indicator_pos = rect
diff --git a/frontend/apps/reader/modules/readerwikipedia.lua b/frontend/apps/reader/modules/readerwikipedia.lua
index 18d1c10f2..a57002355 100644
--- a/frontend/apps/reader/modules/readerwikipedia.lua
+++ b/frontend/apps/reader/modules/readerwikipedia.lua
@@ -260,32 +260,40 @@ You can choose an existing folder, or use a default folder named "Wikipedia" in
separator = true,
},
{
- text = _("Enable Wikipedia history"),
+ text = _("Wikipedia lookup history"),
checked_func = function()
return not self.disable_history
end,
- callback = function()
- self.disable_history = not self.disable_history
- G_reader_settings:saveSetting("wikipedia_disable_history", self.disable_history)
- end,
- },
- {
- text = _("Clean Wikipedia history"),
- enabled_func = function()
- return wikipedia_history:has("wikipedia_history")
- end,
- keep_menu_open = true,
- callback = function(touchmenu_instance)
- UIManager:show(ConfirmBox:new{
- text = _("Clean Wikipedia history?"),
- ok_text = _("Clean"),
- ok_callback = function()
- -- empty data table to replace current one
- wikipedia_history:reset{}
- touchmenu_instance:updateItems()
+ sub_item_table = {
+ {
+ text = _("Enable Wikipedia history"),
+ checked_func = function()
+ return not self.disable_history
end,
- })
- end,
+ callback = function()
+ self.disable_history = not self.disable_history
+ G_reader_settings:saveSetting("wikipedia_disable_history", self.disable_history)
+ end,
+ },
+ {
+ text = _("Clean Wikipedia history"),
+ enabled_func = function()
+ return wikipedia_history:has("wikipedia_history")
+ end,
+ keep_menu_open = true,
+ callback = function(touchmenu_instance)
+ UIManager:show(ConfirmBox:new{
+ text = _("Clean Wikipedia history?"),
+ ok_text = _("Clean"),
+ ok_callback = function()
+ -- empty data table to replace current one
+ wikipedia_history:reset{}
+ touchmenu_instance:updateItems()
+ end,
+ })
+ end,
+ },
+ },
separator = true,
},
{ -- setting used in wikipedia.lua
diff --git a/frontend/dispatcher.lua b/frontend/dispatcher.lua
index 9113ea317..d5f266474 100644
--- a/frontend/dispatcher.lua
+++ b/frontend/dispatcher.lua
@@ -34,6 +34,7 @@ local Device = require("device")
local Event = require("ui/event")
local FileManager = require("apps/filemanager/filemanager")
local Notification = require("ui/widget/notification")
+local ReaderDictionary = require("apps/reader/modules/readerdictionary")
local ReaderFooter = require("apps/reader/modules/readerfooter")
local ReaderHighlight = require("apps/reader/modules/readerhighlight")
local ReaderZooming = require("apps/reader/modules/readerzooming")
@@ -60,6 +61,8 @@ local settingsList = {
collections_search = {category="none", event="ShowCollectionsSearchDialog", title=_("Collections search"), general=true, separator=true},
----
dictionary_lookup = {category="none", event="ShowDictionaryLookup", title=_("Dictionary lookup"), general=true},
+ load_dictionary_preset = {category="string", event="LoadDictionaryPreset", title=_("Load dictionary preset"), args_func=ReaderDictionary.getPresets, general=true},
+ cycle_dictionary_preset = {category="none", event="CycleDictionaryPresets", title=_("Cycle through dictionary presets"), general=true,},
wikipedia_lookup = {category="none", event="ShowWikipediaLookup", title=_("Wikipedia lookup"), general=true, separator=true},
----
show_menu = {category="none", event="ShowMenu", title=_("Show menu"), general=true},
@@ -300,6 +303,8 @@ local dispatcher_menu_order = {
"collections_search",
----
"dictionary_lookup",
+ "load_dictionary_preset",
+ "cycle_dictionary_preset",
"wikipedia_lookup",
----
"show_menu",
diff --git a/frontend/gettext.lua b/frontend/gettext.lua
index b74ea73b8..e4895a88f 100644
--- a/frontend/gettext.lua
+++ b/frontend/gettext.lua
@@ -21,6 +21,12 @@ See @{ffi.util.template}() for more information about the template function.
local isAndroid, android = pcall(require, "android")
local logger = require("logger")
+local buffer = require("string.buffer")
+local ffi = require("ffi")
+local C = ffi.C
+
+require "table.new"
+require "ffi/posix_h"
local GetText = {
context = {},
@@ -61,21 +67,6 @@ function GetText_mt.__call(gettext, msgid)
return gettext.translation[msgid] and gettext.translation[msgid][0] or gettext.translation[msgid] or gettext.wrapUntranslated(msgid)
end
-local function c_escape(what_full, what)
- if what == "\n" then return ""
- elseif what == "a" then return "\a"
- elseif what == "b" then return "\b"
- elseif what == "f" then return "\f"
- elseif what == "n" then return "\n"
- elseif what == "r" then return "\r"
- elseif what == "t" then return "\t"
- elseif what == "v" then return "\v"
- elseif what == "0" then return "\0" -- shouldn't happen, though
- else
- return what_full
- end
-end
-
--- Converts C logical operators to Lua.
local function logicalCtoLua(logical_str)
logical_str = logical_str:gsub("&&", "and")
@@ -137,9 +128,13 @@ local function getPluralFunc(pl_tests, nplurals, plural_default)
end
local function addTranslation(msgctxt, msgid, msgstr, n)
- -- translated string
- local unescaped_string = string.gsub(msgstr, "(\\(.))", c_escape)
- if msgctxt and msgctxt ~= "" then
+ assert(not msgctxt or msgctxt ~= "")
+ assert(msgid and msgid ~= "")
+ assert(msgstr)
+ if msgstr == "" then
+ return
+ end
+ if msgctxt then
if not GetText.context[msgctxt] then
GetText.context[msgctxt] = {}
end
@@ -147,26 +142,22 @@ local function addTranslation(msgctxt, msgid, msgstr, n)
if not GetText.context[msgctxt][msgid] then
GetText.context[msgctxt][msgid] = {}
end
- GetText.context[msgctxt][msgid][n] = unescaped_string ~= "" and unescaped_string or nil
+ GetText.context[msgctxt][msgid][n] = msgstr
else
- GetText.context[msgctxt][msgid] = unescaped_string ~= "" and unescaped_string or nil
+ GetText.context[msgctxt][msgid] = msgstr
end
else
if n then
if not GetText.translation[msgid] then
GetText.translation[msgid] = {}
end
- GetText.translation[msgid][n] = unescaped_string ~= "" and unescaped_string or nil
+ GetText.translation[msgid][n] = msgstr
else
- GetText.translation[msgid] = unescaped_string ~= "" and unescaped_string or nil
+ GetText.translation[msgid] = msgstr
end
end
end
--- for PO file syntax, see
--- https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html
--- we only implement a sane subset for now
-
function GetText_mt.__index.changeLang(new_lang)
GetText.context = {}
GetText.translation = {}
@@ -180,119 +171,210 @@ function GetText_mt.__index.changeLang(new_lang)
-- strip encoding suffix in locale like "zh_CN.utf8"
new_lang = new_lang:sub(1, new_lang:find(".%."))
- local file = GetText.dirname .. "/" .. new_lang .. "/" .. GetText.textdomain .. ".po"
- local po = io.open(file, "r")
-
- if not po then
- logger.dbg("cannot open translation file:", file)
+ local mo = GetText.dirname .. "/" .. new_lang .. "/" .. GetText.textdomain .. ".mo"
+ if not GetText.loadMO(mo) then
return false
end
- local data = {}
- local in_comments = false
- local fuzzy = false
- local headers
- local what = nil
- while true do
- local line = po:read("*l")
- if line == nil or line == "" then
- if data.msgid and data.msgid_plural and data["msgstr[0]"] then
- for k, v in pairs(data) do
- local n = tonumber(k:match("msgstr%[([0-9]+)%]"))
- local msgstr = v
+ GetText.current_lang = new_lang
+ return true
+end
- if n and msgstr and msgstr ~= "" then
- addTranslation(data.msgctxt, data.msgid, msgstr, n)
- end
- end
- elseif data.msgid and data.msgstr and data.msgstr ~= "" then
- -- header
- if not headers and data.msgid == "" then
- headers = data.msgstr
- local plural_forms = data.msgstr:match("Plural%-Forms: (.*)")
- local nplurals = plural_forms:match("nplurals=([0-9]+);") or 2
- local plurals = plural_forms:match("plural=%((.*)%);")
+local function parse_headers(headers)
+ local plural_forms = headers:match("Plural%-Forms: (.*)")
+ local nplurals = plural_forms:match("nplurals=([0-9]+);") or 2
+ local plurals = plural_forms:match("plural=%((.*)%);")
- -- Hardcoded workaround for Hebrew which has 4 plural forms.
- if plurals == "n == 1) ? 0 : ((n == 2) ? 1 : ((n > 10 && n % 10 == 0) ? 2 : 3)" then
- plurals = "n == 1 ? 0 : (n == 2) ? 1 : (n > 10 && n % 10 == 0) ? 2 : 3"
- end
- -- Hardcoded workaround for Latvian.
- if plurals == "n % 10 == 0 || n % 100 >= 11 && n % 100 <= 19) ? 0 : ((n % 10 == 1 && n % 100 != 11) ? 1 : 2" then
- plurals = "n % 10 == 0 || n % 100 >= 11 && n % 100 <= 19 ? 0 : (n % 10 == 1 && n % 100 != 11) ? 1 : 2"
- end
- -- Hardcoded workaround for Romanian which has 3 plural forms.
- if plurals == "n == 1) ? 0 : ((n == 0 || n != 1 && n % 100 >= 1 && n % 100 <= 19) ? 1 : 2" then
- plurals = "n == 1 ? 0 : (n == 0 || n != 1 && n % 100 >= 1 && n % 100 <= 19) ? 1 : 2"
- end
+ -- Hardcoded workaround for Hebrew which has 4 plural forms.
+ if plurals == "n == 1) ? 0 : ((n == 2) ? 1 : ((n > 10 && n % 10 == 0) ? 2 : 3)" then
+ plurals = "n == 1 ? 0 : (n == 2) ? 1 : (n > 10 && n % 10 == 0) ? 2 : 3"
+ end
+ -- Hardcoded workaround for Latvian.
+ if plurals == "n % 10 == 0 || n % 100 >= 11 && n % 100 <= 19) ? 0 : ((n % 10 == 1 && n % 100 != 11) ? 1 : 2" then
+ plurals = "n % 10 == 0 || n % 100 >= 11 && n % 100 <= 19 ? 0 : (n % 10 == 1 && n % 100 != 11) ? 1 : 2"
+ end
+ -- Hardcoded workaround for Romanian which has 3 plural forms.
+ if plurals == "n == 1) ? 0 : ((n == 0 || n != 1 && n % 100 >= 1 && n % 100 <= 19) ? 1 : 2" then
+ plurals = "n == 1 ? 0 : (n == 0 || n != 1 && n % 100 >= 1 && n % 100 <= 19) ? 1 : 2"
+ end
- if not plurals then
- -- Some languages (e.g., Arabic) may not use parentheses.
- -- However, the following more inclusive match is more likely
- -- to accidentally include junk and seldom relevant.
- -- We might also be dealing with a language without plurals.
- -- That would look like `plural=0`.
- plurals = plural_forms:match("plural=(.*);")
- end
+ if not plurals then
+ -- Some languages (e.g., Arabic) may not use parentheses.
+ -- However, the following more inclusive match is more likely
+ -- to accidentally include junk and seldom relevant.
+ -- We might also be dealing with a language without plurals.
+ -- That would look like `plural=0`.
+ plurals = plural_forms:match("plural=(.*);")
+ end
- if plurals:find("[^n!=%%<>&:%(%)|?0-9 ]") then
- -- we don't trust this input, go with default instead
- plurals = GetText.plural_default
- end
+ if plurals:find("[^n!=%%<>&:%(%)|?0-9 ]") then
+ -- we don't trust this input, go with default instead
+ plurals = GetText.plural_default
+ end
- local pl_tests = {}
- for pl_test in plurals:gmatch("[^:]+") do
- table.insert(pl_tests, pl_test)
- end
+ local pl_tests = {}
+ for pl_test in plurals:gmatch("[^:]+") do
+ table.insert(pl_tests, pl_test)
+ end
- GetText.getPlural = getPluralFunc(pl_tests, nplurals, GetText.plural_default)
- if not GetText.getPlural then
- GetText.getPlural = getDefaultPlural
- end
- end
+ GetText.getPlural = getPluralFunc(pl_tests, nplurals, GetText.plural_default)
+ if not GetText.getPlural then
+ GetText.getPlural = getDefaultPlural
+ end
+end
- addTranslation(data.msgctxt, data.msgid, data.msgstr)
+-- for MO file format, see
+-- https://www.gnu.org/software/gettext/manual/html_node/MO-Files.html
+
+ffi.cdef[[
+struct __attribute__((packed)) mo_header {
+ uint32_t magic;
+ uint16_t revision_major;
+ uint16_t revision_minor;
+ uint32_t nb_strings;
+ uint32_t original_strings_table_offset;
+ uint32_t translated_strings_table_offset;
+ uint32_t hash_table_size;
+ uint32_t hash_table_offset;
+};
+
+struct __attribute__((packed)) mo_string_table {
+ uint32_t length;
+ uint32_t offset;
+};
+]]
+local MO_MAGIC = 0x950412de
+
+function GetText_mt.__index.loadMO(file)
+ local fd = C.open(file, C.O_RDONLY)
+ if fd < 0 then
+ logger.dbg(string.format("cannot open translation file: %s", file))
+ return false
+ end
+ local strerror = function()
+ return ffi.string(C.strerror(ffi.errno()))
+ end
+ local seek_and_read = function(off, ptr, len)
+ local ret
+ ret = C.lseek(fd, off, C.SEEK_SET)
+ if ret ~= off then
+ logger.err(string.format("loading translation file failed: %s [%s]", file, ret < 0 and strerror() or "lseek"))
+ return false
+ end
+ ret = C.read(fd, ptr, len)
+ if ret ~= len then
+ logger.err(string.format("loading translation file failed: %s [%s]", file), ret < 0 and strerror() or "short read")
+ return false
+ end
+ return true
+ end
+ local mo_hdr = ffi.new("struct mo_header")
+ if not seek_and_read(0, mo_hdr, ffi.sizeof(mo_hdr)) then
+ C.close(fd)
+ return false
+ end
+ if mo_hdr.magic ~= MO_MAGIC then
+ logger.err(string.format("bad translation file: %s [magic]", file))
+ C.close(fd)
+ return false
+ end
+ if mo_hdr.revision_major ~= 0 then
+ logger.err(string.format("bad translation file: %s [revision]", file))
+ C.close(fd)
+ return false
+ end
+ local table_buf = buffer:new()
+ local table_size = mo_hdr.nb_strings * ffi.sizeof("struct mo_string_table")
+ local table_ptr = table_buf:reserve(table_size)
+ local read_strings_count
+ local read_strings = function(check_for_context)
+ local m_str_tbl = ffi.cast("struct mo_string_table *", table_ptr)
+ local str_buf = buffer:new()
+ read_strings_count = -1
+ return function()
+ read_strings_count = read_strings_count + 1
+ if read_strings_count >= mo_hdr.nb_strings then
+ return
end
- -- stop at EOF:
- if line == nil then break end
- data = {}
- what = nil
- else
- -- comment
- if line:match("^#") then
- if not in_comments then
- in_comments = true
- fuzzy = false
+ local str_len = m_str_tbl[read_strings_count].length
+ local str_off = m_str_tbl[read_strings_count].offset
+ local str_ptr = str_buf:reserve(str_len)
+ if not seek_and_read(str_off, str_ptr, str_len) then
+ return
+ end
+ local ctx
+ local pos = 0
+ if check_for_context then
+ -- 4: ␄ (End of Transmission).
+ local p = C.memchr(str_ptr, 4, str_len)
+ if p ~= nil then
+ local l = ffi.cast("ssize_t", p) - ffi.cast("ssize_t", str_ptr)
+ ctx = ffi.string(str_ptr, l)
+ pos = l + 1
end
- if line:match(", fuzzy") then
- fuzzy = true
+ end
+ local l = C.strnlen(str_ptr + pos, str_len - pos)
+ if l + pos < str_len then
+ -- Plurals!
+ local strings = {ffi.string(str_ptr + pos, l)}
+ pos = pos + l + 1
+ while pos < str_len do
+ l = C.strnlen(str_ptr + pos, str_len - pos)
+ table.insert(strings, ffi.string(str_ptr + pos, l))
+ pos = pos + l + 1
end
- elseif fuzzy then
- in_comments = false
+ return read_strings_count + 1, strings, ctx
else
- in_comments = false
- -- new data item (msgid, msgstr, ...
- local w, s = line:match("^%s*([%a_%[%]0-9]+)%s+\"(.*)\"%s*$")
- if w then
- what = w
- else
- -- string continuation
- s = line:match("^%s*\"(.*)\"%s*$")
- end
- if what and s then
- -- unescape \n or msgid won't match
- s = s:gsub("\\n", "\n")
- -- unescape " or msgid won't match
- s = s:gsub('\\"', '"')
- -- unescape \\ or msgid won't match
- s = s:gsub("\\\\", "\\")
- data[what] = (data[what] or "") .. s
- end
+ return read_strings_count + 1, ffi.string(str_ptr + pos, str_len - pos), ctx
end
end
end
- po:close()
- GetText.current_lang = new_lang
+ -- Read original strings.
+ if not seek_and_read(mo_hdr.original_strings_table_offset, table_ptr, table_size) then
+ C.close(fd)
+ return false
+ end
+ local original_context = {}
+ local original_strings = table.new(mo_hdr.nb_strings, 0)
+ for n, s, ctx in read_strings(true) do
+ if ctx then
+ original_context[n] = ctx
+ end
+ original_strings[n] = s
+ end
+ if read_strings_count ~= mo_hdr.nb_strings then
+ C.close(fd)
+ return false
+ end
+ -- Read translated strings.
+ if not seek_and_read(mo_hdr.translated_strings_table_offset, table_ptr, table_size) then
+ C.close(fd)
+ return false
+ end
+ for n, ts in read_strings() do
+ local ctx = original_context[n]
+ local os = original_strings[n]
+ if type(os) == "table" then
+ if type(ts) == "table" then
+ for pn, pts in ipairs(ts) do
+ addTranslation(ctx, os[1], pts, pn - 1)
+ end
+ else
+ addTranslation(ctx, os[1], ts, 0)
+ end
+ elseif type(ts) == "table" then
+ logger.warn(string.format("bad translation file: %s [singular / plurals mismatch]", file))
+ else
+ if n == 1 and #os == 0 then
+ parse_headers(ts)
+ else
+ addTranslation(ctx, os, ts)
+ end
+ end
+ end
+ local ok = read_strings_count == mo_hdr.nb_strings
+ C.close(fd)
+ return ok
end
GetText_mt.__index.getPlural = getDefaultPlural
@@ -412,7 +494,6 @@ elseif os.getenv("LANG") then
end
if isAndroid then
- local ffi = require("ffi")
local buf = ffi.new("char[?]", 16)
android.lib.AConfiguration_getLanguage(android.app.config, buf)
local lang = ffi.string(buf)
diff --git a/frontend/ui/presets.lua b/frontend/ui/presets.lua
new file mode 100644
index 000000000..29f1aae32
--- /dev/null
+++ b/frontend/ui/presets.lua
@@ -0,0 +1,325 @@
+--[[--
+This module provides a unified interface for managing presets across different KOReader modules.
+It handles creation, loading, updating, and deletion of presets, as well as menu generation.
+
+Usage:
+ local Presets = require("ui/presets")
+
+ -- 1. In your module's init() method, set up a preset object:
+ self.preset_obj = {
+ presets = G_reader_settings:readSetting("my_module_presets", {}), -- or custom storage
+ cycle_index = G_reader_settings:readSetting("my_module_presets_cycle_index"), -- optional, only needed if cycling through presets
+ dispatcher_name = "load_my_module_preset", -- must match dispatcher.lua entry
+ saveCycleIndex = function(this) -- Save cycle index to persistent storage
+ G_reader_settings:saveSetting("my_module_presets_cycle_index", this.cycle_index)
+ end,
+ buildPreset = function() return self:buildPreset() end, -- Closure to build a preset from current state
+ loadPreset = function(preset) self:loadPreset(preset) end, -- Closure to apply a preset to the module
+ }
+
+ -- 2. Implement required methods in your module:
+ function MyModule:buildPreset()
+ return {
+ -- Return a table with the settings you want to save in the preset
+ setting1 = self.setting1,
+ setting2 = self.setting2,
+ enabled_features = self.enabled_features,
+ }
+ end
+
+ function MyModule:loadPreset(preset)
+ -- Apply the preset settings to your module
+ self.setting1 = preset.setting1
+ self.setting2 = preset.setting2
+ self.enabled_features = preset.enabled_features
+ -- Update UI or perform other necessary changes
+ self:refresh()
+ end
+
+ -- 3. Create menu items for presets: (Alternatively, you could call Presets.genPresetMenuItemTable directly from touchmenu_instance)
+ function MyModule:genPresetMenuItemTable(touchmenu_instance)
+ return Presets.genPresetMenuItemTable(
+ self.preset_obj, -- preset object
+ _("Create new preset from current settings"), -- optional: custom text for UI menu
+ function() return self:hasValidSettings() end, -- optional: function to enable/disable creating presets
+ )
+ end
+
+ -- 4. Load a preset by name (for dispatcher/event handling):
+ function MyModule:onLoadMyModulePreset(preset_name)
+ return Presets.onLoadPreset(
+ self.preset_obj,
+ preset_name,
+ true -- show notification
+ )
+ end
+
+ -- 5. Cycle through presets (for dispatcher/event handling):
+ function MyModule:onCycleMyModulePresets()
+ return Presets.cycleThroughPresets(
+ self.preset_obj,
+ true -- show notification
+ )
+ end
+
+ -- 6. Get list of available presets (for dispatcher):
+ function MyModule.getPresets() -- Note: This is a static method on MyModule
+ local config = {
+ presets = G_reader_settings:readSetting("my_module_presets", {})
+ }
+ return Presets.getPresets(config)
+ end
+
+ -- 7. Add to dispatcher.lua:
+ load_my_module_preset = {
+ category = "string",
+ event = "LoadMyModulePreset",
+ title = _("Load my module preset"),
+ args_func = MyModule.getPresets,
+ reader = true
+ },
+ cycle_my_module_preset = {
+ category = "none",
+ event = "CycleMyModulePresets",
+ title = _("Cycle through my module presets"),
+ reader = true
+ },
+
+Required preset_obj fields:
+ - presets: table containing saved presets
+ - cycle_index: current index for cycling through presets (optional, defaults to 0)
+ - dispatcher_name: string matching the dispatcher action name (for dispatcher integration)
+ - saveCycleIndex(this): function to save cycle index (optional, only needed if cycling is used)
+
+Required module methods:
+ - buildPreset(): returns a table with the current settings to save as a preset
+ - loadPreset(preset): applies the settings from the preset table to the module
+
+The preset system handles:
+ - Creating, updating, deleting, and renaming presets through UI dialogs
+ - Generating menu items with hold actions for preset management
+ - Saving/loading presets to/from G_reader_settings (or custom storage)
+ - Cycling through presets with wrap-around
+ - User notifications when presets are loaded/updated/created
+ - Integration with Dispatcher for gesture/hotkey/profile support
+ - Broadcasting events to update dispatcher when presets change
+ - Input validation and duplicate name prevention
+--]]
+
+local ConfirmBox = require("ui/widget/confirmbox")
+local Event = require("ui/event")
+local InfoMessage = require("ui/widget/infomessage")
+local InputDialog = require("ui/widget/inputdialog")
+local Notification = require("ui/widget/notification")
+local UIManager = require("ui/uimanager")
+local ffiUtil = require("ffi/util")
+local T = require("ffi/util").template
+local _ = require("gettext")
+
+local Presets = {}
+
+function Presets.editPresetName(options, preset_obj, on_success_callback)
+ local input_dialog
+ input_dialog = InputDialog:new{
+ title = options.title or _("Enter preset name"),
+ input = options.initial_value or "",
+ buttons = {
+ {
+ {
+ text = _("Cancel"),
+ id = "close",
+ callback = function()
+ UIManager:close(input_dialog)
+ end,
+ },
+ {
+ text = options.confirm_button_text or _("Create"),
+ is_enter_default = true,
+ callback = function()
+ local entered_preset_name = input_dialog:getInputText()
+ if entered_preset_name == "" or entered_preset_name:match("^%s*$") then
+ UIManager:show(InfoMessage:new{
+ text = _("Invalid preset name. Please choose a different name."),
+ timeout = 2,
+ })
+ return false
+ end
+ if options.initial_value and entered_preset_name == options.initial_value then
+ UIManager:close(input_dialog)
+ return false
+ end
+ if preset_obj.presets[entered_preset_name] then
+ UIManager:show(InfoMessage:new{
+ text = T(_("A preset named '%1' already exists. Please choose a different name."), entered_preset_name),
+ timeout = 2,
+ })
+ return false
+ end
+
+ -- If all validation passes, call the success callback
+ on_success_callback(entered_preset_name)
+ UIManager:close(input_dialog)
+ end,
+ },
+ },
+ },
+ }
+ UIManager:show(input_dialog)
+ input_dialog:onShowKeyboard()
+end
+
+function Presets.genPresetMenuItemTable(preset_obj, text, enabled_func)
+ local presets = preset_obj.presets
+ local items = {
+ {
+ text = text or _("Create new preset from current settings"),
+ keep_menu_open = true,
+ enabled_func = enabled_func,
+ callback = function(touchmenu_instance)
+ Presets.editPresetName({}, preset_obj,
+ function(entered_preset_name)
+ local preset_data = preset_obj.buildPreset()
+ preset_obj.presets[entered_preset_name] = preset_data
+ touchmenu_instance.item_table = Presets.genPresetMenuItemTable(preset_obj)
+ touchmenu_instance:updateItems()
+ end
+ )
+ end,
+ separator = true,
+ },
+ }
+ for preset_name in ffiUtil.orderedPairs(presets) do
+ table.insert(items, {
+ text = preset_name,
+ keep_menu_open = true,
+ callback = function()
+ preset_obj.loadPreset(presets[preset_name])
+ -- There's no guarantee that it'll be obvious to the user that the preset was loaded so, we show a notification.
+ UIManager:show(InfoMessage:new{
+ text = T(_("Preset '%1' loaded successfully."), preset_name),
+ timeout = 2,
+ })
+ end,
+ hold_callback = function(touchmenu_instance, item)
+ UIManager:show(ConfirmBox:new{
+ text = T(_("What would you like to do with preset '%1'?"), preset_name),
+ icon = "notice-question",
+ ok_text = _("Update"),
+ ok_callback = function()
+ UIManager:show(ConfirmBox:new{
+ text = T(_("Are you sure you want to overwrite preset '%1' with current settings?"), preset_name),
+ ok_callback = function()
+ presets[preset_name] = preset_obj.buildPreset()
+ UIManager:show(InfoMessage:new{
+ text = T(_("Preset '%1' was updated with current settings"), preset_name),
+ timeout = 2,
+ })
+ end,
+ })
+ end,
+ other_buttons_first = true,
+ other_buttons = {
+ {
+ {
+ text = _("Delete"),
+ callback = function()
+ UIManager:show(ConfirmBox:new{
+ text = T(_("Are you sure you want to delete preset '%1'?"), preset_name),
+ ok_text = _("Delete"),
+ ok_callback = function()
+ presets[preset_name] = nil
+ local action_key = preset_obj.dispatcher_name
+ if action_key then
+ UIManager:broadcastEvent(Event:new("DispatcherActionValueChanged", {
+ name = action_key,
+ old_value = preset_name,
+ new_value = nil -- delete the action
+ }))
+ end
+ table.remove(touchmenu_instance.item_table, item.idx)
+ touchmenu_instance:updateItems()
+ end,
+ })
+ end,
+ },
+ {
+ text = _("Rename"),
+ callback = function()
+ Presets.editPresetName({
+ title = _("Enter new preset name"),
+ initial_value = preset_name,
+ confirm_button_text = _("Rename"),
+ }, preset_obj,
+ function(new_name)
+ presets[new_name] = presets[preset_name]
+ presets[preset_name] = nil
+ local action_key = preset_obj.dispatcher_name
+ if action_key then
+ UIManager:broadcastEvent(Event:new("DispatcherActionValueChanged", {
+ name = action_key,
+ old_value = preset_name,
+ new_value = new_name
+ }))
+ end
+ touchmenu_instance.item_table = Presets.genPresetMenuItemTable(preset_obj)
+ touchmenu_instance:updateItems()
+ end) -- editPresetName
+ end, -- rename callback
+ },
+ },
+ }, -- end of other_buttons
+ }) -- end of ConfirmBox
+ end, -- hold_callback
+ }) -- end of table.insert
+ end -- for each preset
+ return items
+end
+
+
+function Presets.onLoadPreset(preset_obj, preset_name, show_notification)
+ local presets = preset_obj.presets
+ if presets and presets[preset_name] then
+ preset_obj.loadPreset(presets[preset_name])
+ if show_notification then
+ Notification:notify(T(_("Preset '%1' was loaded"), preset_name))
+ end
+ end
+ return true
+end
+
+function Presets.cycleThroughPresets(preset_obj, show_notification)
+ local preset_names = Presets.getPresets(preset_obj)
+ if #preset_names == 0 then
+ Notification:notify(_("No presets available"), Notification.SOURCE_ALWAYS_SHOW)
+ return true -- we *must* return true here to prevent further event propagation, i.e multiple notifications
+ end
+ -- Get and increment index, wrap around if needed
+ local index = (preset_obj.cycle_index or 0) + 1
+ if index > #preset_names then
+ index = 1
+ end
+ local next_preset_name = preset_names[index]
+ preset_obj.loadPreset(preset_obj.presets[next_preset_name])
+ preset_obj.cycle_index = index
+ preset_obj:saveCycleIndex()
+ if show_notification then
+ Notification:notify(T(_("Loaded preset: %1"), next_preset_name))
+ end
+ return true
+end
+
+function Presets.getPresets(preset_obj)
+ local presets = preset_obj.presets
+ local actions = {}
+ if presets and next(presets) then
+ for preset_name in pairs(presets) do
+ table.insert(actions, preset_name)
+ end
+ if #actions > 1 then
+ table.sort(actions)
+ end
+ end
+ return actions, actions
+end
+
+return Presets
diff --git a/frontend/ui/widget/dictquicklookup.lua b/frontend/ui/widget/dictquicklookup.lua
index 576eec263..e5de1a624 100644
--- a/frontend/ui/widget/dictquicklookup.lua
+++ b/frontend/ui/widget/dictquicklookup.lua
@@ -21,6 +21,7 @@ local Size = require("ui/size")
local TextWidget = require("ui/widget/textwidget")
local TitleBar = require("ui/widget/titlebar")
local Translator = require("ui/translator")
+local Presets = require("ui/presets")
local UIManager = require("ui/uimanager")
local VerticalGroup = require("ui/widget/verticalgroup")
local VerticalSpan = require("ui/widget/verticalspan")
@@ -1159,12 +1160,7 @@ function DictQuickLookup:changeDictionary(index, skip_update)
-- add queried word to 1st result's definition, so we can see
-- what was the selected text and if we selected wrong
if index == 1 then
- if self.is_html then
- self.definition = self.definition.."
_______
"
- else
- self.definition = self.definition.."\n_______\n"
- end
- self.definition = self.definition..T(_("(query : %1)"), self.word)
+ self:addQueryWordToResult()
end
end
self.displaydictname = self.dictionary
@@ -1187,6 +1183,16 @@ function DictQuickLookup:changeDictionary(index, skip_update)
end
end
+function DictQuickLookup:addQueryWordToResult()
+ -- Extracted to a separate method so it can be removed by user patches.
+ if self.is_html then
+ self.definition = self.definition.."
_______
"
+ else
+ self.definition = self.definition.."\n_______\n"
+ end
+ self.definition = self.definition..T(_("(query : %1)"), self.word)
+end
+
--[[ No longer used
function DictQuickLookup:changeToDefaultDict()
if self.dictionary then
@@ -1404,57 +1410,96 @@ function DictQuickLookup:onForwardingPanRelease(arg, ges)
end
function DictQuickLookup:onLookupInputWord(hint)
+ local buttons = {
+ {
+ {
+ text = _("Translate"),
+ callback = function()
+ local text = self.input_dialog:getInputText()
+ if text ~= "" then
+ UIManager:close(self.input_dialog)
+ Translator:showTranslation(text, true)
+ end
+ end,
+ },
+ {
+ text = _("Search Wikipedia"),
+ is_enter_default = self.is_wiki,
+ callback = function()
+ local text = self.input_dialog:getInputText()
+ if text ~= "" then
+ UIManager:close(self.input_dialog)
+ self.is_wiki = true
+ self:lookupWikipedia(false, text, true)
+ end
+ end,
+ },
+ },
+ {
+ {
+ text = _("Cancel"),
+ id = "close",
+ callback = function()
+ UIManager:close(self.input_dialog)
+ end,
+ },
+ {
+ text = _("Search dictionary"),
+ is_enter_default = not self.is_wiki,
+ callback = function()
+ local text = self.input_dialog:getInputText()
+ if text ~= "" then
+ UIManager:close(self.input_dialog)
+ self.is_wiki = false
+ self.ui:handleEvent(Event:new("LookupWord", text, true))
+ end
+ end,
+ },
+ },
+ }
+ local preset_names = Presets.getPresets(self.ui.dictionary.preset_obj)
+ if preset_names and #preset_names > 0 then
+ table.insert(buttons, 2, {
+ {
+ text = _("Search with preset"),
+ callback = function()
+ local text = self.input_dialog:getInputText()
+ if text == "" or text:match("^%s*$") then return end
+ local current_dict_state = self.ui.dictionary:buildPreset()
+ local button_dialog, dialog_buttons = nil, {} -- CI won't like it if we call it buttons :( so dialog_buttons
+ for _, preset_name in ipairs(preset_names) do
+ table.insert(dialog_buttons, {
+ {
+ align = "left",
+ text = preset_name,
+ callback = function()
+ self.ui.dictionary:loadPreset(self.ui.dictionary.preset_obj.presets[preset_name], true)
+ UIManager:close(button_dialog)
+ UIManager:close(self.input_dialog)
+ self.ui:handleEvent(Event:new("LookupWord", text, true, nil, nil, nil,
+ function()
+ -- Restore original preset _after_ lookup is complete
+ self.ui.dictionary:loadPreset(current_dict_state, true)
+ end
+ ))
+ end
+ }
+ })
+ end
+ button_dialog = ButtonDialog:new{
+ buttons = dialog_buttons,
+ shrink_unneeded_width = true,
+ }
+ UIManager:show(button_dialog)
+ end,
+ }
+ })
+ end
self.input_dialog = InputDialog:new{
title = _("Enter a word or phrase to look up"),
input = hint,
input_hint = hint,
- buttons = {
- {
- {
- text = _("Translate"),
- callback = function()
- local text = self.input_dialog:getInputText()
- if text ~= "" then
- UIManager:close(self.input_dialog)
- Translator:showTranslation(text, true)
- end
- end,
- },
- {
- text = _("Search Wikipedia"),
- is_enter_default = self.is_wiki,
- callback = function()
- local text = self.input_dialog:getInputText()
- if text ~= "" then
- UIManager:close(self.input_dialog)
- self.is_wiki = true
- self:lookupWikipedia(false, text, true)
- end
- end,
- },
- },
- {
- {
- text = _("Cancel"),
- id = "close",
- callback = function()
- UIManager:close(self.input_dialog)
- end,
- },
- {
- text = _("Search dictionary"),
- is_enter_default = not self.is_wiki,
- callback = function()
- local text = self.input_dialog:getInputText()
- if text ~= "" then
- UIManager:close(self.input_dialog)
- self.is_wiki = false
- self.ui:handleEvent(Event:new("LookupWord", text, true))
- end
- end,
- },
- },
- },
+ buttons = buttons,
}
UIManager:show(self.input_dialog)
self.input_dialog:onShowKeyboard()
diff --git a/make/gettext.mk b/make/gettext.mk
new file mode 100644
index 000000000..d1b1949f8
--- /dev/null
+++ b/make/gettext.mk
@@ -0,0 +1,33 @@
+PHONY += mo mo-clean po pot
+
+SELF := $(lastword $(MAKEFILE_LIST))
+
+DOMAIN = koreader
+TEMPLATE_DIR = l10n/templates
+MSGFMT_BIN = msgfmt
+XGETTEXT_BIN = xgettext
+
+PO_FILES = $(wildcard l10n/*/*.po)
+MO_FILES = $(PO_FILES:%.po=%.mo)
+
+%.mo: %.po
+ @$(MSGFMT_BIN) --no-hash -o $@ $<
+
+mo:
+ $(MAKE) $(if $(PARALLEL_JOBS),--jobs=$(PARALLEL_JOBS)) $(if $(PARALLEL_LOAD),--load-average=$(PARALLEL_LOAD)) --silent --file=$(SELF) $(MO_FILES)
+
+mo-clean:
+ rm -f $(MO_FILES)
+
+pot: po
+ mkdir -p $(TEMPLATE_DIR)
+ $(XGETTEXT_BIN) --from-code=utf-8 \
+ --keyword=C_:1c,2 --keyword=N_:1,2 --keyword=NC_:1c,2,3 \
+ --add-comments=@translators \
+ reader.lua `find frontend -iname "*.lua" | sort` \
+ `find plugins -iname "*.lua" | sort` \
+ `find tools -iname "*.lua" | sort` \
+ -o $(TEMPLATE_DIR)/$(DOMAIN).pot
+
+po:
+ git submodule update --remote l10n
diff --git a/plugins/hotkeys.koplugin/defaults.lua b/plugins/hotkeys.koplugin/defaults.lua
index 3dc71314e..9c56ce108 100644
--- a/plugins/hotkeys.koplugin/defaults.lua
+++ b/plugins/hotkeys.koplugin/defaults.lua
@@ -40,9 +40,9 @@ return {
alt_plus_a = nil,
alt_plus_b = nil,
alt_plus_c = nil,
- alt_plus_d = {dictionary_lookup = true,},
+ alt_plus_d = Device:hasKeyboard() and {dictionary_lookup = true,} or {},
alt_plus_e = nil,
- alt_plus_f = {file_search = true,},
+ alt_plus_f = Device:hasKeyboard() and {file_search = true,} or {},
alt_plus_g = nil,
alt_plus_h = nil,
alt_plus_i = nil,
@@ -59,7 +59,7 @@ return {
alt_plus_t = nil,
alt_plus_u = nil,
alt_plus_v = nil,
- alt_plus_w = {wikipedia_lookup = true,},
+ alt_plus_w = Device:hasKeyboard() and {wikipedia_lookup = true,} or {},
alt_plus_x = nil,
alt_plus_y = nil,
alt_plus_z = nil,
@@ -96,9 +96,9 @@ return {
alt_plus_a = nil,
alt_plus_b = nil,
alt_plus_c = nil,
- alt_plus_d = {dictionary_lookup = true,},
+ alt_plus_d = Device:hasKeyboard() and {dictionary_lookup = true,} or {},
alt_plus_e = nil,
- alt_plus_f = {file_search = true,},
+ alt_plus_f = Device:hasKeyboard() and {file_search = true,} or {},
alt_plus_g = nil,
alt_plus_h = nil,
alt_plus_i = nil,
@@ -111,11 +111,11 @@ return {
alt_plus_p = nil,
alt_plus_q = nil,
alt_plus_r = nil,
- alt_plus_s = {fulltext_search = true,},
+ alt_plus_s = Device:hasKeyboard() and {fulltext_search = true,} or {},
alt_plus_t = nil,
alt_plus_u = nil,
alt_plus_v = nil,
- alt_plus_w = {wikipedia_lookup = true,},
+ alt_plus_w = Device:hasKeyboard() and {wikipedia_lookup = true,} or {},
alt_plus_x = nil,
alt_plus_y = nil,
alt_plus_z = nil,
diff --git a/spec/unit/gettext_spec.lua b/spec/unit/gettext_spec.lua
index eb3b0107d..29e802b41 100644
--- a/spec/unit/gettext_spec.lua
+++ b/spec/unit/gettext_spec.lua
@@ -106,75 +106,44 @@ msgstr "Fuzzy translated"
describe("GetText module", function()
local GetText
- local test_po_ar
- local test_po_nl, test_po_ru
- local test_po_none, test_po_simple
- local test_po_many
setup(function()
require("commonrequire")
GetText = require("gettext")
- GetText.dirname = "i18n-test"
+ GetText.dirname = (os.getenv("KO_HOME") or ".").."/i18n-test"
local lfs = require("libs/libkoreader-lfs")
+
lfs.mkdir(GetText.dirname)
- lfs.mkdir(GetText.dirname.."/nl_NL")
- lfs.mkdir(GetText.dirname.."/none")
- lfs.mkdir(GetText.dirname.."/ar")
- lfs.mkdir(GetText.dirname.."/ru")
- lfs.mkdir(GetText.dirname.."/simple")
- lfs.mkdir(GetText.dirname.."/many")
- test_po_nl = GetText.dirname.."/nl_NL/koreader.po"
- local f = io.open(test_po_nl, "w")
- f:write(test_po_part1, test_plurals_nl, test_po_part2)
- f:close()
+ local pocreate = function(lang, ...)
+ local dir = GetText.dirname.."/"..lang
+ local po = dir.."/koreader.po"
+ local mo = dir.."/koreader.mo"
+ lfs.mkdir(dir)
+ local f = io.open(po, "w")
+ f:write(...)
+ f:close()
+ local ok = os.execute(string.format("msgfmt --no-hash -o %s %s", mo, po))
+ assert(ok == 0)
+ end
+
+ pocreate("nl_NL", test_po_part1, test_plurals_nl, test_po_part2)
-- same file, just different plural for testing
- test_po_none = GetText.dirname.."/none/koreader.po"
- f = io.open(test_po_none, "w")
- f:write(test_po_part1, test_plurals_none, test_po_part2)
- f:close()
+ pocreate("none", test_po_part1, test_plurals_none, test_po_part2)
-- same file, just different plural for testing
- test_po_ar = GetText.dirname.."/ar/koreader.po"
- f = io.open(test_po_ar, "w")
- f:write(test_po_part1, test_plurals_ar, test_po_part2)
- f:close()
+ pocreate("ar", test_po_part1, test_plurals_ar, test_po_part2)
-- same file, just different plural for testing
- test_po_ru = GetText.dirname.."/ru/koreader.po"
- f = io.open(test_po_ru, "w")
- f:write(test_po_part1, test_plurals_ru, test_po_part2)
- f:close()
+ pocreate("ru", test_po_part1, test_plurals_ru, test_po_part2)
-- same file, just different plural for testing
- test_po_simple = GetText.dirname.."/simple/koreader.po"
- f = io.open(test_po_simple, "w")
- f:write(test_po_part1, test_plurals_simple, test_po_part2)
- f:close()
+ pocreate("simple", test_po_part1, test_plurals_simple, test_po_part2)
-- same file, just different plural for testing
- test_po_many = GetText.dirname.."/many/koreader.po"
- f = io.open(test_po_many, "w")
- f:write(test_po_part1, test_plurals_many, test_po_part2)
- f:close()
- end)
-
- teardown(function()
- os.remove(test_po_nl)
- os.remove(test_po_none)
- os.remove(test_po_ar)
- os.remove(test_po_ru)
- os.remove(test_po_simple)
- os.remove(test_po_many)
- os.remove(GetText.dirname.."/nl_NL")
- os.remove(GetText.dirname.."/none")
- os.remove(GetText.dirname.."/ar")
- os.remove(GetText.dirname.."/ru")
- os.remove(GetText.dirname.."/simple")
- os.remove(GetText.dirname.."/many")
- os.remove(GetText.dirname)
+ pocreate("many", test_po_part1, test_plurals_many, test_po_part2)
end)
describe("changeLang", function()