Recently Written · git

subread.koplugin

KOReader plugin: the book follows the narration of an audiobook, from an .srt made by subread.space

git clone https://github.com/equwal/subread.koplugin

Log | Files | Refs


commit ded68097d271952a8813f842cdfec90c5e3c5bb8
equwal <13551856+equwal@users.noreply.github.com>
2026-09-26 12:58:07 -0700

Add a More apps menu entry with the addresses of the other apps

The SubRead menu has a new last entry, "More apps". For each app, a
TextViewer shows the name, one line about the app and the address.
KOReader cannot open a web browser on each device, so the addresses are
text. The list is in subread/more_apps.lua, and a new spec tests it. The
README has a More projects list.

 README.md               | 24 ++++++++++++++++++++
 main.lua                | 18 +++++++++++++++
 spec/more_apps_spec.lua | 33 ++++++++++++++++++++++++++++
 spec/run.lua            |  2 +-
 subread/more_apps.lua   | 58 +++++++++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 134 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index a97a19a..b8ef084 100644
--- a/README.md
+++ b/README.md
@@ -166,6 +166,30 @@ Without a Lua interpreter with busted:
 lua spec/run.lua
 ```
 
+## More projects
+
+- [SubRead](https://subread.space/): read along with an audiobook, in the browser.
+  Also [for Android](https://github.com/equwal/subread-android/releases/latest),
+  [for YouTube](https://github.com/equwal/subread-extension/releases/latest)
+  and [for KOReader](https://github.com/equwal/subread.koplugin).
+- [SubRead Overlay](https://github.com/equwal/subread-overlay/releases/latest): subtitle lines over any Android media player.
+- [SubRead Dictionary](https://github.com/equwal/subread-dictionary/releases/latest): a pop-up dictionary for Android that reads Yomitan dictionaries.
+- [SubRead Anki](https://github.com/equwal/subread-anki): one tap makes an Anki card from any Android app.
+- [Subrep](https://github.com/equwal/subrep-android/releases/latest): live captions of the sound of your phone.
+- [Book Simulator](https://booksimulator.com/): a reading room for Aozora Bunko and Project Gutenberg books.
+- [honjimaku.com](https://honjimaku.com/): subtitles for Japanese audiobooks.
+- [sbm Sync](https://sbmsync.com/): your bookmarks, the same on every device,
+  with [sbm](https://github.com/equwal/sbm) for dmenu,
+  [sbm for Android](https://github.com/equwal/sbm-android/releases/latest)
+  and the [sbm add-on](https://github.com/equwal/sbm-extension/releases/latest) for Firefox and Chrome.
+- [Rebind](https://github.com/equwal/rebind/releases): remap the hardware buttons of e-ink readers and Android,
+  with [Ink Recents](https://github.com/equwal/ink-recents/releases/latest),
+  [Ink Dim](https://github.com/equwal/ink-dim/releases/latest)
+  and [Ink Update](https://github.com/equwal/ink-update/releases/latest).
+- [dickt.store](https://dickt.store/): language-learning tools, flashcards and web toys.
+- [hentaibun.online](https://hentaibun.online/): learn kanbun and kobun.
+- [Recently Written](https://recentlywritten.com/): the blog, and a list of [all projects](https://recentlywritten.com/projects.html).
+
 ## License
 
 AGPL-3.0-or-later. See `LICENSE`. KOReader is AGPL-3.0, and this plugin is
diff --git a/main.lua b/main.lua
index 53bce65..ee17b41 100644
--- a/main.lua
+++ b/main.lua
@@ -34,6 +34,7 @@ local T = require("ffi/util").template
 
 local Clock = require("subread.clock")
 local Cues = require("subread.cues")
+local MoreApps = require("subread.more_apps")
 local PlayerState = require("subread.player_state")
 local Srt = require("subread.srt")
 local Text = require("subread.text")
@@ -970,6 +971,17 @@ function SubRead:showNotification(text)
     UIManager:show(Notification:new{ text = text, timeout = 2 })
 end
 
+--- Shows the other apps of the author. The list is long, so a TextViewer
+--- shows it. KOReader cannot open a web browser on each device, so the
+--- addresses are text.
+function SubRead:showMoreApps()
+    local TextViewer = require("ui/widget/textviewer")
+    UIManager:show(TextViewer:new{
+        title = _("More apps"),
+        text = MoreApps.text(),
+    })
+end
+
 function SubRead:statusText()
     local now = self:_now()
     local clock_text = datetime.secondsToClock(self.clock:getPosition(now), false)
@@ -1260,6 +1272,12 @@ function SubRead:addToMainMenu(menu_items)
             self:showOffset()
             if touchmenu_instance then touchmenu_instance:updateItems() end
         end,
+        separator = true,
+    })
+    table.insert(items, {
+        text = _("More apps"),
+        keep_menu_open = true,
+        callback = function() self:showMoreApps() end,
     })
 end
 
diff --git a/spec/more_apps_spec.lua b/spec/more_apps_spec.lua
new file mode 100644
index 0000000..bd0e29d
--- /dev/null
+++ b/spec/more_apps_spec.lua
@@ -0,0 +1,33 @@
+local MoreApps = require("subread.more_apps")
+
+describe("MoreApps", function()
+    it("gives each app a name, a line and an https address without parameters", function()
+        assert.is_true(#MoreApps.list > 1)
+        local seen = {}
+        for _, app in ipairs(MoreApps.list) do
+            assert.is_true(type(app.name) == "string" and app.name ~= "")
+            assert.is_true(type(app.line) == "string" and app.line ~= "")
+            -- No query and no fragment, so no tracking parameters.
+            assert.is_not_nil(app.url:match("^https://[^?#%s]+$"))
+            assert.is_nil(seen[app.url])
+            seen[app.url] = true
+        end
+    end)
+
+    it("does not list the plugin itself", function()
+        for _, app in ipairs(MoreApps.list) do
+            assert.is_nil(app.url:find("subread.koplugin", 1, true))
+        end
+    end)
+
+    it("ends with the list of all projects", function()
+        assert.equals("https://recentlywritten.com/projects.html", MoreApps.list[#MoreApps.list].url)
+    end)
+
+    it("shows the name, the line and the address of each app as text", function()
+        local text = MoreApps.text()
+        for _, app in ipairs(MoreApps.list) do
+            assert.is_not_nil(text:find(app.name .. "\n" .. app.line .. "\n" .. app.url, 1, true))
+        end
+    end)
+end)
diff --git a/spec/run.lua b/spec/run.lua
index 78d7db8..31c071e 100644
--- a/spec/run.lua
+++ b/spec/run.lua
@@ -38,7 +38,7 @@ function _G.it(name, body)
 end
 _G.setup, _G.teardown, _G.before_each, _G.after_each = function(f) f() end, function() end, nil, nil
 
-for _, name in ipairs({ "text", "srt", "cues", "clock", "player_state", "languages" }) do
+for _, name in ipairs({ "text", "srt", "cues", "clock", "player_state", "languages", "more_apps" }) do
     dofile(here .. name .. "_spec.lua")
 end
 print(string.format("%d passed, %d failed", passed, failed))
diff --git a/subread/more_apps.lua b/subread/more_apps.lua
new file mode 100644
index 0000000..d621432
--- /dev/null
+++ b/subread/more_apps.lua
@@ -0,0 +1,58 @@
+--[[--
+The other apps and sites of the author, for the "More apps" entry of the
+menu.
+
+Pure Lua. No KOReader dependency, so it can be unit tested on a PC.
+
+KOReader cannot open a web browser on each device, so the menu shows each
+address as text.
+--]]--
+
+local MoreApps = {}
+
+-- The order of the link catalog: the sites, then the Android apps, then the
+-- list of all projects. The plugin itself is not in the list.
+MoreApps.list = {
+    { name = "SubRead", url = "https://subread.space/",
+      line = "Read along with an audiobook." },
+    { name = "Book Simulator", url = "https://booksimulator.com/",
+      line = "A reading room for Aozora Bunko and Project Gutenberg books." },
+    { name = "honjimaku.com", url = "https://honjimaku.com/",
+      line = "Subtitles for Japanese audiobooks." },
+    { name = "sbm Sync", url = "https://sbmsync.com/",
+      line = "Your bookmarks, the same on every device." },
+    { name = "SubRead for Android", url = "https://github.com/equwal/subread-android/releases/latest",
+      line = "Times an audiobook against its ebook on the device." },
+    { name = "SubRead Overlay", url = "https://github.com/equwal/subread-overlay/releases/latest",
+      line = "Subtitle lines over any Android media player." },
+    { name = "SubRead Dictionary", url = "https://github.com/equwal/subread-dictionary/releases/latest",
+      line = "Pop-up dictionary that reads Yomitan dictionaries." },
+    { name = "SubRead Anki", url = "https://github.com/equwal/subread-anki",
+      line = "One tap makes an Anki card from any app." },
+    { name = "Subrep", url = "https://github.com/equwal/subrep-android/releases/latest",
+      line = "Live captions of the sound of your phone." },
+    { name = "sbm for Android", url = "https://github.com/equwal/sbm-android/releases/latest",
+      line = "Fuzzy search for your bookmarks." },
+    { name = "Rebind", url = "https://github.com/equwal/rebind/releases",
+      line = "Remap the hardware buttons of e-ink readers and Android." },
+    { name = "Ink Recents", url = "https://github.com/equwal/ink-recents/releases/latest",
+      line = "A recent-apps switcher for e-ink." },
+    { name = "Ink Dim", url = "https://github.com/equwal/ink-dim/releases/latest",
+      line = "Frontlight below the lowest system level." },
+    { name = "Ink Update", url = "https://github.com/equwal/ink-update/releases/latest",
+      line = "Tells you when Rebind and its extensions update." },
+    { name = "All projects", url = "https://recentlywritten.com/projects.html",
+      line = "Everything, with source code." },
+}
+
+--- Returns the list as plain text: the name, the line and the address of
+--- each app, with an empty line between two apps.
+function MoreApps.text()
+    local blocks = {}
+    for _, app in ipairs(MoreApps.list) do
+        blocks[#blocks + 1] = app.name .. "\n" .. app.line .. "\n" .. app.url
+    end
+    return table.concat(blocks, "\n\n")
+end
+
+return MoreApps