App Store Plugin for KOReader
Discover, install, and update community-created KOReader plugins and user patches without leaving your device. The AppStore plugin searches both GitHub topics (koreader-plugin, koreader-user-patch) and repositories whose names follow patterns like "KOReader.patches" or "[NAME].plugins". The plugin supports standard owner/topic/description filters, surfacing curated lists with filtering, sorting, and hands-on install flows that feel native on e-ink hardware.
Installation
- Download the latest release from the releases page or clone this repository.
- Copy the
appstore.koplugin folder to your KOReader’s plugins directory:
- Kobo/Kindle:
koreader/plugins/
- Android:
/sdcard/koreader/plugins/
- Desktop (Linux):
~/.config/koreader/plugins/
- Restart KOReader.
- Access via Tools → App Store.
If you’ve pointed KOReader’s extra_plugin_paths setting at a different
folder (to keep third-party plugins separate from the default location),
AppStore follows it automatically: it looks in every directory KOReader
itself would load plugins from, not just the default one.
When installing a new plugin (not updating one already installed):
- If exactly one custom folder is configured, new plugins install there.
- If you have more than one custom folder and haven’t set
plugin_install_path in appstore_configuration.lua, you’ll be asked
once which folder to use, with a “don’t ask again” checkbox to remember
your choice for next time. plugin_install_path is matched against the
real, resolved directory of each lookup path, so set it to an absolute
path pointing at one of your actual extra_plugin_paths entries.
- Updating an already-installed plugin always writes back to wherever that
plugin currently lives, regardless of any of the above.
Every plugin AppStore finds (including KOReader’s own bundled built-in plugins) is fully
manageable by default. If you’d rather not see some of them in the installed/updates list —
most commonly the bundled built-ins — open Installed Plugins Settings → Manage plugin
paths and uncheck any folder you don’t want AppStore to track. Hiding a folder only affects
what AppStore shows here; KOReader still loads plugins from it normally. A hidden folder is
also skipped when picking where to install a brand-new plugin.
Key Capabilities
- Unified browser for both plugins and user patches with persistent filters and paging.
- Offline-friendly cache stored under
data/cache/AppStore so existing results remain accessible when you lose connectivity.
- Per-entry README viewer that fetches
README.md directly from GitHub and opens it in KOReader’s document renderer.
- Install/update pipeline that handles
.koplugin archives, verifies metadata, and copies files to data/plugins or data/patches accordingly.
- Update tracking for installed plugins and numbered patch files, complete with SHA comparisons and refreshable summaries.
- Ignore release feature to temporarily hide specific plugin updates until a newer version is available.
- Optional authentication through a GitHub Personal Access Token (PAT) to increase API rate limits.
Requirements
- KOReader nightly or 2024.12+ with LuaSocket, Archiver, SHA2, and SQLite dependencies (bundled in default releases).
- Network connectivity for refreshing caches, fetching README files, and downloading plugin or patch archives.
- (Optional) A GitHub PAT with
public_repo scope to avoid unauthenticated rate limits (~10 requests/minute shared across the device).
Configuration (GitHub PAT)
If you routinely browse many repositories or hit rate-limit warnings, supply a PAT in plugins/appstore.koplugin/appstore_configuration.lua:
return {
auth = {
github = {
type = "github",
token = "ghp_your_token_here",
},
},
}
How to create a GitHub PAT
Use a classic token, not a fine-grained one. GitHub’s search API (which this plugin relies on to discover plugins/patches) does not support fine-grained personal access tokens at all — requests with one will fail regardless of the permissions you grant it. Classic tokens work correctly.
- Sign in at github.com and open Settings → Developer settings → Personal access tokens → Tokens (classic), You can follow this link.
- Click Generate new token (classic).
- Name the token (e.g.,
KOReader AppStore), set an expiration, and grant at least the public_repo scope.
- Generate and copy the token immediately—GitHub will not show it again.
- Paste it into
appstore_configuration.lua (You can create that file from appstore_configuration.sample.lua) and restart KOReader.
The plugin automatically includes the token in GitHub API requests, raising your quota to the standard authenticated limits.
Navigating the Four AppStore Sections
The UI is built around four dedicated dialogs.
-
Plugins Browser (App Store · Plugins)
- Lists repositories tagged as KOReader plugins.
- Actions per entry: Install / Update / Remove, View README, open detail dialogs with description, stars, last updated timestamp, and repo owner.
- Toolbar actions: switch to patches tab, refresh cache, adjust filters (
search text, owner, minimum stars), change sorting (stars desc/asc, updated, name), and jump into the installed plugins page.
- Gear icon (⚙️): Access settings including Install plugin from URL to manually install plugins by entering GitHub owner/repo.
-
Patches Browser (App Store · Patches)
- Lists repositories tagged as KOReader user patches and enumerates files matching
^[number]-*.lua.
- Actions per patch: Install patch, retry downloads, or open the repo README for full instructions.
- Toolbar actions: mirror the plugin tab plus shortcuts to the installed patches page and a “matching patch” banner when reconciling installed files with remote entries.
- Gear icon (⚙️): Access settings including Install patch from URL to manually install patches by entering GitHub owner/repo.
-
Installed Plugins (Check plugin updates)
- Summarizes every installed plugin, both linked (matched with a repository) and unlinked.
- Actions: Check all updates, toggle between “Only outdated” and “Show all plugins,” and jump straight back to the Plugins browser.
- Each row shows the local version, cached remote version/SHA (for linked plugins), last checked timestamp, and provides Update or Reinstall buttons when differences are detected.
- Per-plugin actions: Disable/Enable, Delete, Match from List or Match with URL (for unlinked plugins), Unlink the repo (for linked plugins).
-
Installed Patches (Check patch updates)
- Works just like the installed plugins page but focuses on numbered patch files under
data/patches.
- Actions: Check all updates, toggle between “Only outdated” and “Show all patches,” and jump straight back to the Patches browser.
- Each row shows the patch filename, cached remote version/SHA (for linked patches), last checked timestamp, and provides Update or Reinstall buttons when differences are detected.
- Per-patch actions: Disable/Enable (renames with
.disabled suffix), Delete, Match from List or Match with URL (for unlinked patches), Unlink the repo (for linked patches).
Typical Workflow
- Open KOReader → Tools → App Store.
- Pick Plugins or Patches tab. Use the filter dialog to narrow by owner, name, topics, or star threshold.
- Tap an entry for a quick action menu. Choosing Install downloads the repo ZIP (
/zipball/<ref>), extracts it to a temp folder, validates _meta.lua, then copies it to data/plugins/<name>.koplugin.
- After installation, KOReader prompts for a restart so the new plugin becomes available in the Tools menu.
- For patches, selecting an item downloads the raw
.lua file from the default branch and stores it under data/patches/ while retaining the numbered filename used for KOReader’s patch loader.
- Return anytime to the Updates dialogs to check for newer commits. The plugin compares cached SHAs with the installed files and highlights items needing attention.
- Use View README to fetch the upstream documentation and open it in KOReader’s file viewer. Files are cached under
data/cache/appstore/readme/owner_repo_README.md for offline rereads.
Cache & Offline Behavior
- Repository metadata is stored in SQLite under
data/cache/appstore.db (automatically created).
- The browser always reads from the cache first, keeping scrolling smooth even when the network is slow.
- When cached data is older than 7 days, the banner reminds you to trigger Refresh cache.
- README files, download archives, and installed SHAs are cached so subsequent operations reuse existing data whenever possible.
Managing Installed Plugins and Patches
The AppStore plugin provides built-in tools to disable, delete, and match with repositories for installed plugins and patches directly from the update dialogs.
Disable/Enable
- Plugins: Uses KOReader’s native disable mechanism. Disabled plugins appear with a
[DISABLED] label and sync with the Tools → Plugin management screen.
- Patches: Renames the file with a
.disabled suffix (e.g., 2-patch.lua → 2-patch.lua.disabled). Disabled patches are shown with a [DISABLED] label.
Delete (Uninstall)
- Linked items (matched with a repository): Can always be deleted.
- Unlinked items (not matched): Can only be deleted if the “Allow delete unlinked plugins/patches” setting is enabled.
Repository Matching
Link installed plugins/patches with GitHub repositories to enable update tracking:
- Match from List: Opens the repository browser with automatic filtering by plugin/patch name for quick matching.
- Match with URL: Manually enter GitHub owner and repository name to link items not found in search results.
- Unlink the repo: Remove repository association from linked items to return them to unlinked state.
Ignore Release
Temporarily hide specific plugin updates from the update notifications:
- How it works: When you ignore a release (e.g., v1.8.2), it won’t appear in “Check Updates” or “Check All Updates” lists until a newer version is released.
- Ignore a release: Open the Download Options dialog for the latest release and tap Ignore this release.
- Visual indicators: Ignored releases are marked with
[Ignored] in both the Download Options title and the release list.
- Automatic cleanup: The ignore status is automatically cleared when:
- A newer version is released
- You install the ignored version
- You unlink the repository
- Manual access: You can still see and install ignored releases by tapping Update plugin → Other releases… from the plugin’s action menu.
Accessing Management Options
- Open Check plugin updates or Check patch updates.
- Tap any installed item to open its action dialog.
- Use Disable/Enable buttons to toggle the item’s state.
- Use Delete button to permanently remove the item (confirmation required).
- Use Match from List or Match with URL for unlinked items to enable update tracking.
- Use Unlink the repo for linked items to remove repository association.
- Access the gear icon (⚙️) to enable deletion of unlinked items.
Note: All disable/enable/delete operations require a KOReader restart to take effect.
Include 0-Star Forks Setting
By default, the AppStore plugin excludes repositories that are forks with zero stars from search results.
How It Works
- Default behavior (off): The plugin skips the
fork:only stars:0 query at the GitHub API level, so 0-star forks never reach your device. This reduces noise and speeds up browsing.
- When enabled: The plugin includes an additional
fork:only stars:0 query, bringing all forks into the search results regardless of star count.
Accessing the Setting
- Open the Plugins or Patches browser.
- Tap the gear icon (⚙️) in the top-left corner of the title bar.
- Toggle Include 0-star forks on or off.
- The setting takes effect immediately and applies to all future cache refreshes.
Note: Changing this setting does not automatically refresh the cache. Use the Refresh cache action from the toolbar to apply the new filter scope to your current results.
Download Source (mirror / proxy)
Where GitHub itself is slow or unreachable, downloads can be routed through a proxy prefix instead. The setting lives under the gear icon (⚙️) → Download source, and is also reachable from the installed-plugins and installed-patches settings dialogs. It defaults to Direct (GitHub); leave it there and nothing about the plugin’s network behaviour changes.
Three public mirrors are offered as presets (gh-proxy.com, gh.ddlc.top, ghproxy.net), plus Custom for a prefix of your own — including one you host yourself, e.g. http://192.168.1.10:8080/. A custom prefix must be a complete http:// or https:// URL with no query string; a trailing / is added if you omit one. Plain http:// is accepted for self-hosted mirrors but asks for confirmation first, because the connection is unencrypted and what comes back through it is plugin code that gets extracted and run.
Scope, worth knowing before you switch:
- What is routed: plugin archives, release assets, patch files and README fetches — everything that goes through a raw or download URL.
- What is not routed: every
api.github.com call, i.e. browsing, searching, repository metadata and update checks. Those keep going to GitHub directly. This is deliberate: your GitHub PAT travels in the Authorization header of those requests, and sending it through a third-party proxy would hand your token to whoever runs it. So a mirror helps with downloads, not with a blocked API host.
- Trust: a mirror operator controls the bytes that land in your plugins folder, and there is no checksum or signature check. Prefer Direct (GitHub) unless you have a reason not to.
The same setting can also be written in appstore_configuration.lua as download_mirror_preset / download_mirror_prefix (see appstore_configuration.sample.lua). The two do not fight: whichever was touched last applies. Pick a source in the UI and it overrides the config file; edit the config file afterwards and it wins again. Removing the key counts as an edit and restores the default. This is how a config file you sync between devices keeps working even after you have poked at the setting on one of them.
Full vs. Incremental Refresh
Refresh cache doesn’t always do the same amount of work. A full refresh
re-discovers every repository from scratch (the complete topic + name search across
GitHub); an incremental one narrows that same search to repositories pushed since
the last successful check and merges the (usually small) result into the existing
cache instead of replacing it.
How it decides
- The first refresh ever for a kind (plugins/patches) is always full.
- After that, a refresh is full only once Full refresh interval (days) has elapsed
since the last successful full refresh; otherwise it runs incremental.
- Force full refresh (gear icon ⚙️) always runs a full refresh immediately,
regardless of the interval.
- Setting Full refresh interval (days) to
0 disables incremental mode entirely —
every refresh is full.
- A refresh that fails, is stopped, or comes back incomplete does not count as
successful, so it never gets credit for having refreshed anything — the next refresh
still sees the same interval as if it hadn’t run.
What incremental mode catches, and what it doesn’t
Incremental mode filters by GitHub’s pushed: search qualifier (time of the last code
push), plus an Incremental lookback (days) safety margin subtracted from the last
successful check, so an update landing right at that moment is never skipped. It’s
cheap because it usually returns a handful of repositories instead of the whole
ecosystem — but that also means it will not notice:
- A repository removed, renamed, or untagged since the last full refresh (there is no
full listing to prune it from).
- A repository whose only change is its star count — GitHub does not move
pushed_at
when someone stars a repo, so incremental mode can’t see that change. The cached star
count (used for sort order) stays as it was until the next full refresh.
Both gaps close automatically at the next full refresh, so any drift is bounded by the
configured interval, not permanent.
Accessing the settings
- Open the Plugins or Patches browser.
- Tap the gear icon (⚙️).
- Full refresh every: N days and Incremental lookback: N days each open a
numeric spinner; Force full refresh runs one immediately.
Troubleshooting
| Symptom |
Likely Cause |
Suggested Fix |
| Rate limit exceeded |
Anonymous GitHub quota exhausted |
Configure a PAT and retry after a few minutes |
| Missing README |
Repo lacks README.md or request failed |
Confirm file exists upstream, then rerun View README while online |
| Patch not listed |
Repository is not named KOReader.patches (or similar) and/or the koreader-user-patch topic is missing |
Ask the maintainer to add the correct topic |
| Star count / sort order looks stale |
Cache has only been refreshed incrementally since the last full refresh |
Wait for the next full refresh, or use Force full refresh |
Non-touch devices (Kindle 4 NT, Kindle Keyboard, …)
The AppStore plugin is fully usable on devices without a touchscreen. The browser and updates dialogs follow KOReader’s standard non-touch conventions:
- Up / Down — move focus between list rows; the list scrolls automatically when the focused row would otherwise be off-screen.
- Left / Right — move focus across the title-bar buttons (gear / close) and the pagination row (Previous / Next), or between the top control buttons in the Updates dialog.
- Press (centre key on the 5-way) — activate the focused entry, equivalent to a tap.
- Hold / Long-press (where supported) — open the secondary action of the focused entry, equivalent to a tap-and-hold.
- Page-forward / Page-back keys — flip to the next / previous page in the browser.
- Back — close the dialog. On few-keys devices that do not provide a Back key (e.g. Kindle 4 NT), Left acts as the close shortcut, mirroring KOReader’s built-in
Menu widget.
Web Browser Access
You can also browse the repository list from your PC browser by visiting https://omer-faruq.github.io/appstore.koplugin/, or by downloading and opening the docs/index.html file locally.
Localization
The plugin UI can be shown in the language selected in KOReader (Settings → Language).
Bundled translations: Simplified Chinese (zh_CN), Turkish (tr), Spanish (es),
French (fr), German (de), Brazilian Portuguese (pt_BR), Hungarian (hu),
Azerbaijani (az). Any untranslated string falls back to English, and an unsupported
language shows the full English UI.
Translations live in l10n/<code>.lua, plain Lua tables mapping the English source
string to its translation. The plugin loads them through appstore_gettext.lua, a
drop-in wrapper around KOReader’s gettext (needed because the core catalog does not
cover standalone plugins). The active language is read once at startup; changing the
UI language restarts KOReader, so the new catalog is picked up on the next launch.
Adding or updating a language
Tools live in l10n/tools/ (run with the LuaJIT that ships with KOReader, or any LuaJIT):
# 1. Regenerate the English key template after changing source strings
luajit l10n/tools/extract.lua main.lua appstore_updates.lua appstore_repo_content.lua appstore_mirror.lua _meta.lua > l10n/template.lua
# 2. Copy template.lua to l10n/<code>.lua (matching KOReader's locale code) and translate the values
# 3. Verify keys and %s/%d placeholders match the source
luajit l10n/tools/validate.lua l10n/template.lua l10n/<code>.lua
Keep the keys byte-for-byte identical to the source strings (including %s/%d
placeholders, newlines and characters such as → · — ≥ ⭐ …) — only the values change.
Tests
tests/ holds standalone scripts for the modules whose logic can be exercised without
the UI. Each prints one line per check and exits non-zero on the first failure, so they
can be chained. Run them from an extracted KOReader directory:
./luajit plugins/appstore.koplugin/tests/appstore_plugin_paths_test.lua
./luajit plugins/appstore.koplugin/tests/appstore_mirror_test.lua
The plugin_paths one needs KOReader’s own LuaJIT (it loads datastorage and lfs).
The mirror one stubs its KOReader dependencies and runs under any LuaJIT, as long as
the working directory is a KOReader tree so that common/socket/url.lua is reachable.
Credits
This plugin and documentation were prepared with Windsurf (AI).
License
This project is licensed under the GNU General Public License v3.0 (GPL-3.0). See the LICENSE file for details.
评论