1 local BD
= require("ui/bidi")
2 local WidgetContainer
= require("ui/widget/container/widgetcontainer")
3 local Event
= require("ui/event")
4 local Device
= require("device")
5 local UIManager
= require("ui/uimanager")
6 local ConfirmBox
= require("ui/widget/confirmbox")
7 local InputDialog
= require("ui/widget/inputdialog")
8 local MultiInputDialog
= require("ui/widget/multiinputdialog")
9 local FileManager
= require("apps/filemanager/filemanager")
10 local DocSettings
= require("docsettings")
11 local DataStorage
= require("datastorage")
12 local ReadHistory
= require("readhistory")
13 local Trapper
= require("ui/trapper")
14 local InfoMessage
= require("ui/widget/infomessage")
15 local ButtonDialogTitle
= require("ui/widget/buttondialogtitle")
16 local CheckButton
= require("ui/widget/checkbutton")
17 local MultiConfirmBox
= require("ui/widget/multiconfirmbox")
18 local SpinWidget
= require("ui/widget/spinwidget")
19 local Menu
= require("ui/widget/menu")
20 local Persist
= require("persist")
21 local NetworkMgr
= require("ui/network/manager")
22 local DocumentRegistry
= require("document/documentregistry")
23 local Font
= require("ui/font")
24 local Dispatcher
= require("dispatcher")
25 local TextViewer
= require("ui/widget/textviewer")
26 local KeyValuePage
= require("ui/widget/keyvaluepage")
27 local Screen
= require("device").screen
28 local Version
= require("version")
29 local filemanagerutil
= require("apps/filemanager/filemanagerutil")
30 local lfs
= require("libs/libkoreader-lfs")
31 local url
= require("socket.url")
32 local sha256
= require("ffi/sha2").sha256
33 local util
= require("util")
34 local ffiutil
= require("ffi/util")
35 local _
= require("gettext")
36 local T
= require("ffi/util").template
37 local gemini
= require("gemini")
39 local gemini_dir
= DataStorage
:getDataDir() .. "/gemini"
40 local ids_dir
= gemini_dir
.. "/identities"
41 local history_dir
= "/tmp/gemini-history"
42 local queue_dir
= "/tmp/gemini-queue"
43 local marks_path
= gemini_dir
.. "/bookmarks.gmi"
45 --local cafile_path = DataStorage:getDataDir() .. "/data/ca-bundle.crt"
47 local active_identities_persist
= Persist
:new
{ path
= gemini_dir
.. "/identities.lua" }
48 local queue_persist
= Persist
:new
{ path
= gemini_dir
.. "/queue.lua" }
49 local trust_store_persist
= Persist
:new
{ path
= gemini_dir
.. "/known_hosts.lua" }
50 local scheme_proxies_persist
= Persist
:new
{ path
= gemini_dir
.. "/scheme_proxies.lua" }
51 local active_identities
= active_identities_persist
:load() or {}
52 local queue
= queue_persist
:load() or {}
53 local trust_store
= trust_store_persist
:load() or {}
54 local scheme_proxies
= scheme_proxies_persist
:load() or { gopher
= {}, ["http(s)"] = {} }
58 local trust_overrides
= {}
60 local default_max_cache_history_items
= 20
61 local max_cache_history_items
= G_reader_settings
:readSetting("gemini_max_cache_history_items") or default_max_cache_history_items
63 local function getDefaultSavesDir()
64 local dir
= G_reader_settings
:readSetting("home_dir") or filemanagerutil
.getDefaultDir()
65 if dir
:sub(-1) ~= "/" then
68 return dir
.. "downloaded"
70 local save_dir
= G_reader_settings
:readSetting("gemini_save_dir") or getDefaultSavesDir()
72 local GeminiPlugin
= WidgetContainer
:extend
{
74 fullname
= _("Gemini plugin"),
79 -- Parsed URL of current item
80 function GeminiPlugin
:purl()
82 return history
[1].purl
86 local done_setup
= false
87 function GeminiPlugin
:setup()
88 util
.makePath(ids_dir
)
89 self
:onDispatcherRegisterActions()
90 if not done_setup
then
92 local GeminiDocument
= require("geminidocument")
93 DocumentRegistry
:addProvider("gmi", "text/gemini", GeminiDocument
, 100)
97 local activating
= false
99 function GeminiPlugin
:init()
102 self
.active
= activating
105 local postcb
= self
.ui
.registerPostReaderReadyCallback
107 -- Name for the callback in versions <= 2024.04
108 postcb
= self
.ui
.registerPostReadyCallback
111 if self
.ui
.document
.file
then
112 -- Keep gemini history files out of reader history
113 postcb(self
.ui
, function ()
114 ReadHistory
:removeItemByPath(self
.ui
.document
.file
)
118 local local_repl_purl
= repl_purl
119 postcb(self
.ui
, function ()
120 -- XXX: Input widget painted over without this delay. Better way?
121 UIManager
:scheduleIn(0.1, function ()
122 self
:promptInput(local_repl_purl
, "[Repeating]", false, true)
131 if self
.ui
and self
.ui
.link
then
132 if self
.ui
.link
.registerScheme
then
133 self
.ui
.link
:registerScheme("gemini")
134 self
.ui
.link
:registerScheme("about")
135 for scheme
,proxy
in pairs(scheme_proxies
) do
137 if scheme
== "http(s)" then
138 self
.ui
.link
:registerScheme("http")
139 self
.ui
.link
:registerScheme("https")
141 self
.ui
.link
:registerScheme(scheme
)
146 self
.ui
.link
:registerScheme("")
149 self
.ui
.link
:addToExternalLinkDialog("23_gemini", function(this
, link_url
)
151 text
= _("Open via Gemini"),
152 callback
= function()
153 UIManager
:close(this
.external_link_dialog
)
154 this
.ui
:handleEvent(Event
:new("FollowGeminiLink", link_url
))
156 show_in_dialog_func
= function(u
)
157 local scheme
= u
:match("^(%w[%w+%-.]*):") or ""
158 return scheme
== "gemini" or scheme
== "about" or
159 (scheme
== "" and self
.active
)
165 self
.ui
.menu
:registerToMainMenu(self
)
167 if self
.ui
and self
.ui
.highlight
then
168 self
.ui
.highlight
:addToHighlightDialog("20_queue_links", function(this
)
170 text
= _("Add links to queue"),
171 show_in_highlight_dialog_func
= function()
174 callback
= function()
175 self
:queueLinksInSelected(this
.selected_text
)
182 if self
.active
and Device
:isTouchDevice() then
183 self
.ui
:registerTouchZones({
185 id
= "tap_link_gemini",
188 ratio_x
= 0, ratio_y
= 0,
189 ratio_w
= 1, ratio_h
= 1,
192 -- Tap on gemini links has priority over everything
194 "readerhighlight_tap",
195 "tap_top_left_corner",
196 "tap_top_right_corner",
197 "tap_left_bottom_corner",
198 "tap_right_bottom_corner",
200 "readerconfigmenu_ext_tap",
201 "readerconfigmenu_tap",
202 "readermenu_ext_tap",
207 handler
= function(ges
) return self
:onTap(nil, ges
) end,
210 id
= "hold_release_link_gemini",
211 ges
= "hold_release",
213 ratio_x
= 0, ratio_y
= 0,
214 ratio_w
= 1, ratio_h
= 1,
217 "readerhighlight_hold_release",
219 handler
= function(ges
) return self
:onHoldRelease(nil, ges
) end,
222 id
= "hold_pan_link_gemini",
225 ratio_x
= 0, ratio_y
= 0,
226 ratio_w
= 1, ratio_h
= 1,
229 "readerhighlight_hold_pan",
231 handler
= function(ges
) return self
:onHoldPan(nil, ges
) end,
234 id
= "double_tap_link_gemini",
237 ratio_x
= 0, ratio_y
= 0,
238 ratio_w
= 1, ratio_h
= 1,
241 "double_tap_top_left_corner",
242 "double_tap_top_right_corner",
243 "double_tap_bottom_left_corner",
244 "double_tap_bottom_right_corner",
245 "double_tap_left_side",
246 "double_tap_right_side",
248 handler
= function(ges
) return self
:onDoubleTap(nil, ges
) end,
254 ratio_x
= 0, ratio_y
= 0,
255 ratio_w
= 1, ratio_h
= 1,
257 handler
= function(ges
) return self
:onSwipe(nil, ges
) end,
263 function GeminiPlugin
:mimeToExt(mimetype
)
264 return (mimetype
== "text/plain" and "txt")
265 or DocumentRegistry
:mimeToExt(mimetype
)
266 or (mimetype
:find("^text/") and "txt")
269 local function writeBodyToFile(body
, path
)
270 local o
= io
.open(path
, "w")
272 if type(body
) == "string" then
277 local chunk
, aborted
= body
:read(256)
278 while chunk
and chunk
~= "" do
280 chunk
, aborted
= body
:read(256)
291 function GeminiPlugin
:saveBody(body
, mimetype
, purl
)
292 self
:getSavePath(purl
, mimetype
, function(path
)
293 if not writeBodyToFile(body
, path
) then
294 -- clear up partial write
295 FileManager
:deleteFile(path
, true)
300 function GeminiPlugin
:openBody(body
, mimetype
, purl
, cert_info
, replace_history
)
301 util
.makePath(history_dir
)
303 local function get_ext(p
)
305 local ext
, m
= p
.path
:gsub(".*%.","",1)
312 if replace_history
then
315 local tn
= history_dir
.. "/Gemini " .. hn
316 local ext
= self
:mimeToExt(mimetype
) or get_ext(purl
)
318 tn
= tn
.. "." .. ext
321 if not DocumentRegistry
:hasProvider(tn
) then
322 UIManager
:show(ConfirmBox
:new
{
323 text
= T(_("Can't view file (%1). Save it instead?"), mimetype
),
324 ok_text
= _("Save file"),
325 cancel_text
= _("Cancel"),
326 ok_callback
= function()
327 self
:saveBody(body
, mimetype
, purl
)
333 if not replace_history
then
334 -- Delete history tail
335 local ok
, iter
, dir_obj
= pcall(lfs
.dir
, history_dir
)
340 for f
in iter
, dir_obj
do
341 local path
= history_dir
.."/"..f
342 local attr
= lfs
.attributes(path
) or {}
343 if attr
.mode
== "file" or attr
.mode
== "link" then
344 local n
= tonumber(f
:match("^Gemini (%d+)%f[^%d]"))
345 if n
and (n
>= #history
or n
<= #history
- max_cache_history_items
) then
346 FileManager
:deleteFile(path
, true)
347 local h
= history
[#history
- n
]
348 if h
and h
.path
== path
then
354 while table.remove(unhistory
) do end
357 if not writeBodyToFile(body
, tn
) then
361 local history_item
= { purl
= purl
, path
= tn
, mimetype
= mimetype
, cert_info
= cert_info
}
362 if replace_history
then
363 history
[1] = history_item
365 table.insert(history
, 1, history_item
)
371 function GeminiPlugin
:openCurrent()
372 if not history
[1].path
then
373 return self
:openUrl(history
[1].purl
, { replace_history
= true })
376 -- as in ReaderUI:switchDocument, but with seamless option
377 local function switchDocumentSeamlessly(new_file
)
378 -- Mimic onShowingReader's refresh optimizations
379 self
.ui
.tearing_down
= true
380 self
.ui
.dithered
= nil
382 self
.ui
:handleEvent(Event
:new("CloseReaderMenu"))
383 self
.ui
:handleEvent(Event
:new("CloseConfigMenu"))
384 self
.ui
.highlight
:onClose() -- close highlight dialog if any
385 self
.ui
:onClose(false)
387 self
.ui
:showReader(new_file
, nil, true)
390 --self.ui:switchDocument(history[1].path)
391 local open_msg
= InfoMessage
:new
{
392 text
= T(_("%1\nOpening..."), gemini
.showUrl(history
[1].purl
, true)),
394 UIManager
:show(open_msg
)
396 -- Tell new GeminiPlugin instance to consider itself active
399 if self
.ui
.name
== "ReaderUI" then
400 --self.ui:switchDocument(history[1].path)
401 switchDocumentSeamlessly(history
[1].path
)
403 local ReaderUI
= require("apps/reader/readerui")
404 ReaderUI
:showReader(history
[1].path
, nil, true)
406 UIManager
:close(open_msg
)
409 local function normaliseIdentUrl(u
)
410 local purl
= url
.parse(u
, {scheme
= "gemini", port
= "1965"})
411 if purl
== nil or purl
.scheme
~= "gemini" then
415 return url
.build(purl
)
418 function GeminiPlugin
:setIdentity(at_url
, identity
)
419 local n
= normaliseIdentUrl(at_url
)
421 active_identities
[n
] = identity
422 active_identities_persist
:save(active_identities
)
426 -- Return first identity at or above at_url, if any
427 function GeminiPlugin
:getIdentity(at_url
)
428 local n
= normaliseIdentUrl(at_url
)
433 for u
,id
in pairs(active_identities
) do
439 local up
= url
.absolute(at_url
, "./")
441 up
= url
.absolute(at_url
, "../")
444 return self
:getIdentity(up
)
448 function GeminiPlugin
:writeDefaultBookmarks()
449 if not util
.fileExists(marks_path
) then
450 local f
= io
.open(marks_path
, "w")
452 f
:write(require("staticgemtexts").default_bookmarks
)
458 function GeminiPlugin
:openUrl(article_url
, opts
)
459 if type(article_url
) ~= "string" then
460 article_url
= url
.build(article_url
)
463 local body_cb
= opts
.body_cb
or function(f
, mimetype
, p
, cert_info
)
464 self
:openBody(f
, mimetype
, p
, cert_info
, opts
.replace_history
)
466 local after_err_cb
= opts
.after_err_cb
or function() end
468 article_url
= url
.absolute(self
:purl(), article_url
)
471 local purl
= url
.parse(article_url
, {port
= "1965"})
473 if purl
and purl
.scheme
== "about" then
475 if purl
.path
== "bookmarks" then
476 self
:writeDefaultBookmarks()
477 G_reader_settings
:makeTrue("gemini_initiated")
478 body
= io
.open(marks_path
, "r")
480 body
= require("staticgemtexts")[purl
.path
]
483 body_cb(body
, "text/gemini", purl
)
485 UIManager
:show(InfoMessage
:new
{text
= T(_("Unknown \"about:\" URL: %1"), article_url
)})
490 if not purl
or not purl
.host
then
491 UIManager
:show(InfoMessage
:new
{text
= T(_("Invalid URL: %1"), article_url
)})
495 local proxy
= self
:getSchemeProxy(purl
.scheme
)
496 if purl
.scheme
~= "gemini" and not proxy
then
497 UIManager
:show(InfoMessage
:new
{text
= T(_("No proxy configured for scheme: %1"), purl
.scheme
)})
501 if NetworkMgr
:willRerunWhenConnected(function() self
:openUrl(article_url
, opts
) end) then
502 -- Not connected yet, nothing more to do here, NetworkMgr will forward the callback and run it once connected!
506 local id
= self
:getIdentity(article_url
)
507 local id_path
= id
and ids_dir
.."/"..id
508 local function success_cb(f
, p
, mimetype
, params
, cert_info
)
509 if opts
.repl_purl
then
510 repl_purl
= opts
.repl_purl
512 body_cb(f
, mimetype
, p
, cert_info
)
514 local function error_cb(msg
, p
, major
, minor
, server_msg
)
516 msg
= T(_("Server reports %1: %2"), msg
, server_msg
)
519 self
:promptInput(p
, server_msg
, minor
== "1")
520 elseif major
== "6" then
521 UIManager
:show(ConfirmBox
:new
{
523 ok_text
= _("Set identity"),
524 cancel_text
= _("Cancel"),
525 ok_callback
= function()
526 self
:confIdentAt(gemini
.showUrl(p
), function(new_id
)
528 self
:openUrl(p
, opts
)
534 UIManager
:show(InfoMessage
:new
{
536 dismiss_callback
= after_err_cb
,
540 local function check_trust_cb(host
, new_fp
, old_trusted_times
, old_expiry
, cb
)
541 if trust_overrides
[new_fp
] and os
.time() < trust_overrides
[new_fp
] then
544 self
:promptUnexpectedCert(host
, new_fp
, old_trusted_times
, old_expiry
, cb
)
547 local function trust_modified_cb()
548 trust_store_persist
:save(trust_store
)
550 local perm_redir_cb
= nil -- TODO: automatically edit bookmarks file?
551 local function info_cb(msg
, fast
)
552 return Trapper
:info(msg
, fast
)
555 Trapper
:wrap(function()
556 Trapper
:setPausedText(T(_("Abort connection?")))
557 gemini
.makeRequest(gemini
.showUrl(purl
),
558 id
and id_path
..".key",
559 id
and id_path
..".crt",
560 nil, -- disable CA-based verification
568 G_reader_settings
:isTrue("gemini_confirm_tofu"),
574 local function basicInputDialog(title
, cb
, input
, is_secret
)
576 input_dialog
= InputDialog
:new
{
579 text_type
= is_secret
and "password",
586 callback
= function()
587 UIManager
:close(input_dialog
)
600 -- Prompt user for input. May modify `purl.query`.
601 function GeminiPlugin
:promptInput(purl
, prompt
, is_secret
, repl
, initial
)
604 local multiline_button
605 local function submit()
606 local input
= input_dialog
:getInputText()
607 purl
.query
= url
.escape(input
)
608 if #url
.build(purl
) > 1024 then
609 UIManager
:show(InfoMessage
:new
{ text
=
610 T(_("Input too long (by %1 bytes)"), #url
.build(purl
) - 1024) })
612 UIManager
:close(input_dialog
)
614 repl_purl
= repl_button
.checked
and purl
,
615 after_err_cb
= function() self
:promptInput(purl
, prompt
, is_secret
, repl
, input
) end,
619 input_dialog
= basicInputDialog(prompt
, submit
, initial
, is_secret
)
621 -- read-eval-print-loop mode: keep presenting input dialog
622 repl_button
= CheckButton
:new
{
625 parent
= input_dialog
,
627 multiline_button
= CheckButton
:new
{
628 text
= _("Multiline input"),
630 callback
= function()
631 input_dialog
.allow_newline
= multiline_button
.checked
632 -- FIXME: less hacky way to do this?
633 if multiline_button
.checked
then
634 input_dialog
._input_widget
.enter_callback
= nil
636 input_dialog
._input_widget
.enter_callback
= submit
639 parent
= input_dialog
,
641 input_dialog
:addWidget(multiline_button
)
642 input_dialog
:addWidget(repl_button
)
646 -- Draw just above keyboard (in vertical mode),
647 -- so we can see as much as possible of the newly loaded page
648 y_offset
= Screen
:scaleBySize(120)
649 if G_reader_settings
:isTrue("keyboard_key_compact") then
650 y_offset
= y_offset
+ 50
654 UIManager
:show(input_dialog
, nil, nil, nil, y_offset
)
655 input_dialog
:onShowKeyboard()
659 function GeminiPlugin
:userPromptInput(purl
)
660 self
:promptInput(purl
, "[Input]", false, false, url
.unescape(purl
.query
or ""))
663 function GeminiPlugin
:promptUnexpectedCert(host
, new_fp
, old_trusted_times
, old_expiry
, cb
)
664 local widget
= MultiConfirmBox
:new
{
665 text
= old_trusted_times
> 0 and
667 The server identity presented by %1 does not match that previously trusted (%2 times).
668 Digest of received certificate: SHA256:%3
669 Previously trusted certificate expiry date: %4]]), host
, old_trusted_times
, new_fp
, old_expiry
) or
671 No trusted server identity known for %1. Trust provided server identity?
672 Digest of received certificate: SHA256:%2]]), host
, new_fp
),
673 face
= Font
:getFace("x_smallinfofont"),
674 choice1_text
= _("Trust new certificate"),
675 choice1_callback
= function()
678 choice2_text
= _("Connect without trust"),
679 choice2_callback
= function()
681 trust_overrides
[new_fp
] = os
.time() + 3600
684 cancel_callback
= function()
688 UIManager
:show(widget
)
691 function GeminiPlugin
:goBack(n
)
693 if n
> #history
-1 then
695 elseif n
< -#unhistory
then
702 table.insert(unhistory
, 1, table.remove(history
, 1))
706 table.insert(history
, 1, table.remove(unhistory
, 1))
713 function GeminiPlugin
:clearHistory()
714 local function delete_item(item
)
716 FileManager
:deleteFile(item
.path
, true)
720 while #history
> 1 do
721 delete_item(table.remove(history
, 2))
723 while #unhistory
> 0 do
724 delete_item(table.remove(unhistory
, 1))
728 function GeminiPlugin
:onTap(_
, ges
)
730 return self
:followGesLink(ges
)
734 function GeminiPlugin
:onDoubleTap(_
, ges
)
736 return self
:followGesLink(ges
, true)
740 function GeminiPlugin
:onHoldPan(_
, ges
)
744 function GeminiPlugin
:onHoldRelease(_
, ges
)
745 if self
.active
and not self
.hold_pan
then
746 if self
:followGesLink(ges
, true) then
747 if self
.ui
.highlight
then
748 self
.ui
.highlight
:clear()
753 self
.hold_pan
= false
756 function GeminiPlugin
:onSwipe(_
, ges
)
758 local direction
= BD
.flipDirectionIfMirroredUILayout(ges
.direction
)
759 if direction
== "south" then
761 elseif direction
== "north" then
768 function GeminiPlugin
:followGesLink(ges
, nav
)
769 local link
= self
.ui
.link
:getLinkFromGes(ges
)
770 if link
and link
.xpointer
then
771 local scheme
= link
.xpointer
:match("^(%w[%w+%-.]*):") or ""
772 if scheme
== "gemini" or scheme
== "about"
773 or self
:getSchemeProxy(scheme
)
774 or (scheme
== "" and self
.active
) then
776 self
:showNav(link
.xpointer
)
778 self
:openUrl(link
.xpointer
)
785 function GeminiPlugin
:onFollowGeminiLink(u
)
786 return self
:showNav(u
)
789 function GeminiPlugin
:onEndOfBook()
790 -- TODO: seems we can't override the usual reader onEndOfBook handling.
791 -- Leaving this as a hidden option for now.
792 if G_reader_settings
:isTrue("gemini_next_on_end") then
793 if self
.active
and #queue
> 0 then
800 function GeminiPlugin
:queueLinksInSelected(selected
)
801 local html
= self
.ui
.document
:getHTMLFromXPointers(selected
.pos0
, selected
.pos1
, nil, true)
803 -- Following pattern isn't strictly correct in general,
804 -- but is for the html generated from a gemini document.
806 for u
in html
:gmatch('<a[^>]*href="([^"]*)"') do
810 UIManager
:show(InfoMessage
:new
{ text
=
811 n
== 0 and _("No links found in selected text.") or
812 T(_("Added %1 links to queue."), n
)
817 function GeminiPlugin
:queueBody(body
, u
, mimetype
, cert_info
, existing_item
, prepend
)
818 util
.makePath(queue_dir
)
819 local path
= queue_dir
.."/"..sha256(u
)
820 if writeBodyToFile(body
, path
) then
821 if existing_item
then
822 existing_item
.path
= path
823 existing_item
.mimetype
= mimetype
824 existing_item
.cert_info
= cert_info
826 self
:queueItem({ url
= u
, path
= path
, mimetype
= mimetype
, cert_info
= cert_info
}, prepend
)
828 elseif not existing_item
then
829 self
:queueItem({ url
= u
}, prepend
)
833 function GeminiPlugin
:queueCachedHistoryItem(h
, prepend
)
834 local body
= io
.open(h
.path
, "r")
836 self
:queueBody(body
, gemini
.showUrl(h
.purl
), h
.mimetype
, h
.cert_info
, nil, prepend
)
840 function GeminiPlugin
:fetchLink(u
, item
, prepend
)
841 self
:openUrl(u
, { body_cb
= function(body
, mimetype
, purl
, cert_info
)
842 self
:queueBody(body
, gemini
.showUrl(purl
), mimetype
, cert_info
, item
, prepend
)
846 function GeminiPlugin
:fetchQueue()
847 for _n
,item
in ipairs(queue
) do
848 if not item
.path
then
849 self
:fetchLink(item
.url
, item
)
854 function GeminiPlugin
:queueLink(u
, prepend
)
855 local purl
= url
.parse(u
)
856 if purl
and purl
.scheme
~= "about" and
857 not G_reader_settings
:isTrue("gemini_no_fetch_on_add") and NetworkMgr
:isConnected() then
858 self
:fetchLink(u
, nil, prepend
)
860 self
:queueItem({ url
= u
}, prepend
)
864 function GeminiPlugin
:queueItem(item
, prepend
)
865 for k
= #queue
,1,-1 do
866 if queue
[k
].url
== item
.url
then
867 table.remove(queue
,k
)
871 table.insert(queue
, 1, item
)
873 table.insert(queue
, item
)
875 queue_persist
:save(queue
)
878 function GeminiPlugin
:openQueueItem(n
)
880 local item
= queue
[n
]
883 local f
= io
.open(item
.path
, "r")
885 UIManager
:show(InfoMessage
:new
{text
= T(_("Failed to open %1 for reading."), item
.path
)})
887 self
:openBody(f
, item
.mimetype
, url
.parse(item
.url
), item
.cert_info
)
888 FileManager
:deleteFile(item
.path
, true)
891 elseif item
.url
:match("^about:") or NetworkMgr
:isConnected() then
892 self
:openUrl(item
.url
)
895 UIManager
:show(InfoMessage
:new
{text
= T(_("Need network connection to fetch %1"), item
.url
)})
900 function GeminiPlugin
:popQueue(n
)
902 local item
= table.remove(queue
, n
)
903 queue_persist
:save(queue
)
907 function GeminiPlugin
:clearQueue()
909 local item
= table.remove(queue
, 1)
911 FileManager
:deleteFile(item
.path
, true)
914 queue_persist
:save(queue
)
917 function GeminiPlugin
:getSavePath(purl
, mimetype
, cb
)
919 local add_ext
= false
921 basename
= purl
.path
:gsub("/+$","",1):gsub(".*/","",1)
922 if basename
== "" and purl
.host
then
926 if add_ext
or not basename
:match(".+%..+") then
927 local ext
= self
:mimeToExt(mimetype
)
929 basename
= basename
.."."..ext
936 local function do_save()
937 local fields
= widget
:getFields()
938 local dir
= fields
[2]
941 local path
= dir
.."/"..bn
942 local tp
= lfs
.attributes(path
, "mode")
943 if tp
== "directory" then
944 UIManager
:show(InfoMessage
:new
{text
= _("Path is a directory")})
945 elseif tp
~= nil then
946 UIManager
:show(ConfirmBox
:new
{
947 text
= _("File exists. Overwrite?"),
948 ok_text
= _("Overwrite"),
949 cancel_text
= _("Cancel"),
950 ok_callback
= function()
951 UIManager
:close(widget
)
956 UIManager
:close(widget
)
963 widget
= MultiInputDialog
:new
{
964 title
= _("Save as"),
967 description
= _("Filename"),
971 description
= _("Directory to save under"),
980 callback
= function()
981 UIManager
:close(widget
)
986 is_enter_default
= true,
992 widget
.input_field
[1].enter_callback
= do_save
993 widget
.input_field
[2].enter_callback
= do_save
994 UIManager
:show(widget
)
995 widget
:onShowKeyboard()
998 function GeminiPlugin
:saveCurrent()
999 self
:getSavePath(history
[1].purl
, history
[1].mimetype
, function(path
)
1000 ffiutil
.copyFile(history
[1].path
, path
)
1001 self
.ui
:saveSettings()
1002 if DocSettings
.updateLocation
then
1003 DocSettings
.updateLocation(history
[1].path
, path
, true)
1008 function GeminiPlugin
:confIdentAt(uri
, cb
)
1009 local n
= normaliseIdentUrl(uri
)
1015 local id
= self
:getIdentity(n
)
1016 local function set_id(new_id
)
1018 self
:setIdentity(n
, new_id
)
1020 UIManager
:close(widget
)
1028 widget
= ButtonDialogTitle
:new
{
1029 title
= T(_("Identity at %1"), gemini
.showUrl(n
)),
1033 text
= id
and T(_("Stop using identity %1"), id
) or _("No identity in use"),
1034 enabled
= id
~= nil,
1035 callback
= function()
1038 local c_id
, at_url
= self
:getIdentity(n
)
1040 self
:setIdentity(at_url
, nil)
1045 UIManager
:close(widget
)
1056 callback
= function()
1057 UIManager
:close(widget
)
1061 text
= _("Choose identity"),
1062 callback
= function()
1063 self
:chooseIdentity(set_id
)
1067 text
= _("Create identity"),
1068 enabled
= os
.execute("openssl version >& /dev/null"),
1069 callback
= function()
1070 self
:createIdentInteractive(set_id
)
1076 UIManager
:show(widget
)
1079 function GeminiPlugin
:getIds()
1081 util
.findFiles(ids_dir
, function(path
,crt
)
1082 if crt
:find("%.crt$") then
1083 table.insert(ids
, crt
:sub(0,-5))
1090 function GeminiPlugin
:chooseIdentity(callback
)
1091 local ids
= self
:getIds()
1095 for _i
,id
in ipairs(ids
) do
1099 callback
= function()
1101 UIManager
:close(widget
)
1106 title
= _("Choose identity"),
1108 width
= Screen
:getWidth(), -- backwards compatibility;
1109 height
= Screen
:getHeight(), -- can delete for KOReader >= 2023.04
1111 UIManager
:show(widget
)
1114 function GeminiPlugin
:createIdentInteractive(callback
)
1117 local function createId(id
, common_name
, rsa
)
1118 local path
= ids_dir
.."/"..id
1119 local shell_quoted_name
= common_name
:gsub("'","'\\''")
1120 local subj
= shell_quoted_name
== "" and "/" or "/CN="..shell_quoted_name
1122 os
.execute("openssl ecparam -genkey -name prime256v1 > "..path
..".key")
1123 os
.execute("openssl req -x509 -new -key "..path
..".key -sha256 -out "..path
..".crt -days 2000000 -subj '"..subj
.."'")
1125 os
.execute("openssl req -x509 -newkey rsa:2048 -keyout "..path
..".key -sha256 -out "..path
..".crt -days 2000000 -nodes -subj '"..subj
.."'")
1127 UIManager
:close(widget
)
1130 local function create_cb()
1131 local fields
= widget
:getFields()
1132 if fields
[1] == "" then
1133 UIManager
:show(InfoMessage
:new
{text
= _("Enter a petname for this identity, to be used in this client to refer to the identity.")})
1134 elseif not fields
[1]:match("^[%w_%-]+$") then
1135 UIManager
:show(InfoMessage
:new
{text
= _("Punctuation not allowed in petname.")})
1136 elseif fields
[1]:len() > 12 then
1137 UIManager
:show(InfoMessage
:new
{text
= _("Petname too long.")})
1138 elseif util
.fileExists(ids_dir
.."/"..fields
[1]..".crt") then
1139 UIManager
:show(ConfirmBox
:new
{
1140 text
= _("Identity already exists. Overwrite?"),
1141 ok_text
= _("Destroy existing identity"),
1142 cancel_text
= _("Cancel"),
1143 ok_callback
= function()
1144 createId(fields
[1], fields
[2], rsa_button
.checked
)
1148 createId(fields
[1], fields
[2])
1151 widget
= MultiInputDialog
:new
{
1152 title
= _("Create identity"),
1155 description
= _("Identity petname"),
1158 description
= _("Name (optional, sent to server)"),
1166 callback
= function()
1167 UIManager
:close(widget
)
1172 callback
= function()
1179 widget
.input_field
[1].enter_callback
= create_cb
1180 widget
.input_field
[2].enter_callback
= create_cb
1182 -- FIXME: Seems checkbuttons added to MultiInputDialog don't appear...
1183 rsa_button
= CheckButton
:new
{
1184 text
= _("Use RSA instead of ECDSA"),
1188 widget
:addWidget(rsa_button
)
1190 UIManager
:show(widget
)
1191 widget
:onShowKeyboard()
1194 function GeminiPlugin
:addMark(u
, desc
)
1195 if url
.parse(u
) then
1196 self
:writeDefaultBookmarks()
1197 local line
= "=> " .. u
1198 if desc
and desc
~= "" then
1199 line
= line
.. " " .. desc
1202 local f
= io
.open(marks_path
, "a")
1211 function GeminiPlugin
:addMarkInteractive(uri
)
1213 local function add_mark()
1214 local fields
= widget
:getFields()
1215 if self
:addMark(fields
[2], fields
[1]) then
1216 UIManager
:close(widget
)
1219 widget
= MultiInputDialog
:new
{
1220 title
= _("Add bookmark"),
1223 description
= _("Description (optional)"),
1226 description
= _("URL"),
1227 text
= gemini
.showUrl(uri
),
1235 callback
= function()
1236 UIManager
:close(widget
)
1241 is_enter_default
= true,
1242 callback
= add_mark
,
1247 widget
.input_field
[1].enter_callback
= add_mark
1248 widget
.input_field
[2].enter_callback
= add_mark
1249 UIManager
:show(widget
)
1250 widget
:onShowKeyboard()
1253 function GeminiPlugin
:showHistoryMenu(cb
)
1254 cb
= cb
or function(n
) self
:goBack(n
) end
1256 local history_items
= {}
1257 local function show_history_item(h
)
1258 return gemini
.showUrl(h
.purl
) ..
1259 (h
.path
and " " .. _("(fetched)") or "")
1261 for n
,h
in ipairs(history
) do
1262 table.insert(history_items
, {
1263 text
= T("%1 %2", n
-1, show_history_item(h
)),
1264 callback
= function()
1266 UIManager
:close(menu
)
1268 hold_callback
= function()
1269 UIManager
:close(menu
)
1270 self
:showNav(h
.purl
)
1274 for n
,h
in ipairs(unhistory
) do
1275 table.insert(history_items
, 1, {
1276 text
= T("%1 %2", -n
, show_history_item(h
)),
1277 callback
= function()
1279 UIManager
:close(menu
)
1283 if #history_items
> 1 then
1284 table.insert(history_items
, {
1285 text
= _("Clear all history"),
1286 callback
= function()
1287 UIManager
:show(ConfirmBox
:new
{
1288 text
= T(_("Clear %1 history items?"), #history_items
-1),
1289 ok_text
= _("Clear history"),
1290 cancel_text
= _("Cancel"),
1291 ok_callback
= function()
1293 UIManager
:close(menu
)
1300 title
= _("History"),
1301 item_table
= history_items
,
1302 onMenuHold
= function(_
, item
)
1303 if item
.hold_callback
then
1304 item
.hold_callback()
1307 width
= Screen
:getWidth(), -- backwards compatibility;
1308 height
= Screen
:getHeight(), -- can delete for KOReader >= 2023.04
1310 UIManager
:show(menu
)
1313 function GeminiPlugin
:getSchemeProxy(scheme
)
1314 local proxy
= scheme_proxies
[scheme
]
1315 if proxy
and proxy
.host
then
1318 if scheme
== "http" or scheme
== "https" then
1319 return self
:getSchemeProxy("http(s)")
1323 function GeminiPlugin
:schemeProxyMenu()
1326 for scheme
,proxy
in pairs(scheme_proxies
) do
1327 table.insert(kv_pairs
, { scheme
, proxy
.host
or "", callback
= function()
1329 input_dialog
= basicInputDialog(
1330 T(_("Set proxy server for %1 URLs"), scheme
),
1332 local host
= input_dialog
:getInputText()
1336 scheme_proxies
[scheme
].host
= host
1337 scheme_proxies_persist
:save(scheme_proxies
)
1338 UIManager
:close(input_dialog
)
1339 UIManager
:close(menu
)
1340 self
:schemeProxyMenu()
1343 UIManager
:show(input_dialog
)
1344 input_dialog
:onShowKeyboard()
1347 table.insert(kv_pairs
, { _("[New]"), _("Select to add new scheme"), callback
= function()
1349 input_dialog
= basicInputDialog(_("Add new scheme to proxy"), function()
1350 local scheme
= input_dialog
:getInputText()
1351 UIManager
:close(input_dialog
)
1353 scheme_proxies
[scheme
] = {}
1354 UIManager
:close(menu
)
1355 self
:schemeProxyMenu()
1358 UIManager
:show(input_dialog
)
1359 input_dialog
:onShowKeyboard()
1361 menu
= KeyValuePage
:new
{
1362 title
= _("Scheme proxies"),
1363 kv_pairs
= kv_pairs
,
1365 UIManager
:show(menu
)
1369 function GeminiPlugin
:viewCurrentAsText()
1370 local h
= history
[1]
1371 local f
= io
.open(h
.path
,"r")
1372 UIManager
:show(TextViewer
:new
{
1373 title
= gemini
.showUrl(h
.purl
),
1374 text
= f
and f
:read("a") or "[Error reading file]"
1379 function GeminiPlugin
:showCurrentInfo()
1380 local h
= history
[1]
1382 { _("URL"), gemini
.showUrl(h
.purl
) },
1383 { _("Mimetype"), h
.mimetype
}
1387 table.insert(kv_pairs
, "----")
1388 local cert_info
= history
[1].cert_info
1389 if cert_info
.ca
then
1390 table.insert(kv_pairs
, { _("Trust type"), _("Chain to Certificate Authority") })
1391 for k
, v
in ipairs(cert_info
.ca
) do
1392 table.insert(kv_pairs
, { v
.name
, v
.value
})
1395 if cert_info
.trusted_times
> 0 then
1396 table.insert(kv_pairs
, { _("Trust type"), _("Trust On First Use"), callback
= function()
1397 UIManager
:close(widget
)
1398 self
:openUrl("about:tofu")
1400 table.insert(kv_pairs
, { _("SHA256 digest"), cert_info
.fp
})
1401 table.insert(kv_pairs
, { _("Times seen"), cert_info
.trusted_times
})
1403 table.insert(kv_pairs
, { _("Trust type"), _("Temporarily accepted") })
1404 table.insert(kv_pairs
, { _("SHA256 digest"), cert_info
.fp
})
1406 table.insert(kv_pairs
, { _("Expiry date"), cert_info
.expiry
})
1409 table.insert(kv_pairs
, "----")
1410 table.insert(kv_pairs
, { "Source", _("Select to view page as text"), callback
= function()
1411 self
:viewCurrentAsText()
1413 widget
= KeyValuePage
:new
{
1414 title
= _("Page info"),
1415 kv_pairs
= kv_pairs
,
1417 UIManager
:show(widget
)
1420 function GeminiPlugin
:editQueue()
1423 local function show_queue_item(item
)
1424 return gemini
.showUrl(item
.url
) ..
1425 (item
.path
and " " .. _("(fetched)") or "")
1428 for n
,item
in ipairs(queue
) do
1429 table.insert(items
, {
1430 text
= n
.. " " .. show_queue_item(item
),
1431 callback
= function()
1432 UIManager
:close(menu
)
1433 self
:openQueueItem(n
)
1435 hold_callback
= function()
1436 UIManager
:close(menu
)
1437 self
:showNav(item
.url
)
1440 if not item
.path
then
1441 unfetched
= unfetched
+ 1
1444 if unfetched
> 0 then
1445 table.insert(items
, {
1446 text
= T(_("Fetch %1 unfetched items"), unfetched
),
1447 callback
= function()
1449 UIManager
:close(menu
)
1455 table.insert(items
, {
1456 text
= _("Clear queue"),
1457 callback
= function()
1458 UIManager
:show(ConfirmBox
:new
{
1459 text
= T(_("Clear %1 items from queue?"), #queue
),
1460 ok_text
= _("Clear queue"),
1461 cancel_text
= _("Cancel"),
1462 ok_callback
= function()
1464 UIManager
:close(menu
)
1473 onMenuHold
= function(_
, item
)
1474 if item
.hold_callback
then
1475 item
.hold_callback()
1478 width
= Screen
:getWidth(), -- backwards compatibility;
1479 height
= Screen
:getHeight(), -- can delete for KOReader >= 2023.04
1481 UIManager
:show(menu
)
1484 function GeminiPlugin
:editActiveIdentities()
1487 for u
,id
in pairs(active_identities
) do
1488 local show_u
= gemini
.showUrl(u
)
1489 table.insert(items
, {
1490 text
= T("%1: %2", id
, show_u
),
1491 callback
= function()
1492 UIManager
:show(ConfirmBox
:new
{
1493 text
= T(_("Stop using identity %1 at %2?"), id
, show_u
),
1494 ok_text
= _("Stop"),
1495 cancel_text
= _("Cancel"),
1496 ok_callback
= function()
1497 UIManager
:close(menu
)
1498 self
:setIdentity(u
, nil)
1499 self
:editActiveIdentities()
1505 table.sort(items
, function(i1
,i2
) return i1
.text
< i2
.text
end)
1507 title
= _("Active identities"),
1509 width
= Screen
:getWidth(), -- backwards compatibility;
1510 height
= Screen
:getHeight(), -- can delete for KOReader >= 2023.04
1512 UIManager
:show(menu
)
1515 function GeminiPlugin
:showNav(uri
, showKbd
)
1516 if uri
and type(uri
) ~= "string" then
1517 uri
= url
.build(uri
)
1519 showKbd
= showKbd
or not uri
or uri
== ""
1521 uri
= gemini
.showUrl(self
:purl())
1522 elseif self
:purl() and uri
~= "" then
1523 uri
= url
.absolute(self
:purl(), uri
)
1527 local advanced
= false
1528 local function current_nav_url()
1529 local u
= nav
:getInputText()
1530 if u
:match("^[./?]") then
1531 -- explicitly relative url
1533 u
= url
.absolute(self
:purl(), u
)
1536 -- absolutise if necessary
1537 local purl
= url
.parse(u
)
1538 if purl
and purl
.scheme
== nil and purl
.host
== nil then
1539 u
= "gemini://" .. u
1544 local function current_input_nonempty()
1545 local purl
= url
.parse(current_nav_url())
1546 return purl
and (purl
.host
or purl
.path
)
1548 local function close_nav_keyboard()
1549 if nav
.onCloseKeyboard
then
1550 nav
:onCloseKeyboard()
1551 elseif Version
:getNormalizedCurrentVersion() < 202309010000 then
1552 -- backwards compatibility
1553 if nav
._input_widget
.onCloseKeyboard
then
1554 nav
._input_widget
:onCloseKeyboard()
1558 local function show_hist()
1559 close_nav_keyboard()
1560 self
:showHistoryMenu(function(n
)
1561 UIManager
:close(nav
)
1565 local function queue_nav_url(prepend
)
1566 if current_input_nonempty() then
1567 local u
= current_nav_url()
1568 if u
== gemini
.showUrl(self
:purl()) and history
[1].path
then
1569 self
:queueCachedHistoryItem(history
[1], prepend
)
1571 self
:queueLink(u
, prepend
)
1573 UIManager
:close(nav
)
1576 local function update_buttons()
1577 local u
= current_nav_url()
1578 local purl
= url
.parse(u
)
1579 local id
= self
:getIdentity(u
)
1580 local text
= T(_("Identity: %1"), id
or _("[none]"))
1581 local id_button
= nav
.button_table
:getButtonById("ident")
1582 if not advanced
then
1583 id_button
:setText(text
, id_button
.width
)
1585 id_button
:enableDisable(advanced
or (purl
and purl
.scheme
== "gemini" and purl
.host
~= ""))
1586 UIManager
:setDirty(id_button
, "ui")
1588 local save_button
= nav
.button_table
:getButtonById("save")
1589 save_button
:enableDisable(purl
and purl
.scheme
and purl
.scheme
~= "about")
1590 UIManager
:setDirty(save_button
, "ui")
1592 local info_button
= nav
.button_table
:getButtonById("info")
1593 info_button
:enableDisable(u
== gemini
.showUrl(self
:purl()))
1594 UIManager
:setDirty(info_button
, "ui")
1596 local function toggle_advanced()
1597 advanced
= not advanced
1598 for _
,row
in ipairs(nav
.button_table
.buttons_layout
) do
1599 for _
,button
in ipairs(row
) do
1600 if button
.text_func
and button
.hold_callback
then
1601 button
:setText(button
.text_func(), button
.width
)
1602 button
.callback
, button
.hold_callback
= button
.hold_callback
, button
.callback
1607 UIManager
:setDirty(nav
, "ui")
1610 nav
= InputDialog
:new
{
1611 title
= _("Gemini navigation"),
1612 width
= Screen
:scaleBySize(550), -- in pixels
1613 input_type
= "text",
1614 input
= uri
and gemini
.showUrl(uri
) or "gemini://",
1618 text_func
= function() return advanced
and _("Edit identity URLs") or _("Identity") end,
1620 callback
= function()
1621 close_nav_keyboard()
1622 self
:confIdentAt(current_nav_url(), function()
1626 hold_callback
= function()
1627 close_nav_keyboard()
1628 self
:editActiveIdentities()
1632 text_func
= function() return advanced
and _("View as text") or _("Page info") end,
1634 callback
= function()
1635 UIManager
:close(nav
)
1636 self
:showCurrentInfo()
1638 hold_callback
= function()
1639 UIManager
:close(nav
)
1640 self
:viewCurrentAsText()
1646 text_func
= function() return advanced
and _("History") or _("Back") end,
1647 enabled
= #history
> 1,
1648 callback
= function()
1649 UIManager
:close(nav
)
1652 hold_callback
= show_hist
,
1655 text_func
= function() return advanced
and _("History") or _("Unback") end,
1656 enabled
= #unhistory
> 0,
1657 callback
= function()
1658 UIManager
:close(nav
)
1661 hold_callback
= show_hist
,
1664 text_func
= function() return advanced
and _("Edit queue") or _("Next") end,
1665 enabled
= #queue
> 0,
1666 callback
= function()
1667 UIManager
:close(nav
)
1668 self
:openQueueItem()
1670 hold_callback
= function()
1671 UIManager
:close(nav
)
1676 text_func
= function() return advanced
and _("Edit marks") or _("Bookmarks") end,
1677 callback
= function()
1678 UIManager
:close(nav
)
1679 self
:openUrl("about:bookmarks")
1681 hold_callback
= function()
1682 if self
.ui
.texteditor
and self
.ui
.texteditor
.quickEditFile
then
1683 UIManager
:close(nav
)
1684 self
:writeDefaultBookmarks()
1685 local function done_cb()
1686 if self
:purl() and url
.build(self
:purl()) == "about:bookmarks" then
1687 self
:openUrl("about:bookmarks", { replace_history
= true })
1690 self
.ui
.texteditor
:quickEditFile(marks_path
, done_cb
, true)
1692 UIManager
:show(InfoMessage
:new
{text
= T(_([[
1693 Can't load TextEditor: Plugin disabled or incompatible.
1694 To edit bookmarks, please edit the file %1 in the koreader directory manually.
1702 text_func
= function() return advanced
and _("Root") or _("Up") end,
1703 callback
= function()
1704 local u
= current_nav_url()
1705 local up
= url
.absolute(u
, "./")
1707 up
= url
.absolute(u
, "../")
1709 nav
:setInputText(up
)
1712 hold_callback
= function()
1713 nav
:setInputText(url
.absolute(current_nav_url(), "/"))
1720 callback
= function()
1721 local u
= current_nav_url()
1722 local purl
= url
.parse(u
)
1723 if purl
and purl
.scheme
and purl
.scheme
== "about" then
1724 UIManager
:show(InfoMessage
:new
{text
= _("Can't save about: pages")})
1725 elseif u
== gemini
.showUrl(self
:purl()) then
1726 UIManager
:close(nav
)
1729 UIManager
:close(nav
)
1730 self
:openUrl(u
, { body_cb
= function(f
, mimetype
, p2
)
1731 self
:saveBody(f
, mimetype
, p2
)
1737 text_func
= function() return advanced
and _("Prepend") or _("Add") end,
1738 callback
= queue_nav_url
,
1739 hold_callback
= function() queue_nav_url(true) end,
1742 text_func
= function() return advanced
and _("Quick mark") or _("Mark") end,
1743 callback
= function()
1744 if current_input_nonempty() then
1745 self
:addMarkInteractive(current_nav_url())
1746 UIManager
:close(nav
)
1749 hold_callback
= function()
1750 if current_input_nonempty()
1751 and self
:addMark(current_nav_url()) then
1752 UIManager
:close(nav
)
1761 callback
= function()
1762 UIManager
:close(nav
)
1766 text_func
= function() return advanced
and _("Input") or _("Go") end,
1767 is_enter_default
= true,
1768 callback
= function()
1769 UIManager
:close(nav
)
1770 local u
= current_nav_url()
1771 self
:openUrl(u
, { after_err_cb
= function() self
:showNav(u
, true) end})
1773 hold_callback
= function()
1774 local purl
= url
.parse(current_nav_url())
1776 self
:userPromptInput(purl
)
1777 UIManager
:close(nav
)
1785 -- FIXME: less hacky way to do this?
1786 nav
._input_widget
.edit_callback
= function(edited
)
1792 nav
.title_bar
.right_icon
= "appbar.settings"
1793 nav
.title_bar
.right_icon_tap_callback
= toggle_advanced
1794 nav
.title_bar
:init()
1798 nav
:onShowKeyboard()
1802 function GeminiPlugin
:onDispatcherRegisterActions()
1803 Dispatcher
:registerAction("browse_gemini", {category
= "none", event
= "BrowseGemini", title
= _("Browse Gemini"), general
= true, separator
= true })
1804 Dispatcher
:registerAction("gemini_back", {category
= "none", event
= "GeminiBack", title
= _("Gemini: Back"), reader
= true })
1805 Dispatcher
:registerAction("gemini_unback", {category
= "none", event
= "GeminiUnback", title
= _("Gemini: Unback"), reader
= true })
1806 Dispatcher
:registerAction("gemini_history", {category
= "none", event
= "GeminiHistory", title
= _("Gemini: History"), reader
= true })
1807 Dispatcher
:registerAction("gemini_bookmarks", {category
= "none", event
= "GeminiBookmarks", title
= _("Gemini: Bookmarks"), reader
= true })
1808 Dispatcher
:registerAction("gemini_mark", {category
= "none", event
= "GeminiMark", title
= _("Gemini: Mark"), reader
= true })
1809 Dispatcher
:registerAction("gemini_next", {category
= "none", event
= "GeminiNext", title
= _("Gemini: Next"), reader
= true })
1810 Dispatcher
:registerAction("gemini_add", {category
= "none", event
= "GeminiAdd", title
= _("Gemini: Add"), reader
= true })
1811 Dispatcher
:registerAction("gemini_nav", {category
= "none", event
= "GeminiNav", title
= _("Gemini: Open nav"), reader
= true })
1812 Dispatcher
:registerAction("gemini_input", {category
= "none", event
= "GeminiInput", title
= _("Gemini: Input"), reader
= true })
1813 Dispatcher
:registerAction("gemini_reload", {category
= "none", event
= "GeminiReload", title
= _("Gemini: Reload"), reader
= true })
1814 Dispatcher
:registerAction("gemini_up", {category
= "none", event
= "GeminiUp", title
= _("Gemini: Up"), reader
= true })
1815 Dispatcher
:registerAction("gemini_goNew", {category
= "none", event
= "GeminiGoNew", title
= _("Gemini: Enter URL"), reader
= true, separator
= true })
1818 function GeminiPlugin
:onBrowseGemini()
1821 elseif #history
> 0 then
1823 elseif G_reader_settings
:nilOrFalse("gemini_initiated") then
1824 self
:openUrl("about:welcome")
1826 self
:openUrl("about:bookmarks")
1831 function GeminiPlugin
:onGeminiBack()
1837 function GeminiPlugin
:onGeminiUnback()
1843 function GeminiPlugin
:onGeminiHistory()
1845 self
:showHistoryMenu()
1849 function GeminiPlugin
:onGeminiBookmarks()
1850 self
:openUrl("about:bookmarks")
1853 function GeminiPlugin
:onGeminiMark()
1855 self
:addMarkInteractive(gemini
.showUrl(self
:purl()))
1859 function GeminiPlugin
:onGeminiNext()
1860 self
:openQueueItem()
1863 function GeminiPlugin
:onGeminiAdd()
1865 self
:queueCachedHistoryItem(history
[1])
1869 function GeminiPlugin
:onGeminiInput()
1871 self
:userPromptInput(history
[1].purl
)
1875 function GeminiPlugin
:onGeminiReload()
1877 self
:openUrl(history
[1].purl
, { replace_history
= true })
1881 function GeminiPlugin
:onGeminiUp()
1883 local u
= gemini
.showUrl(self
:purl())
1884 local up
= url
.absolute(u
, "./")
1886 up
= url
.absolute(u
, "../")
1894 function GeminiPlugin
:onGeminiGoNew()
1898 function GeminiPlugin
:onGeminiNav()
1903 function GeminiPlugin
:addToMainMenu(menu_items
)
1904 menu_items
.gemini
= {
1905 sorting_hint
= "search",
1906 text
= _("Browse Gemini"),
1907 callback
= function()
1908 self
:onBrowseGemini()
1911 local hint
= "search_settings"
1912 if Version
:getNormalizedCurrentVersion() < 202305180000 then
1913 -- backwards compatibility
1916 menu_items
.gemini_settings
= {
1917 text
= _("Gemini settings"),
1918 sorting_hint
= hint
,
1921 text
= _("Show help"),
1922 callback
= function()
1923 self
:openUrl("about:help")
1927 text
= T(_("Max cached history items: %1"), max_cache_history_items
),
1928 help_text
= _("History items up to this limit will be stored on the filesystem and can be accessed offline with Back."),
1929 keep_menu_open
= true,
1930 callback
= function(touchmenu_instance
)
1931 local widget
= SpinWidget
:new
{
1932 title_text
= _("Max cached history items"),
1933 value
= max_cache_history_items
,
1936 default_value
= default_max_cache_history_items
,
1937 callback
= function(spin
)
1938 max_cache_history_items
= spin
.value
1939 G_reader_settings
:saveSetting("gemini_max_cache_history_items", spin
.value
)
1940 touchmenu_instance
:updateItems()
1943 UIManager
:show(widget
)
1947 text
= _("Set directory for saved documents"),
1948 keep_menu_open
= true,
1949 callback
= function()
1950 local title_header
= _("Current directory for saved gemini documents:")
1951 local current_path
= save_dir
1952 local default_path
= getDefaultSavesDir()
1953 local function caller_callback(path
)
1955 G_reader_settings
:saveSetting("gemini_save_dir", path
)
1956 if not util
.pathExists(path
) then
1960 filemanagerutil
.showChooseDialog(title_header
, caller_callback
, current_path
, default_path
)
1964 text
= _("Configure scheme proxies"),
1965 help_text
= _("Configure proxy servers to use for non-gemini URL schemes."),
1966 callback
= function()
1967 self
:schemeProxyMenu()
1971 text
= _("Disable fetch on add"),
1972 help_text
= _("Disables immediately fetching URLs added to the queue when connected."),
1973 checked_func
= function()
1974 return G_reader_settings
:isTrue("gemini_no_fetch_on_add")
1976 callback
= function()
1977 G_reader_settings
:flipNilOrFalse("gemini_no_fetch_on_add")
1981 text
= _("Confirm certificates for new hosts"),
1982 help_text
= _("Overrides the default behaviour of silently trusting the first server identity seen for a host, allowing you to confirm the certificate hash out-of-band."),
1983 checked_func
= function()
1984 return G_reader_settings
:isTrue("gemini_confirm_tofu")
1986 callback
= function()
1987 G_reader_settings
:flipNilOrFalse("gemini_confirm_tofu")