local BD = require ( " ui/bidi " )
local ButtonDialog = require ( " ui/widget/buttondialog " )
local ButtonDialogTitle = require ( " ui/widget/buttondialogtitle " )
local Cache = require ( " cache " )
local ConfirmBox = require ( " ui/widget/confirmbox " )
local DocumentRegistry = require ( " document/documentregistry " )
local Font = require ( " ui/font " )
local InfoMessage = require ( " ui/widget/infomessage " )
local Menu = require ( " ui/widget/menu " )
local MultiInputDialog = require ( " ui/widget/multiinputdialog " )
local InputDialog = require ( " ui/widget/inputdialog " )
local NetworkMgr = require ( " ui/network/manager " )
local OPDSParser = require ( " opdsparser " )
local Screen = require ( " device " ) . screen
local UIManager = require ( " ui/uimanager " )
local http = require ( " socket.http " )
local lfs = require ( " libs/libkoreader-lfs " )
local logger = require ( " logger " )
local ltn12 = require ( " ltn12 " )
local socket = require ( " socket " )
local socketutil = require ( " socketutil " )
local url = require ( " socket.url " )
local util = require ( " util " )
local _ = require ( " gettext " )
local T = require ( " ffi/util " ) . template
-- cache catalog parsed from feed xml
local CatalogCache = Cache : new {
-- Make it 20 slots, with no storage space constraints
slots = 20 ,
}
local OPDSBrowser = Menu : extend {
opds_servers = G_reader_settings : readSetting ( " opds_servers " , {
{
title = " Project Gutenberg " ,
url = " https://m.gutenberg.org/ebooks.opds/?format=opds " ,
} ,
{
title = " Feedbooks " ,
url = " https://catalog.feedbooks.com/catalog/public_domain.atom " ,
} ,
{
title = " ManyBooks " ,
url = " http://manybooks.net/opds/index.php " ,
} ,
{
title = " Internet Archive " ,
url = " https://bookserver.archive.org/ " ,
} ,
{
title = " textos.info (Spanish) " ,
url = " https://www.textos.info/catalogo.atom " ,
} ,
{
title = " Gallica (French) " ,
url = " https://gallica.bnf.fr/opds " ,
} ,
} ) ,
calibre_name = _ ( " Local calibre library " ) ,
calibre_opds = G_reader_settings : readSetting ( " calibre_opds " , { } ) ,
catalog_type = " application/atom%+xml " ,
search_type = " application/opensearchdescription%+xml " ,
search_template_type = " application/atom%+xml " ,
acquisition_rel = " ^http://opds%-spec%.org/acquisition " ,
image_rel = " http://opds-spec.org/image " ,
thumbnail_rel = " http://opds-spec.org/image/thumbnail " ,
width = Screen : getWidth ( ) ,
height = Screen : getHeight ( ) ,
no_title = false ,
parent = nil ,
}
function OPDSBrowser : init ( )
self.item_table = self : genItemTableFromRoot ( )
self.catalog_title = nil
Menu.init ( self ) -- call parent's init()
end
-- This function is a callback fired from the new
-- catalog dialog, 'addNewCatalog'.
function OPDSBrowser : addServerFromInput ( fields )
logger.info ( " New OPDS catalog input: " , fields )
local new_server = {
title = fields [ 1 ] ,
url = ( fields [ 2 ] : match ( " ^%a+:// " ) and fields [ 2 ] or " http:// " .. fields [ 2 ] ) ,
searchable = ( fields [ 2 ] : match ( " %%s " ) and true or false ) ,
username = fields [ 3 ] ~= " " and fields [ 3 ] or nil ,
-- Allow empty passwords
password = fields [ 4 ] ,
}
table.insert ( self.opds_servers , new_server )
self : init ( )
end
-- This function is a callback fired from the Calibre input
-- dialog 'editCalibreServer'.
function OPDSBrowser : editCalibreFromInput ( fields )
logger.dbg ( " Edit calibre server input: " , fields )
if fields [ 1 ] then
self.calibre_opds . host = fields [ 1 ]
end
if tonumber ( fields [ 2 ] ) then
self.calibre_opds . port = fields [ 2 ]
end
if fields [ 3 ] and fields [ 3 ] ~= " " then
self.calibre_opds . username = fields [ 3 ]
else
self.calibre_opds . username = nil
end
if fields [ 4 ] then
self.calibre_opds . password = fields [ 4 ]
else
self.calibre_opds . password = nil
end
self : init ( )
end
-- This function shows a dialog with input fields
-- for entering information for an OPDS catalog.
function OPDSBrowser : addNewCatalog ( )
self.add_server_dialog = MultiInputDialog : new {
title = _ ( " Add OPDS catalog " ) ,
fields = {
{
text = " " ,
hint = _ ( " Catalog name " ) ,
} ,
{
text = " " ,
hint = _ ( " Catalog URL " ) ,
} ,
{
text = " " ,
hint = _ ( " Username (optional) " ) ,
} ,
{
text = " " ,
hint = _ ( " Password (optional) " ) ,
text_type = " password " ,
} ,
} ,
buttons = {
{
{
text = _ ( " Cancel " ) ,
callback = function ( )
self.add_server_dialog : onClose ( )
UIManager : close ( self.add_server_dialog )
end
} ,
{
text = _ ( " Add " ) ,
callback = function ( )
self.add_server_dialog : onClose ( )
UIManager : close ( self.add_server_dialog )
self : addServerFromInput ( MultiInputDialog : getFields ( ) )
end
} ,
} ,
} ,
}
UIManager : show ( self.add_server_dialog )
self.add_server_dialog : onShowKeyboard ( )
end
-- This function shows a dialog to the user with input fields
-- for setting Calibre server information.
-- (I think that the Calibre stuff could be moved to a separate file.)
function OPDSBrowser : editCalibreServer ( )
self.add_server_dialog = MultiInputDialog : new {
title = _ ( " Edit local calibre host and port " ) ,
fields = {
{
--- @todo get IP address of current device
text = self.calibre_opds . host or " 192.168.1.1 " ,
hint = _ ( " calibre host " ) ,
} ,
{
text = self.calibre_opds . port and tostring ( self.calibre_opds . port ) or " 8080 " ,
hint = _ ( " calibre port " ) ,
} ,
{
text = self.calibre_opds . username or " " ,
hint = _ ( " Username (optional) " ) ,
} ,
{
text = self.calibre_opds . password or " " ,
hint = _ ( " Password (optional) " ) ,
text_type = " password " ,
} ,
} ,
buttons = {
{
{
text = _ ( " Cancel " ) ,
callback = function ( )
self.add_server_dialog : onClose ( )
UIManager : close ( self.add_server_dialog )
end
} ,
{
text = _ ( " Apply " ) ,
callback = function ( )
self.add_server_dialog : onClose ( )
UIManager : close ( self.add_server_dialog )
self : editCalibreFromInput ( MultiInputDialog : getFields ( ) )
end
} ,
} ,
} ,
}
UIManager : show ( self.add_server_dialog )
self.add_server_dialog : onShowKeyboard ( )
end
-- This function creates the "main menu" for the plugin,
-- wherein the user is shown the default servers, their
-- custom servers, and an item to allow them to add more of their
-- own servers.
function OPDSBrowser : genItemTableFromRoot ( )
local item_table = { }
-- Loop through the default servers and add them
-- to the item table.
for _ , server in ipairs ( self.opds_servers ) do
table.insert ( item_table , {
text = server.title ,
content = server.subtitle ,
url = server.url ,
username = server.username ,
password = server.password ,
deletable = true ,
editable = true ,
searchable = server.searchable ,
} )
end
-- Handle the Calibre server. If it's not set, then place
-- an item that would prompt the user to enter their Calibre settings.
if not self.calibre_opds . host or not self.calibre_opds . port then
-- Here's where we allow the Calibre server to be set.
table.insert ( item_table , {
text = self.calibre_name ,
callback = function ( )
self : editCalibreServer ( )
end ,
deletable = false ,
} )
else
-- Here's where we show the existing Calibre server with
-- the login details stored on the device.
table.insert ( item_table , {
text = self.calibre_name ,
url = string.format ( " http://%s:%d/opds " ,
self.calibre_opds . host , self.calibre_opds . port ) ,
username = self.calibre_opds . username ,
password = self.calibre_opds . password ,
editable = true ,
deletable = false ,
searchable = false ,
} )
end
-- Show the user a list item that would let them add more items
-- to their OPDS server list.
table.insert ( item_table , {
text = _ ( " Add new OPDS catalog " ) ,
callback = function ( )
self : addNewCatalog ( )
end ,
} )
return item_table
end
function OPDSBrowser : fetchFeed ( item_url , username , password , method )
local sink = { }
socketutil : set_timeout (
socketutil.LARGE_BLOCK_TIMEOUT ,
socketutil.LARGE_TOTAL_TIMEOUT
)
-- Prepare the request to send to the server.
local request = {
url = item_url ,
method = method and method or " GET " ,
-- Explicitly specify that we don't support compressed content.
-- Some servers will still break RFC2616 14.3 and send crap instead.
headers = {
[ " Accept-Encoding " ] = " identity " ,
} ,
sink = ltn12.sink . table ( sink ) ,
user = username ,
password = password ,
}
logger.info ( " Request: " , request )
-- Fire off the request and wait to see what we get back.
local code , headers = socket.skip ( 1 , http.request ( request ) )
socketutil : reset_timeout ( )
-- Check the response and raise error message when network is unavailable.
if headers == nil then
error ( code )
end
-- Below are numerous if cases to handle different response codes.
if code == 200 then
-- 200 means the request succeeded.
-- If the method sent was HEAD, then we're probably checking for
-- an update and therefore only interested in the last-modified
-- time of the resource (who needs a body when you have a head?).
if method == " HEAD " then
if headers [ " last-modified " ] then
return headers [ " last-modified " ]
else
return
end
end
-- If the method sent was not HEAD, then we are interested in
-- the payload of the request. We'll add that to a table below
-- and return that as the result of this function.
local xml = table.concat ( sink )
-- Obviously, check to see if the payload exists.
if xml ~= " " then
return xml
end
elseif method == " HEAD " then
-- Don't show error messages when we check headers only.
return
elseif code == 301 then -- Page has permanently moved
UIManager : show ( InfoMessage : new {
text = T ( _ ( " The catalog has been permanently moved. Please update catalog URL to '%1'. " ) ,
BD.url ( headers [ ' Location ' ] ) ) ,
} )
elseif code == 302
and item_url : match ( " ^https " )
and headers.location : match ( " ^http[^s] " ) then -- Page is redirecting
UIManager : show ( InfoMessage : new {
text = T ( _ ( " Insecure HTTPS → HTTP downgrade attempted by redirect from: \n \n '%1' \n \n to \n \n '%2'. \n \n Please inform the server administrator that many clients disallow this because it could be a downgrade attack. " ) ,
BD.url ( item_url ) ,
BD.url ( headers.location ) ) ,
icon = " notice-warning " ,
} )
elseif code == 401 then -- Not authorized
UIManager : show ( InfoMessage : new {
text = T ( _ ( " Authentication required for catalog. Please add a username and password. " ) ) ,
} )
elseif code == 403 then -- Authorization attemp failed
UIManager : show ( InfoMessage : new {
text = T ( _ ( " Failed to authenticate. Please check your username and password. " ) ) ,
} )
elseif code == 404 then -- Page not found
UIManager : show ( InfoMessage : new {
text = T ( _ ( " Catalog not found. " ) ) ,
} )
elseif code == 406 then -- Server cannot fulfil our request
UIManager : show ( InfoMessage : new {
text = T ( _ ( " Cannot get catalog. Server refuses to serve uncompressed content. " ) ) ,
} )
else
-- This block handles all other requests and supplies the user with a generic
-- error message and no more information than the code.
UIManager : show ( InfoMessage : new {
text = T ( _ ( " Cannot get catalog. Server response code %1. " ) , code ) ,
} )
end
end
function OPDSBrowser : parseFeed ( item_url , username , password )
local feed_last_modified = self : fetchFeed ( item_url , username , password , " HEAD " )
local hash = " opds|catalog| " .. item_url
if feed_last_modified then
hash = hash .. " | " .. feed_last_modified
end
local feed = CatalogCache : check ( hash )
if feed then
logger.dbg ( " Cache hit for " , hash )
else
logger.dbg ( " Cache miss for " , hash )
feed = self : fetchFeed ( item_url , username , password )
if feed then
logger.dbg ( " Caching " , hash )
CatalogCache : insert ( hash , feed )
end
end
if feed then
return OPDSParser : parse ( feed )
end
end
function OPDSBrowser : getCatalog ( item_url , username , password )
local ok , catalog = pcall ( self.parseFeed , self , item_url , username , password )
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.
4 years ago
if not ok and catalog then
logger.info ( " Cannot get catalog info from " , item_url or " nil " , catalog )
UIManager : show ( InfoMessage : new {
text = T ( _ ( " Cannot get catalog info from %1 " ) , ( item_url and BD.url ( item_url ) or " nil " ) ) ,
} )
return
end
if ok and catalog then
return catalog
end
end
function OPDSBrowser : genItemTableFromURL ( item_url , username , password )
local catalog = self : getCatalog ( item_url , username , password )
return self : genItemTableFromCatalog ( catalog , item_url , username , password )
end
function OPDSBrowser : getSearchTemplate ( osd_url , username , password )
-- parse search descriptor
local search_descriptor = self : parseFeed ( osd_url , username , password )
if search_descriptor and search_descriptor.OpenSearchDescription and search_descriptor.OpenSearchDescription . Url then
for _ , candidate in ipairs ( search_descriptor.OpenSearchDescription . Url ) do
if candidate.type and candidate.template and candidate.type : find ( self.search_template_type ) then
return candidate.template : gsub ( " {searchTerms} " , " %%s " )
end
end
end
end
function OPDSBrowser : genItemTableFromCatalog ( catalog , item_url , username , password )
local item_table = { }
if not catalog then
return item_table
end
local feed = catalog.feed or catalog
local function build_href ( href )
return url.absolute ( item_url , href )
end
local hrefs = { }
if feed.link then
for _ , link in ipairs ( feed.link ) do
if link.type ~= nil then
if link.type : find ( self.catalog_type ) then
if link.rel and link.href then
hrefs [ link.rel ] = build_href ( link.href )
end
end
if link.type : find ( self.search_type ) then
if link.href then
local stpl = self : getSearchTemplate ( build_href ( link.href ) , username , password )
-- The OpenSearchDescription/Url template field might *also* be a relative path...
stpl = build_href ( stpl )
-- insert the search item
local item = { }
item.acquisitions = { }
item.text = " Search "
item.callback = function ( )
self : browseSearchable ( stpl , username , password )
end
table.insert ( item_table , item )
end
end
end
end
end
item_table.hrefs = hrefs
if username then
item_table.username = username
end
if password then
item_table.password = password
end
if not feed.entry then
if # hrefs == 0 then
UIManager : show ( InfoMessage : new {
text = _ ( " Failed to parse the catalog. " ) ,
} )
end
return item_table
end
for _ , entry in ipairs ( feed.entry ) do
local item = { }
item.acquisitions = { }
if entry.link then
for _ , link in ipairs ( entry.link ) do
if link.type and link.type : find ( self.catalog_type )
and ( not link.rel
or link.rel == " subsection "
or link.rel == " http://opds-spec.org/subsection "
or link.rel == " http://opds-spec.org/sort/popular "
or link.rel == " http://opds-spec.org/sort/new " ) then
item.url = build_href ( link.href )
end
-- Some catalogs do not use the rel attribute to denote
-- a publication. Arxiv uses title. Specifically, it uses
-- a title attribute that contains pdf. (title="pdf")
if link.rel or link.title then
if link.rel : match ( self.acquisition_rel ) then
table.insert ( item.acquisitions , {
type = link.type ,
href = build_href ( link.href ) ,
title = link.title ,
} )
elseif link.rel == self.thumbnail_rel then
item.thumbnail = build_href ( link.href )
elseif link.rel == self.image_rel then
item.image = build_href ( link.href )
end
-- This statement grabs the catalog items that are
-- indicated by title="pdf" or whose type is
-- "application/pdf"
if link.title == " pdf " or link.type == " application/pdf "
and link.rel ~= " subsection " then
-- Check for the presence of the pdf suffix and add it
-- if it's missing.
local href = link.href
local filetype = util.getFileNameSuffix ( link.href )
if filetype ~= " pdf " then
href = href .. " .pdf "
end
table.insert ( item.acquisitions , {
type = link.title ,
href = build_href ( href ) ,
} )
end
end
end
end
local title = " Unknown "
if type ( entry.title ) == " string " then
title = entry.title
elseif type ( entry.title ) == " table " then
if type ( entry.title . type ) == " string " and entry.title . div ~= " " then
title = entry.title . div
end
end
if title == " Unknown " then
logger.info ( " Cannot handle title " , entry.title )
end
item.text = title
local author = " Unknown Author "
if type ( entry.author ) == " table " and entry.author . name then
author = entry.author . name
if type ( author ) == " table " then
if # author > 0 then
author = table.concat ( author , " , " )
else
-- we may get an empty table on https://gallica.bnf.fr/opds
author = nil
end
end
if author then
item.text = title .. " - " .. author
end
end
item.title = title
item.author = author
item.id = entry.id
item.content = entry.content
item.updated = entry.updated
if username then
item.username = username
end
if password then
item.password = password
end
table.insert ( item_table , item )
end
return item_table
end
function OPDSBrowser : updateCatalog ( item_url , username , password )
local menu_table = self : genItemTableFromURL ( item_url , username , password )
if # menu_table > 0 then
self : switchItemTable ( self.catalog_title , menu_table )
if self.page_num <= 1 then
self : onNext ( )
end
return true
end
end
function OPDSBrowser : appendCatalog ( item_url , username , password )
local new_table = self : genItemTableFromURL ( item_url , username , password )
if # new_table == 0 then return false end
for _ , item in ipairs ( new_table ) do
table.insert ( self.item_table , item )
end
self.item_table . hrefs = new_table.hrefs
self : switchItemTable ( self.catalog_title , self.item_table , - 1 )
return true
end
function OPDSBrowser . getCurrentDownloadDir ( )
local lastdir = G_reader_settings : readSetting ( " lastdir " )
return G_reader_settings : readSetting ( " download_dir " ) or lastdir
end
function OPDSBrowser : downloadFile ( item , filetype , remote_url )
-- Download to user selected folder or last opened folder.
local download_dir = self.getCurrentDownloadDir ( )
local filename = item.title .. " . " .. filetype
if item.author then
filename = item.author .. " - " .. filename
end
filename = util.getSafeFilename ( filename , download_dir )
local local_path = download_dir .. " / " .. filename
local_path = util.fixUtf8 ( local_path , " _ " )
local function download ( )
UIManager : scheduleIn ( 1 , function ( )
logger.dbg ( " Downloading file " , local_path , " from " , remote_url )
local parsed = url.parse ( remote_url )
local code , headers
if parsed.scheme == " http " or parsed.scheme == " https " then
socketutil : set_timeout ( socketutil.FILE_BLOCK_TIMEOUT , socketutil.FILE_TOTAL_TIMEOUT )
code , headers = socket.skip ( 1 , http.request {
url = remote_url ,
headers = {
[ " Accept-Encoding " ] = " identity " ,
} ,
sink = ltn12.sink . file ( io.open ( local_path , " w " ) ) ,
user = item.username ,
password = item.password ,
} )
socketutil : reset_timeout ( )
else
UIManager : show ( InfoMessage : new {
text = T ( _ ( " Invalid protocol: \n %1 " ) , parsed.scheme ) ,
timeout = 3 ,
} )
end
if code == 200 then
logger.dbg ( " File downloaded to " , local_path )
if self.file_downloaded_callback then
self.file_downloaded_callback ( local_path )
end
elseif code == 302 and remote_url : match ( " ^https " ) and headers.location : match ( " ^http[^s] " ) then
util.removeFile ( local_path )
UIManager : show ( InfoMessage : new {
text = T ( _ ( " Insecure HTTPS → HTTP downgrade attempted by redirect from: \n \n '%1' \n \n to \n \n '%2'. \n \n Please inform the server administrator that many clients disallow this because it could be a downgrade attack. " ) , BD.url ( remote_url ) , BD.url ( headers.location ) ) ,
icon = " notice-warning " ,
} )
else
util.removeFile ( local_path )
UIManager : show ( InfoMessage : new {
text = _ ( " Could not save file to: \n " ) .. BD.filepath ( local_path ) ,
timeout = 3 ,
} )
end
end )
UIManager : show ( InfoMessage : new {
text = _ ( " Downloading may take several minutes… " ) ,
timeout = 1 ,
} )
end
if lfs.attributes ( local_path , " mode " ) == " file " then
UIManager : show ( ConfirmBox : new {
text = T ( _ ( " The file %1 already exists. Do you want to overwrite it? " ) , BD.filepath ( local_path ) ) ,
ok_text = _ ( " Overwrite " ) ,
ok_callback = function ( )
download ( )
end ,
} )
else
download ( )
end
end
function OPDSBrowser : createNewDownloadDialog ( path , buttons )
self.download_dialog = ButtonDialogTitle : new {
title = T ( _ ( " Download folder: \n %1 \n \n Download file type: " ) , BD.dirpath ( path ) ) ,
use_info_style = true ,
buttons = buttons
}
end
function OPDSBrowser : showDownloads ( item )
local acquisitions = item.acquisitions
local downloadsperline = 2
local lines = math.ceil ( # acquisitions / downloadsperline )
local buttons = { }
for i = 1 , lines do
local line = { }
for j = 1 , downloadsperline do
local button = { }
local index = ( i - 1 ) * downloadsperline + j
local acquisition = acquisitions [ index ]
if acquisition then
local filetype = util.getFileNameSuffix ( acquisition.href )
logger.dbg ( " Filetype for download is " , filetype )
if not DocumentRegistry : hasProvider ( " dummy. " .. filetype ) then
filetype = nil
end
if not filetype and DocumentRegistry : hasProvider ( nil , acquisition.type ) then
filetype = DocumentRegistry : mimeToExt ( acquisition.type )
end
if filetype then
filetype = string.lower ( filetype )
-- append DOWNWARDS BLACK ARROW ⬇ U+2B07 to format
if acquisition.title then
button.text = acquisition.title .. " \xE2 \xAC \x87 "
else
button.text = string.upper ( filetype ) .. " \xE2 \xAC \x87 "
end
button.callback = function ( )
self : downloadFile ( item , filetype , acquisition.href )
UIManager : close ( self.download_dialog )
end
table.insert ( line , button )
end
elseif # acquisitions > downloadsperline then
table.insert ( line , { text = " " } )
end
end
table.insert ( buttons , line )
end
table.insert ( buttons , { } )
-- Set download folder and book info buttons.
table.insert ( buttons , {
{
text = _ ( " Choose folder " ) ,
callback = function ( )
require ( " ui/downloadmgr " ) : new {
onConfirm = function ( path )
logger.info ( " Download folder set to " , path )
G_reader_settings : saveSetting ( " download_dir " , path )
UIManager : nextTick ( function ( )
UIManager : close ( self.download_dialog )
self : createNewDownloadDialog ( path , buttons )
UIManager : show ( self.download_dialog )
end )
end ,
} : chooseDir ( )
end ,
} ,
{
text = _ ( " Book information " ) ,
enabled = type ( item.content ) == " string " ,
callback = function ( )
local TextViewer = require ( " ui/widget/textviewer " )
UIManager : show ( TextViewer : new {
title = item.text ,
text = util.htmlToPlainTextIfHtml ( item.content ) ,
text_face = Font : getFace ( " x_smallinfofont " , G_reader_settings : readSetting ( " items_font_size " ) ) ,
} )
end ,
} ,
} )
self : createNewDownloadDialog ( self.getCurrentDownloadDir ( ) , buttons )
UIManager : show ( self.download_dialog )
end
function OPDSBrowser : browse ( browse_url , username , password )
logger.dbg ( " Browse OPDS url " , browse_url or " nil " )
table.insert ( self.paths , {
url = browse_url ,
username = username ,
password = password ,
title = self.catalog_title ,
} )
if not self : updateCatalog ( browse_url , username , password ) then
table.remove ( self.paths )
end
end
function OPDSBrowser : browseSearchable ( browse_url , username , password )
self.search_server_dialog = InputDialog : new {
title = _ ( " Search OPDS catalog " ) ,
input = " " ,
-- @translators: This is an input hint for something to search for in an OPDS catalog, namely a famous author everyone knows. It probably doesn't need to be localized, but this is just here in case another name or book title would be more appropriate outside of a European context.
input_hint = _ ( " Alexandre Dumas " ) ,
input_type = " string " ,
description = _ ( " %s in url will be replaced by your input " ) ,
buttons = {
{
{
text = _ ( " Cancel " ) ,
callback = function ( )
UIManager : close ( self.search_server_dialog )
end ,
} ,
{
text = _ ( " Search " ) ,
is_enter_default = true ,
callback = function ( )
UIManager : close ( self.search_server_dialog )
local search = self.search_server_dialog : getInputText ( ) : gsub ( " " , " + " )
local searched_url = browse_url : gsub ( " %%s " , search )
self : browse ( searched_url , username , password )
end ,
} ,
}
} ,
}
UIManager : show ( self.search_server_dialog )
self.search_server_dialog : onShowKeyboard ( )
end
-- This function is fired when a list item is selected. The function
-- determines what action to performed based on the item's values.
-- Possible actions include: adding a catalog, acquiring a publication,
-- and navigating to another catalog.
function OPDSBrowser : onMenuSelect ( item )
logger.dbg ( " Menu select item " , item )
self.catalog_title = self.catalog_title or _ ( " OPDS Catalog " )
-- add catalog
if item.callback then
item.callback ( )
-- acquisition
elseif item.acquisitions and # item.acquisitions > 0 then
logger.dbg ( " Downloads available: " , item )
self : showDownloads ( item )
-- navigation
else
self.catalog_title = item.text or self.catalog_title
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.
4 years ago
local connect_callback
if item.searchable then
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.
4 years ago
connect_callback = function ( )
self : browseSearchable ( item.url , item.username , item.password )
end
else
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.
4 years ago
connect_callback = function ( )
self : browse ( item.url , item.username , item.password )
end
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.
4 years ago
NetworkMgr : runWhenConnected ( connect_callback )
end
return true
end
function OPDSBrowser : editServerFromInput ( item , fields )
logger.info ( " Edit OPDS catalog input: " , fields )
for _ , server in ipairs ( self.opds_servers ) do
if server.title == item.text or server.url == item.url then
server.title = fields [ 1 ]
server.url = ( fields [ 2 ] : match ( " ^%a+:// " ) and fields [ 2 ] or " http:// " .. fields [ 2 ] )
server.searchable = ( fields [ 2 ] : match ( " %%s " ) and true or false )
server.username = fields [ 3 ] ~= " " and fields [ 3 ] or nil
server.password = fields [ 4 ]
end
end
self : init ( )
end
function OPDSBrowser : editOPDSServer ( item )
logger.info ( " Edit OPDS Server: " , item )
self.edit_server_dialog = MultiInputDialog : new {
title = _ ( " Edit OPDS catalog " ) ,
fields = {
{
text = item.text or " " ,
hint = _ ( " Catalog name " ) ,
} ,
{
text = item.url or " " ,
hint = _ ( " Catalog URL " ) ,
} ,
{
text = item.username or " " ,
hint = _ ( " Username (optional) " ) ,
} ,
{
text = item.password or " " ,
hint = _ ( " Password (optional) " ) ,
text_type = " password " ,
} ,
} ,
buttons = {
{
{
text = _ ( " Cancel " ) ,
callback = function ( )
self.edit_server_dialog : onClose ( )
UIManager : close ( self.edit_server_dialog )
end
} ,
{
text = _ ( " Apply " ) ,
callback = function ( )
self.edit_server_dialog : onClose ( )
UIManager : close ( self.edit_server_dialog )
self : editServerFromInput ( item , MultiInputDialog : getFields ( ) )
end
} ,
} ,
} ,
}
UIManager : show ( self.edit_server_dialog )
self.edit_server_dialog : onShowKeyboard ( )
end
function OPDSBrowser : deleteOPDSServer ( item )
logger.info ( " Delete OPDS server: " , item )
for i = # self.opds_servers , 1 , - 1 do
local server = self.opds_servers [ i ]
if server.title == item.text and server.url == item.url then
table.remove ( self.opds_servers , i )
end
end
self : init ( )
end
function OPDSBrowser : onMenuHold ( item )
if item.deletable or item.editable then
self.opds_server_dialog = ButtonDialog : new {
buttons = {
{
{
text = _ ( " Edit " ) ,
enabled = item.editable ,
callback = function ( )
UIManager : close ( self.opds_server_dialog )
if item.text ~= self.calibre_name then
self : editOPDSServer ( item )
else
self : editCalibreServer ( item )
end
end
} ,
{
text = _ ( " Delete " ) ,
enabled = item.deletable ,
callback = function ( )
UIManager : close ( self.opds_server_dialog )
self : deleteOPDSServer ( item )
end
} ,
} ,
}
}
UIManager : show ( self.opds_server_dialog )
return true
end
end
function OPDSBrowser : onReturn ( )
if # self.paths > 0 then
table.remove ( self.paths )
local path = self.paths [ # self.paths ]
if path then
-- return to last path
self.catalog_title = path.title
self : updateCatalog ( path.url , path.username , path.password )
else
-- return to root path, we simply reinit opdsbrowser
self : init ( )
end
end
return true
end
function OPDSBrowser : onHoldReturn ( )
if # self.paths > 1 then
local path = self.paths [ 1 ]
if path then
for i = # self.paths , 2 , - 1 do
table.remove ( self.paths )
end
self.catalog_title = path.title
self : updateCatalog ( path.url , path.username , path.password )
end
end
return true
end
function OPDSBrowser : onNext ( )
-- self.page_num comes from menu.lua
local page_num = self.page_num
-- fetch more entries until we fill out one page or reach the end
while page_num == self.page_num do
local hrefs = self.item_table . hrefs
if hrefs and hrefs.next then
if not self : appendCatalog ( hrefs.next , self.item_table . username , self.item_table . password ) then
break -- reach end of paging
end
else
break
end
end
return true
end
return OPDSBrowser