Compare commits

...
Author SHA1 Message Date
Leopoldo Pla Sempere d09c755cf2 chore: translate empty bookmark state (#3004) 2026-08-13 19:56:24 +03:00
Georgi Valchanov bccb4cc43f feat: add Bulgarian translation (#2908) 2026-08-13 19:55:34 +03:00
Leopoldo Pla Sempere f4378bae4f chore: translate firmware device mismatch error (#2902) 2026-08-13 19:55:34 +03:00
metoli86andMetin Arslan 7ab9c21afc fix: add missing STR_FIRMWARE_WRONG_DEVICE to Turkish translation (#2941)
Co-authored-by: Metin Arslan <GITHUB-EPOSTAN>
2026-08-13 19:55:34 +03:00
Foulad f449ce9e7d fix: narrow pr-formatting-check.yml to pull_request_target only (#3006) 2026-08-13 19:52:26 +03:00
Thiago Kenji Okada 48e39eb0ee fix: prevent reader resume position rollback (#2962) 2026-08-13 12:05:01 +00:00
Justin Mitchell 9b1fb712de fix: Guard GPIO13 power control for Xteink C3 boards only (#2998) 2026-08-12 22:06:52 -04:00
Justin Mitchell e715532f90 fix: settings UI not updating after language change & line height padding (#2997) 2026-08-12 21:32:03 -04:00
Brian Pugh 70faa29dac perf: light-sleep idle + refresh downclock — ~3.2x active reading time (X3-measured) (#2525)
## Summary

Reduces active-reading power draw ~3.2x, measured end-to-end on an X3
with a [Nordic
PPK2](https://www.nordicsemi.com/Products/Development-hardware/Power-Profiler-Kit-2)
at the battery terminals (slim build, USB cable removed, 3.8v). Three
independent changes:

1. **Light-sleep between input polls when idle** (the big one): after 1
s of inactivity the main loop replaces its busy `delay(50)` with 50 ms
timer-paced `esp_light_sleep_start()` slices. Poll cadence and input
latency are unchanged — the CPU sleeps between polls instead of spinning
at 10 MHz. Guards: never sleeps while a `HalPowerManager::Lock` is held
(render in flight), WiFi is up, or USB is connected (light sleep kills
the CDC link).
2. **Two-stage idle backoff**: downclock at 500 ms, light sleep at 1 s,
replacing the flat 3 s full-speed window. Renders re-raise the clock via
`Lock` the moment they start, so the full-speed tail only ever served
loop bookkeeping. (The 3 s value in #852 was a placeholder;
responsiveness verified on device against stock.)
3. **Refresh BUSY-wait downclock**: drops the CPU to 10 MHz while
`pollBusy()` polls the BUSY pin (0.3–2 s per refresh), via new optional
hooks in the SDK — **depends on Free-Ink/freeink-sdk/pull/8**. Plus a 1
Hz throttle on the X3's I2C-based USB detection (was polled every loop).

(1+2) reduced average static-page-current from 9.68mA to 3.45mA.
Reducing the battery-monitor polling frequency further reduced it to
2.78mA.

Before merging, we'll need to update the submodule reference.

## Measured results (PPK2, X3)

| Metric | Before | After |
|---|---|---|
| Idle on a static page | 9.68 mA | **2.78 mA** |
| Fast page turn (incl. post-turn behavior) | ~100 mC | **28.98 mC** |
| Post-turn 160 MHz tail | 21.2 mA x 3 s | eliminated |
| Session average @ 30 s/page | ~11.6 mA | **~3.6 mA (~3.2x reading
time)** |
| Deep sleep | 12.8 µA | unchanged |

## Typical-day profiles

Assuming someone spends 30s per page.

- **Light**: 1 h reading, 10 lock/wake cycles, remaining ~23 h deep
sleep
- **Heavy**: 2 h reading, 20 lock/wake cycles, remaining ~22 h deep
sleep
- Per-day cost components: reading 11.6 → 3.6 mA (session avg),
lock+wake cycle
0.113 → 0.099 mAh each (sleep-screen mode), deep sleep 12.8 µA
(unchanged,
  ~0.29 mAh/day)
- Excluded: WiFi sessions, cold chapter indexing, self-discharge

### Days per charge (650mAh)

| Profile | Before | After | Ratio |
|---|---|---|---|
| Light (1 h/day, 10 locks) | 13.0 mAh/day → ~50 days | 4.9 mAh/day →
~133 days | 2.7× |
| Heavy (2 h/day, 20 locks) | 25.7 mAh/day → ~25 days | 9.5 mAh/day →
~68 days | 2.7× |
| Pure active reading | 11.6 mA → ~56 h | 3.6 mA → ~180 h | 3.2× |

### Caveats

- "After" saturates in reality: Li-ion self-discharge (~2–3%/month) is
not modeled
  and becomes the limiter once firmware draw is this low.
- Faster page-turners trend toward ~2.8×, slower readers slightly above
3.2×.
- Frequent locking dilutes the ratio (wake = full cold boot, barely
touched by the
power branch): at 10–20 locks/day, cycles are ~20% of the "after"
budget.
---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**YES**_

---------
2026-08-12 20:57:45 -04:00
Leopoldo Pla SempereandJulia Nguyen cb995809ba fix: backport File Transfer guard from CrossInk (#2990)
Co-authored-by: Julia Nguyen <julia@uxj.io>
2026-08-13 00:19:03 +03:00
Uri Tauber f16451c729 feat: render HTML dictionary definitions via EPUB engine (#2836) 2026-08-12 17:17:25 -04:00
Leopoldo Pla SempereandPablo Cabeza 268c27f7d0 feat: add transparent sleep overlays (#2937)
Co-authored-by: Pablo Cabeza <josepablocg@gmail.com>
2026-08-12 23:21:43 +03:00
Uri Tauber b1d53ba810 feat: add orangutan translation (#2992)
Translated by Dr Horace Worblehat, B. Thau, D.M.
2026-08-12 22:18:39 +03:00
Julia e970abae89 feat: skip boot screen on startup (#2989) 2026-08-12 21:45:21 +03:00
Justin Mitchell 9e3cea2e54 feat: convert all lists and tabs to FUI (#2957) 2026-08-12 20:40:09 +03:00
Thiago Kenji Okada 86eb1cbde2 fix: properly link AGENTS.md (#2976) 2026-08-11 22:24:24 +03:00
Uri Tauber 2cac597143 fix symlink AGENTS.md 2026-08-11 08:24:13 +03:00
Julia 7212129318 fix: ignore empty watchdog reports (#2968)
## Summary

* **What is the goal of this PR?**
* Prevent reset-only watchdog events from opening the crash-report flow
with an empty panic reason.

* **What changes are included?**
* Record a marker in RTC memory only when panic diagnostics are
captured.
  * Continue reporting explicit panics and CPU lockups.
* Require that marker before treating watchdog resets as crashes, and
clear it with the existing panic state.


## Scope Check

CrossPoint is intentionally narrow. See
[SCOPE.md](../blob/master/SCOPE.md) and
[ROADMAP.md](../blob/master/ROADMAP.md).
Please confirm:

- [x] I have read SCOPE.md and ROADMAP.md.
- [x] This PR is **not** a new built-in theme (themes are temporarily
closed pending the move to SD-loaded themes).
- [x] This PR is **not** a new external network connector (sync engine,
cloud storage, remote file access, etc.).
- [x] This PR is **not** an interactive app, writing tool,
RSS/news/browser, media playback, or PDF feature.
- [x] The stock firmware does not already handle this well, **and** no
other popular CrossPoint fork already does
      (or, if one does, I explain why CrossPoint still needs it below).
- [x] If this PR touches `freeink-sdk/`, `lib/hal/`, the bootloader,
OTA, or recovery code, I have coordinated with
      the relevant maintainer.

**If this PR was opened against the previous (broader) scope and was
already in flight under Phase 0, link the
relevant Discussion or issue so reviewers can see the history.**

## Additional Context

* This is a focused port of CrossInk commit `c96d4275`.
* The change adds 4 bytes of RTC no-init state; it does not affect
storage formats, normal boot routing, or real panic reporting.
* Hardware follow-up: on an X3/X4, confirm a reset-only watchdog boots
normally without an empty crash report, while a genuine panic still
reaches the crash-report screen.
---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**<YES>**_
2026-08-11 03:02:56 +00:00
Justin Mitchell 9b090266cf fix: MAC address reading when WiFi is off (#2960) 2026-08-10 15:38:58 -04:00
124 changed files with 8114 additions and 3925 deletions

No files matched your search

+1 -2
View File
@@ -1,7 +1,6 @@
name: "PR Formatting"
on:
pull_request:
pull_request_target:
types:
- opened
@@ -9,7 +8,7 @@ on:
- edited
permissions:
statuses: write
pull-requests: read
jobs:
title-check:
+1 -1
View File
@@ -1 +1 @@
/home/urit/Documents/CrossPoint/crosspoint-reader/.skills/SKILL.md
.skills/SKILL.md
+2 -2
View File
@@ -36,7 +36,7 @@ CrossPoint is open-source e-reader firmware - community-built, fully hackable, f
- OPDS browser with saved servers (up to 8), search, pagination, and direct download
- OTA update checks and installs from GitHub releases
- **Customization**: multiple themes (Classic, Lyra, Lyra Extended, RoundedRaff), sleep screen modes, front/side button remapping, status bar controls, power-button behavior, refresh cadence, and more.
- **Customization**: multiple themes (Classic, Lyra, Lyra Extended, RoundedRaff), sleep screen modes including transparent overlays, front/side button remapping, status bar controls, power-button behavior, refresh cadence, and more.
- **Localization**: 24 UI languages and counting. RTL support.
@@ -136,7 +136,7 @@ Conversion runs the firmware repo's `lib/EpdFont/scripts/fontconvert_sdcard.py`
- [Web server endpoints](./docs/webserver-endpoints.md)
- [Project scope](./SCOPE.md)
- [Contributing docs](./docs/contributing/README.md)
- [Touch and UI development](./docs/contributing/touch-and-ui.md) - FreeInkUI components for new screens, the touch bridge for existing ones, and build envs for the non-Xteink touch devices
- [Touch and UI development](./docs/contributing/touch-and-ui.md) - how to build new screens on the FreeInkUI activity bases (UiListActivity and friends), plus build envs for the non-Xteink touch devices
---
+14 -4
View File
@@ -186,7 +186,7 @@ The Settings screen allows you to configure the device's behavior. There are a f
- "None" - A blank screen
- "Cover + Custom" - The book cover image while actively reading, falls back to "Custom" behavior otherwise
- "Quick resume" - The text of the last page read will be displayed on the sleep screen and a moon icon is shown on the edge of the screen. Waking up the device will return to the same page of the opened book. This is useful for quickly resuming reading without waiting for the device to fully wake up and load the book.
- "Transparent" - A transparent overlay image drawn over the current screen; see [Sleep Screen](#37-sleep-screen) below for more information
- **Sleep Screen Cover Mode**: How to display the book cover when "Cover" sleep screen is selected:
- "Fit" (default) - Scale the image down to fit centered on the screen, padding with white borders as necessary
@@ -501,6 +501,7 @@ The **Sleep Screen** setting controls what is displayed when the device goes to
| **Custom** | A custom image from the SD card (see below). Falls back to **Dark** if no custom image is found. |
| **Cover** | The cover of the currently open book. Falls back to **Dark** if no book is open. |
| **Cover + Custom** | The cover of the currently open book, shown only while actively reading. Falls back to **Custom** behavior when not reading. |
| **Transparent** | A BMP overlay drawn over the current screen. Supports alpha transparency in 32-bit BGRA images and treats white as transparent in regular BMPs. Falls back to **Dark** if no valid overlay image is found. |
| **None** | A blank screen. |
#### Cover settings
@@ -515,12 +516,21 @@ When using **Cover** or **Cover + Custom**, two additional settings apply:
To use custom sleep images, set the sleep screen mode to **Custom** or **Cover + Custom**, then place images on the SD card:
- **Multiple Images (recommended):** Create a `.sleep` directory in the root of the SD card and place any number of `.bmp` images inside. One will be randomly selected each time the device sleeps. (A directory named `sleep` is also accepted as a fallback.)
- **Single Image:** Place a file named `sleep.bmp` in the root directory. This is used as a fallback if no valid images are found in the `.sleep`/`sleep` directory.
- **Single Image:** Place a file named `sleep.bmp` in the root directory. This takes priority over the `.sleep`/`sleep` directories.
#### Transparent overlay images
To use transparent sleep overlays, set the sleep screen mode to **Transparent**, then place BMP files on the SD card:
- **Multiple Images (recommended):** Create a `.sleep-overlay` directory in the root of the SD card and place any number of valid overlay `.bmp` images inside. One will be randomly selected each time the device sleeps. A directory named `sleep-overlay` is also accepted as a fallback.
- **Single Image:** Place a file named `sleep-overlay.bmp` in the root directory. This takes priority over the `.sleep-overlay`/`sleep-overlay` directories.
Transparent overlay files are intentionally separate from normal sleep images. Regular BMP formats supported by CrossPoint are accepted; white pixels leave the existing screen unchanged. For per-pixel alpha transparency, use a 32-bit BGRA BMP with both visible and non-opaque pixels. Opaque white pixels in these alpha images erase the content behind them.
> [!TIP]
> For best results:
>
> - Use uncompressed BMP files with 24-bit color depth
> - For non-transparent **Custom** mode, use uncompressed BMP files with 24-bit color depth.
> - For **Transparent** mode, use a regular BMP for white-as-transparent artwork or an uncompressed 32-bit BGRA BMP for per-pixel alpha.
> - X4: Use a resolution of 480x800 pixels to match the device's screen resolution.
> - X3: Use a resolution of 528x792 pixels to match the device's screen resolution.
+94 -113
View File
@@ -1,23 +1,88 @@
# Touch and UI Development
CrossPoint now runs on touch devices (Seeed Sticky, M5Paper, LilyGo T5) alongside the button-only Xteink X3/X4. Every screen must work with both input styles. There are two supported ways to get there:
CrossPoint runs on touch devices (Seeed Sticky, M5Paper, LilyGo T5) alongside the button-only Xteink X3/X4. Every screen must work with both input styles.
1. **New screens: build them with FreeInkUI components.** Touch hit-testing, tap highlighting, long-press, and button focus navigation come with the component; you never hand-roll coordinate math.
2. **Existing screens and in-flight features: use the MappedInputManager touch bridge.** A small set of helpers adds tap/hold/swipe support to hand-rolled rendering without restructuring the activity.
**There is one supported way to build a new screen: FreeInkUI, hosted through the firmware base classes below.** Touch hit-testing, tap highlighting, long-press, swipe scrolling, and button focus navigation all come from the shared stack; you never hand-roll coordinate math.
If you are starting a new activity, use FreeInkUI. If you have a feature branch with a hand-rolled screen already working on buttons, use the bridge; do not rewrite mid-flight.
The old bridge helpers (`rowTouch`, `colTouch`, `wasTapInRect`, manual rect `contains()` checks) are legacy. They survive only for the two remaining hand-rolled surfaces (the theme-driven home screen and the reader page) and must not appear in new code. PRs that add new uses will be asked to convert.
---
## Path 1: New screens with FreeInkUI
## Picking the right host
FreeInkUI (`freeink-sdk/libs/ui/FreeInkUI`, namespace `freeink::ui`) is an immediate-mode component library. The core idea:
| Your screen is... | Use | In-tree reference |
|---|---|---|
| A single list of rows | subclass `UiListActivity` | [`LanguageSelectActivity`](../../src/activities/settings/LanguageSelectActivity.cpp) (minimal), [`RecentBooksActivity`](../../src/activities/home/RecentBooksActivity.cpp) (long-press) |
| Tabbed lists | subclass `UiTabListActivity` | [`SettingsActivity`](../../src/activities/settings/SettingsActivity.cpp) |
| Custom FUI layout (sliders, prompts, state machines) | inherit `UiAppHost` directly | [`EpubReaderPercentSelectionActivity`](../../src/activities/reader/EpubReaderPercentSelectionActivity.cpp), [`WifiSelectionActivity`](../../src/activities/network/WifiSelectionActivity.cpp) |
| A modal picker or confirm inside a legacy activity | `OptionPopup` (or push `ConfirmationActivity`) | [`OtaUpdateActivity`](../../src/activities/settings/OtaUpdateActivity.cpp) |
| A Yes/No prompt inside a FUI screen | build `fui::optionDialog` into the screen | `WifiSelectionActivity::buildPromptDialog` |
- While a component renders, it registers its tappable areas ("hit rects") into the frame's interaction buffer via `Frame::hit(rect, action, value, inputMask, state)`.
- Each loop you build an `InputSnapshot` from the input manager and route it against that buffer. If a tap (or mapped button press) lands in a registered rect, the routed `ActionEvent` tells you which action fired and with what value.
- Components query `frame.stateFor(action, value)` while painting, so touch-down highlight and focus states render correctly without any per-activity code.
All four paths route input through the same SDK interaction table, so touch and physical buttons fire the same actions with no per-screen coordinate code.
You get touch, hold highlighting, long-press, minimum touch-target sizing, and orientation-aware coordinates for free. The same action IDs fire from physical buttons, so one code path serves both input styles.
## The hosting stack
`UiAppHost` ([src/components/UiAppHost.h](../../src/components/UiAppHost.h)) owns what every FUI screen shares: the font-bound render target, the `FreeInkApp`, and the `uiReady` handshake that lets the loop task route touch snapshots against the interaction table the render task rebuilds. Never re-implement that handshake; it is a cross-task protocol and lives in exactly one place.
`UiListActivity` ([src/activities/UiListActivity.h](../../src/activities/UiListActivity.h)) layers the list protocol on top: swipes scroll the viewport without moving the selection, buttons move the selection and pull the viewport along (`fui::ListNav`), plus the render skeleton (header chrome, app, footer hints). `UiTabListActivity` is the tab-bar sibling; its selection ring puts the tab bar at position 0 and the rows after it.
### New list screen skeleton
Subclasses supply the data; the base owns the loop. The whole contract:
```cpp
class MyListActivity final : public UiListActivity {
public:
MyListActivity(GfxRenderer& r, MappedInputManager& in) : UiListActivity("MyList", r, in) {}
private:
std::vector<Entry> entries; // the activity's data, loaded in onEnter
std::vector<fui::ListItem> rows; // activity-owned row cache: rebuilt only when entries changes, reused every render
int listCount() const override { return entries.size(); }
const char* headerTitle() const override { return tr(STR_MY_TITLE); }
void onEnter() override {
UiListActivity::onEnter();
entries = /* ... load from wherever ... */;
rebuildRows();
}
// Called whenever entries changes (here, only onEnter; a mutable list would
// also call this after any add/remove). NOT called from buildScreen().
void rebuildRows() {
rows.clear();
rows.reserve(entries.size());
// ... one fui::ListItem per entry (label, actionValue = index) ...
}
void buildScreen(UiScreen& screen) override {
// set content margin from the theme safe area, then:
fui::ListProps props;
props.items = rows.data();
props.count = rows.size();
props.action = ACTION_ROW;
props.inputMask = fui::InputTouch; // physical buttons stay in the base loop
syncListViewport(screen, props); // selection/viewport handoff, always right before list()
screen.list(props);
}
void activateIndex(int index) override {
app.clearTapFlash(); // leaving the screen: a lingering flash would gray the next render
// ... open the thing ...
}
};
```
See [`FileBrowserActivity`](../../src/activities/home/FileBrowserActivity.cpp)'s `rebuildRowItems()` for this pattern applied to a directory listing that can run into the hundreds of entries, and `LanguageSelectActivity.cpp` for the rest of the skeleton (content-margin math, footer, etc.) — note that file still builds its row vector locally inside `buildScreen()`; match `FileBrowserActivity`, not that file, for the row cache. Optional overrides: `onRowLongPress(index)`, `drawFooter()`, `handleButtons()` for extra physical-button handling, and `ACTION_USER`-and-up action ids for non-row elements (register handlers in `onEnter` after the base's).
### Rules that apply to every FUI screen
- Register actions with `app.on(...)` in `onEnter` (after `resetUi()` for direct `UiAppHost` users). Handlers receive a `void* user` you cast back to the activity.
- Handlers that leave the current screen call `app.clearTapFlash()` first.
- Theme tokens are shared and bound by `resetUi()`; never call `app.setTheme` yourself. Metrics flow from the active UITheme through [`UIThemeTokens.h`](../../src/components/UIThemeTokens.h), including the per-board bezel insets that keep scrollbars visible.
- `TextStyle.maxLines` defaults to 1 and truncates with an ellipsis. Set `maxLines` explicitly on any dialog headline or message that can wrap.
- Everything stays allocation-free in steady state. A local `std::vector` inside `buildScreen()` is **not** allocation-free even with `reserve()` first: it starts at zero capacity on every call, `reserve()` allocates, and the destructor frees that storage before the call returns — real allocator work and fragmentation risk on every repaint (cursor move, tap flash, ...), not just on data changes. Build `ListItem` rows into activity-owned storage instead, reserved once when the underlying data loads (`onEnter()`/a `load*()` — see the skeleton above and `FileBrowserActivity::rebuildRowItems()`), and reused unchanged by every `buildScreen()` call. Use a fixed-capacity array (e.g. `ListItem rows[MAX]`, as `OptionPopup` and `KOReaderSyncActivity`'s action rows do) when the count is small and bounded. Do not hold FUI `props` across renders — only the row storage they point into.
### Component inventory
@@ -33,106 +98,11 @@ All under `freeink-sdk/libs/ui/FreeInkUI/include/components/`:
| Media | `book-card`, `cover-grid`, `cover-carousel`, `metric-card` |
| Text | `text-field`, `text-area` |
### Integration skeleton
The in-tree reference is [`src/activities/util/KeyboardEntryActivity.cpp`](../../src/activities/util/KeyboardEntryActivity.cpp); it drives the FreeInkUI keyboard component inside a normal Activity. The shape:
```cpp
#include <FreeInkUIGfxRenderer.h>
namespace fui = freeink::ui;
// Render step: draw the component and (re)register its hit rects.
fui::GfxRendererTarget target(renderer); // adapts GfxRenderer to FreeInkUI's DrawTarget
target.setFont(fui::GfxRendererTarget::FONT_BODY, UI_12_FONT_ID);
const fui::DeviceContext device = target.deviceContext(); // orientation, safe area, touch sizing
fui::Frame<48> frame(target, device, fui::InputSnapshot{}, interactions); // 48 = max hit rects
fui::KeyboardProps props;
props.layout = &currentLayout();
props.keyAction = ACTION_KEY; // one action id; the key is the event value
props.inputMask = fui::InputTouch | fui::InputLongPress;
fui::keyboard(frame, kbRect, props); // draws AND registers hit rects
```
```cpp
// Input step (each loop): feed touch state, act on routed events.
int tx, ty, tapX, tapY, hx, hy;
const bool pressedDown = mappedInput.wasScreenTouchDown(tx, ty);
const bool tapped = mappedInput.wasScreenTapped(tapX, tapY);
const bool inContact = mappedInput.isScreenTouchHeld(hx, hy);
const auto result = touchRouter.update(interactions, pressedDown, tx, ty,
tapped, tapX, tapY, inContact, millis());
if (result.event) {
activateValue(result.event.value, result.event.longPress);
requestUpdate();
} else if (result.activeChanged) {
requestUpdate(); // repaint touch-down highlight
}
```
Notes:
- `Frame<N>` is templated on the max number of hit rects; the buffer is stack/static, no heap.
- `fui::TouchHoldRouter` (see `components/keyboard/keyboard.h`) synthesizes long-press while the finger is still down and swallows the eventual release. Use it whenever a component distinguishes tap from long-press.
- For simpler screens without long-press, `FreeInkApp` / `Screen<N>` (`FreeInkApp.h`) is the ergonomic layer: it owns the interaction buffer, offers a top-to-bottom builder (`takeTop`, footer helpers, etc.), and its `render(input)` call routes the snapshot and dispatches registered action callbacks in one step. `snapshotFrom(input, device)` in `FreeInkUIInputManager.h` builds the `InputSnapshot` with orientation-aware touch mapping.
- Everything stays allocation-free and works on the button-only devices unchanged: physical buttons route through the same interaction table via each hit rect's `inputMask`.
One exception to "always go through a host": `KeyboardEntryActivity` drives the keyboard component with a raw `fui::Frame` and `TouchHoldRouter` because per-key hold repeat needs its own routing. If you think your screen needs that, raise it in the PR first.
---
## Path 2: Adding touch to existing hand-rolled screens
For activities that draw their own rows, menus, and buttons, `MappedInputManager` ([src/MappedInputManager.h](../../src/MappedInputManager.h)) exposes bridge helpers. They all return **logical screen coordinates** (orientation already applied via `GfxRenderer::tapToLogical`); never touch raw normalized panel coordinates or the SDK `InputManager` directly.
| Helper | Use for |
|---|---|
| `wasScreenTapped(x, y)` | A completed tap (press + release), with logical coords |
| `wasScreenTouchDown(x, y)` | Touch-down (held > 90 ms, not yet released): draw selection highlight |
| `isScreenTouchHeld(x, y)` | Live contact position while the finger is down (drag tracking) |
| `wasTapInRect(x, y, w, h)` | One-off hit test on a rectangle (a single button, a banner) |
| `wasListItemTapped(index, count, selected, listTop, listHeight, hasSubtitle)` | Taps on a standard UITheme list; does the row/paging math for you |
| `wasListItemTouchedDown(...)` | Same geometry, touch-down phase (highlight before activate) |
| `rowTouch(row, top, rowStep, rowCount, xStart, xEnd, rowHeight)` | Any custom band of equal-height rows; returns `RowTouch::None/Down/Tap` |
| `colTouch(col, left, colStep, colCount, yStart, yEnd, colWidth)` | Horizontal button bands (dialogs, prompts) |
| `wasSwipe()` | Raw swipe direction if you need one beyond the global gestures |
| `hasTouch()` | True when the device has a touch panel (rarely needed; helpers simply never fire without one) |
### Pattern A: standard themed list
One call per loop; UITheme owns the row geometry ([EpubReaderBookmarksActivity.cpp:123](../../src/activities/reader/EpubReaderBookmarksActivity.cpp)):
```cpp
int tapped = -1;
if (mappedInput.wasListItemTapped(tapped, bookmarks.size(), selectorIndex, listY, listHeight, true)) {
selectorIndex = tapped;
openBookmark();
return;
}
```
### Pattern B: custom rows with hold highlight
For non-theme row layouts, use `rowTouch` and distinguish `Down` (highlight) from `Tap` (activate), as in [EpubReaderFootnotesActivity.cpp](../../src/activities/reader/EpubReaderFootnotesActivity.cpp):
```cpp
int row = -1;
const auto touch = mappedInput.rowTouch(row, listTop, lineHeight, visibleCount,
contentX, contentX + contentWidth);
if (touch != MappedInputManager::RowTouch::None) {
const int touched = scrollOffset + row;
if (touch == MappedInputManager::RowTouch::Down) {
if (selectedIndex != touched) { selectedIndex = touched; requestUpdate(); }
} else { // RowTouch::Tap
selectedIndex = touched;
activateSelection();
}
return;
}
```
The `Down` state exists because e-ink repaints are slow: highlight on touch-down gives immediate feedback, activation happens on release.
### Global gestures: do not reimplement these
## Global gestures: do not reimplement these
Three gestures are handled once, for every screen. Activities must not add their own edge-swipe handling:
@@ -142,14 +112,25 @@ Three gestures are handled once, for every screen. Activities must not add their
| Home | Up-swipe starting in the bottom 14% | `ActivityManager::loop()`; pops to Home (activities can override via `handleHomeGesture()`) |
| Menu | Down-swipe starting in the top 14% | Activities that have a menu check `wasMenuGesture()` themselves (the reader does this) |
Because the back gesture arrives as `Button::Back`, most button-era activities gain back-swipe support with zero changes. That is the bar to aim for: bridge helpers should make touch an additive layer over the button flow, not a second input state machine.
Because the back gesture arrives as `Button::Back`, most button-era activities gain back-swipe support with zero changes.
### Bridge rules
---
- Handle touch in `loop()` next to the existing button handling, one helper call per interaction zone, and `return` after consuming an event (mirrors the button pattern).
- Never call the SDK `InputManager` or read GPIO directly; the HAL rule from the main guide applies to touch too.
- Coordinates from the helpers are logical and orientation-correct on all four rotations; test at least Portrait and one Landscape mode before PR.
- Nothing to clean up in `onExit()`; the helpers are stateless from the activity's point of view.
## Legacy bridge helpers (do not use in new code)
`MappedInputManager` still exposes raw touch accessors. The FUI stack consumes them internally (via `touchSnapshotFrom` in [`UiAppHelpers.h`](../../src/components/UiAppHelpers.h)); activities should not.
| Helper | Status |
|---|---|
| `wasScreenTapped` / `wasScreenTouchDown` / `isScreenTouchHeld` | Consumed by the FUI snapshot builder. Direct use only in the two legacy surfaces |
| `wasTapInRect(x, y, w, h)` | Legacy one-off hit test |
| `rowTouch` / `colTouch` | Legacy row/column band math. Sole remaining user: `HomeActivity` (theme-driven layout) |
| `wasSwipe()` | Raw swipe direction, for behavior beyond the global gestures (reader page turns) |
| `hasTouch()` | Still fine anywhere: gate touch-only chrome (on-screen Cancel/OK pairs) on it |
The `wasListItemTapped` / `wasListItemTouchedDown` helpers from the button-era bridge have been removed; every themed list is a `UiListActivity` now. If you are porting a branch that still calls them, convert the screen to `UiListActivity` rather than resurrecting the helpers.
As with all input: never call the SDK `InputManager` or read GPIO directly. The HAL rule from the main guide applies to touch too.
---
+3 -1
View File
@@ -13,7 +13,7 @@ A dictionary folder must contain:
- `.syn` — synonym index (optional; maps alternate spellings and irregular forms to their headword)
- `.ifo` — metadata (optional)
Not supported: dictionaries with 64-bit index offsets (`idxoffsetbits=64` in the `.ifo` — rare, and rejected with an error), and HTML-formatted definitions render as raw markup rather than styled text.
Not supported: dictionaries with 64-bit index offsets (`idxoffsetbits=64` in the `.ifo` — rare, and rejected with an error).
## Setting Up a Dictionary
@@ -49,6 +49,8 @@ On the very first lookup with a dictionary (and again whenever the `.idx` or `.s
When a word is found, the definition screen shows the matched headword at the top and the definition text below, with a page counter for long definitions.
HTML dictionaries that declare `sametypesequence=h` use the EPUB text-layout engine for semantic formatting such as headings, bold, italics, lists, and line breaks. Images and CSS styling are ignored. Definitions that are too large or cannot be laid out within the available memory fall back to plain text.
- **Left/Right** or side **Up/Down** — previous / next page
- **Back** — return to word selection
+27 -20
View File
@@ -1,6 +1,7 @@
#include "GfxRenderer.h"
#include <BidiUtils.h>
#include <BoardConfig.h>
#include <BuildScratch.h>
#include <FontDecompressor.h>
#include <HalGPIO.h>
@@ -726,11 +727,14 @@ void GfxRenderer::drawRect(const int x, const int y, const int width, const int
// Border is inside the rectangle
void GfxRenderer::drawRect(const int x, const int y, const int width, const int height, const int lineWidth,
const bool state) const {
// Keep the border inside [x, x+width) like the thin overload: the previous
// right/bottom edges at x+width / y+height sat one pixel outside the rect,
// so stroked boxes looked shifted against fills computed from the rect.
for (int i = 0; i < lineWidth; i++) {
drawLine(x + i, y + i, x + width - i, y + i, state);
drawLine(x + width - i, y + i, x + width - i, y + height - i, state);
drawLine(x + width - i, y + height - i, x + i, y + height - i, state);
drawLine(x + i, y + height - i, x + i, y + i, state);
drawLine(x + i, y + i, x + width - 1 - i, y + i, state);
drawLine(x + width - 1 - i, y + i, x + width - 1 - i, y + height - 1 - i, state);
drawLine(x + width - 1 - i, y + height - 1 - i, x + i, y + height - 1 - i, state);
drawLine(x + i, y + height - 1 - i, x + i, y + i, state);
}
}
@@ -2220,30 +2224,33 @@ void GfxRenderer::cleanupGrayscaleWithFrameBuffer() const {
}
void GfxRenderer::getOrientedViewableTRBL(int* outTop, int* outRight, int* outBottom, int* outLeft) const {
// Board truth: the bezel insets live in the active profile (panel-native
// portrait frame); this only rotates them into the current orientation.
const BoardConfig::ViewableInsets& vi = BoardConfig::ACTIVE.viewableInsets;
switch (orientation) {
case Portrait:
*outTop = VIEWABLE_MARGIN_TOP;
*outRight = VIEWABLE_MARGIN_RIGHT;
*outBottom = VIEWABLE_MARGIN_BOTTOM;
*outLeft = VIEWABLE_MARGIN_LEFT;
*outTop = vi.top;
*outRight = vi.right;
*outBottom = vi.bottom;
*outLeft = vi.left;
break;
case LandscapeClockwise:
*outTop = VIEWABLE_MARGIN_LEFT;
*outRight = VIEWABLE_MARGIN_TOP;
*outBottom = VIEWABLE_MARGIN_RIGHT;
*outLeft = VIEWABLE_MARGIN_BOTTOM;
*outTop = vi.left;
*outRight = vi.top;
*outBottom = vi.right;
*outLeft = vi.bottom;
break;
case PortraitInverted:
*outTop = VIEWABLE_MARGIN_BOTTOM;
*outRight = VIEWABLE_MARGIN_LEFT;
*outBottom = VIEWABLE_MARGIN_TOP;
*outLeft = VIEWABLE_MARGIN_RIGHT;
*outTop = vi.bottom;
*outRight = vi.left;
*outBottom = vi.top;
*outLeft = vi.right;
break;
case LandscapeCounterClockwise:
*outTop = VIEWABLE_MARGIN_RIGHT;
*outRight = VIEWABLE_MARGIN_BOTTOM;
*outBottom = VIEWABLE_MARGIN_LEFT;
*outLeft = VIEWABLE_MARGIN_TOP;
*outTop = vi.right;
*outRight = vi.bottom;
*outBottom = vi.left;
*outLeft = vi.top;
break;
}
}
-5
View File
@@ -109,11 +109,6 @@ class GfxRenderer {
: display(halDisplay), renderMode(BW), orientation(Portrait), fadingFix(false) {}
~GfxRenderer() { freeBwBufferChunks(); }
static constexpr int VIEWABLE_MARGIN_TOP = 9;
static constexpr int VIEWABLE_MARGIN_RIGHT = 3;
static constexpr int VIEWABLE_MARGIN_BOTTOM = 3;
static constexpr int VIEWABLE_MARGIN_LEFT = 3;
// Setup
void begin(); // must be called right after display.begin()
void insertFont(int fontId, EpdFontFamily font);
+431
View File
@@ -0,0 +1,431 @@
_language_name: "Български"
_language_code: "BG"
_order: "31"
STR_CROSSPOINT: "CrossPoint"
STR_BOOTING: "ЗАРЕЖДАНЕ"
STR_SLEEPING: "СПЯЩ РЕЖИМ"
STR_ENTERING_SLEEP: "Преминаване в спящ режим"
STR_BROWSE_FILES: "Разглеждане на файлове"
STR_FILE_TRANSFER: "Прехвърляне на файлове"
STR_SETTINGS_TITLE: "Настройки"
STR_CONTINUE_READING: "Продължи четенето"
STR_RESUME: "Продължи"
STR_NO_OPEN_BOOK: "Няма отворена книга"
STR_START_READING: "Започни четене по-долу"
STR_NO_FILES_FOUND: "Не са намерени файлове"
STR_SELECT_CHAPTER: "Избери глава"
STR_NO_CHAPTERS: "Няма глави"
STR_END_OF_BOOK: "Край на книгата"
STR_EMPTY_CHAPTER: "Празна глава"
STR_INDEXING: "Индексиране"
STR_INDEX_FAILED: "Неуспешно индексиране - невалидна книга"
STR_MEMORY_ERROR: "Грешка в паметта"
STR_PAGE_LOAD_ERROR: "Грешка при зареждане на страницата"
STR_EMPTY_FILE: "Празен файл"
STR_INVALID_BMP_FILE: "Невалиден BMP файл"
STR_FILE_OPEN_FAILED: "Файлът не може да бъде отворен"
STR_OUT_OF_BOUNDS: "Извън обхвата"
STR_LOADING: "Зареждане..."
STR_LOADING_POPUP: "Зареждане"
STR_LOOKUP: "Търсене"
STR_DICT_LOOKING_UP: "Търсене..."
STR_DICT_INDEXING: "Индексиране на речника..."
STR_DICT_NOT_FOUND: "Не е намерено"
STR_DICT_NO_DICT_SET: "Не е зададен речник"
STR_DICT_ERROR: "Грешка в речника"
STR_DICT_READ_FAILED: "Определението не може да бъде прочетено"
STR_DICT_DECOMPRESS_ERROR: "Определението не може да бъде декомпресирано"
STR_DICT_LOW_MEMORY: "Недостатъчна памет"
STR_WIFI_NETWORKS: "Wi-Fi мрежи"
STR_NO_NETWORKS: "Не са намерени мрежи"
STR_NETWORKS_FOUND: "Намерени мрежи: %zu"
STR_SCANNING: "Сканиране..."
STR_FINDING_SAVED_WIFI: "Търсене на запазен Wi-Fi..."
STR_CONNECTING: "Свързване..."
STR_CONNECTING_SAVED_WIFI: "Свързване към запазен Wi-Fi..."
STR_SHOW_NETWORKS: "Покажи"
STR_CONNECTED: "Свързан!"
STR_CONNECTION_FAILED: "Неуспешна връзка"
STR_FORGET_NETWORK: "Забравяне на мрежата?"
STR_SAVE_PASSWORD: "Запазване на паролата за следващия път?"
STR_PRESS_OK_SCAN: "Натисни OK за ново сканиране"
STR_JOIN_NETWORK: "Присъединяване към мрежа"
STR_CREATE_HOTSPOT: "Създаване на hotspot"
STR_JOIN_DESC: "Свързване към съществуваща Wi-Fi мрежа"
STR_HOTSPOT_DESC: "Създаване на Wi-Fi мрежа, за други устройства"
STR_STARTING_HOTSPOT: "Стартиране на hotspot..."
STR_HOTSPOT_MODE: "Режим на hotspot"
STR_CONNECT_WIFI_HINT: "Свържи устройството си към Wi-Fi мрежа"
STR_OPEN_URL_HINT: "Отвори този URL адрес в браузъра си"
STR_OR_HTTP_PREFIX: "или http://"
STR_SCAN_QR_HINT: "или сканирай QR кода с телефона си:"
STR_CALIBRE_WIRELESS: "Calibre безжично"
STR_NETWORK_LEGEND: "* = Криптирана | + = Запазена"
STR_MAC_ADDRESS: "MAC адрес:"
STR_CHECKING_WIFI: "Проверка на Wi-Fi..."
STR_ENTER_WIFI_PASSWORD: "Въведи Wi-Fi парола"
STR_ADD_HIDDEN_NETWORK: "Добави скрита мрежа..."
STR_ENTER_WIFI_SSID: "Въведи име на мрежата (SSID)"
STR_TO_PREFIX: "към "
STR_CALIBRE_RECEIVING: "Получаване: "
STR_CALIBRE_RECEIVED: "Получено: "
STR_CALIBRE_INSTRUCTION_1: "1) Инсталирай плъгина CrossPoint Reader"
STR_CALIBRE_INSTRUCTION_2: "2) Бъди в същата Wi-Fi мрежа"
STR_CALIBRE_INSTRUCTION_3: "3) В Calibre: \"Изпрати към устройство\""
STR_CALIBRE_INSTRUCTION_4: "\"Дръж този екран отворен по време на изпращане\""
STR_CAT_DISPLAY: "Екран"
STR_CAT_READER: "Четец"
STR_CAT_CONTROLS: "Контрол"
STR_CAT_SYSTEM: "Система"
STR_SLEEP_SCREEN: "Екран при спящ режим"
STR_QUICK_RESUME_TIMEOUT: "Бързо продълж. при неактивност"
STR_SLEEP_COVER_MODE: "Мащабиране на спящ екран"
STR_HIDE_BATTERY: "Скрий % на батерията"
STR_EXTRA_SPACING: "Доп. интервал между абзаци"
STR_TEXT_AA: "Изглаждане на текста"
STR_IMAGES: "Изображения"
STR_IMAGES_DISPLAY: "Показване"
STR_IMAGES_PLACEHOLDER: "Празен слот"
STR_IMAGES_SUPPRESS: "Скриване"
STR_EOB_HOME: "Начало"
STR_EOB_CONTINUE_WITH: "Продължи с"
STR_SHORT_PWR_BTN: "Натиск. бутона за захранване"
STR_ORIENTATION: "Ориентация на четене"
STR_SIDE_BTN_LAYOUT: "Странични бутони"
STR_TOUCH_READER_CONTROLS: "Сензорно управление на четеца"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Ориентация на предните бутони"
STR_LONG_PRESS_BEHAVIOR: "Задържане на преден бутон"
STR_LONG_PRESS_BEHAVIOR_OFF: "ИЗКЛ."
STR_LONG_PRESS_BEHAVIOR_SKIP: "Пропускане на глава"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Промяна на ориентацията"
STR_LONG_PRESS_MENU: "Задържане на меню бутон"
STR_FONT_PREVIEW_TEXT: "Под южно дърво, цъфтящо в синьо, бягаше малко пухкаво зайче."
STR_FONT_FAMILY: "Шрифт на четеца"
STR_FONT_SIZE: "Размер на шрифта на четеца"
STR_TEXT_SETTINGS: "Настройки на текста"
STR_FONT: "Шрифт"
STR_SIZE: "Размер"
STR_LAYOUT: "Изглед"
STR_ALIGNMENT: "Подравняване"
STR_STYLE: "Стил"
STR_NOT_IN_PREVIEW: "Не се показва в предварителния преглед"
STR_LINE_SPACING: "Интервал между редовете"
STR_SCREEN_MARGIN: "Отстъпи на екрана"
STR_PARA_ALIGNMENT: "Подравняване на абзаца"
STR_HYPHENATION: "Разделяне на срички"
STR_TIME_TO_SLEEP: "Време до заспиване"
STR_SHOW_HIDDEN_FILES: "Показване на скритите файлове"
STR_REMOVE_READ_FROM_RECENTS: "Махни прочетените от скорошни"
STR_MOVE_FINISHED_TO_READ: "Измести завършените в папка read"
STR_REFRESH_FREQ: "Честота на опресняване"
STR_KOREADER_SYNC: "KOReader синхронизация"
STR_CHECK_UPDATES: "Проверка за актуализации"
STR_LANGUAGE: "Език"
STR_CLEAR_READING_CACHE: "Изчисти кеша на четенето"
STR_USERNAME: "Потребителско име"
STR_PASSWORD: "Парола"
STR_SYNC_SERVER_URL: "URL адрес на сървъра за синхронизация"
STR_DOCUMENT_MATCHING: "Съпоставяне на документи"
STR_SEND_METADATA: "Изпращане на метаданни на документа"
STR_SYNC_BEHAVIOR: "Поведение при синхронизация"
STR_ASK_EVERY_TIME: "Питай всеки път"
STR_SMART_SYNC: "Умна синхронизация"
STR_AUTHENTICATE: "Удостовери се"
STR_SIGN_UP: "Регистрация"
STR_CREATING_ACCOUNT: "Създаване на профил..."
STR_ACCOUNT_CREATED: "Профилът е създаден"
STR_SIGNUP_FAILED: "Неуспешна регистрация"
STR_USERNAME_TAKEN: "Потребителското име вече е регистрирано"
STR_KOREADER_USERNAME: "KOReader потребителско име"
STR_KOREADER_PASSWORD: "KOReader парола"
STR_FILENAME: "Име на файла"
STR_BINARY: "Двоичен"
STR_SET_CREDENTIALS_FIRST: "Първо задай данни за вход"
STR_WIFI_CONN_FAILED: "Неуспешна Wi-Fi връзка"
STR_AUTHENTICATING: "Удостоверяване..."
STR_AUTH_SUCCESS: "Успешно удостоверяване!"
STR_KOREADER_AUTH: "KOReader удостоверяване"
STR_SYNC_READY: "KOReader синхронизацията е готова за използване"
STR_AUTH_FAILED: "Неуспешно удостоверяване"
STR_DONE: "Готово"
STR_CLEAR_CACHE_WARNING_1: "Това ще изчисти всички кеширани данни за книгите."
STR_CLEAR_CACHE_WARNING_2: "Целият прогрес на четенето ще бъде загубен!"
STR_CLEAR_CACHE_WARNING_3: "Книгите ще трябва да бъдат преиндексирани"
STR_CLEAR_CACHE_WARNING_4: "при следващо отваряне."
STR_CLEARING_CACHE: "Изчистване на кеша..."
STR_CACHE_CLEARED: "Кешът е изчистен"
STR_ITEMS_REMOVED: "премахнати елемента"
STR_FAILED_LOWER: "неуспешно"
STR_CLEAR_CACHE_FAILED: "Неуспешно изчистване на кеша"
STR_CHECK_SERIAL_OUTPUT: "Провери серийния изход за подробности"
STR_DARK: "Тъмен"
STR_LIGHT: "Светъл"
STR_CUSTOM: "Персонализиран"
STR_COVER: "Корица"
STR_NONE_OPT: "Няма"
STR_FIT: "Побиране"
STR_CROP: "Изрязване"
STR_NEVER: "Никога"
STR_IN_READER: "В четеца"
STR_ALWAYS: "Винаги"
STR_IGNORE: "Игнорирай"
STR_SLEEP: "Спящ режим"
STR_PAGE_TURN: "Обръщане на страница"
STR_FORCE_REFRESH: "Опресни екрана"
STR_PORTRAIT: "Портрет"
STR_LANDSCAPE_CW: "Пейзаж (по часовниковата стрелка)"
STR_INVERTED: "Обърнато"
STR_ORIENTATION_INVERTED: "Портрет 180°"
STR_LANDSCAPE_CCW: "Пейзаж (срещу часовниковата стрелка)"
STR_PREV_NEXT: "Предишен/Следващ"
STR_NEXT_PREV: "Следващ/Предишен"
STR_KOSYNC: "KOSync"
STR_BOOKMARK_OPTION: "Отметка"
STR_DICTIONARY: "Речник"
STR_DISABLED: "Изключено"
STR_NOTO_SERIF: "Noto Serif"
STR_NOTO_SANS: "Noto Sans"
STR_TIGHT: "Тесен"
STR_NORMAL: "Нормален"
STR_WIDE: "Широк"
STR_JUSTIFY: "Двустранно"
STR_ALIGN_LEFT: "Ляво"
STR_CENTER: "Центрирано"
STR_ALIGN_RIGHT: "Дясно"
STR_PAGES_1: "1 страница"
STR_PAGES_5: "5 страници"
STR_PAGES_10: "10 страници"
STR_PAGES_15: "15 страници"
STR_PAGES_30: "30 страници"
STR_UPDATE: "Актуализация"
STR_CHECKING_UPDATE: "Проверка за актуализация..."
STR_NEW_UPDATE: "Налична е нова актуализация!"
STR_CURRENT_VERSION: "Текуща версия: "
STR_NEW_VERSION: "Нова версия: "
STR_UPDATING: "Актуализиране..."
STR_NO_UPDATE: "Няма налична актуализация"
STR_UPDATE_FAILED: "Неуспешна актуализация"
STR_UPDATE_COMPLETE: "Актуализацията завърши"
STR_POWER_ON_HINT: "Натисни и задръж бутона за захранване за включване"
STR_RESTARTING_HINT: "Рестартиране... Ако устройството не се рестартира, задръж бутона за захранване за няколко секунди."
STR_NO_ENTRIES: "Не са намерени записи"
STR_DOWNLOADING: "Изтегляне..."
STR_DOWNLOAD_FAILED: "Неуспешно изтегляне"
STR_ERROR_MSG: "Грешка:"
STR_UNNAMED: "Без име"
STR_HOLD_OPEN_TO_DELETE: "Задръж отворено за изтриване"
STR_NO_SERVER_URL: "Не е зададен URL адрес на сървъра"
STR_FETCH_FEED_FAILED: "Неуспешно извличане на емисията"
STR_PARSE_FEED_FAILED: "Неуспешен анализ на емисията"
STR_NEXT_PAGE: "Следваща страница »"
STR_PREV_PAGE: "« Предишна страница"
STR_NETWORK_PREFIX: "Мрежа: "
STR_IP_ADDRESS_PREFIX: "IP адрес: "
STR_ERROR_GENERAL_FAILURE: "Грешка: Обща неуспешна операция"
STR_ERROR_NETWORK_NOT_FOUND: "Грешка: Мрежата не е намерена"
STR_ERROR_CONNECTION_TIMEOUT: "Грешка: Изтекло време за връзка"
STR_SD_CARD: "SD карта"
STR_BACK: "« Назад"
STR_EXIT: "« Изход"
STR_HOME: "« Начало"
STR_SELECT: "Избери"
STR_SELECTED: "Избрано"
STR_TOGGLE: "Избор"
STR_TOGGLE_BOOKMARK: "Превключи отметка"
STR_CONFIRM: "Потвърди"
STR_CANCEL: "Отказ"
STR_CONNECT: "Свържи"
STR_OPEN: "Отвори"
STR_DOWNLOAD: "Изтегли"
STR_RETRY: "Повтори"
STR_TAP_TO_RETRY: "Докосни за нов опит"
STR_YES: "Да"
STR_NO: "Не"
STR_SHOW: "Покажи"
STR_HIDE: "Скрий"
STR_STATE_ON: "ВКЛ."
STR_STATE_OFF: "ИЗКЛ."
STR_NOT_SET: "Незададено"
STR_DIR_LEFT: "Ляво"
STR_DIR_RIGHT: "Дясно"
STR_DIR_UP: "Нагоре"
STR_DIR_DOWN: "Надолу"
STR_OK_BUTTON: "OK"
STR_KEY_SHIFT: "Shift"
STR_KEY_MODE_SYMBOLS: "?123"
STR_KEY_MODE_ABC: "abc"
STR_SLEEP_COVER_FILTER: "Филтър корица на спящ екран"
STR_FILTER_CONTRAST: "Контраст"
STR_CUSTOMISE_STATUS_BAR: "Персонализирай лентата на състоянието"
STR_CHAPTER_PAGE_COUNT: "Брой страници на главата"
STR_BOOK_PROGRESS_PERCENTAGE: "Процент прогрес на книгата"
STR_PROGRESS_BAR: "Лента за прогрес"
STR_PROGRESS_BAR_THICKNESS: "Дебелина на лентата за прогрес"
STR_PROGRESS_BAR_THIN: "Тънка"
STR_PROGRESS_BAR_MEDIUM: "Средна"
STR_PROGRESS_BAR_THICK: "Дебела"
STR_BOOK: "Книга"
STR_CHAPTER: "Глава"
STR_EXAMPLE_CHAPTER: "Глава 21"
STR_EXAMPLE_BOOK: "Заглавие на книгата"
STR_PREVIEW: "Преглед"
STR_TITLE: "Заглавие"
STR_BATTERY: "Батерия"
STR_XTC_STATUS_BAR: "XTC лента на състоянието"
STR_BOTTOM: "Долу"
STR_TOP: "Горе"
STR_CLOCK: "Часовник"
STR_CLOCK_UTC_OFFSET: "UTC отместване на часовника"
STR_CLOCK_FORMAT: "Формат на часовника"
STR_CLOCK_FORMAT_24H: "24-часов"
STR_CLOCK_FORMAT_12H: "12-часов"
STR_CURRENT_TIME: "Текущо време:"
STR_NEXT_FIELD: "Следващо"
STR_CLOCK_SYNC: "Синхронизирай часовника"
STR_CLOCK_SYNC_NOW: "Синхронизирай часовника сега"
STR_CLOCK_SYNCING: "Синхронизиране от NTP..."
STR_CLOCK_SYNC_OK: "Часовникът е синхронизиран"
STR_CLOCK_SYNC_FAIL: "Неуспешна синхронизация"
STR_CLOCK_SYNC_NO_WIFI: "Wi-Fi не е свързан"
STR_CLOCK_SYNC_NO_WIFI_HINT: "Свържи се с Wi-Fi и опитай отново."
STR_CLOCK_SYNCED: "Часовникът е синхронизиран"
STR_UI_THEME: "Тема на интерфейса"
STR_THEME_CLASSIC: "Класическа"
STR_THEME_LYRA: "Lyra"
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
STR_SUNLIGHT_FADING_FIX: "Защита от избледняване при слънце"
STR_REMAP_FRONT_BUTTONS: "Преназначаване на предните бутони"
STR_BOOKMARKS: "Отметки"
STR_BOOKMARK_ADDED: "Отметката е добавена."
STR_BOOKMARK_REMOVED: "Отметката е премахната."
STR_OPDS_BROWSER: "OPDS браузър"
STR_SEARCH: "Търсене"
STR_COVER_CUSTOM: "Корица + Персонализиран"
STR_QUICK_RESUME: "Бързо продължаване"
STR_MENU_RECENT_BOOKS: "Скорошни книги"
STR_REMOVE_FROM_RECENTS: "Премахване от скорошните книги?"
STR_NO_RECENT_BOOKS: "Няма скорошни книги"
STR_CALIBRE_DESC: "Използвай безжично прехвърляне през Calibre"
STR_FORGET_AND_REMOVE: "Забравяне на мрежата и премахване на запазената парола?"
STR_FORGET_BUTTON: "Забрави"
STR_CALIBRE_STARTING: "Стартиране на Calibre..."
STR_CALIBRE_SETUP: "Настройка"
STR_CALIBRE_STATUS: "Състояние"
STR_CLEAR_BUTTON: "Изчисти"
STR_DEFAULT_VALUE: "По подразбиране"
STR_REMAP_PROMPT: "Натисни предния бутон за всяка роля"
STR_UNASSIGNED: "Незададено"
STR_ALREADY_ASSIGNED: "Вече е зададено"
STR_REMAP_RESET_HINT: "Страничен бутон Нагоре: Рестарт на настройки"
STR_REMAP_CANCEL_HINT: "Страничен бутон Надолу: Отказ от преназначаване"
STR_HW_BACK_LABEL: "Назад (1-ви бутон)"
STR_HW_CONFIRM_LABEL: "Потвърди (2-ри бутон)"
STR_HW_LEFT_LABEL: "Ляво (3-ти бутон)"
STR_HW_RIGHT_LABEL: "Дясно (4-ти бутон)"
STR_GO_TO_PERCENT: "Отиди на %"
STR_GO_HOME_BUTTON: "Начало"
STR_SYNC_PROGRESS: "Синхронизирай прогреса"
STR_DELETE_CACHE: "Изтрий кеша на книгата"
STR_DELETE: "Изтрий"
STR_CONFIRM_DELETE_BOOKMARK: "Изтриване на тази отметка?"
STR_DISPLAY_QR: "Покажи страницата като QR код"
STR_CHAPTER_PREFIX: "Глава: "
STR_PAGES_SEPARATOR: " страници | "
STR_BOOK_PREFIX: "Книга: "
STR_CALIBRE_URL_HINT: "За Calibre добави /opds към твоя URL адрес"
STR_SYNCING_TIME: "Синхронизиране на времето..."
STR_CALC_HASH: "Изчисляване на хеш на документа..."
STR_HASH_FAILED: "Неуспешно изчисляване на хеша на документа"
STR_FETCH_PROGRESS: "Извличане на прогреса от сървъра..."
STR_UPLOAD_PROGRESS: "Качване на прогреса..."
STR_NO_CREDENTIALS_MSG: "Не са зададени данни за вход"
STR_KOREADER_SETUP_HINT: "Настрой KOReader профил в Настройки"
STR_PROGRESS_FOUND: "Прогресът е намерен!"
STR_REMOTE_LABEL: "От сървъра:"
STR_LOCAL_LABEL: "Локален:"
STR_PAGE_OVERALL_FORMAT: "Страница %d, %.2f%% общо"
STR_PAGE_TOTAL_OVERALL_FORMAT: "Страница %d/%d, %.2f%% общо"
STR_DEVICE_FROM_FORMAT: " От: %s"
STR_APPLY_REMOTE: "Приложи прогреса от сървъра"
STR_UPLOAD_LOCAL: "Качи локалния прогрес"
STR_NO_REMOTE_MSG: "Не е намерен прогрес на сървъра"
STR_UPLOAD_PROMPT: "Качване на текущата позиция?"
STR_UPLOAD_SUCCESS: "Прогресът е качен!"
STR_ALREADY_SYNCED: "Вече е синхронизиран"
STR_SYNC_FAILED_MSG: "Неуспешна синхронизация"
STR_SAVE_PROGRESS_FAILED: "Прогресът не може да бъде запазен"
STR_SECTION_PREFIX: "Раздел "
STR_UPLOAD: "Качи"
STR_BOOK_S_STYLE: "Стил на книгата"
STR_EMBEDDED_STYLE: "Вграден стил"
STR_FOCUS_READING: "Фокусирано четене"
STR_OPDS_SERVER_URL: "URL адрес на OPDS сървъра"
STR_PWR_BTN_FOOTNOTE_BACK: "Бързо връщане от бележки под линия"
STR_BACK_SHORT_TO_FILE_BROWSER: "Натиск. назад към файловия браузър"
STR_SET_SLEEP_COVER: "Задай корица"
STR_FOOTNOTES: "Бележки под линия"
STR_NO_FOOTNOTES: "Няма бележки под линия на тази страница"
STR_LINK: "[връзка]"
STR_SCREENSHOT_BUTTON: "Направи снимка на екрана"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u мин"
STR_SLEEP_NEVER: "Никога"
STR_STEP_HINT_FRONT: "Предни бутони:"
STR_STEP_HINT_SIDE: "Странични бутони:"
STR_ADD_SERVER: "Добави сървър"
STR_SERVER_NAME: "Име на сървъра"
STR_NO_SERVERS: "Няма конфигурирани OPDS сървъри"
STR_DELETE_SERVER: "Изтрий сървъра"
STR_OPDS_SERVERS: "OPDS сървъри"
STR_OPDS_DOWNLOAD_FOLDER: "Папка за изтегляне"
STR_OPDS_FILENAME_FORMAT: "Формат на името на файла"
STR_FMT_AUTHOR_TITLE: "Автор - Заглавие"
STR_FMT_TITLE_AUTHOR: "Заглавие - Автор"
STR_FMT_TITLE: "Заглавие"
STR_OPDS_SD_ROOT: "Корен на SD картата"
STR_AUTO_TURN_ENABLED: "Автоматично обръщане на страница: "
STR_AUTO_TURN_PAGES_PER_MIN: "Автоматично обръщане (страници в минута)"
STR_MANAGE_FONTS: "Управление на шрифтовете"
STR_FONT_BROWSER: "Браузър на шрифтове"
STR_LOADING_FONT_LIST: "Зареждане на списъка с шрифтове..."
STR_NO_FONTS_AVAILABLE: "Няма налични шрифтове"
STR_FONT_INSTALLED: "Шрифтът е инсталиран!"
STR_FONT_INSTALL_FAILED: "Неуспешна инсталация на шрифта"
STR_INSTALLED: "Инсталиран"
STR_DOWNLOAD_ALL: "Изтегли всички"
STR_UPDATE_ALL: "Актуализирай всички"
STR_UPDATE_AVAILABLE: "Актуализация"
STR_CRASH_TITLE: "Системен срив"
STR_CRASH_DESCRIPTION: "Подробен отчет е запазен в crash_report.txt. Моля, приложи този файл към твоя бъг репорт."
STR_CRASH_REASON: "Причина за срива:"
STR_CRASH_NO_REASON: "(Не е записана причина)"
STR_TILT_PAGE_TURN: "Обръщане на страница чрез накланяне"
STR_KB_HINT_MOVE_CURSOR: "Натисни ЛЯВО или ДЯСНО за преместване на курсора"
STR_KB_HINT_RETURN_CURSOR: "Натисни ЛЯВО за връщане към позицията на курсора"
STR_KB_HINT_HIDE_PASSWORD: "Задръж ДЯСНО и натисни [***] за скриване на паролата"
STR_KB_HINT_SHOW_PASSWORD: "Задръж ДЯСНО и натисни [abc] за показване на паролата"
STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Натисни [***] за скриване на паролата"
STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Натисни [abc] за показване на паролата"
STR_KB_HINT_EDIT_ENTRY: "Задръж НАГОРЕ за редактиране на записа"
STR_KB_TIPS: "Съвети:"
STR_KB_HINT_RETURN_KEYBOARD: "Натисни НАДОЛУ за връщане към клавиатурата"
STR_KB_HINT_EXIT_URL_MODE: "Натисни ABC за изход от URL режим"
STR_KB_HINT_CLEAR_TEXT: "Задръж DEL за изчистване на целия текст"
STR_KB_HINT_SECONDARY_CHAR: "Задръж SELECT за символи"
STR_KB_HINT_UPPER_SECONDARY: "Задръж SELECT за ГЛАВНИ БУКВИ или символи"
STR_KB_HINT_LOWER_SECONDARY: "Задръж SELECT за малки букви или символи"
STR_KB_HINT_URL_SNIPPETS: "Натисни URL за фрагменти"
STR_SD_FIRMWARE_UPDATE: "Актуализация на фърмуера чрез SD карта"
STR_SELECT_FIRMWARE_FILE: "Избери файл с фърмуер (.bin)"
STR_NO_BIN_FILES: "Не са намерени .bin файлове"
STR_VALIDATING_FIRMWARE: "Валидиране на фърмуера..."
STR_INVALID_FIRMWARE: "Невалиден файл с фърмуер"
STR_FIRMWARE_TOO_LARGE: "Фърмуерът е твърде голям за дяла"
STR_FIRMWARE_TOO_SMALL: "Файлът с фърмуер е твърде малък"
STR_FIRMWARE_UPDATE_PROMPT: "Актуализиране на фърмуера?"
STR_FIRMWARE_FILE_OPEN_FAILED: "Файлът не може да бъде отворен"
STR_FIRMWARE_WRITE_FAILED: "Неуспешен запис на фърмуера"
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Не изключвай захранването!"
STR_RECOVERY_MODE: "Режим за възстановяване"
STR_RECOVERY_MODE_HINT: "Постави firmware.bin в основната директория на SD картата и го избери"
+3
View File
@@ -273,11 +273,13 @@ STR_SUNLIGHT_FADING_FIX: "Correcció de l'esvaïment pel sol"
STR_QUICK_RESUME_TIMEOUT: "Represa ràpida per inactivitat"
STR_REMAP_FRONT_BUTTONS: "Reassigna els botons frontals"
STR_BOOKMARKS: "Punts de llibre"
STR_NO_BOOKMARKS: "No hi ha punts de llibre"
STR_BOOKMARK_ADDED: "S'ha afegit el punt de llibre."
STR_BOOKMARK_REMOVED: "S'ha eliminat el punt de llibre."
STR_OPDS_BROWSER: "Navegador OPDS"
STR_COVER_CUSTOM: "Portada + Personalitzat"
STR_QUICK_RESUME: "Represa ràpida"
STR_TRANSPARENT: "Transparent"
STR_MENU_RECENT_BOOKS: "Llibres recents"
STR_REMOVE_FROM_RECENTS: "Vols suprimir-lo de Llibres recents?"
STR_NO_RECENT_BOOKS: "No hi ha llibres recents"
@@ -422,6 +424,7 @@ STR_VALIDATING_FIRMWARE: "S'està validant el firmware..."
STR_INVALID_FIRMWARE: "Fitxer de firmware no vàlid"
STR_FIRMWARE_TOO_LARGE: "El firmware és massa gran per a la partició"
STR_FIRMWARE_TOO_SMALL: "El fitxer de firmware és massa petit"
STR_FIRMWARE_WRONG_DEVICE: "El firmware és per a un altre dispositiu"
STR_FIRMWARE_UPDATE_PROMPT: "Vols actualitzar el firmware?"
STR_FIRMWARE_FILE_OPEN_FAILED: "No es pot obrir el fitxer"
STR_FIRMWARE_WRITE_FAILED: "Ha fallat l'escriptura del firmware"
+2
View File
@@ -299,12 +299,14 @@ STR_THEME_LYRA_EXTENDED: "Lyra Extended"
STR_SUNLIGHT_FADING_FIX: "Sunlight Fading Fix"
STR_REMAP_FRONT_BUTTONS: "Remap Front Buttons"
STR_BOOKMARKS: "Bookmarks"
STR_NO_BOOKMARKS: "No bookmarks"
STR_BOOKMARK_ADDED: "Bookmark added."
STR_BOOKMARK_REMOVED: "Bookmark removed."
STR_OPDS_BROWSER: "OPDS Browser"
STR_SEARCH: "Search"
STR_COVER_CUSTOM: "Cover + Custom"
STR_QUICK_RESUME: "Quick Resume"
STR_TRANSPARENT: "Transparent"
STR_MENU_RECENT_BOOKS: "Recent Books"
STR_REMOVE_FROM_RECENTS: "Remove from Recent Books?"
STR_NO_RECENT_BOOKS: "No recent books"
+328
View File
@@ -0,0 +1,328 @@
_language_name: "Orangutan"
_language_code: "OOK"
_order: "34"
STR_CROSSPOINT: "CrossPoint"
STR_BOOTING: "OOK"
STR_SLEEPING: "Oooook"
STR_ENTERING_SLEEP: "Ook..."
STR_BROWSE_FILES: "Ook? OOK"
STR_FILE_TRANSFER: "Oook!"
STR_SETTINGS_TITLE: "OOK!"
STR_CONTINUE_READING: "Oooook ook"
STR_RESUME: "OOk!"
STR_NO_OPEN_BOOK: "OOK?"
STR_START_READING: "Ook ook"
STR_NO_FILES_FOUND: "Oook?"
STR_SELECT_CHAPTER: "Ook? Oook OOK!"
STR_NO_CHAPTERS: "Ook..."
STR_END_OF_BOOK: "Oooook."
STR_EMPTY_CHAPTER: "OOk?"
STR_INDEXING: "Oook..."
STR_LOADING: "Ooook..."
STR_LOADING_POPUP: "Oook"
STR_LOOKUP: "Ook? Oook! OOK"
STR_WIFI_NETWORKS: "Wi-Fi OOK"
STR_NO_NETWORKS: "Oook?"
STR_NETWORKS_FOUND: "%zu OOK"
STR_SCANNING: "Oook..."
STR_FINDING_SAVED_WIFI: "Oook Wi-Fi..."
STR_CONNECTING: "Ooook..."
STR_CONNECTING_SAVED_WIFI: "OOk Wi-Fi..."
STR_SHOW_NETWORKS: "OOK"
STR_CONNECTED: "Oook!"
STR_CONNECTION_FAILED: "OOk?"
STR_FORGET_NETWORK: "Oook?"
STR_SAVE_PASSWORD: "OOK?"
STR_PRESS_OK_SCAN: "OOk OK"
STR_JOIN_NETWORK: "Oook!"
STR_CREATE_HOTSPOT: "Ooook"
STR_JOIN_DESC: "Oook Wi-Fi"
STR_HOTSPOT_DESC: "Oooook Wi-Fi"
STR_STARTING_HOTSPOT: "Ooook..."
STR_HOTSPOT_MODE: "OOk!"
STR_CONNECT_WIFI_HINT: "Oook Wi-Fi"
STR_OPEN_URL_HINT: "Oook URL"
STR_SCAN_QR_HINT: "Oook QR:"
STR_CALIBRE_WIRELESS: "Calibre OOK"
STR_NETWORK_LEGEND: "* = OOK | + = OOk"
STR_MAC_ADDRESS: "MAC!"
STR_CHECKING_WIFI: "OOk Wi-Fi..."
STR_ENTER_WIFI_PASSWORD: "Oook Wi-Fi"
STR_ADD_HIDDEN_NETWORK: "Oook..."
STR_ENTER_WIFI_SSID: "OOk (SSID)"
STR_TO_PREFIX: "Oo "
STR_CALIBRE_RECEIVING: "Oook: "
STR_CALIBRE_RECEIVED: "OOK!: "
STR_CAT_DISPLAY: "OOK"
STR_CAT_READER: "OOk"
STR_CAT_CONTROLS: "Ook!"
STR_CAT_SYSTEM: "Oook"
STR_SLEEP_SCREEN: "Oooook"
STR_NIGHT_MODE: "Ook!"
STR_QUICK_RESUME_TIMEOUT: "Oook ook"
STR_SLEEP_COVER_MODE: "Ook ook"
STR_HIDE_BATTERY: "OOk %"
STR_EXTRA_SPACING: "Oook ook"
STR_TEXT_AA: "Ook-ook"
STR_IMAGES: "OOk!"
STR_IMAGES_DISPLAY: "Oook"
STR_IMAGES_PLACEHOLDER: "Ooook"
STR_IMAGES_SUPPRESS: "OOK"
STR_EOB_HOME: "Oook"
STR_EOB_CONTINUE_WITH: "Oook ook"
STR_SHORT_PWR_BTN: "Oooook OOK"
STR_ORIENTATION: "Ook? Oooook OOK"
STR_SIDE_BTN_LAYOUT: "Oook ook"
STR_TOUCH_READER_CONTROLS: "OOk! OOK"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Oook OOK"
STR_LONG_PRESS_BEHAVIOR: "Oooook OOK"
STR_LONG_PRESS_BEHAVIOR_OFF: "OFF"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Oook!"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Ook? OOK"
STR_LONG_PRESS_MENU: "Oooook!"
STR_FONT_PREVIEW_TEXT: "Ook ook ook."
STR_FONT_FAMILY: "Oook OOK"
STR_FONT_SIZE: "OOk OOK"
STR_TEXT_SETTINGS: "Oooook OOK: Ook!"
STR_FONT: "OOk!"
STR_SIZE: "OOK"
STR_LAYOUT: "Oook"
STR_ALIGNMENT: "Oooook"
STR_STYLE: "OOk!"
STR_NOT_IN_PREVIEW: "Oook?"
STR_LINE_SPACING: "Oook ook"
STR_SCREEN_MARGIN: "OOk ook"
STR_PARA_ALIGNMENT: "Ook OOK"
STR_HYPHENATION: "Ooook"
STR_TIME_TO_SLEEP: "Oook..."
STR_SHOW_HIDDEN_FILES: "OOk?"
STR_REMOVE_READ_FROM_RECENTS: "Oook OOK"
STR_MOVE_FINISHED_TO_READ: "Ook! OOK"
STR_REFRESH_FREQ: "Oook!"
STR_KOREADER_SYNC: "KOReader OOK"
STR_CHECK_UPDATES: "Oook!"
STR_LANGUAGE: "OOk?"
STR_CLEAR_READING_CACHE: "Oook OOK"
STR_USERNAME: "Oook"
STR_PASSWORD: "OOk!"
STR_SYNC_SERVER_URL: "Oook URL"
STR_DOCUMENT_MATCHING: "Oook OOK"
STR_SEND_METADATA: "Ook! OOK"
STR_SYNC_BEHAVIOR: "OOk ook"
STR_ASK_EVERY_TIME: "Oook?"
STR_SMART_SYNC: "Oook!"
STR_AUTHENTICATE: "Oooook"
STR_SIGN_UP: "OOk!"
STR_KOREADER_USERNAME: "KOReader OOK"
STR_KOREADER_PASSWORD: "KOReader OOk"
STR_FILENAME: "Oook"
STR_KOREADER_AUTH: "KOReader OOK"
STR_DONE: "Oook"
STR_DARK: "OOk!"
STR_LIGHT: "Oook"
STR_CUSTOM: "Ooook"
STR_COVER: "OOk?"
STR_NONE_OPT: "OOk!"
STR_FIT: "OOk"
STR_CROP: "OOK!"
STR_NEVER: "Oook!"
STR_IN_READER: "Ook? OOK"
STR_ALWAYS: "Oooook"
STR_IGNORE: "OOk?"
STR_SLEEP: "Oook!"
STR_PAGE_TURN: "Oook OOK"
STR_FORCE_REFRESH: "Ook! OOK"
STR_PORTRAIT: "Oook"
STR_LANDSCAPE_CW: "Oook OOK"
STR_INVERTED: "OOk!"
STR_ORIENTATION_INVERTED: "Ook 180°"
STR_LANDSCAPE_CCW: "OOk OOK"
STR_PREV_NEXT: "OOk/OOK"
STR_NEXT_PREV: "OOK/OOk"
STR_KOSYNC: "KOSync"
STR_BOOKMARK_OPTION: "Oook!"
STR_DICTIONARY: "Oooook"
STR_DISABLED: "OOk!"
STR_NOTO_SERIF: "Noto OOK"
STR_NOTO_SANS: "Noto OOk"
STR_TIGHT: "OOk!"
STR_NORMAL: "Oook"
STR_WIDE: "OOk!"
STR_JUSTIFY: "Ooook!"
STR_ALIGN_LEFT: "OOk"
STR_CENTER: "Oook"
STR_ALIGN_RIGHT: "OOK!"
STR_PAGES_1: "1 OOK"
STR_PAGES_5: "5 OOk"
STR_PAGES_10: "10 OOK"
STR_PAGES_15: "15 OOk"
STR_PAGES_30: "30 OooK"
STR_UPDATE: "Oook!"
STR_CURRENT_VERSION: "OOK: "
STR_NEW_VERSION: "OOk: "
STR_DOWNLOADING: "Oooook..."
STR_UNNAMED: "OOk?"
STR_HOLD_OPEN_TO_DELETE: "Oook OOK"
STR_NEXT_PAGE: "OOk »"
STR_PREV_PAGE: "« OOK"
STR_NETWORK_PREFIX: "OOk: "
STR_IP_ADDRESS_PREFIX: "IP: "
STR_SD_CARD: "SD OOK"
STR_BACK: "« OOK"
STR_EXIT: "« OOk"
STR_HOME: "« Oook"
STR_SELECT: "OOk!"
STR_SELECTED: "Oooook"
STR_TOGGLE: "Oook!"
STR_TOGGLE_BOOKMARK: "Ook? OOK! Oooook"
STR_CONFIRM: "Oooook"
STR_CANCEL: "OOk?"
STR_CONNECT: "Oooook"
STR_OPEN: "Oook"
STR_DOWNLOAD: "Oooook"
STR_RETRY: "Oook!"
STR_YES: "OOk"
STR_NO: "Oo"
STR_SHOW: "Oook"
STR_HIDE: "OOk!"
STR_STATE_ON: "OK"
STR_STATE_OFF: "OOk"
STR_NOT_SET: "Oook"
STR_DIR_LEFT: "OOk!"
STR_DIR_RIGHT: "OOK!"
STR_DIR_UP: "OK"
STR_DIR_DOWN: "OOk!"
STR_OK_BUTTON: "OK"
STR_KEY_SHIFT: "OOk!"
STR_KEY_MODE_SYMBOLS: "?123"
STR_KEY_MODE_ABC: "abc"
STR_SLEEP_COVER_FILTER: "Oook OOK"
STR_FILTER_CONTRAST: "Oooook"
STR_CUSTOMISE_STATUS_BAR: "Oook OOK"
STR_CHAPTER_PAGE_COUNT: "OOk OOK"
STR_BOOK_PROGRESS_PERCENTAGE: "Oook OOK"
STR_PROGRESS_BAR: "Oook OOK"
STR_PROGRESS_BAR_THICKNESS: "Oook OOK"
STR_PROGRESS_BAR_THIN: "OOk!"
STR_PROGRESS_BAR_MEDIUM: "Oook"
STR_PROGRESS_BAR_THICK: "Oook!"
STR_BOOK: "Oook"
STR_CHAPTER: "OOk!"
STR_EXAMPLE_CHAPTER: "OOk 21"
STR_EXAMPLE_BOOK: "Oook OOK"
STR_PREVIEW: "Oook!"
STR_TITLE: "OOk?"
STR_BATTERY: "Oook"
STR_XTC_STATUS_BAR: "XTC OOK"
STR_BOTTOM: "Oooook"
STR_TOP: "OOk"
STR_CLOCK: "Oook!"
STR_CLOCK_UTC_OFFSET: "Oook OOK"
STR_CLOCK_FORMAT: "OOk OOK"
STR_CLOCK_FORMAT_24H: "24-OOK"
STR_CLOCK_FORMAT_12H: "12-OOk"
STR_CURRENT_TIME: "OOk:"
STR_NEXT_FIELD: "Oook"
STR_CLOCK_SYNC: "OOk OOK"
STR_CLOCK_SYNC_NOW: "Oook OOK"
STR_CLOCK_SYNCING: "Oooook..."
STR_CLOCK_SYNC_OK: "Oook!"
STR_CLOCK_SYNC_FAIL: "OOk?"
STR_CLOCK_SYNC_NO_WIFI: "Wi-Fi OOK"
STR_CLOCK_SYNCED: "Oooook"
STR_UI_THEME: "OOk OOK"
STR_THEME_CLASSIC: "Oook!"
STR_THEME_LYRA: "OOk!"
STR_THEME_ROUNDEDRAFF: "Oook"
STR_THEME_LYRA_EXTENDED: "Oook OOK"
STR_SUNLIGHT_FADING_FIX: "Oook OOK"
STR_REMAP_FRONT_BUTTONS: "Oook OOK"
STR_BOOKMARKS: "Ook! OOK Oooook"
STR_NO_BOOKMARKS: "OOk?"
STR_BOOKMARK_ADDED: "Ook!"
STR_BOOKMARK_REMOVED: "Oook!"
STR_OPDS_BROWSER: "OPDS OOK"
STR_SEARCH: "Oook!"
STR_COVER_CUSTOM: "OOk + OOK"
STR_QUICK_RESUME: "Oook OOK"
STR_MENU_RECENT_BOOKS: "Oook OOK"
STR_REMOVE_FROM_RECENTS: "OOk?"
STR_NO_RECENT_BOOKS: "Oook?"
STR_CALIBRE_DESC: "Calibre OOK"
STR_FORGET_AND_REMOVE: "Oook?"
STR_FORGET_BUTTON: "Oook!"
STR_CALIBRE_STARTING: "Calibre OOK"
STR_CALIBRE_SETUP: "OOk!"
STR_CALIBRE_STATUS: "Oook"
STR_CLEAR_BUTTON: "Oook!"
STR_DEFAULT_VALUE: "OOk OOK"
STR_UNASSIGNED: "OOk?"
STR_ALREADY_ASSIGNED: "Oook OOK"
STR_GO_TO_PERCENT: "Oook! Ook? %"
STR_GO_HOME_BUTTON: "Oook OOK! Oooook"
STR_SYNC_PROGRESS: "Oooook OOK! Ook!"
STR_DELETE_CACHE: "Oook OOK? Oooook!"
STR_DELETE: "Oooook"
STR_CONFIRM_DELETE_BOOKMARK: "Oook?"
STR_DISPLAY_QR: "Oooook OOK QR!"
STR_CHAPTER_PREFIX: "OOk: "
STR_BOOK_PREFIX: "Oook: "
STR_SYNCING_TIME: "Oook..."
STR_FETCH_PROGRESS: "OOk..."
STR_UPLOAD_PROGRESS: "Oooook..."
STR_KOREADER_SETUP_HINT: "KOReader OOK"
STR_PROGRESS_FOUND: "Oook!"
STR_REMOTE_LABEL: "OOk:"
STR_LOCAL_LABEL: "Oook:"
STR_PAGE_OVERALL_FORMAT: "OOk %d, %.2f%%"
STR_PAGE_TOTAL_OVERALL_FORMAT: "Oook %d/%d, %.2f%%"
STR_DEVICE_FROM_FORMAT: "OOk: %s"
STR_APPLY_REMOTE: "Oook OOK"
STR_UPLOAD_LOCAL: "OOk OOK"
STR_UPLOAD_PROMPT: "Oook?"
STR_UPLOAD_SUCCESS: "Oooook!"
STR_ALREADY_SYNCED: "Oook!"
STR_SECTION_PREFIX: "OOk "
STR_UPLOAD: "Oooook"
STR_BOOK_S_STYLE: "Oook OOK"
STR_EMBEDDED_STYLE: "OOk OOK"
STR_FOCUS_READING: "Oook OOK"
STR_OPDS_SERVER_URL: "OPDS OOK"
STR_PWR_BTN_FOOTNOTE_BACK: "Oook OOK"
STR_BACK_SHORT_TO_FILE_BROWSER: "OOk OOK"
STR_SET_SLEEP_COVER: "Oook OOK"
STR_FOOTNOTES: "Ook! Oook OOK"
STR_NO_FOOTNOTES: "Oook?"
STR_LINK: "[Ook]"
STR_SCREENSHOT_BUTTON: "Oook! OOK? Ook!"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u OOK"
STR_SLEEP_NEVER: "Oook!"
STR_ADD_SERVER: "Oook!"
STR_SERVER_NAME: "OOk OOK"
STR_DELETE_SERVER: "Oook OOK"
STR_OPDS_SERVERS: "OPDS OOK"
STR_OPDS_DOWNLOAD_FOLDER: "Oook OOK"
STR_OPDS_FILENAME_FORMAT: "OOk OOK"
STR_FMT_AUTHOR_TITLE: "OOk - OOK"
STR_FMT_TITLE_AUTHOR: "OOK - OOk"
STR_FMT_TITLE: "Oook"
STR_OPDS_SD_ROOT: "SD OOK"
STR_AUTO_TURN_ENABLED: "OOk: "
STR_AUTO_TURN_PAGES_PER_MIN: "Oooook OOK (Ook!)"
STR_MANAGE_FONTS: "Oook OOK"
STR_FONT_BROWSER: "OOk OOK"
STR_LOADING_FONT_LIST: "Oooook..."
STR_NO_FONTS_AVAILABLE: "Oook?"
STR_FONT_INSTALLED: "Oook!"
STR_INSTALLED: "OOk!"
STR_DOWNLOAD_ALL: "Oook OOK"
STR_UPDATE_ALL: "OOk OOK"
STR_UPDATE_AVAILABLE: "Oook!"
STR_TILT_PAGE_TURN: "Oook OOK"
STR_SD_FIRMWARE_UPDATE: "SD Oooook Ooook Ooooook"
STR_STEP_HINT_FRONT: "Ooook Ooooook:"
STR_STEP_HINT_SIDE: "OOK Oook:"
+3
View File
@@ -297,12 +297,14 @@ STR_SUNLIGHT_FADING_FIX: "Corrección de desvanecimiento"
STR_QUICK_RESUME_TIMEOUT: "Reanudación rápida tras tiempo"
STR_REMAP_FRONT_BUTTONS: "Reconfigurar botones frontales"
STR_BOOKMARKS: "Marcadores"
STR_NO_BOOKMARKS: "No hay marcadores"
STR_BOOKMARK_ADDED: "Marcador añadido."
STR_BOOKMARK_REMOVED: "Marcador eliminado."
STR_OPDS_BROWSER: "Navegador OPDS"
STR_SEARCH: "Buscar"
STR_COVER_CUSTOM: "Portada + Pers."
STR_QUICK_RESUME: "Reanudación rápida"
STR_TRANSPARENT: "Transparente"
STR_MENU_RECENT_BOOKS: "Libros recientes"
STR_REMOVE_FROM_RECENTS: "¿Eliminar de Libros recientes?"
STR_NO_RECENT_BOOKS: "No hay libros recientes"
@@ -422,6 +424,7 @@ STR_VALIDATING_FIRMWARE: "Validando el firmware..."
STR_INVALID_FIRMWARE: "Archivo de firmware no válido"
STR_FIRMWARE_TOO_LARGE: "Firmware demasiado grande p/la partición"
STR_FIRMWARE_TOO_SMALL: "Firmware demasiado pequeño"
STR_FIRMWARE_WRONG_DEVICE: "El firmware es para otro dispositivo"
STR_FIRMWARE_UPDATE_PROMPT: "¿Actualizar el firmware?"
STR_FIRMWARE_FILE_OPEN_FAILED: "No se puede abrir el archivo"
STR_FIRMWARE_WRITE_FAILED: "Falló la escritura del firmware"
+1
View File
@@ -381,6 +381,7 @@ STR_FIRMWARE_TOO_SMALL: "Firmware dosyası çok küçük"
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Cihazı kapatmayın!"
STR_FIRMWARE_UPDATE_PROMPT: "Firmware güncellensin mi?"
STR_FIRMWARE_WRITE_FAILED: "Firmware yazılamadı"
STR_FIRMWARE_WRONG_DEVICE: "Firmware farklı bir cihaz için"
STR_FONT_BROWSER: "Yazı Tipi Tarayıcısı"
STR_FONT_INSTALLED: "Yazı tipi yüklendi!"
STR_FONT_INSTALL_FAILED: "Yazı tipi yüklenemedi"
+3
View File
@@ -278,11 +278,13 @@ STR_THEME_LYRA_EXTENDED: "Lyra Ampliat"
STR_SUNLIGHT_FADING_FIX: "Correcció de l'esvaïment pel sol"
STR_REMAP_FRONT_BUTTONS: "Reassigna els botons frontals"
STR_BOOKMARKS: "Punts de llibre"
STR_NO_BOOKMARKS: "No hi ha punts de llibre"
STR_BOOKMARK_ADDED: "S'ha afegit el punt de llibre."
STR_BOOKMARK_REMOVED: "S'ha eliminat el punt de llibre."
STR_OPDS_BROWSER: "Navegador OPDS"
STR_COVER_CUSTOM: "Portada + Personalitzat"
STR_QUICK_RESUME: "Represa ràpida"
STR_TRANSPARENT: "Transparent"
STR_MENU_RECENT_BOOKS: "Llibres recents"
STR_REMOVE_FROM_RECENTS: "Vols eliminar-lo de Llibres recents?"
STR_NO_RECENT_BOOKS: "No hi ha llibres recents"
@@ -422,6 +424,7 @@ STR_VALIDATING_FIRMWARE: "S'està validant el firmware..."
STR_INVALID_FIRMWARE: "Arxiu de firmware no vàlid"
STR_FIRMWARE_TOO_LARGE: "El firmware és massa gran per a la partició"
STR_FIRMWARE_TOO_SMALL: "L'arxiu de firmware és massa menut"
STR_FIRMWARE_WRONG_DEVICE: "El firmware és per a un altre dispositiu"
STR_FIRMWARE_UPDATE_PROMPT: "Vols actualitzar el firmware?"
STR_FIRMWARE_FILE_OPEN_FAILED: "No es pot obrir l'arxiu"
STR_FIRMWARE_WRITE_FAILED: "Ha fallat l'escriptura del firmware"
+4
View File
@@ -93,6 +93,10 @@ bool HalDisplay::isInverted() const { return einkDisplay.isInverted(); }
void HalDisplay::deepSleep() { einkDisplay.deepSleep(); }
void HalDisplay::setBusyWaitSliceHook(bool (*sliceHook)(int8_t busyPin, uint8_t busyLevel)) {
einkDisplay.setBusyWaitSliceHook(sliceHook);
}
uint8_t* HalDisplay::getFrameBuffer() const { return einkDisplay.getFrameBuffer(); }
uint8_t* HalDisplay::lendFrameBufferStorage(uint32_t* sizeOut) { return einkDisplay.lendBuildStorage(sizeOut); }
+4
View File
@@ -61,6 +61,10 @@ class HalDisplay {
// Power management
void deepSleep();
// Install the slice hook that replaces the BUSY poll delay on proven-long
// waits (see EpdBus::setBusyWaitSliceHook for the contract)
void setBusyWaitSliceHook(bool (*sliceHook)(int8_t busyPin, uint8_t busyLevel));
// Access to frame buffer
uint8_t* getFrameBuffer() const;
+56 -2
View File
@@ -6,6 +6,7 @@
#include <Wire.h>
#include <XteinkDetect.h>
#include <esp_sleep.h>
#include <soc/usb_serial_jtag_struct.h>
// Global HalGPIO instance
HalGPIO gpio;
@@ -140,11 +141,44 @@ void HalGPIO::begin() {
void HalGPIO::update() {
inputMgr.update();
const bool connected = isUsbConnected();
updateUsbState(millis());
}
void HalGPIO::updateUsbState(const unsigned long now) {
// SOF-based host-link sampling (see the member comment). A cheap register
// read, so it runs at its own short cadence on both devices and is never
// behind the I2C throttle below — a fresh enumeration must cancel light
// sleep within a poll or two, or the next slice kills the CDC link again.
if (sofLastSampleMs == 0 || now - sofLastSampleMs >= SOF_SAMPLE_MS) {
const auto sof = static_cast<uint16_t>(USB_SERIAL_JTAG.fram_num.sof_frame_index);
usbSofActive = (sof != lastSofFrameIndex);
lastSofFrameIndex = sof;
sofLastSampleMs = now;
}
// Throttle the X3's I2C-based USB detection; see USB_POLL_X3_MS. First call
// (usbLastPollMs == 0) always polls so boot state is correct. The combined
// verdict below is still recomputed every call so a SOF-detected attach is
// not held back by the throttle window.
if (usbLastPollMs == 0 || !deviceIsX3() || now - usbLastPollMs >= USB_POLL_X3_MS) {
usbLastPollMs = now;
usbElectricalConnected = isUsbElectricalConnected();
}
const bool connected = usbSofActive || usbElectricalConnected;
usbStateChanged = (connected != lastUsbConnected);
lastUsbConnected = connected;
}
void HalGPIO::pollUsbState() {
// Wait out the SOF sample floor so the comparison sees a real frame delta:
// two reads inside one USB frame compare equal and read as "no host".
const unsigned long elapsed = millis() - sofLastSampleMs;
if (sofLastSampleMs != 0 && elapsed < SOF_SAMPLE_MS) {
delay(SOF_SAMPLE_MS - elapsed);
}
updateUsbState(millis());
}
bool HalGPIO::wasUsbStateChanged() const { return usbStateChanged; }
bool HalGPIO::isPressed(uint8_t buttonIndex) const { return inputMgr.isPressed(buttonIndex); }
@@ -157,6 +191,8 @@ bool HalGPIO::wasReleased(uint8_t buttonIndex) const { return inputMgr.wasReleas
bool HalGPIO::wasAnyReleased() const { return inputMgr.wasAnyReleased(); }
bool HalGPIO::isDebouncePending() const { return inputMgr.isDebouncePending(); }
unsigned long HalGPIO::getHeldTime() const { return inputMgr.getHeldTime(); }
unsigned long HalGPIO::getPowerButtonHeldTime() const { return inputMgr.getPowerButtonHeldTime(); }
@@ -167,12 +203,18 @@ bool HalGPIO::wasTouchTap(float& nx, float& ny) const { return inputMgr.wasTouch
bool HalGPIO::wasTouchDown(float& nx, float& ny) const { return inputMgr.wasTouchPressedAt(nx, ny); }
bool HalGPIO::wasTouchReleased() const { return inputMgr.wasTouchReleased(); }
bool HalGPIO::isTouchTapCandidate(float& nx, float& ny, unsigned long& heldMs) const {
return inputMgr.isTouchTapCandidate(nx, ny, heldMs);
}
bool HalGPIO::isTouchHeldAt(float& nx, float& ny) const { return inputMgr.isTouchHeldAt(nx, ny); }
bool HalGPIO::wasTouchLongPress(float& nx, float& ny) const { return inputMgr.wasTouchLongPress(nx, ny); }
void HalGPIO::suppressTouchContact() { inputMgr.suppressTouchContact(); }
unsigned long HalGPIO::lastTouchHeldMs() const { return inputMgr.lastTouchHeldMs(); }
bool HalGPIO::wasSwipe(float& nxStart, float& nyStart, float& nxEnd, float& nyEnd) const {
@@ -185,6 +227,11 @@ void HalGPIO::setSharedConfirmPowerShortPressEmitsPower(const bool enabled) {
InputManager::setSharedConfirmPowerShortPressEmitsPower(enabled);
}
bool HalGPIO::hasEdgeSideButtons() const {
return BoardConfig::ACTIVE.board == BoardConfig::Board::XteinkX3 ||
BoardConfig::ACTIVE.board == BoardConfig::Board::XteinkX4Pro;
}
bool HalGPIO::isXteinkDevice() const {
return BoardConfig::ACTIVE.board == BoardConfig::Board::XteinkX3 ||
BoardConfig::ACTIVE.board == BoardConfig::Board::XteinkX3Uc8279 ||
@@ -233,9 +280,16 @@ bool HalGPIO::verifyPowerButtonWakeup(uint16_t requiredDurationMs, bool shortPre
}
bool HalGPIO::isUsbConnected() const {
// Recent SOF activity means an enumerated host regardless of what the
// electrical check says (false at boot until update() has sampled twice).
return usbSofActive || isUsbElectricalConnected();
}
bool HalGPIO::isUsbElectricalConnected() const {
if (deviceIsX3()) {
// X3: infer USB/charging via BQ27220 Current() register (0x0C, signed mA).
// Positive current means charging.
// Positive current means charging. Misses a data-only cable and a full
// battery — the SOF check in update() covers those.
for (uint8_t attempt = 0; attempt < 2; ++attempt) {
int16_t currentMa = 0;
if (X3GPIO::readBQ27220CurrentMA(&currentMa)) {
+65
View File
@@ -45,6 +45,35 @@ class HalGPIO {
bool lastUsbConnected = false;
bool usbStateChanged = false;
unsigned long usbLastPollMs = 0;
bool usbElectricalConnected = false; // last result of the per-device electrical/charge check
// X3 USB detection is a BQ27220 I2C read (~0.3-1 ms of awake CPU per call);
// polled every loop it costs a few percent of the light-sleep idle floor for
// nothing. At >=1 s intervals the energy cost is unmeasurable (~µC/s), so 1 s
// is chosen for prompt plug/unplug UX (battery icon, light-sleep USB guard /
// CDC recovery). X4 detection is a single digitalRead and stays per-loop.
static constexpr unsigned long USB_POLL_X3_MS = 1000;
// USB-Serial-JTAG SOF activity, sampled by update(): the host sends a SOF
// frame every 1 ms while the bus is enumerated, so a frame index that moved
// between two samples means a live host link. Catches what the charge-based
// X3 check misses: a data-only cable, and any cable once the battery is full
// (charge current ~0). Samples must be >SOF_SAMPLE_MS apart — update() can be
// called back-to-back (inner input loops), and adjacent reads would compare
// equal and flicker the verdict.
uint16_t lastSofFrameIndex = 0;
unsigned long sofLastSampleMs = 0;
bool usbSofActive = false;
static constexpr unsigned long SOF_SAMPLE_MS = 10;
// Per-device electrical/charge-inference USB check (fresh read; X3 = BQ27220
// charge current over I2C, X4 = VBUS-driven level on GPIO20).
bool isUsbElectricalConnected() const;
// Shared body of update()/pollUsbState(): SOF sampling + throttled electrical
// check + combined-verdict edge tracking.
void updateUsbState(unsigned long now);
public:
enum class DeviceType : uint8_t { X4, X3 };
@@ -60,6 +89,12 @@ class HalGPIO {
inline bool deviceIsX4() const { return _deviceType == DeviceType::X4; }
bool isXteinkDevice() const;
// True when the board's page buttons sit on the left/right screen edges
// (X3, X4 Pro) rather than an off-screen vertical rocker. Drives side-hint
// placement and the flipped large-step direction in selection activities.
// Keyed off the active BoardConfig profile, not the X3/X4 runtime detection.
bool hasEdgeSideButtons() const;
// Start button GPIO and setup SPI for screen and SD card
void begin();
@@ -70,13 +105,30 @@ class HalGPIO {
bool wasAnyPressed() const;
bool wasReleased(uint8_t buttonIndex) const;
bool wasAnyReleased() const;
// True while a raw button-state change is still inside the debounce window.
// The idle loop polls fast while this is set so the confirming sample lands
// ~10 ms after the first; at the 50 ms light-sleep cadence a short tap can
// otherwise appear in a single sample and never commit (dropped press).
bool isDebouncePending() const;
unsigned long getHeldTime() const;
unsigned long getPowerButtonHeldTime() const;
bool hasTouch() const;
bool wasTouchTap(float& nx, float& ny) const;
bool wasTouchDown(float& nx, float& ny) const;
// Raw release edge, reported even when the contact was not a tap (swipe end,
// drag-off). Snapshot builders forward it so interaction routing can clear
// pressed state.
bool wasTouchReleased() const;
bool isTouchTapCandidate(float& nx, float& ny, unsigned long& heldMs) const;
bool isTouchHeldAt(float& nx, float& ny) const;
// One-shot long-press, fired by the SDK classifier while the finger is still
// down (stationary contact held past its threshold). Position = touch-down
// point. Callers that act on it should suppressTouchContact() so the lift
// cannot also tap.
bool wasTouchLongPress(float& nx, float& ny) const;
// Ignore the remainder of the current contact (its continued hold and its
// release edge). Self-clears once the contact ends.
void suppressTouchContact();
unsigned long lastTouchHeldMs() const;
bool wasSwipe(float& nxStart, float& nyStart, float& nxEnd, float& nyEnd) const;
bool wasTouchActivity() const;
@@ -90,6 +142,19 @@ class HalGPIO {
// Check if USB is connected
bool isUsbConnected() const;
// Sample USB state without a full input update. Called during setup() BEFORE
// the first e-ink refresh: the boot paint happens before loop() ever runs
// update(), so without this the light-sleep slice guards see the unsampled
// default ("no USB") and sleeping through the boot refresh kills a live CDC
// link (charge-based X3 detection also reads false whenever the battery is
// full). Two calls >=SOF_SAMPLE_MS apart establish the SOF verdict; the
// method itself waits out the floor if called too soon after the last sample.
void pollUsbState();
// USB state as sampled by the last update() call.
// Prefer this in per-loop polling: isUsbConnected() performs a fresh I2C read on X3.
bool isUsbConnectedCached() const { return lastUsbConnected; }
// Returns true once per edge (plug or unplug) since the last update()
bool wasUsbStateChanged() const;
Loaded 20 of 124 files, more files were not shown because too many files have changed in this diff. Show more