2020-01-04 00:18:51 +00:00
local BD = require ( " ui/bidi " )
2017-06-18 16:08:57 +00:00
local ConfirmBox = require ( " ui/widget/confirmbox " )
2015-10-03 06:18:47 +00:00
local DataStorage = require ( " datastorage " )
2015-04-15 12:28:32 +00:00
local Device = require ( " device " )
2017-04-07 13:20:57 +00:00
local DictQuickLookup = require ( " ui/widget/dictquicklookup " )
2022-05-31 20:11:35 +00:00
local Event = require ( " ui/event " )
2018-01-15 22:51:43 +00:00
local Geom = require ( " ui/geometry " )
2017-04-07 13:20:57 +00:00
local InfoMessage = require ( " ui/widget/infomessage " )
2019-03-02 12:29:10 +00:00
local InputDialog = require ( " ui/widget/inputdialog " )
2015-03-21 03:39:25 +00:00
local JSON = require ( " json " )
2017-10-07 20:13:46 +00:00
local KeyValuePage = require ( " ui/widget/keyvaluepage " )
local LuaData = require ( " luadata " )
2019-10-25 15:25:26 +00:00
local MultiConfirmBox = require ( " ui/widget/multiconfirmbox " )
2018-12-13 06:27:49 +00:00
local NetworkMgr = require ( " ui/network/manager " )
2020-07-30 13:14:45 +00:00
local SortWidget = require ( " ui/widget/sortwidget " )
2017-09-19 19:24:48 +00:00
local Trapper = require ( " ui/trapper " )
2017-04-07 13:20:57 +00:00
local UIManager = require ( " ui/uimanager " )
Clarify our OOP semantics across the codebase (#9586)
Basically:
* Use `extend` for class definitions
* Use `new` for object instantiations
That includes some minor code cleanups along the way:
* Updated `Widget`'s docs to make the semantics clearer.
* Removed `should_restrict_JIT` (it's been dead code since https://github.com/koreader/android-luajit-launcher/pull/283)
* Minor refactoring of LuaSettings/LuaData/LuaDefaults/DocSettings to behave (mostly, they are instantiated via `open` instead of `new`) like everything else and handle inheritance properly (i.e., DocSettings is now a proper LuaSettings subclass).
* Default to `WidgetContainer` instead of `InputContainer` for stuff that doesn't actually setup key/gesture events.
* Ditto for explicit `*Listener` only classes, make sure they're based on `EventListener` instead of something uselessly fancier.
* Unless absolutely necessary, do not store references in class objects, ever; only values. Instead, always store references in instances, to avoid both sneaky inheritance issues, and sneaky GC pinning of stale references.
* ReaderUI: Fix one such issue with its `active_widgets` array, with critical implications, as it essentially pinned *all* of ReaderUI's modules, including their reference to the `Document` instance (i.e., that was a big-ass leak).
* Terminal: Make sure the shell is killed on plugin teardown.
* InputText: Fix Home/End/Del physical keys to behave sensibly.
* InputContainer/WidgetContainer: If necessary, compute self.dimen at paintTo time (previously, only InputContainers did, which might have had something to do with random widgets unconcerned about input using it as a baseclass instead of WidgetContainer...).
* OverlapGroup: Compute self.dimen at *init* time, because for some reason it needs to do that, but do it directly in OverlapGroup instead of going through a weird WidgetContainer method that it was the sole user of.
* ReaderCropping: Under no circumstances should a Document instance member (here, self.bbox) risk being `nil`ed!
* Kobo: Minor code cleanups.
2022-10-06 00:14:48 +00:00
local WidgetContainer = require ( " ui/widget/container/widgetcontainer " )
2022-09-14 01:49:50 +00:00
local ffi = require ( " ffi " )
local C = ffi.C
2018-12-13 06:27:49 +00:00
local ffiUtil = require ( " ffi/util " )
2022-09-27 23:10:50 +00:00
local lfs = require ( " libs/libkoreader-lfs " )
2016-12-29 08:10:38 +00:00
local logger = require ( " logger " )
2022-05-05 19:00:22 +00:00
local time = require ( " ui/time " )
2016-12-06 21:15:52 +00:00
local util = require ( " util " )
2014-07-24 14:11:25 +00:00
local _ = require ( " gettext " )
2022-05-23 11:52:52 +00:00
local Input = Device.input
2020-09-15 18:39:32 +00:00
local T = ffiUtil.template
2013-04-23 22:59:52 +00:00
2017-08-29 10:52:14 +00:00
-- We'll store the list of available dictionaries as a module local
-- so we only have to look for them on the first :init()
local available_ifos = nil
2017-10-07 20:13:46 +00:00
local lookup_history = nil
2017-08-29 10:52:14 +00:00
local function getIfosInDir ( path )
2017-08-29 20:27:43 +00:00
-- Get all the .ifo under directory path.
2018-01-11 19:28:34 +00:00
-- Don't walk into "res/" subdirectories, as per Stardict specs, they
-- may contain possibly many resource files (image, audio files...)
-- that could slow down our walk here.
2017-08-29 10:52:14 +00:00
local ifos = { }
local ok , iter , dir_obj = pcall ( lfs.dir , path )
if ok then
for name in iter , dir_obj do
2018-01-11 19:28:34 +00:00
if name ~= " . " and name ~= " .. " and name ~= " res " then
2017-08-29 10:52:14 +00:00
local fullpath = path .. " / " .. name
2018-01-11 19:28:34 +00:00
local attributes = lfs.attributes ( fullpath )
if attributes ~= nil then
if attributes.mode == " directory " then
local dirifos = getIfosInDir ( fullpath ) -- recurse
for _ , ifo in pairs ( dirifos ) do
table.insert ( ifos , ifo )
end
elseif fullpath : match ( " %.ifo$ " ) then
table.insert ( ifos , fullpath )
2017-08-29 10:52:14 +00:00
end
end
end
end
end
return ifos
end
Clarify our OOP semantics across the codebase (#9586)
Basically:
* Use `extend` for class definitions
* Use `new` for object instantiations
That includes some minor code cleanups along the way:
* Updated `Widget`'s docs to make the semantics clearer.
* Removed `should_restrict_JIT` (it's been dead code since https://github.com/koreader/android-luajit-launcher/pull/283)
* Minor refactoring of LuaSettings/LuaData/LuaDefaults/DocSettings to behave (mostly, they are instantiated via `open` instead of `new`) like everything else and handle inheritance properly (i.e., DocSettings is now a proper LuaSettings subclass).
* Default to `WidgetContainer` instead of `InputContainer` for stuff that doesn't actually setup key/gesture events.
* Ditto for explicit `*Listener` only classes, make sure they're based on `EventListener` instead of something uselessly fancier.
* Unless absolutely necessary, do not store references in class objects, ever; only values. Instead, always store references in instances, to avoid both sneaky inheritance issues, and sneaky GC pinning of stale references.
* ReaderUI: Fix one such issue with its `active_widgets` array, with critical implications, as it essentially pinned *all* of ReaderUI's modules, including their reference to the `Document` instance (i.e., that was a big-ass leak).
* Terminal: Make sure the shell is killed on plugin teardown.
* InputText: Fix Home/End/Del physical keys to behave sensibly.
* InputContainer/WidgetContainer: If necessary, compute self.dimen at paintTo time (previously, only InputContainers did, which might have had something to do with random widgets unconcerned about input using it as a baseclass instead of WidgetContainer...).
* OverlapGroup: Compute self.dimen at *init* time, because for some reason it needs to do that, but do it directly in OverlapGroup instead of going through a weird WidgetContainer method that it was the sole user of.
* ReaderCropping: Under no circumstances should a Document instance member (here, self.bbox) risk being `nil`ed!
* Kobo: Minor code cleanups.
2022-10-06 00:14:48 +00:00
local ReaderDictionary = WidgetContainer : extend {
2015-04-15 12:28:32 +00:00
data_dir = nil ,
2017-10-07 20:13:46 +00:00
lookup_msg = _ ( " Searching dictionary for: \n %1 " ) ,
2015-04-15 12:28:32 +00:00
}
2014-10-15 10:01:58 +00:00
2018-01-09 20:38:49 +00:00
-- For a HTML dict, one can specify a specific stylesheet
-- in a file named as the .ifo with a .css extension
2018-01-07 19:24:15 +00:00
local function readDictionaryCss ( path )
local f = io.open ( path , " r " )
if not f then
return nil
end
local content = f : read ( " *all " )
f : close ( )
return content
end
2018-01-09 20:38:49 +00:00
-- For a HTML dict, one can specify a function called on
-- the raw returned definition to "fix" the HTML if needed
-- (as MuPDF, used for rendering, is quite sensitive to the
-- HTML quality) in a file named as the .ifo with a .lua
-- extension, containing for example:
-- return function(html)
2018-04-29 13:15:11 +00:00
-- html = html:gsub("<hr>", "<hr/>")
-- return html
2018-01-09 20:38:49 +00:00
-- end
local function getDictionaryFixHtmlFunc ( path )
if lfs.attributes ( path , " mode " ) == " file " then
local ok , func = pcall ( dofile , path )
if ok and func then
return func
else
logger.warn ( " Dict's user provided file failed: " , func )
end
end
end
2014-10-15 10:01:58 +00:00
function ReaderDictionary : init ( )
2021-03-06 21:44:18 +00:00
self.disable_lookup_history = G_reader_settings : isTrue ( " disable_lookup_history " )
self.dicts_order = G_reader_settings : readSetting ( " dicts_order " , { } )
self.dicts_disabled = G_reader_settings : readSetting ( " dicts_disabled " , { } )
2021-10-23 10:13:09 +00:00
if self.ui then
self.ui . menu : registerToMainMenu ( self )
end
2022-09-27 23:10:50 +00:00
self.data_dir = G_defaults : readSetting ( " STARDICT_DATA_DIR " ) or
2020-04-14 17:36:44 +00:00
os.getenv ( " STARDICT_DATA_DIR " ) or
2015-10-03 06:18:47 +00:00
DataStorage : getDataDir ( ) .. " /data/dict "
2017-08-29 10:52:14 +00:00
2021-01-07 17:05:18 +00:00
-- Show the "Seaching..." InfoMessage after this delay
self.lookup_msg_delay = 0.5
-- Allow quick interruption or dismiss of search result window
-- with tap if done before this delay. After this delay, the
-- result window is shown and dismiss prevented for a few 100ms
2022-05-05 19:00:22 +00:00
self.quick_dismiss_before_delay = time.s ( 3 )
2021-01-07 17:05:18 +00:00
2017-08-29 10:52:14 +00:00
-- Gather info about available dictionaries
if not available_ifos then
available_ifos = { }
logger.dbg ( " Getting list of dictionaries " )
local ifo_files = getIfosInDir ( self.data_dir )
local dict_ext = self.data_dir .. " _ext "
if lfs.attributes ( dict_ext , " mode " ) == " directory " then
local extifos = getIfosInDir ( dict_ext )
for _ , ifo in pairs ( extifos ) do
table.insert ( ifo_files , ifo )
end
end
for _ , ifo_file in pairs ( ifo_files ) do
2017-08-29 20:27:43 +00:00
local f = io.open ( ifo_file , " r " )
2017-08-29 10:52:14 +00:00
if f then
local content = f : read ( " *all " )
f : close ( )
2022-09-20 22:26:10 +00:00
local dictname = content : match ( " \n bookname=(.-) \r ? \n " )
2018-01-07 19:24:15 +00:00
local is_html = content : find ( " sametypesequence=h " , 1 , true ) ~= nil
2023-03-14 21:11:17 +00:00
local lang_in , lang_out = content : match ( " lang=(%a+)-?(%a*) \r ? \n ? " )
2017-08-29 10:52:14 +00:00
-- sdcv won't use dict that don't have a bookname=
if dictname then
table.insert ( available_ifos , {
file = ifo_file ,
name = dictname ,
2018-01-07 19:24:15 +00:00
is_html = is_html ,
2018-01-09 20:38:49 +00:00
css = readDictionaryCss ( ifo_file : gsub ( " %.ifo$ " , " .css " ) ) ,
fix_html_func = getDictionaryFixHtmlFunc ( ifo_file : gsub ( " %.ifo$ " , " .lua " ) ) ,
2023-03-14 21:11:17 +00:00
lang = lang_in and { lang_in = lang_in , lang_out = lang_out } ,
2017-08-29 10:52:14 +00:00
} )
end
end
end
logger.dbg ( " found " , # available_ifos , " dictionaries " )
2020-07-30 13:14:45 +00:00
self : sortAvailableIfos ( )
2017-08-29 10:52:14 +00:00
end
2020-07-30 13:14:45 +00:00
-- Prepare the -u options to give to sdcv the dictionary order and if some are disabled
2017-08-29 20:27:43 +00:00
self : updateSdcvDictNamesOptions ( )
2021-01-06 20:49:23 +00:00
2017-10-07 20:13:46 +00:00
if not lookup_history then
Clarify our OOP semantics across the codebase (#9586)
Basically:
* Use `extend` for class definitions
* Use `new` for object instantiations
That includes some minor code cleanups along the way:
* Updated `Widget`'s docs to make the semantics clearer.
* Removed `should_restrict_JIT` (it's been dead code since https://github.com/koreader/android-luajit-launcher/pull/283)
* Minor refactoring of LuaSettings/LuaData/LuaDefaults/DocSettings to behave (mostly, they are instantiated via `open` instead of `new`) like everything else and handle inheritance properly (i.e., DocSettings is now a proper LuaSettings subclass).
* Default to `WidgetContainer` instead of `InputContainer` for stuff that doesn't actually setup key/gesture events.
* Ditto for explicit `*Listener` only classes, make sure they're based on `EventListener` instead of something uselessly fancier.
* Unless absolutely necessary, do not store references in class objects, ever; only values. Instead, always store references in instances, to avoid both sneaky inheritance issues, and sneaky GC pinning of stale references.
* ReaderUI: Fix one such issue with its `active_widgets` array, with critical implications, as it essentially pinned *all* of ReaderUI's modules, including their reference to the `Document` instance (i.e., that was a big-ass leak).
* Terminal: Make sure the shell is killed on plugin teardown.
* InputText: Fix Home/End/Del physical keys to behave sensibly.
* InputContainer/WidgetContainer: If necessary, compute self.dimen at paintTo time (previously, only InputContainers did, which might have had something to do with random widgets unconcerned about input using it as a baseclass instead of WidgetContainer...).
* OverlapGroup: Compute self.dimen at *init* time, because for some reason it needs to do that, but do it directly in OverlapGroup instead of going through a weird WidgetContainer method that it was the sole user of.
* ReaderCropping: Under no circumstances should a Document instance member (here, self.bbox) risk being `nil`ed!
* Kobo: Minor code cleanups.
2022-10-06 00:14:48 +00:00
lookup_history = LuaData : open ( DataStorage : getSettingsDir ( ) .. " /lookup_history.lua " , " LookupHistory " )
2017-10-07 20:13:46 +00:00
end
2017-08-29 20:27:43 +00:00
end
2020-07-30 13:14:45 +00:00
function ReaderDictionary : sortAvailableIfos ( )
table.sort ( available_ifos , function ( lifo , rifo )
2021-03-06 21:44:18 +00:00
local lord = self.dicts_order [ lifo.file ]
local rord = self.dicts_order [ rifo.file ]
2020-07-30 13:14:45 +00:00
-- Both ifos without an explicit position -> lexical comparison
if lord == rord then
return ffiUtil.strcoll ( lifo.name , rifo.name )
end
-- Ifos without an explicit position come last.
return lord ~= nil and ( rord == nil or lord < rord )
end )
end
2017-08-29 20:27:43 +00:00
function ReaderDictionary : updateSdcvDictNamesOptions ( )
-- We cannot tell sdcv which dictionaries to ignore, but we
-- can tell it which dictionaries to use, by using multiple
-- -u <dictname> options.
2020-07-30 13:14:45 +00:00
-- The order of the -u options controls the dictionary order
-- that sdcv uses to order its results.
2021-01-06 20:49:23 +00:00
self.enabled_dict_names = { }
-- First, insert any preferred dicts, even if globally disabled
-- (this might allow enabling a dict only for a specific book,
-- while keeping it disabled for all others)
local preferred_names_already_in = { }
if self.preferred_dictionaries then
for _ , name in ipairs ( self.preferred_dictionaries ) do
table.insert ( self.enabled_dict_names , name )
preferred_names_already_in [ name ] = true
end
end
2017-08-29 20:27:43 +00:00
local dicts_disabled = G_reader_settings : readSetting ( " dicts_disabled " )
for _ , ifo in pairs ( available_ifos ) do
2021-01-06 20:49:23 +00:00
if not dicts_disabled [ ifo.file ] and not preferred_names_already_in [ ifo.name ] then
2018-01-15 22:51:43 +00:00
table.insert ( self.enabled_dict_names , ifo.name )
2017-08-29 20:27:43 +00:00
end
end
2014-10-15 10:01:58 +00:00
end
2017-03-04 13:46:38 +00:00
function ReaderDictionary : addToMainMenu ( menu_items )
2023-05-17 04:34:37 +00:00
menu_items.search_settings = { -- submenu with Dict, Wiki, Translation settings
text = _ ( " Settings " ) ,
}
2017-03-04 13:46:38 +00:00
menu_items.dictionary_lookup = {
2014-10-15 10:01:58 +00:00
text = _ ( " Dictionary lookup " ) ,
2019-03-02 12:29:10 +00:00
callback = function ( )
self : onShowDictionaryLookup ( )
end ,
2017-02-28 21:46:32 +00:00
}
2017-10-07 20:13:46 +00:00
menu_items.dictionary_lookup_history = {
text = _ ( " Dictionary lookup history " ) ,
enabled_func = function ( )
return lookup_history : has ( " lookup_history " )
end ,
callback = function ( )
local lookup_history_table = lookup_history : readSetting ( " lookup_history " )
local kv_pairs = { }
local previous_title
for i = # lookup_history_table , 1 , - 1 do
local value = lookup_history_table [ i ]
if value.book_title ~= previous_title then
table.insert ( kv_pairs , { value.book_title .. " : " , " " } )
end
previous_title = value.book_title
table.insert ( kv_pairs , {
os.date ( " %Y-%m-%d %H:%M:%S " , value.time ) ,
value.word ,
callback = function ( )
2021-01-01 13:34:51 +00:00
-- Word had been cleaned before being added to history
self : onLookupWord ( value.word , true )
2017-10-07 20:13:46 +00:00
end
} )
end
UIManager : show ( KeyValuePage : new {
title = _ ( " Dictionary lookup history " ) ,
2021-02-20 19:15:43 +00:00
value_overflow_align = " right " ,
2017-10-07 20:13:46 +00:00
kv_pairs = kv_pairs ,
} )
end ,
}
2017-08-22 15:24:31 +00:00
menu_items.dictionary_settings = {
text = _ ( " Dictionary settings " ) ,
sub_item_table = {
2017-08-29 10:52:14 +00:00
{
2020-07-30 13:14:45 +00:00
keep_menu_open = true ,
2017-08-29 10:52:14 +00:00
text_func = function ( )
2017-09-05 05:56:05 +00:00
local nb_available , nb_enabled , nb_disabled = self : getNumberOfDictionaries ( )
2017-08-29 20:27:43 +00:00
local nb_str = nb_available
if nb_disabled > 0 then
nb_str = nb_enabled .. " / " .. nb_available
2017-08-29 10:52:14 +00:00
end
2021-11-09 19:15:52 +00:00
return T ( _ ( " Manage dictionaries: %1 " ) , nb_str )
2017-08-29 10:52:14 +00:00
end ,
2017-09-05 05:56:05 +00:00
enabled_func = function ( )
return self : getNumberOfDictionaries ( ) > 0
end ,
2020-07-30 13:14:45 +00:00
callback = function ( touchmenu_instance )
self : showDictionariesMenu ( function ( )
if touchmenu_instance then touchmenu_instance : updateItems ( ) end
end )
2017-12-17 12:02:08 +00:00
end ,
2017-08-29 10:52:14 +00:00
} ,
2018-12-13 06:27:49 +00:00
{
text = _ ( " Download dictionaries " ) ,
2023-03-14 21:11:17 +00:00
sub_item_table_func = function ( ) return self : _genDownloadDictionariesMenu ( ) end ,
2018-12-13 06:27:49 +00:00
} ,
2017-08-22 15:24:31 +00:00
{
2017-12-17 12:02:08 +00:00
text = _ ( " Enable fuzzy search " ) ,
2017-08-22 15:24:31 +00:00
checked_func = function ( )
2017-12-17 12:02:08 +00:00
return not self.disable_fuzzy_search == true
2017-08-22 15:24:31 +00:00
end ,
callback = function ( )
self.disable_fuzzy_search = not self.disable_fuzzy_search
end ,
hold_callback = function ( )
2019-10-25 15:25:26 +00:00
self : toggleFuzzyDefault ( )
2017-08-22 15:24:31 +00:00
end ,
2017-12-17 12:02:08 +00:00
separator = true ,
2017-08-22 15:24:31 +00:00
} ,
2017-10-07 20:13:46 +00:00
{
2017-12-17 12:02:08 +00:00
text = _ ( " Enable dictionary lookup history " ) ,
2017-10-07 20:13:46 +00:00
checked_func = function ( )
2017-12-17 12:02:08 +00:00
return not self.disable_lookup_history
2017-10-07 20:13:46 +00:00
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 " ) ,
2021-05-06 15:28:54 +00:00
enabled_func = function ( )
return lookup_history : has ( " lookup_history " )
end ,
2018-09-04 21:55:58 +00:00
keep_menu_open = true ,
2021-05-06 15:28:54 +00:00
callback = function ( touchmenu_instance )
2017-10-07 20:13:46 +00:00
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 { }
2021-05-06 15:28:54 +00:00
touchmenu_instance : updateItems ( )
2017-10-07 20:13:46 +00:00
end ,
} )
end ,
2017-12-17 12:02:08 +00:00
separator = true ,
2017-10-07 20:13:46 +00:00
} ,
2017-11-20 08:40:00 +00:00
{ -- setting used by dictquicklookup
text = _ ( " Large window " ) ,
checked_func = function ( )
return G_reader_settings : isTrue ( " dict_largewindow " )
end ,
callback = function ( )
G_reader_settings : flipNilOrFalse ( " dict_largewindow " )
end ,
} ,
2017-08-22 15:24:31 +00:00
{ -- setting used by dictquicklookup
text = _ ( " Justify text " ) ,
checked_func = function ( )
return G_reader_settings : nilOrTrue ( " dict_justify " )
end ,
callback = function ( )
G_reader_settings : flipNilOrTrue ( " dict_justify " )
end ,
2019-11-27 22:59:08 +00:00
} ,
{ -- setting used by dictquicklookup
text_func = function ( )
local font_size = G_reader_settings : readSetting ( " dict_font_size " ) or 20
2021-11-09 19:15:52 +00:00
return T ( _ ( " Font size: %1 " ) , font_size )
2019-11-27 22:59:08 +00:00
end ,
2019-11-28 09:54:06 +00:00
callback = function ( touchmenu_instance )
2019-11-27 22:59:08 +00:00
local SpinWidget = require ( " ui/widget/spinwidget " )
local font_size = G_reader_settings : readSetting ( " dict_font_size " ) or 20
local items_font = SpinWidget : new {
value = font_size ,
value_min = 8 ,
value_max = 32 ,
default_value = 20 ,
2021-04-02 15:59:29 +00:00
title_text = _ ( " Dictionary font size " ) ,
2019-11-27 22:59:08 +00:00
callback = function ( spin )
G_reader_settings : saveSetting ( " dict_font_size " , spin.value )
2019-11-28 09:54:06 +00:00
if touchmenu_instance then touchmenu_instance : updateItems ( ) end
2019-11-27 22:59:08 +00:00
end ,
}
UIManager : show ( items_font )
2019-11-28 09:54:06 +00:00
end ,
keep_menu_open = true ,
2017-08-22 15:24:31 +00:00
}
}
2017-06-18 16:08:57 +00:00
}
2019-07-08 12:19:36 +00:00
if Device : canExternalDictLookup ( ) then
local function genExternalDictItems ( )
local items_table = { }
for i , v in ipairs ( Device : getExternalDictLookupList ( ) ) do
local setting = v [ 1 ]
local dict_name = v [ 2 ]
local is_enabled = v [ 3 ]
table.insert ( items_table , {
text = dict_name ,
checked_func = function ( )
return setting == G_reader_settings : readSetting ( " external_dict_lookup_method " )
end ,
enabled_func = function ( )
return is_enabled == true
end ,
callback = function ( )
G_reader_settings : saveSetting ( " external_dict_lookup_method " , v [ 1 ] )
end ,
} )
end
return items_table
end
table.insert ( menu_items.dictionary_settings . sub_item_table , 1 , {
text = _ ( " Use external dictionary " ) ,
checked_func = function ( )
return G_reader_settings : isTrue ( " external_dict_lookup " )
end ,
callback = function ( )
G_reader_settings : flipNilOrFalse ( " external_dict_lookup " )
end ,
} )
table.insert ( menu_items.dictionary_settings . sub_item_table , 2 , {
text_func = function ( )
local display_name = _ ( " none " )
local ext_id = G_reader_settings : readSetting ( " external_dict_lookup_method " )
for i , v in ipairs ( Device : getExternalDictLookupList ( ) ) do
if v [ 1 ] == ext_id then
display_name = v [ 2 ]
break
end
end
return T ( _ ( " Dictionary: %1 " ) , display_name )
end ,
enabled_func = function ( )
return G_reader_settings : isTrue ( " external_dict_lookup " )
end ,
sub_item_table = genExternalDictItems ( ) ,
separator = true ,
} )
end
2014-10-15 10:01:58 +00:00
end
2013-04-23 22:59:52 +00:00
2022-05-31 20:11:35 +00:00
function ReaderDictionary : onLookupWord ( word , is_sane , boxes , highlight , link , tweak_buttons_func )
2021-10-23 10:12:56 +00:00
logger.dbg ( " dict lookup word: " , word , boxes )
2018-01-15 22:51:43 +00:00
-- escape quotes and other funny characters in word
2021-01-01 13:34:51 +00:00
word = self : cleanSelection ( word , is_sane )
2018-01-15 22:51:43 +00:00
logger.dbg ( " dict stripped word: " , word )
2014-03-13 13:52:43 +00:00
self.highlight = highlight
2018-01-15 22:51:43 +00:00
2017-09-19 19:24:48 +00:00
-- Wrapped through Trapper, as we may be using Trapper:dismissablePopen() in it
Trapper : wrap ( function ( )
2022-05-31 20:11:35 +00:00
self : stardictLookup ( word , self.enabled_dict_names , not self.disable_fuzzy_search , boxes , link , tweak_buttons_func )
2017-09-19 19:24:48 +00:00
end )
2014-08-20 06:41:45 +00:00
return true
2013-04-30 10:45:12 +00:00
end
2018-01-15 22:51:43 +00:00
function ReaderDictionary : onHtmlDictionaryLinkTapped ( dictionary , link )
if not link.uri then
return
end
-- The protocol is either "bword" or there is no protocol, only the word.
-- https://github.com/koreader/koreader/issues/3588#issuecomment-357088125
local url_prefix = " bword:// "
local word
if link.uri : sub ( 1 , url_prefix : len ( ) ) == url_prefix then
word = link.uri : sub ( url_prefix : len ( ) + 1 )
elseif link.uri : find ( " :// " ) then
return
else
word = link.uri
end
if word == " " then
return
end
local link_box = Geom : new {
x = link.x0 ,
y = link.y0 ,
w = math.abs ( link.x1 - link.x0 ) ,
h = math.abs ( link.y1 - link.y0 ) ,
}
-- Only the first dictionary window stores the highlight, this way the highlight
-- is only removed when there are no more dictionary windows open.
self.highlight = nil
-- Wrapped through Trapper, as we may be using Trapper:dismissablePopen() in it
Trapper : wrap ( function ( )
2021-10-23 10:12:56 +00:00
self : stardictLookup ( word , { dictionary } , false , { link_box } , nil )
2018-01-15 22:51:43 +00:00
end )
end
2017-09-05 05:56:05 +00:00
--- Gets number of available, enabled, and disabled dictionaries
-- @treturn int nb_available
-- @treturn int nb_enabled
-- @treturn int nb_disabled
function ReaderDictionary : getNumberOfDictionaries ( )
local nb_available = # available_ifos
2018-01-09 12:28:17 +00:00
local nb_enabled = 0
2017-09-05 05:56:05 +00:00
local nb_disabled = 0
2018-01-09 12:28:17 +00:00
for _ , ifo in pairs ( available_ifos ) do
2021-03-06 21:44:18 +00:00
if self.dicts_disabled [ ifo.file ] then
2018-01-09 12:28:17 +00:00
nb_disabled = nb_disabled + 1
else
nb_enabled = nb_enabled + 1
end
2017-09-05 05:56:05 +00:00
end
return nb_available , nb_enabled , nb_disabled
end
2018-12-13 06:27:49 +00:00
function ReaderDictionary : _genDownloadDictionariesMenu ( )
local downloadable_dicts = require ( " ui/data/dictionaries " )
2023-03-14 21:11:17 +00:00
local IsoLanguage = require ( " ui/data/isolanguage " )
2018-12-13 06:27:49 +00:00
local languages = { }
for i = 1 , # downloadable_dicts do
local dict = downloadable_dicts [ i ]
2023-03-14 21:11:17 +00:00
if not dict.ifo_lang then
-- this only needs to happen the first time this function is called
local ifo_in = IsoLanguage : getBCPLanguageTag ( dict.lang_in )
local ifo_out = IsoLanguage : getBCPLanguageTag ( dict.lang_out )
dict.ifo_lang = ( " %s-%s " ) : format ( ifo_in , ifo_out )
dict.lang_in = IsoLanguage : getLocalizedLanguage ( dict.lang_in )
dict.lang_out = IsoLanguage : getLocalizedLanguage ( dict.lang_out )
end
2018-12-13 06:27:49 +00:00
local dict_lang_in = dict.lang_in
local dict_lang_out = dict.lang_out
if not languages [ dict_lang_in ] then
languages [ dict_lang_in ] = { }
end
table.insert ( languages [ dict_lang_in ] , dict )
if not languages [ dict_lang_out ] then
languages [ dict_lang_out ] = { }
end
table.insert ( languages [ dict_lang_out ] , dict )
end
-- remove duplicates
for lang_key , lang in pairs ( languages ) do
local hash = { }
local res = { }
for k , v in ipairs ( lang ) do
if not hash [ v.name ] then
res [ # res + 1 ] = v
hash [ v.name ] = true
end
end
languages [ lang_key ] = res
end
local menu_items = { }
for lang_key , available_langs in ffiUtil.orderedPairs ( languages ) do
table.insert ( menu_items , {
2018-12-23 18:49:06 +00:00
keep_menu_open = true ,
2018-12-13 06:27:49 +00:00
text = lang_key ,
callback = function ( )
self : showDownload ( available_langs )
end
} )
end
return menu_items
end
2020-07-30 13:14:45 +00:00
function ReaderDictionary : showDictionariesMenu ( changed_callback )
-- Work on local copy, save to settings only when SortWidget is closed with the accept button
2021-03-06 21:44:18 +00:00
local dicts_disabled = util.tableDeepCopy ( self.dicts_disabled )
2020-07-30 13:14:45 +00:00
local sort_items = { }
2017-08-29 10:52:14 +00:00
for _ , ifo in pairs ( available_ifos ) do
2020-07-30 13:14:45 +00:00
table.insert ( sort_items , {
2017-08-29 10:52:14 +00:00
text = ifo.name ,
callback = function ( )
2017-08-29 20:27:43 +00:00
if dicts_disabled [ ifo.file ] then
dicts_disabled [ ifo.file ] = nil
else
dicts_disabled [ ifo.file ] = true
2017-08-29 10:52:14 +00:00
end
end ,
checked_func = function ( )
2017-08-29 20:27:43 +00:00
return not dicts_disabled [ ifo.file ]
2020-07-30 13:14:45 +00:00
end ,
ifo = ifo ,
2017-08-29 10:52:14 +00:00
} )
end
2020-07-30 13:14:45 +00:00
local sort_widget = SortWidget : new {
title = _ ( " Manage installed dictionaries " ) ,
item_table = sort_items ,
callback = function ( )
2021-03-06 21:44:18 +00:00
-- Update both references to point to that new object
self.dicts_disabled = dicts_disabled
G_reader_settings : saveSetting ( " dicts_disabled " , self.dicts_disabled )
2020-07-30 13:14:45 +00:00
2021-04-16 20:27:52 +00:00
-- Write back the sorted items array to dicts_order
2020-07-30 13:14:45 +00:00
local dicts_order = { }
2021-04-16 20:27:52 +00:00
for i , sort_item in ipairs ( sort_items ) do
2020-07-30 13:14:45 +00:00
dicts_order [ sort_item.ifo . file ] = i
end
2021-03-06 21:44:18 +00:00
self.dicts_order = dicts_order
G_reader_settings : saveSetting ( " dicts_order " , self.dicts_order )
2020-07-30 13:14:45 +00:00
self : sortAvailableIfos ( )
self : updateSdcvDictNamesOptions ( )
UIManager : setDirty ( nil , " ui " )
changed_callback ( )
end
}
UIManager : show ( sort_widget )
2017-08-29 10:52:14 +00:00
end
2017-04-26 06:12:25 +00:00
local function dictDirsEmpty ( dict_dirs )
for _ , dict_dir in ipairs ( dict_dirs ) do
if not util.isEmptyDir ( dict_dir ) then
return false
end
end
return true
end
2018-01-07 19:24:15 +00:00
local function getAvailableIfoByName ( dictionary_name )
for _ , ifo in ipairs ( available_ifos ) do
if ifo.name == dictionary_name then
return ifo
end
end
return nil
end
2016-12-06 21:15:52 +00:00
local function tidyMarkup ( results )
2014-10-28 07:57:01 +00:00
local cdata_tag = " <!%[CDATA%[(.-)%]%]> "
local format_escape = " &[29Ib%+]{(.-)} "
for _ , result in ipairs ( results ) do
2018-01-07 19:24:15 +00:00
local ifo = getAvailableIfoByName ( result.dict )
2023-03-14 21:11:17 +00:00
if ifo and ifo.lang then
result.ifo_lang = ifo.lang
end
2018-01-07 19:24:15 +00:00
if ifo and ifo.is_html then
result.is_html = ifo.is_html
result.css = ifo.css
2018-01-09 20:38:49 +00:00
if ifo.fix_html_func then
2020-12-26 17:30:19 +00:00
local dict_path = util.splitFilePathName ( ifo.file )
local ok , fixed_definition = pcall ( ifo.fix_html_func , result.definition , dict_path )
2018-01-09 20:38:49 +00:00
if ok then
result.definition = fixed_definition
else
logger.warn ( " Dict's user provided funcion failed: " , fixed_definition )
end
end
2018-01-07 19:24:15 +00:00
else
local def = result.definition
-- preserve the <br> tag for line break
def = def : gsub ( " <[bB][rR] ?/?> " , " \n " )
-- parse CDATA text in XML
if def : find ( cdata_tag ) then
def = def : gsub ( cdata_tag , " %1 " )
-- ignore format strings
while def : find ( format_escape ) do
def = def : gsub ( format_escape , " %1 " )
end
2014-10-28 07:57:01 +00:00
end
2018-05-01 11:30:02 +00:00
-- convert any htmlentities (>, "...)
def = util.htmlEntitiesToUtf8 ( def )
2018-01-07 19:24:15 +00:00
-- ignore all markup tags
def = def : gsub ( " %b<> " , " " )
-- strip all leading empty lines/spaces
def = def : gsub ( " ^%s+ " , " " )
result.definition = def
2014-10-28 07:57:01 +00:00
end
end
return results
end
2021-01-01 13:34:51 +00:00
function ReaderDictionary : cleanSelection ( text , is_sane )
2016-12-06 21:15:52 +00:00
-- Will be used by ReaderWikipedia too
if not text then
return " "
end
2017-04-09 14:58:41 +00:00
-- crengine does now a much better job at finding word boundaries, but
-- some cleanup is still needed for selection we get from other engines
-- (example: pdf selection "qu’ autrefois," will be cleaned to "autrefois")
2016-12-06 21:15:52 +00:00
--
2017-04-09 14:58:41 +00:00
-- Replace no-break space with regular space
2018-10-22 16:40:28 +00:00
text = text : gsub ( " \xC2 \xA0 " , ' ' ) -- U+00A0 no-break space
2021-01-01 13:34:51 +00:00
-- Trim any space at start or end
2018-10-22 16:40:28 +00:00
text = text : gsub ( " ^%s+ " , " " )
text = text : gsub ( " %s+$ " , " " )
2021-01-01 13:34:51 +00:00
if not is_sane then
-- Replace extended quote (included in the general puncturation range)
-- with plain ascii quote (for french words like "aujourd’ hui")
text = text : gsub ( " \xE2 \x80 \x99 " , " ' " ) -- U+2019 (right single quotation mark)
-- Strip punctuation characters around selection
text = util.stripPunctuation ( text )
-- Strip some common english grammatical construct
text = text : gsub ( " 's$ " , ' ' ) -- english possessive
-- Strip some common french grammatical constructs
text = text : gsub ( " ^[LSDMNTlsdmnt]' " , ' ' ) -- french l' s' t'...
text = text : gsub ( " ^[Qq][Uu]' " , ' ' ) -- french qu'
-- There may be a need to remove some (all?) diacritical marks
-- https://en.wikipedia.org/wiki/Combining_character#Unicode_ranges
-- see discussion at https://github.com/koreader/koreader/issues/1649
-- Commented for now, will have to be checked by people who read
-- languages and texts that use them.
-- text = text:gsub("\204[\128-\191]", '') -- U+0300 to U+033F
-- text = text:gsub("\205[\128-\175]", '') -- U+0340 to U+036F
-- Trim any space now at start or end after above changes
text = text : gsub ( " ^%s+ " , " " )
text = text : gsub ( " %s+$ " , " " )
end
2016-12-06 21:15:52 +00:00
return text
end
2020-12-22 17:45:29 +00:00
function ReaderDictionary : showLookupInfo ( word , show_delay )
2016-12-06 21:15:52 +00:00
local text = T ( self.lookup_msg , word )
2020-12-22 17:45:29 +00:00
self.lookup_progress_msg = InfoMessage : new {
text = text ,
show_delay = show_delay ,
}
2016-12-06 21:15:52 +00:00
UIManager : show ( self.lookup_progress_msg )
end
2017-06-18 16:08:57 +00:00
function ReaderDictionary : dismissLookupInfo ( )
2016-12-06 21:15:52 +00:00
if self.lookup_progress_msg then
UIManager : close ( self.lookup_progress_msg )
end
self.lookup_progress_msg = nil
end
2019-03-02 12:29:10 +00:00
function ReaderDictionary : onShowDictionaryLookup ( )
self.dictionary_lookup_dialog = InputDialog : new {
2021-04-02 15:59:29 +00:00
title = _ ( " Enter a word or phrase to look up " ) ,
2019-03-02 12:29:10 +00:00
input = " " ,
input_type = " text " ,
buttons = {
{
{
text = _ ( " Cancel " ) ,
2022-03-04 20:20:00 +00:00
id = " close " ,
2019-03-02 12:29:10 +00:00
callback = function ( )
UIManager : close ( self.dictionary_lookup_dialog )
end ,
} ,
{
text = _ ( " Search dictionary " ) ,
is_enter_default = true ,
callback = function ( )
2021-04-04 15:27:17 +00:00
if self.dictionary_lookup_dialog : getInputText ( ) == " " then return end
2019-03-02 12:29:10 +00:00
UIManager : close ( self.dictionary_lookup_dialog )
2021-01-01 13:34:51 +00:00
-- Trust that input text does not need any cleaning (allows querying for "-suffix")
self : onLookupWord ( self.dictionary_lookup_dialog : getInputText ( ) , true )
2019-03-02 12:29:10 +00:00
end ,
} ,
}
} ,
}
UIManager : show ( self.dictionary_lookup_dialog )
self.dictionary_lookup_dialog : onShowKeyboard ( )
return true
end
2021-10-23 10:13:00 +00:00
function ReaderDictionary : rawSdcv ( words , dict_names , fuzzy_search , lookup_progress_msg )
2016-12-06 21:15:52 +00:00
-- Allow for two sdcv calls : one in the classic data/dict, and
-- another one in data/dict_ext if it exists
-- We could put in data/dict_ext dictionaries with a great number of words
-- but poor definitions as a fall back. If these were in data/dict,
-- they would prevent fuzzy searches in other dictories with better
-- definitions, and masks such results. This way, we can get both.
local dict_dirs = { self.data_dir }
local dict_ext = self.data_dir .. " _ext "
if lfs.attributes ( dict_ext , " mode " ) == " directory " then
table.insert ( dict_dirs , dict_ext )
end
2017-04-26 06:12:25 +00:00
-- early exit if no dictionaries
if dictDirsEmpty ( dict_dirs ) then
2021-10-23 10:13:00 +00:00
return false , nil
2017-04-26 06:12:25 +00:00
end
2021-11-21 18:13:29 +00:00
local all_results = { }
2017-09-19 19:24:48 +00:00
local lookup_cancelled = false
2016-12-06 21:15:52 +00:00
for _ , dict_dir in ipairs ( dict_dirs ) do
2017-09-19 19:24:48 +00:00
if lookup_cancelled then
break -- don't do any more lookup on additional dict_dirs
end
2018-01-15 22:51:43 +00:00
2021-01-11 13:32:10 +00:00
local args = { " ./sdcv " , " --utf8-input " , " --utf8-output " , " --json-output " , " --non-interactive " , " --data-dir " , dict_dir }
2018-01-15 22:51:43 +00:00
if not fuzzy_search then
table.insert ( args , " --exact-search " )
end
if dict_names then
for _ , opt in pairs ( dict_names ) do
table.insert ( args , " -u " )
table.insert ( args , opt )
end
end
2021-10-23 10:13:09 +00:00
table.insert ( args , " -- " ) -- prevent words starting with a "-" to be interpreted as a sdcv option
2021-10-23 10:13:00 +00:00
util.arrayAppend ( args , words )
2018-01-15 22:51:43 +00:00
2019-08-03 09:46:03 +00:00
local cmd = util.shell_escape ( args )
-- cmd = "sleep 7 ; " .. cmd -- uncomment to simulate long lookup time
-- Some sdcv lookups, when using fuzzy search with many dictionaries
-- and a really bad selected text, can take up to 10 seconds.
-- It is nice to be able to cancel it when noticing wrong text was
-- selected.
-- Because sdcv starts outputing its output only at the end when it has
-- done its work, we can use Trapper:dismissablePopen() to cancel it as
-- long as we are waiting for output.
-- When fuzzy search is enabled, we have a lookup_progress_msg that can
-- be used to catch a tap and trigger cancellation.
-- When fuzzy search is disabled, we provide false instead so an
-- invisible non-event-forwarding TrapWidget is used to catch a tap
-- and trigger cancellation (invisible so there's no need for repaint
-- and refresh with the usually fast non-fuzzy search lookups).
-- We must ensure we will have some output to be readable (if no
-- definition found, sdcv will output some message on stderr, and
-- let stdout empty) by appending an "echo":
cmd = cmd .. " ; echo "
2022-09-14 01:49:50 +00:00
-- NOTE: Bionic doesn't support rpath, but does honor LD_LIBRARY_PATH...
-- Give it a shove so it can actually find the STL.
if Device : isAndroid ( ) then
C.setenv ( " LD_LIBRARY_PATH " , " ./libs " , 1 )
end
2021-10-23 10:13:00 +00:00
local completed , results_str = Trapper : dismissablePopen ( cmd , lookup_progress_msg )
2022-09-14 01:49:50 +00:00
if Device : isAndroid ( ) then
-- NOTE: It's unset by default, so this is perfectly fine.
C.unsetenv ( " LD_LIBRARY_PATH " )
end
2019-08-03 09:46:03 +00:00
lookup_cancelled = not completed
2017-09-19 19:24:48 +00:00
if results_str and results_str ~= " \n " then -- \n is when lookup was cancelled
2021-10-23 10:13:00 +00:00
-- sdcv can return multiple results if we passed multiple words to
-- the cmdline. In this case, the lookup results for each word are
-- newline separated. The JSON output doesn't contain raw newlines
-- so it's safe to split. Ideally luajson would support jsonl but
-- unfortunately it doesn't and it also seems to decode the last
-- object rather than the first one if there are multiple.
2021-12-07 12:51:27 +00:00
local result_word_idx = 0
2021-10-23 10:13:00 +00:00
for _ , entry_str in ipairs ( util.splitToArray ( results_str , " \n " ) ) do
2021-12-07 12:51:27 +00:00
result_word_idx = result_word_idx + 1
2021-10-23 10:13:00 +00:00
local ok , results = pcall ( JSON.decode , entry_str )
2021-12-07 12:51:27 +00:00
if not ok or not results then
2021-10-23 10:13:00 +00:00
logger.warn ( " JSON data cannot be decoded " , results )
-- Need to insert an empty table so that the word entries
-- match up to the result entries (so that callers can
-- batch lookups to reduce the cost of bulk lookups while
-- still being able to figure out which lookup came from
-- which word).
2021-12-07 12:51:27 +00:00
results = { }
2021-10-23 10:13:00 +00:00
end
2021-12-07 12:51:27 +00:00
if all_results [ result_word_idx ] then
util.arrayAppend ( all_results [ result_word_idx ] , results )
else
table.insert ( all_results , results )
end
end
if result_word_idx ~= # words then
logger.warn ( " sdcv returned a different number of results than the number of words " )
2021-10-23 10:13:00 +00:00
end
end
end
return lookup_cancelled , all_results
end
function ReaderDictionary : startSdcv ( word , dict_names , fuzzy_search )
local words = { word }
2021-10-23 10:13:09 +00:00
2021-10-25 06:45:26 +00:00
if self.ui . languagesupport and self.ui . languagesupport : hasActiveLanguagePlugins ( ) then
2021-10-23 10:13:09 +00:00
-- Get any other candidates from any language-specific plugins we have.
-- We prefer the originally selected word first (in case there is a
-- dictionary entry for whatever text the user selected).
local candidates = self.ui . languagesupport : extraDictionaryFormCandidates ( word )
if candidates then
util.arrayAppend ( words , candidates )
end
end
2021-11-21 18:13:29 +00:00
-- If every word contains a CJK character, every word candidate is
-- (probably) a CJK word. We don't want fuzzy searching in this case
-- because sdcv cannot handle CJK text properly when fuzzy searching (with
-- Japanese, it returns hundreds of useless results).
local shouldnt_fuzzy_search = true
2021-11-21 18:33:09 +00:00
for _ , w in ipairs ( words ) do
if not util.hasCJKChar ( w ) then
2021-11-21 18:13:29 +00:00
shouldnt_fuzzy_search = false
break
end
end
if shouldnt_fuzzy_search then
logger.dbg ( " disabling fuzzy searching for all-CJK word search: " , words )
fuzzy_search = false
end
2021-10-24 08:58:14 +00:00
local lookup_cancelled , results = self : rawSdcv ( words , dict_names , fuzzy_search , self.lookup_progress_msg or false )
2021-10-23 10:13:00 +00:00
if results == nil then -- no dictionaries found
return {
{
dict = " " ,
word = word ,
definition = _ ( [[No dictionaries installed. Please search for "Dictionary support" in the KOReader Wiki to get more information about installing new dictionaries.]] ) ,
}
}
else -- flatten any possible results
local flat_results = { }
local seen_results = { }
-- Flatten the array, removing any duplicates we may have gotten (sdcv
-- may do multiple queries, in fixed mode then in fuzzy mode, and the
2021-10-23 10:13:09 +00:00
-- language-specific plugin may have also returned multiple equivalent
2021-10-23 10:13:00 +00:00
-- results).
local h
for _ , term_results in ipairs ( results ) do
for _ , r in ipairs ( term_results ) do
h = r.dict .. r.word .. r.definition
if seen_results [ h ] == nil then
table.insert ( flat_results , r )
seen_results [ h ] = true
2016-12-06 21:15:52 +00:00
end
end
2014-03-13 13:52:43 +00:00
end
2021-10-23 10:13:00 +00:00
results = flat_results
2014-03-13 13:52:43 +00:00
end
2021-10-23 10:13:00 +00:00
if # results == 0 then -- no results found
2016-12-06 21:15:52 +00:00
-- dummy results
2021-10-23 10:13:00 +00:00
results = {
2016-12-06 21:15:52 +00:00
{
2021-05-31 20:19:24 +00:00
dict = _ ( " Not available " ) ,
2016-12-06 21:15:52 +00:00
word = word ,
2021-04-02 15:59:29 +00:00
definition = lookup_cancelled and _ ( " Dictionary lookup interrupted. " ) or _ ( " No results. " ) ,
2020-12-22 17:45:32 +00:00
no_result = true ,
lookup_cancelled = lookup_cancelled ,
2016-12-06 21:15:52 +00:00
}
}
end
2021-01-07 17:05:18 +00:00
if lookup_cancelled then
-- Also put this as a k/v into the results array: when using dict_ext,
-- we may get results from the 1st lookup, and have interrupted the 2nd.
2021-10-23 10:13:00 +00:00
results.lookup_cancelled = true
2021-01-07 17:05:18 +00:00
end
2021-10-23 10:13:00 +00:00
return results
2018-01-15 22:51:43 +00:00
end
2022-05-31 20:11:35 +00:00
function ReaderDictionary : stardictLookup ( word , dict_names , fuzzy_search , boxes , link , tweak_buttons_func )
2018-01-15 22:51:43 +00:00
if word == " " then
return
end
2022-06-12 19:34:17 +00:00
local book_title = self.ui . doc_settings and self.ui . doc_settings : readSetting ( " doc_props " ) . title or _ ( " Dictionary lookup " )
if book_title == " " then -- no or empty metadata title
if self.ui . document and self.ui . document.file then
local directory , filename = util.splitFilePathName ( self.ui . document.file ) -- luacheck: no unused
book_title = util.splitFileNameSuffix ( filename )
2018-01-29 08:46:50 +00:00
end
2022-06-12 19:34:17 +00:00
end
2022-05-31 20:11:35 +00:00
-- Event for plugin to catch lookup with book title
self.ui : handleEvent ( Event : new ( " WordLookedUp " , word , book_title ) )
if not self.disable_lookup_history then
2018-01-15 22:51:43 +00:00
lookup_history : addTableItem ( " lookup_history " , {
book_title = book_title ,
time = os.time ( ) ,
word = word ,
} )
end
2019-07-08 12:19:36 +00:00
if Device : canExternalDictLookup ( ) and G_reader_settings : isTrue ( " external_dict_lookup " ) then
Device : doExternalDictLookup ( word , G_reader_settings : readSetting ( " external_dict_lookup_method " ) , function ( )
if self.highlight then
local clear_id = self.highlight : getClearId ( )
UIManager : scheduleIn ( 0.5 , function ( )
self.highlight : clear ( clear_id )
end )
end
end )
return
end
2021-05-31 20:19:24 +00:00
-- If the user disabled all the dictionaries, go away.
if dict_names and # dict_names == 0 then
-- Dummy result
local nope = {
{
dict = _ ( " Not available " ) ,
word = word ,
definition = _ ( " There are no enabled dictionaries. \n Please check the 'Dictionary settings' menu. " ) ,
no_result = true ,
lookup_cancelled = false ,
}
}
2021-10-23 10:12:56 +00:00
self : showDict ( word , nope , boxes , link )
2021-05-31 20:19:24 +00:00
return
end
2021-01-07 17:05:18 +00:00
self : showLookupInfo ( word , self.lookup_msg_delay )
2018-01-15 22:51:43 +00:00
2022-05-05 19:00:22 +00:00
self._lookup_start_time = UIManager : getTime ( )
2018-01-15 22:51:43 +00:00
local results = self : startSdcv ( word , dict_names , fuzzy_search )
2022-05-05 19:00:22 +00:00
if results and results.lookup_cancelled
and ( time.now ( ) - self._lookup_start_time ) <= self.quick_dismiss_before_delay then
2020-12-22 17:45:32 +00:00
-- If interrupted quickly just after launch, don't display anything
-- (this might help avoiding refreshes and the need to dismiss
-- after accidental long-press when holding a device).
if self.highlight then
self.highlight : clear ( )
end
return
end
2022-05-31 20:11:35 +00:00
self : showDict ( word , tidyMarkup ( results ) , boxes , link , tweak_buttons_func )
2013-04-23 22:59:52 +00:00
end
2013-04-24 14:57:03 +00:00
2022-05-31 20:11:35 +00:00
function ReaderDictionary : showDict ( word , results , boxes , link , tweak_buttons_func )
2014-08-17 16:32:09 +00:00
if results and results [ 1 ] then
2022-10-08 21:10:58 +00:00
logger.dbg ( " showing quick lookup window " , # DictQuickLookup.window_list + 1 , " : " , word , results )
2014-11-21 10:32:43 +00:00
self.dict_window = DictQuickLookup : new {
2014-03-13 13:52:43 +00:00
ui = self.ui ,
highlight = self.highlight ,
2022-05-31 20:11:35 +00:00
tweak_buttons_func = tweak_buttons_func ,
2014-03-13 13:52:43 +00:00
dialog = self.dialog ,
2014-08-20 10:25:37 +00:00
-- original lookup word
word = word ,
2017-09-09 16:30:00 +00:00
-- selected link, if any
selected_link = link ,
2014-03-13 13:52:43 +00:00
results = results ,
2021-10-23 10:12:56 +00:00
word_boxes = boxes ,
2021-01-06 20:49:23 +00:00
preferred_dictionaries = self.preferred_dictionaries ,
2014-08-20 06:41:45 +00:00
-- differentiate between dict and wiki
2016-12-06 21:15:52 +00:00
is_wiki = self.is_wiki ,
2017-03-24 07:20:37 +00:00
refresh_callback = function ( )
2017-05-12 16:28:42 +00:00
if self.view then
-- update info in footer (time, battery, etc)
2020-07-12 18:47:49 +00:00
self.view . footer : onUpdateFooter ( )
2017-05-12 16:28:42 +00:00
end
2017-03-24 07:20:37 +00:00
end ,
2018-01-15 22:51:43 +00:00
html_dictionary_link_tapped_callback = function ( dictionary , html_link )
self : onHtmlDictionaryLinkTapped ( dictionary , html_link )
end ,
2014-11-21 10:32:43 +00:00
}
2020-12-03 16:37:46 +00:00
if self.lookup_progress_msg then
2020-12-22 17:45:29 +00:00
-- If we have a lookup InfoMessage that ended up being displayed, make
-- it *not* refresh on close if it is hidden by our DictQuickLookup
-- to avoid refreshes competition and possible glitches
local msg_dimen = self.lookup_progress_msg : getVisibleArea ( )
if msg_dimen then -- not invisible
local dict_dimen = self.dict_window : getInitialVisibleArea ( )
if dict_dimen and dict_dimen : contains ( msg_dimen ) then
self.lookup_progress_msg . no_refresh_on_close = true
end
2020-12-03 16:37:46 +00:00
end
end
2014-03-13 13:52:43 +00:00
end
2020-12-03 16:37:46 +00:00
self : dismissLookupInfo ( )
if results and results [ 1 ] then
UIManager : show ( self.dict_window )
2022-05-05 19:00:22 +00:00
if not results.lookup_cancelled and self._lookup_start_time
and ( time.now ( ) - self._lookup_start_time ) > self.quick_dismiss_before_delay then
2021-01-07 17:05:18 +00:00
-- If the search took more than a few seconds to be done, discard
AutoSuspend: Don't send LeaveStandby events from a zombie plugin instance (#9124)
Long story short: the LeaveStandby event is sent via `tickAfterNext`, so if we tear down the plugin right after calling it (in this case, that means that the very input event that wakes the device up from suspend is one that kills ReaderUI or FileManager), what's in UIManager's task queue isn't the actual function, but the anonymous nextTick wrapper constructed by `tickAfterNext` (c.f.,
https://github.com/koreader/koreader/issues/9112#issuecomment-1133999385).
Tweak `UIManager:tickAfterNext` to return a reference to said wrapper, so that we can store it and unschedule that one, too, in `AutoSuspend:onCloseWidget`.
Fix #9112 (many thanks to [@boredhominid](https://github.com/boredhominid) for his help in finding a repro for this ;)).
Re: #8638, as the extra debugging facilities (i.e., ebb81b98451e2a8f54c46f51e861c19fdfb40499) added during testing might help pinpoint the root issue for that one, too.
Also includes a minor simplification to `UIManager:_checkTasks`, and various other task queue related codepaths (e.g., `WakeupMgr`) ;).
2022-05-25 21:36:41 +00:00
-- queued and upcoming input events to avoid a voluntary dismissal
2021-01-07 17:05:18 +00:00
-- (because the user felt the result would not come) to kill the
-- result that finally came and is about to be displayed
2022-05-23 11:52:52 +00:00
Input : inhibitInputUntil ( true )
2021-01-01 13:34:55 +00:00
end
2020-12-03 16:37:46 +00:00
end
2013-04-24 14:57:03 +00:00
end
2013-07-21 06:23:54 +00:00
2018-12-13 06:27:49 +00:00
function ReaderDictionary : showDownload ( downloadable_dicts )
local kv_pairs = { }
for dummy , dict in ipairs ( downloadable_dicts ) do
table.insert ( kv_pairs , { dict.name , " " ,
callback = function ( )
Various Wi-Fi QoL improvements (#6424)
* Revamped most actions that require an internet connection to a new/fixed backend that allows forwarding the initial action and running it automatically once connected. (i.e., it'll allow you to set "Action when Wi-Fi is off" to "turn_on", and whatch stuff connect and do what you wanted automatically without having to re-click anywhere instead of showing you a Wi-Fi prompt and then not doing anything without any other feedback).
* Speaking of, fixed the "turn_on" beforeWifi action to, well, actually work. It's no longer marked as experimental.
* Consistently use "Wi-Fi" everywhere.
* On Kobo/Cervantes/Sony, implemented a "Kill Wi-Fi connection when inactive" system that will automatically disconnect from Wi-Fi after sustained *network* inactivity (i.e., you can keep reading, it'll eventually turn off on its own). This should be smart and flexible enough not to murder Wi-Fi while you need it, while still not keeping it uselessly on and murdering your battery.
(i.e., enable that + turn Wi-Fi on when off and enjoy never having to bother about Wi-Fi ever again).
* Made sending `NetworkConnected` / `NetworkDisconnected` events consistent (they were only being sent... sometimes, which made relying on 'em somewhat problematic).
* restoreWifiAsync is now only run when really needed (i.e., we no longer stomp on an existing working connection just for the hell of it).
* We no longer attempt to kill a bogus non-existent Wi-Fi connection when going to suspend, we only do it when it's actually needed.
* Every method of enabling Wi-Fi will now properly tear down Wi-Fi on failure, instead of leaving it in an undefined state.
* Fixed an issue in the fancy crash screen on Kobo/reMarkable that could sometime lead to the log excerpt being missing.
* Worked-around a number of sneaky issues related to low-level Wi-Fi/DHCP/DNS handling on Kobo (see the lengthy comments [below](https://github.com/koreader/koreader/pull/6424#issuecomment-663881059) for details). Fix #6421
Incidentally, this should also fix the inconsistencies experienced re: Wi-Fi behavior in Nickel when toggling between KOReader and Nickel (use NM/KFMon, and run a current FW for best results).
* For developers, this involves various cleanups around NetworkMgr and NetworkListener. Documentation is in-line, above the concerned functions.
2020-07-27 01:39:06 +00:00
local connect_callback = function ( )
self : downloadDictionaryPrep ( dict )
2018-12-13 06:27:49 +00:00
end
Various Wi-Fi QoL improvements (#6424)
* Revamped most actions that require an internet connection to a new/fixed backend that allows forwarding the initial action and running it automatically once connected. (i.e., it'll allow you to set "Action when Wi-Fi is off" to "turn_on", and whatch stuff connect and do what you wanted automatically without having to re-click anywhere instead of showing you a Wi-Fi prompt and then not doing anything without any other feedback).
* Speaking of, fixed the "turn_on" beforeWifi action to, well, actually work. It's no longer marked as experimental.
* Consistently use "Wi-Fi" everywhere.
* On Kobo/Cervantes/Sony, implemented a "Kill Wi-Fi connection when inactive" system that will automatically disconnect from Wi-Fi after sustained *network* inactivity (i.e., you can keep reading, it'll eventually turn off on its own). This should be smart and flexible enough not to murder Wi-Fi while you need it, while still not keeping it uselessly on and murdering your battery.
(i.e., enable that + turn Wi-Fi on when off and enjoy never having to bother about Wi-Fi ever again).
* Made sending `NetworkConnected` / `NetworkDisconnected` events consistent (they were only being sent... sometimes, which made relying on 'em somewhat problematic).
* restoreWifiAsync is now only run when really needed (i.e., we no longer stomp on an existing working connection just for the hell of it).
* We no longer attempt to kill a bogus non-existent Wi-Fi connection when going to suspend, we only do it when it's actually needed.
* Every method of enabling Wi-Fi will now properly tear down Wi-Fi on failure, instead of leaving it in an undefined state.
* Fixed an issue in the fancy crash screen on Kobo/reMarkable that could sometime lead to the log excerpt being missing.
* Worked-around a number of sneaky issues related to low-level Wi-Fi/DHCP/DNS handling on Kobo (see the lengthy comments [below](https://github.com/koreader/koreader/pull/6424#issuecomment-663881059) for details). Fix #6421
Incidentally, this should also fix the inconsistencies experienced re: Wi-Fi behavior in Nickel when toggling between KOReader and Nickel (use NM/KFMon, and run a current FW for best results).
* For developers, this involves various cleanups around NetworkMgr and NetworkListener. Documentation is in-line, above the concerned functions.
2020-07-27 01:39:06 +00:00
NetworkMgr : runWhenOnline ( connect_callback )
2018-12-13 06:27:49 +00:00
end } )
local lang
if dict.lang_in == dict.lang_out then
lang = string.format ( " %s " , dict.lang_in )
else
lang = string.format ( " %s– %s " , dict.lang_in , dict.lang_out )
end
table.insert ( kv_pairs , { lang , " " } )
table.insert ( kv_pairs , { " " .. _ ( " License " ) , dict.license } )
2021-02-20 19:15:43 +00:00
table.insert ( kv_pairs , { " " .. _ ( " Entries " ) , dict.entries , separator = true } )
2018-12-13 06:27:49 +00:00
end
self.download_window = KeyValuePage : new {
2020-03-15 10:40:24 +00:00
title = _ ( " Tap dictionary name to download " ) ,
2018-12-13 06:27:49 +00:00
kv_pairs = kv_pairs ,
}
UIManager : show ( self.download_window )
end
function ReaderDictionary : downloadDictionaryPrep ( dict , size )
local dummy , filename = util.splitFilePathName ( dict.url )
local download_location = string.format ( " %s/%s " , self.data_dir , filename )
if lfs.attributes ( download_location ) then
UIManager : show ( ConfirmBox : new {
text = _ ( " File already exists. Overwrite? " ) ,
ok_text = _ ( " Overwrite " ) ,
ok_callback = function ( )
self : downloadDictionary ( dict , download_location )
end ,
} )
else
self : downloadDictionary ( dict , download_location )
end
end
function ReaderDictionary : downloadDictionary ( dict , download_location , continue )
continue = continue or false
local socket = require ( " socket " )
2021-03-15 00:25:10 +00:00
local socketutil = require ( " socketutil " )
2018-12-13 06:27:49 +00:00
local http = socket.http
local ltn12 = require ( " ltn12 " )
if not continue then
local file_size
2021-03-15 00:25:10 +00:00
-- Skip body & code args
socketutil : set_timeout ( )
local headers = socket.skip ( 2 , http.request {
method = " HEAD " ,
url = dict.url ,
2018-12-13 06:27:49 +00:00
--redirect = true,
} )
2021-03-15 00:25:10 +00:00
socketutil : reset_timeout ( )
2018-12-13 06:27:49 +00:00
--logger.dbg(headers)
file_size = headers and headers [ " content-length " ]
UIManager : show ( ConfirmBox : new {
text = T ( _ ( " Dictionary filesize is %1 (%2 bytes). Continue with download? " ) , util.getFriendlySize ( file_size ) , util.getFormattedSize ( file_size ) ) ,
ok_text = _ ( " Download " ) ,
ok_callback = function ( )
-- call ourselves with continue = true
self : downloadDictionary ( dict , download_location , true )
end ,
} )
return
else
UIManager : nextTick ( function ( )
UIManager : show ( InfoMessage : new {
text = _ ( " Downloading… " ) ,
timeout = 3 ,
} )
end )
end
2021-03-15 00:25:10 +00:00
socketutil : set_timeout ( socketutil.FILE_BLOCK_TIMEOUT , socketutil.FILE_TOTAL_TIMEOUT )
2022-09-16 22:08:00 +00:00
local code , headers , status = socket.skip ( 1 , http.request {
2021-03-15 00:25:10 +00:00
url = dict.url ,
sink = ltn12.sink . file ( io.open ( download_location , " w " ) ) ,
} )
socketutil : reset_timeout ( )
2022-09-16 22:08:00 +00:00
if code == 200 then
2018-12-13 06:27:49 +00:00
logger.dbg ( " file downloaded to " , download_location )
else
2022-09-16 22:08:00 +00:00
logger.dbg ( " ReaderDictionary: Request failed: " , status or code )
logger.dbg ( " ReaderDictionary: Response headers: " , headers )
2018-12-13 06:27:49 +00:00
UIManager : show ( InfoMessage : new {
2020-01-04 00:18:51 +00:00
text = _ ( " Could not save file to: \n " ) .. BD.filepath ( download_location ) ,
2018-12-13 06:27:49 +00:00
--timeout = 3,
} )
return false
end
2023-03-14 21:11:17 +00:00
-- stable target directory is needed so we can look through the folder later
local dict_path = self.data_dir .. " / " .. dict.name
util.makePath ( dict_path )
local ok , error = Device : unpackArchive ( download_location , dict_path , true )
2018-12-13 06:27:49 +00:00
if ok then
2023-03-14 21:11:17 +00:00
if dict.ifo_lang then
self : extendIfoWithLanguage ( dict_path , dict.ifo_lang )
end
2018-12-13 06:27:49 +00:00
available_ifos = false
self : init ( )
UIManager : show ( InfoMessage : new {
text = _ ( " Dictionary downloaded: \n " ) .. dict.name ,
} )
return true
else
UIManager : show ( InfoMessage : new {
text = _ ( " Dictionary failed to download: \n " ) .. string.format ( " %s \n %s " , dict.name , error ) ,
} )
return false
end
end
2023-03-14 21:11:17 +00:00
function ReaderDictionary : extendIfoWithLanguage ( dictionary_location , ifo_lang )
local function cb ( path , filename )
if util.getFileNameSuffix ( filename ) == " ifo " then
local fmt_string = " lang=%s "
local f = io.open ( path , " a+ " )
if f then
local ifo = f : read ( " a* " )
if ifo [ # ifo ] ~= " \n " then
fmt_string = " \n " .. fmt_string
end
f : write ( fmt_string : format ( ifo_lang ) )
f : close ( )
end
end
end
util.findFiles ( dictionary_location , cb )
end
2013-07-21 06:23:54 +00:00
function ReaderDictionary : onReadSettings ( config )
2021-01-06 20:49:23 +00:00
self.preferred_dictionaries = config : readSetting ( " preferred_dictionaries " ) or { }
if # self.preferred_dictionaries == 0 then
-- Legacy setting, when only one dict could be set as default/first to show
local default_dictionary = config : readSetting ( " default_dictionary " )
if default_dictionary then
table.insert ( self.preferred_dictionaries , default_dictionary )
config : delSetting ( " default_dictionary " )
end
end
if # self.preferred_dictionaries > 0 then
self : updateSdcvDictNamesOptions ( )
end
2021-03-06 21:44:18 +00:00
if config : has ( " disable_fuzzy_search " ) then
self.disable_fuzzy_search = config : isTrue ( " disable_fuzzy_search " )
else
2017-06-18 16:08:57 +00:00
self.disable_fuzzy_search = G_reader_settings : isTrue ( " disable_fuzzy_search " )
end
2013-07-21 06:23:54 +00:00
end
2013-12-27 15:18:16 +00:00
function ReaderDictionary : onSaveSettings ( )
2020-02-17 15:53:09 +00:00
if self.ui . doc_settings then
2021-01-06 20:49:23 +00:00
self.ui . doc_settings : saveSetting ( " preferred_dictionaries " , self.preferred_dictionaries )
2020-02-17 15:53:09 +00:00
self.ui . doc_settings : saveSetting ( " disable_fuzzy_search " , self.disable_fuzzy_search )
end
2017-06-18 16:08:57 +00:00
end
2021-01-06 20:49:23 +00:00
function ReaderDictionary : onTogglePreferredDict ( dict )
if not self.preferred_dictionaries then
-- Invoked from FileManager: no preferred dict to manage
return true
end
local removed = false
for idx , name in ipairs ( self.preferred_dictionaries ) do
if dict == name then
removed = true
table.remove ( self.preferred_dictionaries , idx )
break
end
end
if not removed then -- insert it as first
table.insert ( self.preferred_dictionaries , 1 , dict )
end
UIManager : show ( InfoMessage : new {
text = removed and T ( _ ( " %1 is no longer a preferred dictionary for this document. " ) , dict )
or T ( _ ( " %1 is now the preferred dictionary for this document. " ) , dict ) ,
timeout = 2 ,
} )
self : updateSdcvDictNamesOptions ( )
return true
end
2019-10-25 15:25:26 +00:00
function ReaderDictionary : toggleFuzzyDefault ( )
local disable_fuzzy_search = G_reader_settings : isTrue ( " disable_fuzzy_search " )
UIManager : show ( MultiConfirmBox : new {
2017-06-18 16:08:57 +00:00
text = T (
disable_fuzzy_search
2019-10-25 15:25:26 +00:00
and _ ( [ [
Would you like to enable or disable fuzzy search by default ?
Fuzzy search can match epuisante , é puisante and é puisantes to é puisant , even if only the latter has an entry in the dictionary . It can be disabled to improve performance , but it might be worthwhile to look into disabling unneeded dictionaries before disabling fuzzy search .
The current default ( ★ ) is disabled . ] ] )
or _ ( [ [
Would you like to enable or disable fuzzy search by default ?
Fuzzy search can match epuisante , é puisante and é puisantes to é puisant , even if only the latter has an entry in the dictionary . It can be disabled to improve performance , but it might be worthwhile to look into disabling unneeded dictionaries before disabling fuzzy search .
The current default ( ★ ) is enabled . ] ] )
2017-10-07 20:13:46 +00:00
) ,
2019-10-25 15:25:26 +00:00
choice1_text_func = function ( )
return disable_fuzzy_search and _ ( " Disable (★) " ) or _ ( " Disable " )
end ,
choice1_callback = function ( )
2021-03-06 21:44:18 +00:00
G_reader_settings : makeTrue ( " disable_fuzzy_search " )
2019-10-25 15:25:26 +00:00
end ,
choice2_text_func = function ( )
return disable_fuzzy_search and _ ( " Enable " ) or _ ( " Enable (★) " )
end ,
choice2_callback = function ( )
2021-03-06 21:44:18 +00:00
G_reader_settings : makeFalse ( " disable_fuzzy_search " )
2017-06-18 16:08:57 +00:00
end ,
} )
2013-07-21 06:23:54 +00:00
end
2013-10-18 20:38:07 +00:00
return ReaderDictionary