Compare commits

...
18 Commits
Author SHA1 Message Date
Uri Tauber 5ba1d5747f fix: EndOfBookOptions fails to compile
ActivityManager.h forward-declares Activity but holds
std::unique_ptr<Activity> members. Instantiating that unique_ptr's
destructor requires the complete type, so any TU including
ActivityManager.h without Activity.h fails to compile.

Include Activity.h directly. Adding it to ActivityManager.h instead does
not work: Activity.h depends on HomeMenuItem, which ActivityManager.h
defines.
2026-07-19 16:45:42 +03:00
Thomas Symalla 9fe4dc5e38 feat: Add option to switch behavior for "back to browser / home" in Reader activity (#2366)
## Summary

It would be nice to switch back to the file list from an Reader activity
via a short back button press. This change adds an Reader option to
switch the default behavior, so a short back button press in the Reader
activity can now go back to the file list, and a long press on back goes
back to the home view. This does a fair bit of refactoring, introducing
a new constant for the ms limit.

* **What changes are included?**

- Changes to the translation
- Additional global Reader option 
- Refactoring of the back button behavior in the Reader activity
2026-07-19 08:29:13 +03:00
Justin Mitchell b1d037569b feat: Add kosync user registration and switch to crosspoint-sync server (#2587)
Implements createUser() endpoint to allow account creation via the
KOSync protocol. Changes default sync server from sync.koreader.rocks to
sync.crosspointreader.com with migration logic to preserve existing
users' server settings. Extends sync protocol to include position data
(spine index, page numbers, xpath) that crosspoint-sync supports while
remaining compatible with standard kosync servers.
2026-07-18 14:50:51 -04:00
Julia 9737cb335c fix: correct the settings enums for "blank" and "cover + custom" sleep screens (#2635) 2026-07-17 12:35:51 -04:00
Phạm Bình An fdffc2e5d9 fix: reduce CSS parse-time OOM risk in chapter layout (#2606) 2026-07-16 07:21:08 +03:00
a2db43d235 feat: enable CORS headers in the HTTP API (#2594)
Closes #2558.

Enables the Arduino WebServer's built-in CORS support
(`enableCORS(true)`), which adds
`Access-Control-Allow-Origin/Methods/Headers: *` to every response, and
answers preflight `OPTIONS` requests with `204` in `handleNotFound()` —
routes are registered per-method, so OPTIONS always lands there. The
AP-mode captive-portal redirect is untouched (the OPTIONS check runs
before it, and browsers don't send preflights for captive-portal
probes).

This lets web-based clients and PWAs served from other origins call the
JSON API (`/api/status`, `/api/files`, `/api/settings`, ...) directly
from the browser.

Overhead is three static response headers; no behavior change for the
built-in web UI.

Note: not yet tested on hardware.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: metoli <metoli@metoli-Mac-mini.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 18:35:10 -04:00
rxmmahandkira 63093e606c feat: Back on home menu opens the most recent book (#2619)
Co-authored-by: kira <rammah@tuta.io>
2026-07-15 23:50:22 +03:00
a4ac3b2788 feat: add smart KOReader progress sync (#2192)
## Summary

Make KOReader progress sync a one-click flow for common cases while
keeping the existing manual mode available.

### Changes

- Add a **Sync Behavior** setting:
  - **Smart sync** for new configurations
  - **Ask every time** for manual control
- Preserve **Ask every time** when migrating an existing credential file
that predates this setting.
- In Smart mode:
  - upload local progress when no remote record exists;
  - show a short confirmation when already synced;
  - upload when local progress is further ahead;
  - apply remote progress when remote progress is further ahead.
- Probe both KOReader document-matching hashes before making the Smart
decision, while keeping uploads on the user's configured matching
method.
- Auto-return after successful Smart terminal states, with Back/Confirm
still available.
- Persist the setting alongside the current credential, matching-method,
and send-metadata fields.
- Document both behaviors in the user guide.

## Additional context

This pairs well with #2189 (smart Wi-Fi auto-connect), but does not
depend on it. It does not add background Wi-Fi or passive network
detection; sync is still triggered by the user from the reader menu.

Smart sync uses the furthest progress because CrossPoint does not
currently persist local progress timestamps. Users who prefer explicit
conflict resolution can select **Ask every time**.

## Verification

- `git diff --check`
- `platformio check --fail-on-defect low --fail-on-defect medium
--fail-on-defect high` — no defects
- `platformio run -e default` — firmware build successful
- The original implementation was manually smoke-tested on an X4 device.

---

### AI Usage

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

AI tools were used to inspect the codebase, draft and review the
implementation, resolve the rebase against current `develop`, and
prepare the PR text. The resulting firmware was built locally.

---------

Co-authored-by: Alexander Hoffer <git@alexanderhoffer.com>
Co-authored-by: Alexander Hoffer <contact@alexanderhoffer.com>
2026-07-15 14:59:15 -04:00
Víctor Fernández 35a45f9c1e feat: add options to remember web upload settings & rename ebooks to {title} - {author} (#2534) 2026-07-15 14:43:42 -04:00
William Floyd f3cad2f9e1 fix(kosync): reload Epub before reading upload metadata (#2608) 2026-07-15 11:46:10 -04:00
Oscar Nogueira Neto 95d8cb712b feat: configurable OPDS download folder and filename format (#2571) 2026-07-15 10:53:27 -04:00
Mirza Arnaut b9867b0d09 feat(i18n): add Bosnian translation (#2616) 2026-07-15 10:49:42 -04:00
Uri Tauber 3dac4446b7 fix: remove duplicate sleep logic (#2492) 2026-07-15 10:48:08 -04:00
Uri Tauber f180069643 fix: ignore open-x4-sdk and fs_ (#2609) 2026-07-14 21:34:28 +03:00
Husam Younis a0ec5812b1 feat: Arabic translation YAML - PR 3/3 (#2599) 2026-07-14 20:33:42 +03:00
Phạm Bình An e84840b473 fix(css-parser): don't save unusable rules to RAM (#2604) 2026-07-14 17:00:13 +03:00
Tom-Inge Larsen 94ae750e89 feat(i18n): add Norwegian Bokmål translation (#2113) 2026-07-14 16:54:43 +03:00
metoli86andmetoli 8c5cd67f38 feat: complete Turkish translation (390/390) (#2592)
Co-authored-by: metoli <metoli@metoli-Mac-mini.local>
2026-07-14 16:50:03 +03:00
67 changed files with 2534 additions and 265 deletions
+2
View File
@@ -2,6 +2,8 @@
.idea .idea
.DS_Store .DS_Store
.vscode .vscode
open-x4-sdk
fs_
lib/EpdFont/fontsrc lib/EpdFont/fontsrc
lib/I18n/I18nKeys.h lib/I18n/I18nKeys.h
lib/I18n/I18nStrings.h lib/I18n/I18nStrings.h
+40 -27
View File
@@ -28,8 +28,10 @@ Welcome to the **CrossPoint** firmware. This guide outlines the hardware control
- [3.6.5 OPDS Servers (Multiple Libraries)](#365-opds-servers-multiple-libraries) - [3.6.5 OPDS Servers (Multiple Libraries)](#365-opds-servers-multiple-libraries)
- [3.6.6 Web Settings (Wi-Fi + OPDS)](#366-web-settings-wi-fi--opds) - [3.6.6 Web Settings (Wi-Fi + OPDS)](#366-web-settings-wi-fi--opds)
- [3.6.7 KOReader Sync Quick Setup](#367-koreader-sync-quick-setup) - [3.6.7 KOReader Sync Quick Setup](#367-koreader-sync-quick-setup)
- [Option A: Free Public Server (`sync.koreader.rocks`)](#option-a-free-public-server-synckoreaderrocks) - [Option A: CrossPoint Sync Server (`sync.crosspointreader.com`, default)](#option-a-crosspoint-sync-server-synccrosspointreadercom-default)
- [Option B: Self-Hosted Server (Docker Compose)](#option-b-self-hosted-server-docker-compose) - [Option B: Legacy Public KOReader Server (`sync.koreader.rocks`)](#option-b-legacy-public-koreader-server-synckoreaderrocks)
- [Option C: Self-Hosted Server (Docker Compose)](#option-c-self-hosted-server-docker-compose)
- [Syncing While Reading](#syncing-while-reading)
- [3.7 Sleep Screen](#37-sleep-screen) - [3.7 Sleep Screen](#37-sleep-screen)
- [Cover settings](#cover-settings) - [Cover settings](#cover-settings)
- [Custom images](#custom-images) - [Custom images](#custom-images)
@@ -297,7 +299,7 @@ The Settings screen allows you to configure the device's behavior. There are a f
- **Wi-Fi Networks**: Connect to Wi-Fi networks for file transfers and firmware updates. - **Wi-Fi Networks**: Connect to Wi-Fi networks for file transfers and firmware updates.
- **KOReader Sync**: Options for setting up KOReader for syncing book progress. - **KOReader Sync**: Options for setting up KOReader for syncing book progress. **Smart sync** is the default for new configurations and auto-resolves simple push/pull decisions. Existing credential files retain **Ask every time** when migrated; you can switch Sync Behavior at any time if you prefer manual confirmation.
- **OPDS Servers**: Manage one or more OPDS [(Open Publication Distribution System)](https://en.wikipedia.org/wiki/Open_Publication_Distribution_System) libraries for browsing and downloading books. See [OPDS Servers (Multiple Libraries)](#365-opds-servers-multiple-libraries) below. - **OPDS Servers**: Manage one or more OPDS [(Open Publication Distribution System)](https://en.wikipedia.org/wiki/Open_Publication_Distribution_System) libraries for browsing and downloading books. See [OPDS Servers (Multiple Libraries)](#365-opds-servers-multiple-libraries) below.
@@ -360,9 +362,37 @@ Behavior notes:
CrossPoint can sync reading progress with KOReader-compatible sync servers. CrossPoint can sync reading progress with KOReader-compatible sync servers.
It also interoperates with KOReader apps/devices when they use the same server and credentials. It also interoperates with KOReader apps/devices when they use the same server and credentials.
##### Option A: Free Public Server (`sync.koreader.rocks`) ##### Option A: CrossPoint Sync Server (`sync.crosspointreader.com`, default)
1. Register a user once (only if needed): When **Sync Server URL** is left empty, CrossPoint uses the free CrossPoint sync server at `https://sync.crosspointreader.com`. It speaks the standard KOReader sync protocol (so KOReader apps can use it too) and additionally stores an exact spine/page position for lossless CrossPoint-to-CrossPoint sync.
1. On each CrossPoint device:
- Go to **Settings -> System -> KOReader Sync**.
- Set **Username** and **Password** (enter the plain password; CrossPoint computes MD5 internally, and use the same values on all devices).
- Leave **Sync Server URL** empty (or set it to `https://sync.crosspointreader.com`).
- On the first device, run **Sign Up** once to create the account directly from the device. On every other device, just run **Authenticate**.
Accounts are per server. Existing `sync.koreader.rocks` credentials do not exist on the CrossPoint server; either sign up again with the same username/password or use Option B to keep using the legacy server.
##### Option B: Legacy Public KOReader Server (`sync.koreader.rocks`)
Use this if you already sync KOReader devices against the official public server.
1. On each CrossPoint device:
- Go to **Settings -> System -> KOReader Sync**.
- Set **Sync Server URL** to `https://sync.koreader.rocks` (required; an empty URL now points at the CrossPoint server instead).
- Set **Username** and **Password** to your existing KOReader Sync credentials.
- Run **Authenticate**.
2. If you do not have an account yet, run **Sign Up** on the device, or register once with curl:
```bash ```bash
USERNAME="user" USERNAME="user"
@@ -375,27 +405,9 @@ curl -i "https://sync.koreader.rocks/users/create" \
--data "{\"username\":\"$USERNAME\",\"password\":\"$PASSWORD_MD5\"}" --data "{\"username\":\"$USERNAME\",\"password\":\"$PASSWORD_MD5\"}"
``` ```
Already have KOReader Sync credentials? Skip registration; basic sync only requires using the same existing username/password on all devices.
When this returns `HTTP 402` with `{"code":2002,"message":"Username is already registered."}`, pick a different username or use that existing account. When this returns `HTTP 402` with `{"code":2002,"message":"Username is already registered."}`, pick a different username or use that existing account.
2. On each CrossPoint device: ##### Option C: Self-Hosted Server (Docker Compose)
- Go to **Settings -> System -> KOReader Sync**.
- Set **Username** and **Password** (enter the plain password; CrossPoint computes MD5 internally, and use the same values on all devices).
- Set **Sync Server URL** to `https://sync.koreader.rocks`, or leave it empty (both use the same default KOReader sync server).
- Run **Authenticate**.
3. While reading, press **Confirm** to open the reader menu, then select **Sync Progress**.
- Choose **Apply Remote** to jump to remote progress.
- Choose **Upload Local** to push current progress.
##### Option B: Self-Hosted Server (Docker Compose)
1. Start a sync server: 1. Start a sync server:
@@ -468,11 +480,12 @@ If this returns `HTTP 402` with `{"code":2002,"message":"Username is already reg
If you use the HTTPS listener, use `https://<server-ip>:7200` (`curl -k` only for self-signed certificate testing). If you use the HTTPS listener, use `https://<server-ip>:7200` (`curl -k` only for self-signed certificate testing).
5. While reading, press **Confirm** to open the reader menu, then select **Sync Progress**. ##### Syncing While Reading
- Choose **Apply Remote** to jump to remote progress. Once any of the options above is set up, press **Confirm** while reading to open the reader menu, then select **Sync Progress**. Alternatively, set **Settings -> Controls -> Long-press Menu** to **KOSync** and hold Confirm to launch sync directly.
- Choose **Upload Local** to push current progress. - With **Sync Behavior** set to **Ask every time**, choose **Apply Remote** to jump to remote progress or **Upload Local** to push current progress.
- With **Sync Behavior** set to **Smart sync**, CrossPoint auto-resolves simple cases: upload when no remote progress exists, confirm and leave both unchanged when local and remote progress are already synchronized, upload when local progress is further ahead, or apply remote when remote progress is further ahead.
### 3.7 Sleep Screen ### 3.7 Sleep Screen
+25 -23
View File
@@ -287,7 +287,30 @@ void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle,
effectiveNoSpaceBefore = true; effectiveNoSpaceBefore = true;
} }
const auto ensureTokenCapacity = [&](const size_t additionalTokens) {
if (additionalTokens == 0) return;
const size_t requiredSize = words.size() + additionalTokens;
if (words.capacity() >= requiredSize) return;
size_t newCapacity = words.capacity();
if (newCapacity < 16) {
newCapacity = 16;
}
while (newCapacity < requiredSize) {
newCapacity *= 2;
}
words.reserve(newCapacity);
wordStyles.reserve(newCapacity);
wordContinues.reserve(newCapacity);
wordNoSpaceBefore.reserve(newCapacity);
wordIsFocusSuffix.reserve(newCapacity);
};
if (auto breakOffsets = cjkCharacterBreakByteOffsets(word); !breakOffsets.empty()) { if (auto breakOffsets = cjkCharacterBreakByteOffsets(word); !breakOffsets.empty()) {
// CJK-heavy paragraphs can push hundreds of tiny tokens quickly when CSS toggles
// inline styles. Reserve once up front to avoid repeated vector growth reallocations.
ensureTokenCapacity(breakOffsets.size() + 1);
bool firstToken = true; bool firstToken = true;
size_t tokenStart = 0; size_t tokenStart = 0;
for (const size_t breakOffset : breakOffsets) { for (const size_t breakOffset : breakOffsets) {
@@ -326,29 +349,8 @@ void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle,
// --- FOCUS READING LOGIC BELOW --- // --- FOCUS READING LOGIC BELOW ---
// Pre-reserve capacity to prevent mid-word heap reallocations. // Worst case: a segment boundary on each byte (highly punctuated UTF-8 text).
size_t maxPossibleNewTokens = word.length(); ensureTokenCapacity(word.length());
size_t requiredSize = words.size() + maxPossibleNewTokens;
if (words.capacity() < requiredSize) {
// Emulate standard geometric growth (doubling) to ensure we don't reallocate on every word.
size_t newCapacity = words.capacity() * 2;
// Ensure the doubled capacity is actually enough for this specific word
if (newCapacity < requiredSize) {
newCapacity = requiredSize;
}
// Set a sensible minimum starting size so the first few words don't trigger tiny reallocations
if (newCapacity < 16) {
newCapacity = 16;
}
words.reserve(newCapacity);
wordStyles.reserve(newCapacity);
wordContinues.reserve(newCapacity);
wordNoSpaceBefore.reserve(newCapacity);
wordIsFocusSuffix.reserve(newCapacity);
}
// Lambda helper to process and push individual sub-segments of the string // Lambda helper to process and push individual sub-segments of the string
// Use std::string_view to avoid heap allocations when slicing // Use std::string_view to avoid heap allocations when slicing
+5
View File
@@ -431,6 +431,11 @@ CssStyle CssParser::parseDeclarations(std::string_view declBlock) {
// Rule processing // Rule processing
void CssParser::processRuleBlockWithStyle(std::string_view selectorGroup, const CssStyle& style) { void CssParser::processRuleBlockWithStyle(std::string_view selectorGroup, const CssStyle& style) {
// Skip rules that don't define any supported properties to save RAM.
if (!style.defined.anySet()) {
return;
}
// Check if we've reached the rule limit before processing // Check if we've reached the rule limit before processing
if (rulesBySelector_.size() >= MAX_RULES) { if (rulesBySelector_.size() >= MAX_RULES) {
LOG_DBG("CSS", "Reached max rules limit (%zu), stopping CSS parsing", MAX_RULES); LOG_DBG("CSS", "Reached max rules limit (%zu), stopping CSS parsing", MAX_RULES);
+1 -1
View File
@@ -33,7 +33,7 @@
class CssParser { class CssParser {
public: public:
// Bump when CSS cache format or rules change; section caches are invalidated when this changes // Bump when CSS cache format or rules change; section caches are invalidated when this changes
static constexpr uint8_t CSS_CACHE_VERSION = 7; static constexpr uint8_t CSS_CACHE_VERSION = 8;
explicit CssParser(std::string cachePath) : cachePath(std::move(cachePath)) {} explicit CssParser(std::string cachePath) : cachePath(std::move(cachePath)) {}
~CssParser() = default; ~CssParser() = default;
@@ -22,6 +22,17 @@
constexpr size_t MIN_SIZE_FOR_POPUP = 10 * 1024; // 10KB constexpr size_t MIN_SIZE_FOR_POPUP = 10 * 1024; // 10KB
constexpr size_t PARSE_BUFFER_SIZE = 1024; constexpr size_t PARSE_BUFFER_SIZE = 1024;
// This number comes from PR #73
// If we have > 750 words buffered up, perform the layout and consume out all but the last line
// There should be enough here to build out 1-2 full pages and doing this will free up a lot of
// memory.
// Spotted when reading Intermezzo, there are some really long text blocks in there.
constexpr size_t TEXT_BLOCK_SOFT_FLUSH_WORDS = 750;
// When CSS is enabled, flush earlier to save RAM. 320 is still more than enough to build a CJK
// page at font size 14
constexpr size_t TEXT_BLOCK_SOFT_FLUSH_WORDS_WITH_CSS = 320;
// Hard cap on the number of anchor IDs recorded per chapter. Legitimate navigation // Hard cap on the number of anchor IDs recorded per chapter. Legitimate navigation
// anchors (TOC entries, footnotes, cross-references) rarely exceed a few hundred per // anchors (TOC entries, footnotes, cross-references) rarely exceed a few hundred per
// chapter. A runaway count usually means a converter injected machine-generated IDs on // chapter. A runaway count usually means a converter injected machine-generated IDs on
@@ -1155,12 +1166,14 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char
self->partWordBuffer[self->partWordBufferIndex++] = s[i]; self->partWordBuffer[self->partWordBufferIndex++] = s[i];
} }
// If we have > 750 words buffered up, perform the layout and consume out all but the last line // Keep token growth bounded: CSS-heavy spans can fragment text into many tiny
// There should be enough here to build out 1-2 full pages and doing this will free up a lot of // words, so flush earlier when embedded CSS is active. We still keep the
// memory. // "exclude last line" behavior to preserve paragraph flow across chunks.
// Spotted when reading Intermezzo, there are some really long text blocks in there. const size_t blockWordCount = self->currentTextBlock->size();
if (self->currentTextBlock->size() > 750) { const size_t softFlushThreshold =
LOG_DBG("EHP", "Text block too long, splitting into multiple pages"); self->embeddedStyle ? TEXT_BLOCK_SOFT_FLUSH_WORDS_WITH_CSS : TEXT_BLOCK_SOFT_FLUSH_WORDS;
if (blockWordCount > softFlushThreshold) {
LOG_DBG("EHP", "Text block soft flush (%u words)", static_cast<unsigned>(blockWordCount));
const int horizontalInset = self->currentTextBlock->getBlockStyle().totalHorizontalInset(); const int horizontalInset = self->currentTextBlock->getBlockStyle().totalHorizontalInset();
const uint16_t effectiveWidth = (horizontalInset < self->viewportWidth) const uint16_t effectiveWidth = (horizontalInset < self->viewportWidth)
? static_cast<uint16_t>(self->viewportWidth - horizontalInset) ? static_cast<uint16_t>(self->viewportWidth - horizontalInset)
+394
View File
@@ -0,0 +1,394 @@
_language_name: "العربية"
_language_code: "AR"
_order: "28"
STR_CROSSPOINT: "CrossPoint"
STR_BOOTING: "جاري بدء التشغيل"
STR_SLEEPING: "وضع السكون"
STR_ENTERING_SLEEP: "جاري الدخول في وضع السكون"
STR_BROWSE_FILES: "تصفح الملفات"
STR_FILE_TRANSFER: "نقل الملفات"
STR_SETTINGS_TITLE: "الإعدادات"
STR_CONTINUE_READING: "متابعة القراءة"
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_OUT_OF_BOUNDS: "خارج النطاق"
STR_LOADING: "جاري التحميل..."
STR_LOADING_POPUP: "جاري التحميل"
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: "اضغط موافق لإعادة البحث"
STR_JOIN_NETWORK: "الانضمام إلى شبكة"
STR_CREATE_HOTSPOT: "إنشاء نقطة اتصال"
STR_JOIN_DESC: "الاتصال بشبكة Wi-Fi موجودة"
STR_HOTSPOT_DESC: "إنشاء شبكة Wi-Fi يمكن للآخرين الانضمام إليها"
STR_STARTING_HOTSPOT: "جاري تشغيل نقطة الاتصال..."
STR_HOTSPOT_MODE: "وضع نقطة الاتصال"
STR_CONNECT_WIFI_HINT: "قم بتوصيل جهازك بشبكة Wi-Fi هذه"
STR_OPEN_URL_HINT: "افتح هذا العنوان في المتصفح"
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: "أدخل كلمة مرور الشبكة"
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_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_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: "نقل الكتب المنتهية إلى مجلد المقروءة"
STR_REFRESH_FREQ: "معدل تحديث الشاشة"
STR_KOREADER_SYNC: "مزامنة KOReader"
STR_CHECK_UPDATES: "التحقق من التحديثات"
STR_LANGUAGE: "اللغة"
STR_CLEAR_READING_CACHE: "مسح ذاكرة القراءة المؤقتة"
STR_USERNAME: "اسم المستخدم"
STR_PASSWORD: "كلمة المرور"
STR_SYNC_SERVER_URL: "عنوان خادم المزامنة"
STR_DOCUMENT_MATCHING: "مطابقة المستندات"
STR_SEND_METADATA: "إرسال البيانات الوصفية للمستند"
STR_AUTHENTICATE: "تسجيل الدخول"
STR_KOREADER_USERNAME: "اسم مستخدم KOReader"
STR_KOREADER_PASSWORD: "كلمة مرور KOReader"
STR_FILENAME: "اسم الملف"
STR_BINARY: "ثنائي"
STR_SET_CREDENTIALS_FIRST: "أدخل بيانات الاعتماد أولا"
STR_WIFI_CONN_FAILED: "فشل الاتصال بالشبكة"
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_DISABLED: "معطل"
STR_NOTO_SERIF: "Noto Serif"
STR_NOTO_SANS: "Noto Sans"
STR_SMALL: "صغير"
STR_MEDIUM: "متوسط"
STR_LARGE: "كبير"
STR_X_LARGE: "كبير جدا"
STR_TIGHT: "ضيق"
STR_NORMAL: "عادي"
STR_WIDE: "واسع"
STR_JUSTIFY: "ضبط الطرفين"
STR_ALIGN_LEFT: "يسار"
STR_CENTER: "وسط"
STR_ALIGN_RIGHT: "يمين"
STR_PAGES_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: "لم يتم تحديد عنوان الخادم"
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_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: "موافق"
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 موسع"
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 إلى العنوان"
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_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: "عنوان خادم OPDS"
STR_PWR_BTN_FOOTNOTE_BACK: "عودة سريعة من الحواشي"
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_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: "اضغط مطولا على زر الاختيار للحرف الثانوي"
STR_KB_HINT_UPPER_SECONDARY: "اضغط مطولا على زر الاختيار للحرف الكبير أو الثانوي"
STR_KB_HINT_LOWER_SECONDARY: "اضغط مطولا على زر الاختيار للحرف الصغير أو الثانوي"
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 ثم اختره"
+5
View File
@@ -298,6 +298,11 @@ STR_SLEEP_TIMER_VALUE_FORMAT: "%u хв"
STR_SLEEP_NEVER: "Ніколі" STR_SLEEP_NEVER: "Ніколі"
STR_STEP_HINT_FRONT: "Пярэднія кнопкі:" STR_STEP_HINT_FRONT: "Пярэднія кнопкі:"
STR_STEP_HINT_SIDE: "Бакавыя кнопкі:" STR_STEP_HINT_SIDE: "Бакавыя кнопкі:"
STR_OPDS_DOWNLOAD_FOLDER: "Папка для спампоўкі"
STR_OPDS_FILENAME_FORMAT: "Фармат назвы файла"
STR_FMT_AUTHOR_TITLE: "Аўтар - Назва"
STR_FMT_TITLE_AUTHOR: "Назва - Аўтар"
STR_FMT_TITLE: "Назва"
STR_AUTO_TURN_ENABLED: "Аўтаперагортванне: " STR_AUTO_TURN_ENABLED: "Аўтаперагортванне: "
STR_AUTO_TURN_PAGES_PER_MIN: "Аўтаперагортванне (старонак за хвіліну)" STR_AUTO_TURN_PAGES_PER_MIN: "Аўтаперагортванне (старонак за хвіліну)"
STR_TILT_PAGE_TURN: "Перагортванне нахілам" STR_TILT_PAGE_TURN: "Перагортванне нахілам"
+393
View File
@@ -0,0 +1,393 @@
_language_name: "Bosanski"
_language_code: "BS"
_order: "29"
STR_CROSSPOINT: "CrossPoint"
STR_BOOTING: "POKRETANJE"
STR_SLEEPING: "REŽIM MIROVANJA"
STR_ENTERING_SLEEP: "Prelazak u režim mirovanja"
STR_BROWSE_FILES: "Pregled datoteka"
STR_FILE_TRANSFER: "Prijenos datoteka"
STR_SETTINGS_TITLE: "Postavke"
STR_CONTINUE_READING: "Nastavi čitanje"
STR_NO_OPEN_BOOK: "Nema otvorene knjige"
STR_START_READING: "Počni čitati ispod"
STR_NO_FILES_FOUND: "Nema pronađenih datoteka"
STR_SELECT_CHAPTER: "Odaberi poglavlje"
STR_NO_CHAPTERS: "Nema poglavlja"
STR_END_OF_BOOK: "Kraj knjige"
STR_EMPTY_CHAPTER: "Prazno poglavlje"
STR_INDEXING: "Indeksiranje"
STR_INDEX_FAILED: "Indeksiranje nije uspjelo - nevažeća knjiga"
STR_MEMORY_ERROR: "Greška memorije"
STR_PAGE_LOAD_ERROR: "Greška pri učitavanju stranice"
STR_EMPTY_FILE: "Prazna datoteka"
STR_OUT_OF_BOUNDS: "Izvan granica"
STR_LOADING: "Učitavanje..."
STR_LOADING_POPUP: "Učitavanje"
STR_WIFI_NETWORKS: "Wi-Fi mreže"
STR_NO_NETWORKS: "Nema pronađenih mreža"
STR_NETWORKS_FOUND: "%zu pronađenih mreža"
STR_SCANNING: "Skeniranje..."
STR_FINDING_SAVED_WIFI: "Traženje sačuvanog Wi-Fi-a..."
STR_CONNECTING: "Povezivanje..."
STR_CONNECTING_SAVED_WIFI: "Povezivanje na sačuvani Wi-Fi..."
STR_SHOW_NETWORKS: "Prikaži"
STR_CONNECTED: "Povezano!"
STR_CONNECTION_FAILED: "Povezivanje nije uspjelo"
STR_FORGET_NETWORK: "Zaboraviti mrežu?"
STR_SAVE_PASSWORD: "Sačuvati lozinku za sljedeći put?"
STR_PRESS_OK_SCAN: "Pritisni OK za ponovno skeniranje"
STR_JOIN_NETWORK: "Pridruži se mreži"
STR_CREATE_HOTSPOT: "Kreiraj hotspot"
STR_JOIN_DESC: "Poveži se na postojeću Wi-Fi mrežu"
STR_HOTSPOT_DESC: "Kreiraj Wi-Fi mrežu kojoj se drugi mogu pridružiti"
STR_STARTING_HOTSPOT: "Pokretanje hotspota..."
STR_HOTSPOT_MODE: "Hotspot način rada"
STR_CONNECT_WIFI_HINT: "Poveži svoj uređaj na ovu Wi-Fi mrežu"
STR_OPEN_URL_HINT: "Otvori ovaj URL u svom pregledniku"
STR_OR_HTTP_PREFIX: "ili http://"
STR_SCAN_QR_HINT: "ili skeniraj QR kod svojim telefonom:"
STR_CALIBRE_WIRELESS: "Calibre bežično"
STR_NETWORK_LEGEND: "* = Šifrovano | + = Sačuvano"
STR_MAC_ADDRESS: "MAC adresa:"
STR_CHECKING_WIFI: "Provjera Wi-Fi-a..."
STR_ENTER_WIFI_PASSWORD: "Unesi Wi-Fi lozinku"
STR_ADD_HIDDEN_NETWORK: "Dodaj skrivenu mrežu..."
STR_ENTER_WIFI_SSID: "Unesi naziv mreže (SSID)"
STR_TO_PREFIX: "na "
STR_CALIBRE_RECEIVING: "Primanje: "
STR_CALIBRE_RECEIVED: "Primljeno: "
STR_CALIBRE_INSTRUCTION_1: "1) Instaliraj CrossPoint Reader dodatak"
STR_CALIBRE_INSTRUCTION_2: "2) Budi na istoj Wi-Fi mreži"
STR_CALIBRE_INSTRUCTION_3: "3) U Calibreu: \"Pošalji na uređaj\""
STR_CALIBRE_INSTRUCTION_4: "\"Drži ovaj ekran otvorenim tokom slanja\""
STR_CAT_DISPLAY: "Ekran"
STR_CAT_READER: "Čitač"
STR_CAT_CONTROLS: "Kontrole"
STR_CAT_SYSTEM: "Sistem"
STR_SLEEP_SCREEN: "Ekran mirovanja"
STR_QUICK_RESUME_TIMEOUT: "Brzi nastavak nakon isteka vremena"
STR_SLEEP_COVER_MODE: "Način prikaza korica na ekranu mirovanja"
STR_HIDE_BATTERY: "Sakrij % baterije"
STR_EXTRA_SPACING: "Dodatni razmak između paragrafa"
STR_TEXT_AA: "Zaglađivanje teksta"
STR_IMAGES: "Slike"
STR_IMAGES_DISPLAY: "Prikaz"
STR_IMAGES_PLACEHOLDER: "Rezervisano mjesto"
STR_IMAGES_SUPPRESS: "Sakrij"
STR_EOB_HOME: "Početna"
STR_EOB_CONTINUE_WITH: "Nastavi sa"
STR_SHORT_PWR_BTN: "Kratak klik dugmeta napajanja"
STR_ORIENTATION: "Orijentacija čitanja"
STR_SIDE_BTN_LAYOUT: "Raspored bočnih dugmadi (čitač)"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orijentiši prednja dugmad"
STR_LONG_PRESS_BEHAVIOR: "Ponašanje dugog pritiska dugmeta"
STR_LONG_PRESS_BEHAVIOR_OFF: "ISKLJUČENO"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Preskakanje poglavlja"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Promjena orijentacije"
STR_LONG_PRESS_MENU: "Meni dugog pritiska"
STR_FONT_PREVIEW_TEXT: "Brza smeđa lisica preskače lijenog psa"
STR_FONT_FAMILY: "Porodica fonta čitača"
STR_FONT_SIZE: "Veličina fonta čitača"
STR_LINE_SPACING: "Razmak između redova čitača"
STR_SCREEN_MARGIN: "Margina ekrana čitača"
STR_PARA_ALIGNMENT: "Poravnanje paragrafa čitača"
STR_HYPHENATION: "Rastavljanje riječi"
STR_TIME_TO_SLEEP: "Vrijeme do mirovanja"
STR_SHOW_HIDDEN_FILES: "Prikaži skrivene datoteke"
STR_REMOVE_READ_FROM_RECENTS: "Ukloni pročitane knjige sa liste nedavnih"
STR_MOVE_FINISHED_TO_READ: "Premjesti završene knjige u fasciklu Read"
STR_REFRESH_FREQ: "Učestalost osvježavanja"
STR_KOREADER_SYNC: "KOReader sinhronizacija"
STR_CHECK_UPDATES: "Provjeri ažuriranja"
STR_LANGUAGE: "Jezik"
STR_CLEAR_READING_CACHE: "Očisti keš čitanja"
STR_USERNAME: "Korisničko ime"
STR_PASSWORD: "Lozinka"
STR_SYNC_SERVER_URL: "URL servera za sinhronizaciju"
STR_DOCUMENT_MATCHING: "Podudaranje dokumenata"
STR_SEND_METADATA: "Pošalji metapodatke dokumenta"
STR_AUTHENTICATE: "Autentifikuj"
STR_KOREADER_USERNAME: "KOReader korisničko ime"
STR_KOREADER_PASSWORD: "KOReader lozinka"
STR_FILENAME: "Naziv datoteke"
STR_BINARY: "Binarno"
STR_SET_CREDENTIALS_FIRST: "Prvo postavi vjerodajnice"
STR_WIFI_CONN_FAILED: "Wi-Fi povezivanje nije uspjelo"
STR_AUTHENTICATING: "Autentifikacija..."
STR_AUTH_SUCCESS: "Uspješno autentifikovano!"
STR_KOREADER_AUTH: "KOReader autentifikacija"
STR_SYNC_READY: "KOReader sinhronizacija je spremna za upotrebu"
STR_AUTH_FAILED: "Autentifikacija nije uspjela"
STR_DONE: "Gotovo"
STR_CLEAR_CACHE_WARNING_1: "Ovo će očistiti sve keširane podatke knjiga."
STR_CLEAR_CACHE_WARNING_2: "Sav napredak čitanja će biti izgubljen!"
STR_CLEAR_CACHE_WARNING_3: "Knjige će morati biti ponovo indeksirane"
STR_CLEAR_CACHE_WARNING_4: "kada se ponovo otvore."
STR_CLEARING_CACHE: "Čišćenje keša..."
STR_CACHE_CLEARED: "Keš očišćen"
STR_ITEMS_REMOVED: "stavki uklonjeno"
STR_FAILED_LOWER: "nije uspjelo"
STR_CLEAR_CACHE_FAILED: "Čišćenje keša nije uspjelo"
STR_CHECK_SERIAL_OUTPUT: "Provjeri serijski izlaz za detalje"
STR_DARK: "Tamno"
STR_LIGHT: "Svijetlo"
STR_CUSTOM: "Prilagođeno"
STR_COVER: "Korice"
STR_NONE_OPT: "Ništa"
STR_FIT: "Prilagodi"
STR_CROP: "Isjeci"
STR_NEVER: "Nikad"
STR_IN_READER: "U čitaču"
STR_ALWAYS: "Uvijek"
STR_IGNORE: "Ignoriši"
STR_SLEEP: "Mirovanje"
STR_PAGE_TURN: "Okretanje stranice"
STR_FORCE_REFRESH: "Osvježi ekran"
STR_PORTRAIT: "Uspravno"
STR_LANDSCAPE_CW: "Položeno (u smjeru kazaljke na satu)"
STR_INVERTED: "Obrnuto"
STR_ORIENTATION_INVERTED: "Uspravno 180°"
STR_LANDSCAPE_CCW: "Položeno (suprotno smjeru kazaljke na satu)"
STR_PREV_NEXT: "Prethodno/Sljedeće"
STR_NEXT_PREV: "Sljedeće/Prethodno"
STR_KOSYNC: "KOSync"
STR_BOOKMARK_OPTION: "Obilježivač"
STR_DISABLED: "Onemogućeno"
STR_NOTO_SERIF: "Noto Serif"
STR_NOTO_SANS: "Noto Sans"
STR_SMALL: "Malo"
STR_MEDIUM: "Srednje"
STR_LARGE: "Veliko"
STR_X_LARGE: "Vrlo veliko"
STR_TIGHT: "Usko"
STR_NORMAL: "Normalno"
STR_WIDE: "Široko"
STR_JUSTIFY: "Poravnaj obostrano"
STR_ALIGN_LEFT: "Lijevo"
STR_CENTER: "Centrirano"
STR_ALIGN_RIGHT: "Desno"
STR_PAGES_1: "1 stranica"
STR_PAGES_5: "5 stranica"
STR_PAGES_10: "10 stranica"
STR_PAGES_15: "15 stranica"
STR_PAGES_30: "30 stranica"
STR_UPDATE: "Ažuriraj"
STR_CHECKING_UPDATE: "Provjera ažuriranja..."
STR_NEW_UPDATE: "Dostupno je novo ažuriranje!"
STR_CURRENT_VERSION: "Trenutna verzija: "
STR_NEW_VERSION: "Nova verzija: "
STR_UPDATING: "Ažuriranje..."
STR_NO_UPDATE: "Nema dostupnih ažuriranja"
STR_UPDATE_FAILED: "Ažuriranje nije uspjelo"
STR_UPDATE_COMPLETE: "Ažuriranje završeno"
STR_POWER_ON_HINT: "Pritisni i drži dugme napajanja da ponovo uključiš"
STR_RESTARTING_HINT: "Ponovno pokretanje... Ako se uređaj ne pokrene ponovo, drži dugme napajanja nekoliko sekundi."
STR_NO_ENTRIES: "Nema pronađenih unosa"
STR_DOWNLOADING: "Preuzimanje..."
STR_DOWNLOAD_FAILED: "Preuzimanje nije uspjelo"
STR_ERROR_MSG: "Greška:"
STR_UNNAMED: "Neimenovano"
STR_HOLD_OPEN_TO_DELETE: "Drži Otvori za brisanje"
STR_NO_SERVER_URL: "Nije podešen URL servera"
STR_FETCH_FEED_FAILED: "Dohvatanje feeda nije uspjelo"
STR_PARSE_FEED_FAILED: "Obrada feeda nije uspjela"
STR_NEXT_PAGE: "Sljedeća stranica »"
STR_PREV_PAGE: "« Prethodna stranica"
STR_NETWORK_PREFIX: "Mreža: "
STR_IP_ADDRESS_PREFIX: "IP adresa: "
STR_ERROR_GENERAL_FAILURE: "Greška: Opšta greška"
STR_ERROR_NETWORK_NOT_FOUND: "Greška: Mreža nije pronađena"
STR_ERROR_CONNECTION_TIMEOUT: "Greška: Isteklo vrijeme veze"
STR_SD_CARD: "SD kartica"
STR_BACK: "« Nazad"
STR_EXIT: "« Izlaz"
STR_HOME: "« Početna"
STR_SELECT: "Odaberi"
STR_SELECTED: "Odabrano"
STR_TOGGLE: "Promijeni"
STR_TOGGLE_BOOKMARK: "Promijeni obilježivač"
STR_CONFIRM: "Potvrdi"
STR_CANCEL: "Otkaži"
STR_CONNECT: "Poveži"
STR_OPEN: "Otvori"
STR_DOWNLOAD: "Preuzmi"
STR_RETRY: "Pokušaj ponovo"
STR_YES: "Da"
STR_NO: "Ne"
STR_SHOW: "Prikaži"
STR_HIDE: "Sakrij"
STR_STATE_ON: "UKLJUČENO"
STR_STATE_OFF: "ISKLJUČENO"
STR_NOT_SET: "Nije postavljeno"
STR_DIR_LEFT: "Lijevo"
STR_DIR_RIGHT: "Desno"
STR_DIR_UP: "Gore"
STR_DIR_DOWN: "Dolje"
STR_OK_BUTTON: "OK"
STR_SLEEP_COVER_FILTER: "Filter korica ekrana mirovanja"
STR_FILTER_CONTRAST: "Kontrast"
STR_CUSTOMISE_STATUS_BAR: "Prilagodi statusnu traku"
STR_CHAPTER_PAGE_COUNT: "Broj stranica poglavlja"
STR_BOOK_PROGRESS_PERCENTAGE: "Procenat napretka knjige"
STR_PROGRESS_BAR: "Traka napretka"
STR_PROGRESS_BAR_THICKNESS: "Debljina trake napretka"
STR_PROGRESS_BAR_THIN: "Tanka"
STR_PROGRESS_BAR_MEDIUM: "Srednja"
STR_PROGRESS_BAR_THICK: "Debela"
STR_BOOK: "Knjiga"
STR_CHAPTER: "Poglavlje"
STR_EXAMPLE_CHAPTER: "Poglavlje 21"
STR_EXAMPLE_BOOK: "Naslov knjige"
STR_PREVIEW: "Pregled"
STR_TITLE: "Naslov"
STR_BATTERY: "Baterija"
STR_XTC_STATUS_BAR: "XTC statusna traka"
STR_BOTTOM: "Dolje"
STR_TOP: "Gore"
STR_CLOCK: "Sat"
STR_CLOCK_UTC_OFFSET: "UTC odstupanje sata"
STR_CLOCK_FORMAT: "Format sata"
STR_CLOCK_FORMAT_24H: "24-časovni"
STR_CLOCK_FORMAT_12H: "12-časovni"
STR_CURRENT_TIME: "Trenutno vrijeme:"
STR_NEXT_FIELD: "Sljedeće"
STR_CLOCK_SYNC: "Sinhronizuj sat"
STR_CLOCK_SYNC_NOW: "Sinhronizuj sat sada"
STR_CLOCK_SYNCING: "Sinhronizacija sa NTP-a..."
STR_CLOCK_SYNC_OK: "Sat sinhronizovan"
STR_CLOCK_SYNC_FAIL: "Sinhronizacija nije uspjela"
STR_CLOCK_SYNC_NO_WIFI: "Wi-Fi nije povezan"
STR_CLOCK_SYNC_NO_WIFI_HINT: "Prvo se poveži na Wi-Fi, pa pokušaj ponovo."
STR_CLOCK_SYNCED: "Sat sinhronizovan"
STR_UI_THEME: "Tema korisničkog interfejsa"
STR_THEME_CLASSIC: "Klasična"
STR_THEME_LYRA: "Lyra"
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
STR_THEME_LYRA_EXTENDED: "Lyra proširena"
STR_SUNLIGHT_FADING_FIX: "Ispravka blijeđenja na suncu"
STR_REMAP_FRONT_BUTTONS: "Premapiraj prednja dugmad"
STR_BOOKMARKS: "Obilježivači"
STR_BOOKMARK_ADDED: "Obilježivač dodan."
STR_BOOKMARK_REMOVED: "Obilježivač uklonjen."
STR_OPDS_BROWSER: "OPDS pregledač"
STR_SEARCH: "Pretraga"
STR_COVER_CUSTOM: "Korice + Prilagođeno"
STR_QUICK_RESUME: "Brzi nastavak"
STR_MENU_RECENT_BOOKS: "Nedavne knjige"
STR_REMOVE_FROM_RECENTS: "Ukloniti iz nedavnih knjiga?"
STR_NO_RECENT_BOOKS: "Nema nedavnih knjiga"
STR_CALIBRE_DESC: "Koristi Calibre bežični prijenos uređaja"
STR_FORGET_AND_REMOVE: "Zaboraviti mrežu i ukloniti sačuvanu lozinku?"
STR_FORGET_BUTTON: "Zaboravi"
STR_CALIBRE_STARTING: "Pokretanje Calibrea..."
STR_CALIBRE_SETUP: "Podešavanje"
STR_CALIBRE_STATUS: "Status"
STR_CLEAR_BUTTON: "Očisti"
STR_DEFAULT_VALUE: "Zadano"
STR_REMAP_PROMPT: "Pritisni prednje dugme za svaku ulogu"
STR_UNASSIGNED: "Nedodijeljeno"
STR_ALREADY_ASSIGNED: "Već dodijeljeno"
STR_REMAP_RESET_HINT: "Bočno dugme Gore: Vrati na zadani raspored"
STR_REMAP_CANCEL_HINT: "Bočno dugme Dolje: Otkaži premapiranje"
STR_HW_BACK_LABEL: "Nazad (1. dugme)"
STR_HW_CONFIRM_LABEL: "Potvrdi (2. dugme)"
STR_HW_LEFT_LABEL: "Lijevo (3. dugme)"
STR_HW_RIGHT_LABEL: "Desno (4. dugme)"
STR_GO_TO_PERCENT: "Idi na %"
STR_GO_HOME_BUTTON: "Idi na početnu"
STR_SYNC_PROGRESS: "Sinhronizuj napredak"
STR_DELETE_CACHE: "Obriši keš knjige"
STR_DELETE: "Obriši"
STR_CONFIRM_DELETE_BOOKMARK: "Obrisati ovaj obilježivač?"
STR_DISPLAY_QR: "Prikaži stranicu kao QR"
STR_CHAPTER_PREFIX: "Poglavlje: "
STR_PAGES_SEPARATOR: " stranica | "
STR_BOOK_PREFIX: "Knjiga: "
STR_CALIBRE_URL_HINT: "Za Calibre, dodaj /opds na svoj URL"
STR_SYNCING_TIME: "Sinhronizacija vremena..."
STR_CALC_HASH: "Izračunavanje heša dokumenta..."
STR_HASH_FAILED: "Izračunavanje heša dokumenta nije uspjelo"
STR_FETCH_PROGRESS: "Dohvatanje udaljenog napretka..."
STR_UPLOAD_PROGRESS: "Slanje napretka..."
STR_NO_CREDENTIALS_MSG: "Nisu podešene vjerodajnice"
STR_KOREADER_SETUP_HINT: "Podesi KOReader nalog u Postavkama"
STR_PROGRESS_FOUND: "Napredak pronađen!"
STR_REMOTE_LABEL: "Udaljeno:"
STR_LOCAL_LABEL: "Lokalno:"
STR_PAGE_OVERALL_FORMAT: "Stranica %d, %.2f%% ukupno"
STR_PAGE_TOTAL_OVERALL_FORMAT: "Stranica %d/%d, %.2f%% ukupno"
STR_DEVICE_FROM_FORMAT: " Sa: %s"
STR_APPLY_REMOTE: "Primijeni udaljeni napredak"
STR_UPLOAD_LOCAL: "Pošalji lokalni napredak"
STR_NO_REMOTE_MSG: "Nije pronađen udaljeni napredak"
STR_UPLOAD_PROMPT: "Poslati trenutnu poziciju?"
STR_UPLOAD_SUCCESS: "Napredak poslan!"
STR_SYNC_FAILED_MSG: "Sinhronizacija nije uspjela"
STR_SAVE_PROGRESS_FAILED: "Napredak nije mogao biti sačuvan"
STR_SECTION_PREFIX: "Sekcija "
STR_UPLOAD: "Pošalji"
STR_BOOK_S_STYLE: "Stil knjige"
STR_EMBEDDED_STYLE: "Ugrađeni stil"
STR_FOCUS_READING: "Fokusirano čitanje"
STR_OPDS_SERVER_URL: "URL OPDS servera"
STR_PWR_BTN_FOOTNOTE_BACK: "Brzi povratak iz fusnota"
STR_SET_SLEEP_COVER: "Postavi korice"
STR_FOOTNOTES: "Fusnote"
STR_NO_FOOTNOTES: "Nema fusnota na ovoj stranici"
STR_LINK: "[link]"
STR_SCREENSHOT_BUTTON: "Snimi ekran"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Nikad"
STR_STEP_HINT_FRONT: "Prednja dugmad:"
STR_STEP_HINT_SIDE: "Bočna dugmad:"
STR_ADD_SERVER: "Dodaj server"
STR_SERVER_NAME: "Naziv servera"
STR_NO_SERVERS: "Nema podešenih OPDS servera"
STR_DELETE_SERVER: "Obriši server"
STR_OPDS_SERVERS: "OPDS serveri"
STR_AUTO_TURN_ENABLED: "Automatsko okretanje omogućeno: "
STR_AUTO_TURN_PAGES_PER_MIN: "Automatsko okretanje (stranica po minuti)"
STR_MANAGE_FONTS: "Upravljaj fontovima"
STR_FONT_BROWSER: "Pregledač fontova"
STR_LOADING_FONT_LIST: "Učitavanje liste fontova..."
STR_NO_FONTS_AVAILABLE: "Nema dostupnih fontova"
STR_FONT_INSTALLED: "Font instaliran!"
STR_FONT_INSTALL_FAILED: "Instalacija fonta nije uspjela"
STR_INSTALLED: "Instalirano"
STR_DOWNLOAD_ALL: "Preuzmi sve"
STR_UPDATE_ALL: "Ažuriraj sve"
STR_UPDATE_AVAILABLE: "Ažuriranje"
STR_CRASH_TITLE: "Sistemski pad"
STR_CRASH_DESCRIPTION: "Detaljan izvještaj je sačuvan u crash_report.txt. Molimo priloži ovu datoteku u svoj izvještaj o grešci."
STR_CRASH_REASON: "Razlog pada:"
STR_CRASH_NO_REASON: "(Razlog nije zabilježen)"
STR_TILT_PAGE_TURN: "Okretanje stranice naginjanjem"
STR_KB_HINT_MOVE_CURSOR: "Pritisni LIJEVO ili DESNO za pomjeranje kursora"
STR_KB_HINT_RETURN_CURSOR: "Pritisni LIJEVO za povratak na poziciju kursora"
STR_KB_HINT_HIDE_PASSWORD: "Drži DESNO pa pritisni [***] za skrivanje lozinke"
STR_KB_HINT_SHOW_PASSWORD: "Drži DESNO pa pritisni [abc] za prikaz lozinke"
STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Pritisni [***] za skrivanje lozinke"
STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Pritisni [abc] za prikaz lozinke"
STR_KB_HINT_EDIT_ENTRY: "Drži GORE za uređivanje unosa"
STR_KB_TIPS: "Savjeti:"
STR_KB_HINT_RETURN_KEYBOARD: "Pritisni DOLJE za povratak na tastaturu"
STR_KB_HINT_EXIT_URL_MODE: "Pritisni ABC za izlazak iz URL načina"
STR_KB_HINT_CLEAR_TEXT: "Drži DEL za brisanje cijelog teksta"
STR_KB_HINT_SECONDARY_CHAR: "Drži SELECT za sekundarni znak"
STR_KB_HINT_UPPER_SECONDARY: "Drži SELECT za VELIKA SLOVA ili sekundarni znak"
STR_KB_HINT_LOWER_SECONDARY: "Drži SELECT za mala slova ili sekundarni znak"
STR_KB_HINT_URL_SNIPPETS: "Pritisni URL za isječke"
STR_SD_FIRMWARE_UPDATE: "Ažuriranje firmvera sa SD kartice"
STR_SELECT_FIRMWARE_FILE: "Odaberi datoteku firmvera (.bin)"
STR_NO_BIN_FILES: "Nema pronađenih .bin datoteka"
STR_VALIDATING_FIRMWARE: "Provjera firmvera..."
STR_INVALID_FIRMWARE: "Nevažeća datoteka firmvera"
STR_FIRMWARE_TOO_LARGE: "Firmver je prevelik za particiju"
STR_FIRMWARE_TOO_SMALL: "Datoteka firmvera je premala"
STR_FIRMWARE_UPDATE_PROMPT: "Ažurirati firmver?"
STR_FIRMWARE_FILE_OPEN_FAILED: "Datoteka se ne može otvoriti"
STR_FIRMWARE_WRITE_FAILED: "Upisivanje firmvera nije uspjelo"
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Ne isključuj napajanje!"
STR_RECOVERY_MODE: "Način oporavka"
STR_RECOVERY_MODE_HINT: "Stavi firmware.bin u korijenski direktorij SD kartice i odaberi ga"
+5
View File
@@ -347,6 +347,11 @@ STR_SERVER_NAME: "Nom del servidor"
STR_NO_SERVERS: "No hi ha servidors OPDS configurats" STR_NO_SERVERS: "No hi ha servidors OPDS configurats"
STR_DELETE_SERVER: "Suprimeix el servidor" STR_DELETE_SERVER: "Suprimeix el servidor"
STR_OPDS_SERVERS: "Servidors OPDS" STR_OPDS_SERVERS: "Servidors OPDS"
STR_OPDS_DOWNLOAD_FOLDER: "Carpeta de baixades"
STR_OPDS_FILENAME_FORMAT: "Format del nom de fitxer"
STR_FMT_AUTHOR_TITLE: "Autor - Títol"
STR_FMT_TITLE_AUTHOR: "Títol - Autor"
STR_FMT_TITLE: "Títol"
STR_MANAGE_FONTS: "Gestiona les fonts" STR_MANAGE_FONTS: "Gestiona les fonts"
STR_FONT_BROWSER: "Navegador de fonts" STR_FONT_BROWSER: "Navegador de fonts"
STR_LOADING_FONT_LIST: "S'està carregant la llista de fonts..." STR_LOADING_FONT_LIST: "S'està carregant la llista de fonts..."
+5
View File
@@ -270,6 +270,11 @@ STR_BOOK_S_STYLE: "Styl knihy"
STR_EMBEDDED_STYLE: "Vložený styl" STR_EMBEDDED_STYLE: "Vložený styl"
STR_FOCUS_READING: "Soustředěné čtení" STR_FOCUS_READING: "Soustředěné čtení"
STR_OPDS_SERVER_URL: "URL serveru OPDS" STR_OPDS_SERVER_URL: "URL serveru OPDS"
STR_OPDS_DOWNLOAD_FOLDER: "Složka pro stahování"
STR_OPDS_FILENAME_FORMAT: "Formát názvu souboru"
STR_FMT_AUTHOR_TITLE: "Autor - Název"
STR_FMT_TITLE_AUTHOR: "Název - Autor"
STR_FMT_TITLE: "Název"
STR_SCREENSHOT_BUTTON: "Udělat snímek obrazovky" STR_SCREENSHOT_BUTTON: "Udělat snímek obrazovky"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min" STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Nikdy" STR_SLEEP_NEVER: "Nikdy"
+5
View File
@@ -300,6 +300,11 @@ STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Aldrig" STR_SLEEP_NEVER: "Aldrig"
STR_STEP_HINT_FRONT: "Frontknapper:" STR_STEP_HINT_FRONT: "Frontknapper:"
STR_STEP_HINT_SIDE: "Sideknapper:" STR_STEP_HINT_SIDE: "Sideknapper:"
STR_OPDS_DOWNLOAD_FOLDER: "Downloadmappe"
STR_OPDS_FILENAME_FORMAT: "Filnavnsformat"
STR_FMT_AUTHOR_TITLE: "Forfatter - Titel"
STR_FMT_TITLE_AUTHOR: "Titel - Forfatter"
STR_FMT_TITLE: "Titel"
STR_SCREENSHOT_BUTTON: "Tag skærmbillede" STR_SCREENSHOT_BUTTON: "Tag skærmbillede"
STR_AUTO_TURN_ENABLED: "Automatisk sidevendning aktiveret: " STR_AUTO_TURN_ENABLED: "Automatisk sidevendning aktiveret: "
STR_AUTO_TURN_PAGES_PER_MIN: "Automatisk vending (sider per minut)" STR_AUTO_TURN_PAGES_PER_MIN: "Automatisk vending (sider per minut)"
+5
View File
@@ -300,6 +300,11 @@ STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Nooit" STR_SLEEP_NEVER: "Nooit"
STR_STEP_HINT_FRONT: "Voorknoppen:" STR_STEP_HINT_FRONT: "Voorknoppen:"
STR_STEP_HINT_SIDE: "Zijknoppen:" STR_STEP_HINT_SIDE: "Zijknoppen:"
STR_OPDS_DOWNLOAD_FOLDER: "Downloadmap"
STR_OPDS_FILENAME_FORMAT: "Bestandsnaamformaat"
STR_FMT_AUTHOR_TITLE: "Auteur - Titel"
STR_FMT_TITLE_AUTHOR: "Titel - Auteur"
STR_FMT_TITLE: "Titel"
STR_SCREENSHOT_BUTTON: "Screenshot maken" STR_SCREENSHOT_BUTTON: "Screenshot maken"
STR_AUTO_TURN_ENABLED: "Automatisch omslaan ingeschakeld: " STR_AUTO_TURN_ENABLED: "Automatisch omslaan ingeschakeld: "
STR_AUTO_TURN_PAGES_PER_MIN: "Autom. omslaan (pagina's per minuut)" STR_AUTO_TURN_PAGES_PER_MIN: "Autom. omslaan (pagina's per minuut)"
+17
View File
@@ -10,6 +10,7 @@ STR_BROWSE_FILES: "Browse Files"
STR_FILE_TRANSFER: "File Transfer" STR_FILE_TRANSFER: "File Transfer"
STR_SETTINGS_TITLE: "Settings" STR_SETTINGS_TITLE: "Settings"
STR_CONTINUE_READING: "Continue Reading" STR_CONTINUE_READING: "Continue Reading"
STR_RESUME: "Resume"
STR_NO_OPEN_BOOK: "No open book" STR_NO_OPEN_BOOK: "No open book"
STR_START_READING: "Start reading below" STR_START_READING: "Start reading below"
STR_NO_FILES_FOUND: "No files found" STR_NO_FILES_FOUND: "No files found"
@@ -108,7 +109,15 @@ STR_PASSWORD: "Password"
STR_SYNC_SERVER_URL: "Sync Server URL" STR_SYNC_SERVER_URL: "Sync Server URL"
STR_DOCUMENT_MATCHING: "Document Matching" STR_DOCUMENT_MATCHING: "Document Matching"
STR_SEND_METADATA: "Send Document Metadata" STR_SEND_METADATA: "Send Document Metadata"
STR_SYNC_BEHAVIOR: "Sync Behavior"
STR_ASK_EVERY_TIME: "Ask every time"
STR_SMART_SYNC: "Smart sync"
STR_AUTHENTICATE: "Authenticate" STR_AUTHENTICATE: "Authenticate"
STR_SIGN_UP: "Sign Up"
STR_CREATING_ACCOUNT: "Creating account..."
STR_ACCOUNT_CREATED: "Account created"
STR_SIGNUP_FAILED: "Sign up failed"
STR_USERNAME_TAKEN: "Username is already registered"
STR_KOREADER_USERNAME: "KOReader Username" STR_KOREADER_USERNAME: "KOReader Username"
STR_KOREADER_PASSWORD: "KOReader Password" STR_KOREADER_PASSWORD: "KOReader Password"
STR_FILENAME: "Filename" STR_FILENAME: "Filename"
@@ -324,6 +333,7 @@ STR_UPLOAD_LOCAL: "Upload local progress"
STR_NO_REMOTE_MSG: "No remote progress found" STR_NO_REMOTE_MSG: "No remote progress found"
STR_UPLOAD_PROMPT: "Upload current position?" STR_UPLOAD_PROMPT: "Upload current position?"
STR_UPLOAD_SUCCESS: "Progress uploaded!" STR_UPLOAD_SUCCESS: "Progress uploaded!"
STR_ALREADY_SYNCED: "Already synced"
STR_SYNC_FAILED_MSG: "Sync failed" STR_SYNC_FAILED_MSG: "Sync failed"
STR_SAVE_PROGRESS_FAILED: "Could not save progress" STR_SAVE_PROGRESS_FAILED: "Could not save progress"
STR_SECTION_PREFIX: "Section " STR_SECTION_PREFIX: "Section "
@@ -333,6 +343,7 @@ STR_EMBEDDED_STYLE: "Embedded Style"
STR_FOCUS_READING: "Focus Reading" STR_FOCUS_READING: "Focus Reading"
STR_OPDS_SERVER_URL: "OPDS Server URL" STR_OPDS_SERVER_URL: "OPDS Server URL"
STR_PWR_BTN_FOOTNOTE_BACK: "Quick-return from footnotes" STR_PWR_BTN_FOOTNOTE_BACK: "Quick-return from footnotes"
STR_BACK_SHORT_TO_FILE_BROWSER: "Short Back to File Browser"
STR_SET_SLEEP_COVER: "Set Cover" STR_SET_SLEEP_COVER: "Set Cover"
STR_FOOTNOTES: "Footnotes" STR_FOOTNOTES: "Footnotes"
STR_NO_FOOTNOTES: "No footnotes on this page" STR_NO_FOOTNOTES: "No footnotes on this page"
@@ -347,6 +358,12 @@ STR_SERVER_NAME: "Server Name"
STR_NO_SERVERS: "No OPDS servers configured" STR_NO_SERVERS: "No OPDS servers configured"
STR_DELETE_SERVER: "Delete Server" STR_DELETE_SERVER: "Delete Server"
STR_OPDS_SERVERS: "OPDS Servers" STR_OPDS_SERVERS: "OPDS Servers"
STR_OPDS_DOWNLOAD_FOLDER: "Download folder"
STR_OPDS_FILENAME_FORMAT: "Filename format"
STR_FMT_AUTHOR_TITLE: "Author - Title"
STR_FMT_TITLE_AUTHOR: "Title - Author"
STR_FMT_TITLE: "Title"
STR_OPDS_SD_ROOT: "SD root"
STR_AUTO_TURN_ENABLED: "Auto Turn Enabled: " STR_AUTO_TURN_ENABLED: "Auto Turn Enabled: "
STR_AUTO_TURN_PAGES_PER_MIN: "Auto Turn (Pages Per Minute)" STR_AUTO_TURN_PAGES_PER_MIN: "Auto Turn (Pages Per Minute)"
STR_MANAGE_FONTS: "Manage Fonts" STR_MANAGE_FONTS: "Manage Fonts"
+5
View File
@@ -268,6 +268,11 @@ STR_BOOK_S_STYLE: "Kirjan tyyli"
STR_EMBEDDED_STYLE: "Upotettu tyyli" STR_EMBEDDED_STYLE: "Upotettu tyyli"
STR_FOCUS_READING: "Keskittynyt lukeminen" STR_FOCUS_READING: "Keskittynyt lukeminen"
STR_OPDS_SERVER_URL: "OPDS-palvelimen osoite" STR_OPDS_SERVER_URL: "OPDS-palvelimen osoite"
STR_OPDS_DOWNLOAD_FOLDER: "Latauskansio"
STR_OPDS_FILENAME_FORMAT: "Tiedostonimen muoto"
STR_FMT_AUTHOR_TITLE: "Tekijä - Nimi"
STR_FMT_TITLE_AUTHOR: "Nimi - Tekijä"
STR_FMT_TITLE: "Nimi"
STR_SCREENSHOT_BUTTON: "Ota kuvakaappaus" STR_SCREENSHOT_BUTTON: "Ota kuvakaappaus"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min" STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Ei koskaan" STR_SLEEP_NEVER: "Ei koskaan"
+5
View File
@@ -301,6 +301,11 @@ STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Jamais" STR_SLEEP_NEVER: "Jamais"
STR_STEP_HINT_FRONT: "Boutons avant :" STR_STEP_HINT_FRONT: "Boutons avant :"
STR_STEP_HINT_SIDE: "Boutons latéraux :" STR_STEP_HINT_SIDE: "Boutons latéraux :"
STR_OPDS_DOWNLOAD_FOLDER: "Dossier de téléchargement"
STR_OPDS_FILENAME_FORMAT: "Format du nom de fichier"
STR_FMT_AUTHOR_TITLE: "Auteur - Titre"
STR_FMT_TITLE_AUTHOR: "Titre - Auteur"
STR_FMT_TITLE: "Titre"
STR_SCREENSHOT_BUTTON: "Capture d'écran" STR_SCREENSHOT_BUTTON: "Capture d'écran"
STR_AUTO_TURN_ENABLED: "Tourne-page auto : " STR_AUTO_TURN_ENABLED: "Tourne-page auto : "
STR_AUTO_TURN_PAGES_PER_MIN: "Tourne-page auto (pages par minute)" STR_AUTO_TURN_PAGES_PER_MIN: "Tourne-page auto (pages par minute)"
+6
View File
@@ -315,6 +315,7 @@ STR_BOOK_S_STYLE: "Buch-Stil"
STR_EMBEDDED_STYLE: "Eingebetteter Stil" STR_EMBEDDED_STYLE: "Eingebetteter Stil"
STR_FOCUS_READING: "Fokus-Lesen" STR_FOCUS_READING: "Fokus-Lesen"
STR_OPDS_SERVER_URL: "OPDS-Server-URL" STR_OPDS_SERVER_URL: "OPDS-Server-URL"
STR_BACK_SHORT_TO_FILE_BROWSER: "Kurz zurück drücken zum Datei-Browser"
STR_SET_SLEEP_COVER: "Wähle Cover" STR_SET_SLEEP_COVER: "Wähle Cover"
STR_SCREENSHOT_BUTTON: "Screenshot aufnehmen" STR_SCREENSHOT_BUTTON: "Screenshot aufnehmen"
STR_FOOTNOTES: "Fußnoten" STR_FOOTNOTES: "Fußnoten"
@@ -329,6 +330,11 @@ STR_SERVER_NAME: "Servername"
STR_NO_SERVERS: "Keine OPDS-Server konfiguriert" STR_NO_SERVERS: "Keine OPDS-Server konfiguriert"
STR_DELETE_SERVER: "Server entfernen" STR_DELETE_SERVER: "Server entfernen"
STR_OPDS_SERVERS: "OPDS-Server" STR_OPDS_SERVERS: "OPDS-Server"
STR_OPDS_DOWNLOAD_FOLDER: "Download-Ordner"
STR_OPDS_FILENAME_FORMAT: "Dateinamenformat"
STR_FMT_AUTHOR_TITLE: "Autor - Titel"
STR_FMT_TITLE_AUTHOR: "Titel - Autor"
STR_FMT_TITLE: "Titel"
STR_AUTO_TURN_ENABLED: "Auto-Umblättern aktiv: " STR_AUTO_TURN_ENABLED: "Auto-Umblättern aktiv: "
STR_AUTO_TURN_PAGES_PER_MIN: "Auto-Umblättern (Seiten/Min.)" STR_AUTO_TURN_PAGES_PER_MIN: "Auto-Umblättern (Seiten/Min.)"
STR_IMAGES: "Bilder" STR_IMAGES: "Bilder"
+5
View File
@@ -306,6 +306,11 @@ STR_NO_SERVERS: "לא הוגדרו שרתי OPDS"
STR_DELETE_SERVER: "מחק שרת" STR_DELETE_SERVER: "מחק שרת"
STR_DELETE_CONFIRM: "למחוק שרת זה?" STR_DELETE_CONFIRM: "למחוק שרת זה?"
STR_OPDS_SERVERS: "שרתי OPDS" STR_OPDS_SERVERS: "שרתי OPDS"
STR_OPDS_DOWNLOAD_FOLDER: "תיקיית הורדות"
STR_OPDS_FILENAME_FORMAT: "תבנית שם קובץ"
STR_FMT_AUTHOR_TITLE: "מחבר - כותרת"
STR_FMT_TITLE_AUTHOR: "כותרת - מחבר"
STR_FMT_TITLE: "כותרת"
STR_AUTO_TURN_ENABLED: "דפדוף אוטומטי פועל: " STR_AUTO_TURN_ENABLED: "דפדוף אוטומטי פועל: "
STR_AUTO_TURN_PAGES_PER_MIN: "דפדוף אוטומטי (דפים בדקה)" STR_AUTO_TURN_PAGES_PER_MIN: "דפדוף אוטומטי (דפים בדקה)"
STR_MANAGE_FONTS: "ניהול גופנים" STR_MANAGE_FONTS: "ניהול גופנים"
+5
View File
@@ -299,6 +299,11 @@ STR_SLEEP_TIMER_VALUE_FORMAT: "%u perc"
STR_SLEEP_NEVER: "Soha" STR_SLEEP_NEVER: "Soha"
STR_STEP_HINT_FRONT: "Elülső gombok:" STR_STEP_HINT_FRONT: "Elülső gombok:"
STR_STEP_HINT_SIDE: "Oldalsó gombok:" STR_STEP_HINT_SIDE: "Oldalsó gombok:"
STR_OPDS_DOWNLOAD_FOLDER: "Letöltési mappa"
STR_OPDS_FILENAME_FORMAT: "Fájlnév formátuma"
STR_FMT_AUTHOR_TITLE: "Szerző - Cím"
STR_FMT_TITLE_AUTHOR: "Cím - Szerző"
STR_FMT_TITLE: "Cím"
STR_SCREENSHOT_BUTTON: "Képernyőkép készítése" STR_SCREENSHOT_BUTTON: "Képernyőkép készítése"
STR_AUTO_TURN_ENABLED: "Automatikus lapozás bekapcsolva: " STR_AUTO_TURN_ENABLED: "Automatikus lapozás bekapcsolva: "
STR_AUTO_TURN_PAGES_PER_MIN: "Automatikus lapozás (oldal/perc)" STR_AUTO_TURN_PAGES_PER_MIN: "Automatikus lapozás (oldal/perc)"
+5
View File
@@ -315,6 +315,11 @@ STR_SERVER_NAME: "Nome server"
STR_NO_SERVERS: "Nessun server OPDS configurato" STR_NO_SERVERS: "Nessun server OPDS configurato"
STR_DELETE_SERVER: "Elimina server" STR_DELETE_SERVER: "Elimina server"
STR_OPDS_SERVERS: "Server OPDS" STR_OPDS_SERVERS: "Server OPDS"
STR_OPDS_DOWNLOAD_FOLDER: "Cartella download"
STR_OPDS_FILENAME_FORMAT: "Formato nome file"
STR_FMT_AUTHOR_TITLE: "Autore - Titolo"
STR_FMT_TITLE_AUTHOR: "Titolo - Autore"
STR_FMT_TITLE: "Titolo"
STR_AUTO_TURN_ENABLED: "Volta pagina automatico: " STR_AUTO_TURN_ENABLED: "Volta pagina automatico: "
STR_AUTO_TURN_PAGES_PER_MIN: "Volta pagina automatico (pag/min)" STR_AUTO_TURN_PAGES_PER_MIN: "Volta pagina automatico (pag/min)"
STR_MANAGE_FONTS: "Gestisci font" STR_MANAGE_FONTS: "Gestisci font"
+5
View File
@@ -296,6 +296,11 @@ STR_SLEEP_TIMER_VALUE_FORMAT: "%u мин"
STR_SLEEP_NEVER: "Ешқашан" STR_SLEEP_NEVER: "Ешқашан"
STR_STEP_HINT_FRONT: "Алдыңғы түймелер:" STR_STEP_HINT_FRONT: "Алдыңғы түймелер:"
STR_STEP_HINT_SIDE: "Бүйір түймелері:" STR_STEP_HINT_SIDE: "Бүйір түймелері:"
STR_OPDS_DOWNLOAD_FOLDER: "Жүктеп алу қалтасы"
STR_OPDS_FILENAME_FORMAT: "Файл атауының пішімі"
STR_FMT_AUTHOR_TITLE: "Автор - Атауы"
STR_FMT_TITLE_AUTHOR: "Атауы - Автор"
STR_FMT_TITLE: "Атауы"
STR_SCREENSHOT_BUTTON: "Скриншот түсіру" STR_SCREENSHOT_BUTTON: "Скриншот түсіру"
STR_AUTO_TURN_ENABLED: "Автоматты бет аудару қосулы: " STR_AUTO_TURN_ENABLED: "Автоматты бет аудару қосулы: "
STR_AUTO_TURN_PAGES_PER_MIN: "Автоматты бет аудару (минутына бет саны)" STR_AUTO_TURN_PAGES_PER_MIN: "Автоматты бет аудару (минутына бет саны)"
+5
View File
@@ -297,6 +297,11 @@ STR_SLEEP_TIMER_VALUE_FORMAT: "%u min."
STR_SLEEP_NEVER: "Niekada" STR_SLEEP_NEVER: "Niekada"
STR_STEP_HINT_FRONT: "Priekiniai mygtukai:" STR_STEP_HINT_FRONT: "Priekiniai mygtukai:"
STR_STEP_HINT_SIDE: "Šoniniai mygtukai:" STR_STEP_HINT_SIDE: "Šoniniai mygtukai:"
STR_OPDS_DOWNLOAD_FOLDER: "Atsisiuntimų aplankas"
STR_OPDS_FILENAME_FORMAT: "Failo pavadinimo formatas"
STR_FMT_AUTHOR_TITLE: "Autorius - Pavadinimas"
STR_FMT_TITLE_AUTHOR: "Pavadinimas - Autorius"
STR_FMT_TITLE: "Pavadinimas"
STR_SCREENSHOT_BUTTON: "Ekrano nuotrauka" STR_SCREENSHOT_BUTTON: "Ekrano nuotrauka"
STR_AUTO_TURN_ENABLED: "Auto-vertimas: " STR_AUTO_TURN_ENABLED: "Auto-vertimas: "
STR_AUTO_TURN_PAGES_PER_MIN: "Auto-vertimas (psl/min)" STR_AUTO_TURN_PAGES_PER_MIN: "Auto-vertimas (psl/min)"
+385
View File
@@ -0,0 +1,385 @@
_language_name: "Norsk bokmål"
_language_code: "NB"
_order: "26"
STR_CROSSPOINT: "CrossPoint"
STR_BOOTING: "STARTER"
STR_SLEEPING: "HVILER"
STR_ENTERING_SLEEP: "Går i hvilemodus"
STR_BROWSE_FILES: "Bla i filer"
STR_FILE_TRANSFER: "Filoverføring"
STR_SETTINGS_TITLE: "Innstillinger"
STR_CONTINUE_READING: "Fortsett å lese"
STR_NO_OPEN_BOOK: "Ingen åpen bok"
STR_START_READING: "Start å lese nedenfor"
STR_NO_FILES_FOUND: "Ingen filer funnet"
STR_SELECT_CHAPTER: "Velg kapittel"
STR_NO_CHAPTERS: "Ingen kapitler"
STR_END_OF_BOOK: "Slutten av boken"
STR_EMPTY_CHAPTER: "Tomt kapittel"
STR_INDEXING: "Indekserer"
STR_MEMORY_ERROR: "Minnefeil"
STR_PAGE_LOAD_ERROR: "Feil ved sidelasting"
STR_EMPTY_FILE: "Tom fil"
STR_OUT_OF_BOUNDS: "Utenfor område"
STR_LOADING: "Laster..."
STR_LOADING_POPUP: "Laster"
STR_WIFI_NETWORKS: "WiFi-nettverk"
STR_NO_NETWORKS: "Ingen nettverk funnet"
STR_NETWORKS_FOUND: "%zu nettverk funnet"
STR_SCANNING: "Skanner..."
STR_CONNECTING: "Kobler til..."
STR_CONNECTED: "Tilkoblet!"
STR_CONNECTION_FAILED: "Tilkobling mislyktes"
STR_FORGET_NETWORK: "Glem nettverk?"
STR_SAVE_PASSWORD: "Lagre passord til neste gang?"
STR_PRESS_OK_SCAN: "Trykk OK for å skanne på nytt"
STR_JOIN_NETWORK: "Koble til et nettverk"
STR_CREATE_HOTSPOT: "Opprett hotspot"
STR_JOIN_DESC: "Koble til et eksisterende WiFi-nettverk"
STR_HOTSPOT_DESC: "Opprett et WiFi-nettverk andre kan koble seg til"
STR_STARTING_HOTSPOT: "Starter hotspot..."
STR_HOTSPOT_MODE: "Hotspot-modus"
STR_CONNECT_WIFI_HINT: "Koble enheten din til dette WiFi-nettverket"
STR_OPEN_URL_HINT: "Åpne denne URL-en i nettleseren din"
STR_OR_HTTP_PREFIX: "eller http://"
STR_SCAN_QR_HINT: "eller skann QR-kode med telefonen din:"
STR_CALIBRE_WIRELESS: "Calibre Wireless"
STR_NETWORK_LEGEND: "* = Kryptert | + = Lagret"
STR_MAC_ADDRESS: "MAC-adresse:"
STR_CHECKING_WIFI: "Sjekker WiFi..."
STR_ENTER_WIFI_PASSWORD: "Skriv inn WiFi-passord"
STR_TO_PREFIX: "til "
STR_CALIBRE_RECEIVING: "Mottar: "
STR_CALIBRE_RECEIVED: "Mottatt: "
STR_CALIBRE_INSTRUCTION_1: "1) Installer CrossPoint Reader-plugin"
STR_CALIBRE_INSTRUCTION_2: "2) Vær på samme WiFi-nettverk"
STR_CALIBRE_INSTRUCTION_3: "3) I Calibre: \"Send til enhet\""
STR_CALIBRE_INSTRUCTION_4: "\"Hold denne skjermen åpen mens du sender\""
STR_CAT_DISPLAY: "Skjerm"
STR_CAT_READER: "Leser"
STR_CAT_CONTROLS: "Betjening"
STR_CAT_SYSTEM: "System"
STR_SLEEP_SCREEN: "Hvileskjerm"
STR_QUICK_RESUME_TIMEOUT: "Hurtig gjenopptak ved tidsavbrudd"
STR_SLEEP_COVER_MODE: "Omslagsmodus for hvileskjerm"
STR_HIDE_BATTERY: "Skjul batteri %"
STR_EXTRA_SPACING: "Ekstra avsnittsavstand"
STR_TEXT_AA: "Tekstutjevning"
STR_IMAGES: "Bilder"
STR_IMAGES_DISPLAY: "Vis"
STR_IMAGES_PLACEHOLDER: "Plassholder"
STR_IMAGES_SUPPRESS: "Skjul"
STR_SHORT_PWR_BTN: "Kort trykk på av/på-knapp"
STR_ORIENTATION: "Leseretning"
STR_SIDE_BTN_LAYOUT: "Sideknapp-oppsett (leser)"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orienter frontknapper"
STR_LONG_PRESS_BEHAVIOR: "Atferd ved langt trykk"
STR_LONG_PRESS_BEHAVIOR_OFF: "AV"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Kapittelhopp"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Endre orientering"
STR_LONG_PRESS_MENU: "Langt trykk på Meny"
STR_FONT_PREVIEW_TEXT: "Høvdingens kjære squaw får litt pizza i Mexico by"
STR_FONT_FAMILY: "Skrifttype i leser"
STR_FONT_SIZE: "Skriftstørrelse i leser"
STR_LINE_SPACING: "Linjeavstand i leser"
STR_SCREEN_MARGIN: "Skjermmarg i leser"
STR_PARA_ALIGNMENT: "Avsnittsjustering i leser"
STR_HYPHENATION: "Orddeling"
STR_TIME_TO_SLEEP: "Tid før hvile"
STR_SHOW_HIDDEN_FILES: "Vis skjulte filer"
STR_REMOVE_READ_FROM_RECENTS: "Fjern leste bøker fra Nylig-listen"
STR_MOVE_FINISHED_TO_READ: "Flytt fullførte bøker til Read-mappen"
STR_REFRESH_FREQ: "Oppdateringsfrekvens"
STR_KOREADER_SYNC: "KOReader Sync"
STR_CHECK_UPDATES: "Se etter oppdateringer"
STR_LANGUAGE: "Språk"
STR_CLEAR_READING_CACHE: "Tøm lesebuffer"
STR_USERNAME: "Brukernavn"
STR_PASSWORD: "Passord"
STR_SYNC_SERVER_URL: "URL til synk-server"
STR_DOCUMENT_MATCHING: "Dokumentgjenkjenning"
STR_AUTHENTICATE: "Godkjenn"
STR_KOREADER_USERNAME: "KOReader-brukernavn"
STR_KOREADER_PASSWORD: "KOReader-passord"
STR_FILENAME: "Filnavn"
STR_BINARY: "Binær"
STR_SET_CREDENTIALS_FIRST: "Angi påloggingsinfo først"
STR_WIFI_CONN_FAILED: "WiFi-tilkobling mislyktes"
STR_AUTHENTICATING: "Godkjenner..."
STR_AUTH_SUCCESS: "Godkjenning vellykket!"
STR_KOREADER_AUTH: "KOReader-godkjenning"
STR_SYNC_READY: "KOReader-synk er klar til bruk"
STR_AUTH_FAILED: "Godkjenning mislyktes"
STR_DONE: "Ferdig"
STR_CLEAR_CACHE_WARNING_1: "Dette tømmer alle bufrede bokdata."
STR_CLEAR_CACHE_WARNING_2: "All lesefremdrift går tapt!"
STR_CLEAR_CACHE_WARNING_3: "Bøker må indekseres på nytt"
STR_CLEAR_CACHE_WARNING_4: "når de åpnes igjen."
STR_CLEARING_CACHE: "Tømmer buffer..."
STR_CACHE_CLEARED: "Buffer tømt"
STR_ITEMS_REMOVED: "elementer fjernet"
STR_FAILED_LOWER: "mislyktes"
STR_CLEAR_CACHE_FAILED: "Kunne ikke tømme buffer"
STR_CHECK_SERIAL_OUTPUT: "Sjekk seriell utdata for detaljer"
STR_DARK: "Mørk"
STR_LIGHT: "Lys"
STR_CUSTOM: "Egendefinert"
STR_COVER: "Omslag"
STR_NONE_OPT: "Ingen"
STR_FIT: "Tilpass"
STR_CROP: "Beskjær"
STR_NEVER: "Aldri"
STR_IN_READER: "I leseren"
STR_ALWAYS: "Alltid"
STR_IGNORE: "Ignorer"
STR_SLEEP: "Hvile"
STR_PAGE_TURN: "Bla om"
STR_FORCE_REFRESH: "Oppdater skjerm"
STR_PORTRAIT: "Stående"
STR_LANDSCAPE_CW: "Liggende med klokken"
STR_INVERTED: "Opp ned"
STR_ORIENTATION_INVERTED: "Portrett 180°"
STR_LANDSCAPE_CCW: "Liggende mot klokken"
STR_PREV_NEXT: "Forrige/Neste"
STR_NEXT_PREV: "Neste/Forrige"
STR_KOSYNC: "KOSync"
STR_BOOKMARK_OPTION: "Bokmerke"
STR_DISABLED: "Deaktivert"
STR_NOTO_SERIF: "Noto Serif"
STR_NOTO_SANS: "Noto Sans"
STR_SMALL: "Liten"
STR_MEDIUM: "Middels"
STR_LARGE: "Stor"
STR_X_LARGE: "Ekstra stor"
STR_TIGHT: "Tett"
STR_NORMAL: "Normal"
STR_WIDE: "Bred"
STR_JUSTIFY: "Blokkjustert"
STR_ALIGN_LEFT: "Venstre"
STR_CENTER: "Midtstilt"
STR_ALIGN_RIGHT: "Høyre"
STR_PAGES_1: "1 side"
STR_PAGES_5: "5 sider"
STR_PAGES_10: "10 sider"
STR_PAGES_15: "15 sider"
STR_PAGES_30: "30 sider"
STR_UPDATE: "Oppdater"
STR_CHECKING_UPDATE: "Ser etter oppdatering..."
STR_NEW_UPDATE: "Ny oppdatering tilgjengelig!"
STR_CURRENT_VERSION: "Nåværende versjon: "
STR_NEW_VERSION: "Ny versjon: "
STR_UPDATING: "Oppdaterer..."
STR_NO_UPDATE: "Ingen oppdatering tilgjengelig"
STR_UPDATE_FAILED: "Oppdatering mislyktes"
STR_UPDATE_COMPLETE: "Oppdatering fullført"
STR_POWER_ON_HINT: "Trykk og hold av/på-knappen for å slå på igjen"
STR_RESTARTING_HINT: "Starter på nytt... Hvis enheten ikke starter, hold av/på-knappen i noen sekunder."
STR_NO_ENTRIES: "Ingen oppføringer funnet"
STR_DOWNLOADING: "Laster ned..."
STR_DOWNLOAD_FAILED: "Nedlasting mislyktes"
STR_ERROR_MSG: "Feil:"
STR_UNNAMED: "Uten navn"
STR_HOLD_OPEN_TO_DELETE: "Hold Åpne for å slette"
STR_NO_SERVER_URL: "Ingen server-URL konfigurert"
STR_FETCH_FEED_FAILED: "Kunne ikke hente feed"
STR_PARSE_FEED_FAILED: "Kunne ikke tolke feed"
STR_NEXT_PAGE: "Neste side »"
STR_PREV_PAGE: "« Forrige side"
STR_NETWORK_PREFIX: "Nettverk: "
STR_IP_ADDRESS_PREFIX: "IP-adresse: "
STR_ERROR_GENERAL_FAILURE: "Feil: Generell feil"
STR_ERROR_NETWORK_NOT_FOUND: "Feil: Nettverk ikke funnet"
STR_ERROR_CONNECTION_TIMEOUT: "Feil: Tidsavbrudd for tilkobling"
STR_SD_CARD: "SD-kort"
STR_BACK: "« Tilbake"
STR_EXIT: "« Avslutt"
STR_HOME: "« Hjem"
STR_SELECT: "Velg"
STR_SELECTED: "Valgt"
STR_TOGGLE: "Veksle"
STR_TOGGLE_BOOKMARK: "Veksle bokmerke"
STR_CONFIRM: "Bekreft"
STR_CANCEL: "Avbryt"
STR_CONNECT: "Koble til"
STR_OPEN: "Åpne"
STR_DOWNLOAD: "Last ned"
STR_RETRY: "Prøv igjen"
STR_YES: "Ja"
STR_NO: "Nei"
STR_SHOW: "Vis"
STR_HIDE: "Skjul"
STR_STATE_ON: "PÅ"
STR_STATE_OFF: "AV"
STR_NOT_SET: "Ikke angitt"
STR_DIR_LEFT: "Venstre"
STR_DIR_RIGHT: "Høyre"
STR_DIR_UP: "Opp"
STR_DIR_DOWN: "Ned"
STR_OK_BUTTON: "OK"
STR_SLEEP_COVER_FILTER: "Omslagsfilter for hvileskjerm"
STR_FILTER_CONTRAST: "Kontrast"
STR_CUSTOMISE_STATUS_BAR: "Tilpass statuslinje"
STR_CHAPTER_PAGE_COUNT: "Sideantall i kapittel"
STR_BOOK_PROGRESS_PERCENTAGE: "Bokfremdrift i prosent"
STR_PROGRESS_BAR: "Fremdriftslinje"
STR_PROGRESS_BAR_THICKNESS: "Tykkelse på fremdriftslinje"
STR_PROGRESS_BAR_THIN: "Tynn"
STR_PROGRESS_BAR_MEDIUM: "Middels"
STR_PROGRESS_BAR_THICK: "Tykk"
STR_BOOK: "Bok"
STR_CHAPTER: "Kapittel"
STR_EXAMPLE_CHAPTER: "Kapittel 21"
STR_EXAMPLE_BOOK: "Boktittel"
STR_PREVIEW: "Forhåndsvisning"
STR_TITLE: "Tittel"
STR_BATTERY: "Batteri"
STR_XTC_STATUS_BAR: "XTC-statuslinje"
STR_BOTTOM: "Nederst"
STR_TOP: "Øverst"
STR_CLOCK: "Klokke"
STR_CLOCK_UTC_OFFSET: "UTC-forskyvning for klokke"
STR_CLOCK_FORMAT: "Klokkeformat"
STR_CLOCK_FORMAT_24H: "24-timers"
STR_CLOCK_FORMAT_12H: "12-timers"
STR_CURRENT_TIME: "Nåværende tid:"
STR_NEXT_FIELD: "Neste"
STR_CLOCK_SYNC: "Synkroniser klokke"
STR_CLOCK_SYNC_NOW: "Synkroniser klokke nå"
STR_CLOCK_SYNCING: "Synkroniserer fra NTP..."
STR_CLOCK_SYNC_OK: "Klokke synkronisert"
STR_CLOCK_SYNC_FAIL: "Synkronisering mislyktes"
STR_CLOCK_SYNC_NO_WIFI: "WiFi ikke tilkoblet"
STR_CLOCK_SYNC_NO_WIFI_HINT: "Koble til WiFi først, og prøv igjen."
STR_CLOCK_SYNCED: "Klokke synkronisert"
STR_UI_THEME: "Grensesnittstema"
STR_THEME_CLASSIC: "Klassisk"
STR_THEME_LYRA: "Lyra"
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
STR_SUNLIGHT_FADING_FIX: "Retting for soluttoning"
STR_REMAP_FRONT_BUTTONS: "Tilordne frontknapper på nytt"
STR_BOOKMARKS: "Bokmerker"
STR_BOOKMARK_ADDED: "Bokmerke lagt til."
STR_BOOKMARK_REMOVED: "Bokmerke fjernet."
STR_OPDS_BROWSER: "OPDS-leser"
STR_SEARCH: "Søk"
STR_COVER_CUSTOM: "Omslag + Egendefinert"
STR_QUICK_RESUME: "Hurtig gjenopptak"
STR_MENU_RECENT_BOOKS: "Nylige bøker"
STR_REMOVE_FROM_RECENTS: "Fjern fra Nylige bøker?"
STR_NO_RECENT_BOOKS: "Ingen nylige bøker"
STR_CALIBRE_DESC: "Bruk trådløs enhetsoverføring med Calibre"
STR_FORGET_AND_REMOVE: "Glem nettverk og fjern lagret passord?"
STR_FORGET_BUTTON: "Glem"
STR_CALIBRE_STARTING: "Starter Calibre..."
STR_CALIBRE_SETUP: "Oppsett"
STR_CALIBRE_STATUS: "Status"
STR_CLEAR_BUTTON: "Tøm"
STR_DEFAULT_VALUE: "Standard"
STR_REMAP_PROMPT: "Trykk en frontknapp for hver rolle"
STR_UNASSIGNED: "Ikke tilordnet"
STR_ALREADY_ASSIGNED: "Allerede tilordnet"
STR_REMAP_RESET_HINT: "Sideknapp opp: Nullstill til standardoppsett"
STR_REMAP_CANCEL_HINT: "Sideknapp ned: Avbryt omtilordning"
STR_HW_BACK_LABEL: "Tilbake (1. knapp)"
STR_HW_CONFIRM_LABEL: "Bekreft (2. knapp)"
STR_HW_LEFT_LABEL: "Venstre (3. knapp)"
STR_HW_RIGHT_LABEL: "Høyre (4. knapp)"
STR_GO_TO_PERCENT: "Gå til %"
STR_GO_HOME_BUTTON: "Hjem"
STR_SYNC_PROGRESS: "Synkroniser fremdrift"
STR_DELETE_CACHE: "Slett bokbuffer"
STR_DELETE: "Slett"
STR_CONFIRM_DELETE_BOOKMARK: "Slette dette bokmerket?"
STR_DISPLAY_QR: "Vis side som QR"
STR_CHAPTER_PREFIX: "Kapittel: "
STR_PAGES_SEPARATOR: " sider | "
STR_BOOK_PREFIX: "Bok: "
STR_CALIBRE_URL_HINT: "For Calibre, legg til /opds i URL-en din"
STR_PERCENT_STEP_HINT: "Venstre/Høyre: 1% Opp/Ned: 10%"
STR_SYNCING_TIME: "Synkroniserer tid..."
STR_CALC_HASH: "Beregner dokument-hash..."
STR_HASH_FAILED: "Kunne ikke beregne dokument-hash"
STR_FETCH_PROGRESS: "Henter ekstern fremdrift..."
STR_UPLOAD_PROGRESS: "Laster opp fremdrift..."
STR_NO_CREDENTIALS_MSG: "Ingen påloggingsinfo angitt"
STR_KOREADER_SETUP_HINT: "Sett opp KOReader-konto i Innstillinger"
STR_PROGRESS_FOUND: "Fremdrift funnet!"
STR_REMOTE_LABEL: "Ekstern:"
STR_LOCAL_LABEL: "Lokal:"
STR_PAGE_OVERALL_FORMAT: "Side %d, %.2f%% totalt"
STR_PAGE_TOTAL_OVERALL_FORMAT: "Side %d/%d, %.2f%% totalt"
STR_DEVICE_FROM_FORMAT: " Fra: %s"
STR_APPLY_REMOTE: "Bruk ekstern fremdrift"
STR_UPLOAD_LOCAL: "Last opp lokal fremdrift"
STR_NO_REMOTE_MSG: "Ingen ekstern fremdrift funnet"
STR_UPLOAD_PROMPT: "Last opp nåværende posisjon?"
STR_UPLOAD_SUCCESS: "Fremdrift lastet opp!"
STR_SYNC_FAILED_MSG: "Synkronisering mislyktes"
STR_SAVE_PROGRESS_FAILED: "Kunne ikke lagre fremdrift"
STR_SECTION_PREFIX: "Del "
STR_UPLOAD: "Last opp"
STR_BOOK_S_STYLE: "Bokens stil"
STR_EMBEDDED_STYLE: "Innebygd stil"
STR_FOCUS_READING: "Fokuslesing"
STR_OPDS_SERVER_URL: "OPDS-server-URL"
STR_PWR_BTN_FOOTNOTE_BACK: "Hurtig retur fra fotnoter"
STR_SET_SLEEP_COVER: "Velg omslag"
STR_FOOTNOTES: "Fotnoter"
STR_NO_FOOTNOTES: "Ingen fotnoter på denne siden"
STR_LINK: "[lenke]"
STR_SCREENSHOT_BUTTON: "Ta skjermbilde"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Aldri"
STR_SLEEP_TIMER_STEP_HINT: "Venstre/Høyre: 1 min Opp/Ned: 5 min"
STR_ADD_SERVER: "Legg til server"
STR_SERVER_NAME: "Servernavn"
STR_NO_SERVERS: "Ingen OPDS-servere konfigurert"
STR_DELETE_SERVER: "Slett server"
STR_OPDS_SERVERS: "OPDS-servere"
STR_AUTO_TURN_ENABLED: "Automatisk bla aktivert: "
STR_AUTO_TURN_PAGES_PER_MIN: "Automatisk bla (sider per minutt)"
STR_MANAGE_FONTS: "Administrer skrifttyper"
STR_FONT_BROWSER: "Bla i skrifttyper"
STR_LOADING_FONT_LIST: "Laster skrifttypeliste..."
STR_NO_FONTS_AVAILABLE: "Ingen skrifttyper tilgjengelig"
STR_FONT_INSTALLED: "Skrifttype installert!"
STR_FONT_INSTALL_FAILED: "Installasjon av skrifttype mislyktes"
STR_INSTALLED: "Installert"
STR_DOWNLOAD_ALL: "Last ned alle"
STR_UPDATE_ALL: "Oppdater alle"
STR_UPDATE_AVAILABLE: "Oppdater"
STR_CRASH_TITLE: "Systemkrasj"
STR_CRASH_DESCRIPTION: "En detaljert rapport ble lagret i crash_report.txt. Legg ved denne filen i feilrapporten din."
STR_CRASH_REASON: "Krasjårsak:"
STR_CRASH_NO_REASON: "(Ingen årsak ble registrert)"
STR_TILT_PAGE_TURN: "Vipp for å bla"
STR_KB_HINT_MOVE_CURSOR: "Trykk VENSTRE eller HØYRE for å flytte markøren"
STR_KB_HINT_RETURN_CURSOR: "Trykk VENSTRE for å gå tilbake til markørposisjonen"
STR_KB_HINT_HIDE_PASSWORD: "Hold HØYRE og trykk [***] for å skjule passord"
STR_KB_HINT_SHOW_PASSWORD: "Hold HØYRE og trykk [abc] for å vise passord"
STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Trykk [***] for å skjule passord"
STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Trykk [abc] for å vise passord"
STR_KB_HINT_EDIT_ENTRY: "Hold OPP for å redigere oppføring"
STR_KB_TIPS: "Tips:"
STR_KB_HINT_RETURN_KEYBOARD: "Trykk NED for å gå tilbake til tastaturet"
STR_KB_HINT_EXIT_URL_MODE: "Trykk ABC for å avslutte URL-modus"
STR_KB_HINT_CLEAR_TEXT: "Hold DEL for å slette all tekst"
STR_KB_HINT_SECONDARY_CHAR: "Hold VELG for sekundærtegn"
STR_KB_HINT_UPPER_SECONDARY: "Hold VELG for STORE BOKSTAVER eller sekundærtegn"
STR_KB_HINT_LOWER_SECONDARY: "Hold VELG for små bokstaver eller sekundærtegn"
STR_KB_HINT_URL_SNIPPETS: "Trykk URL for tekstbiter"
STR_SD_FIRMWARE_UPDATE: "Fastvareoppdatering fra SD-kort"
STR_SELECT_FIRMWARE_FILE: "Velg fastvarefil (.bin)"
STR_NO_BIN_FILES: "Ingen .bin-filer funnet"
STR_VALIDATING_FIRMWARE: "Validerer fastvare..."
STR_INVALID_FIRMWARE: "Ugyldig fastvarefil"
STR_FIRMWARE_TOO_LARGE: "Fastvaren er for stor for partisjonen"
STR_FIRMWARE_TOO_SMALL: "Fastvarefilen er for liten"
STR_FIRMWARE_UPDATE_PROMPT: "Oppdatere fastvare?"
STR_FIRMWARE_FILE_OPEN_FAILED: "Kan ikke åpne filen"
STR_FIRMWARE_WRITE_FAILED: "Skriving av fastvare mislyktes"
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Ikke slå av!"
STR_RECOVERY_MODE: "Gjenopprettingsmodus"
STR_RECOVERY_MODE_HINT: "Legg firmware.bin i roten av SD-kortet og velg den"
+5
View File
@@ -316,6 +316,11 @@ STR_SERVER_NAME: "Nazwa serwera"
STR_NO_SERVERS: "Brak skonfigurowanych serwerów OPDS" STR_NO_SERVERS: "Brak skonfigurowanych serwerów OPDS"
STR_DELETE_SERVER: "Usuń serwer" STR_DELETE_SERVER: "Usuń serwer"
STR_OPDS_SERVERS: "Serwery OPDS" STR_OPDS_SERVERS: "Serwery OPDS"
STR_OPDS_DOWNLOAD_FOLDER: "Folder pobierania"
STR_OPDS_FILENAME_FORMAT: "Format nazwy pliku"
STR_FMT_AUTHOR_TITLE: "Autor - Tytuł"
STR_FMT_TITLE_AUTHOR: "Tytuł - Autor"
STR_FMT_TITLE: "Tytuł"
STR_AUTO_TURN_ENABLED: "Auto-kartkowanie: " STR_AUTO_TURN_ENABLED: "Auto-kartkowanie: "
STR_AUTO_TURN_PAGES_PER_MIN: "Auto-kartkowanie (str./min)" STR_AUTO_TURN_PAGES_PER_MIN: "Auto-kartkowanie (str./min)"
STR_DOWNLOAD_FONTS: "Pobierz czcionki" STR_DOWNLOAD_FONTS: "Pobierz czcionki"
+5
View File
@@ -339,6 +339,11 @@ STR_SERVER_NAME: "Nome do Servidor"
STR_NO_SERVERS: "Nenhum servidor OPDS configurado" STR_NO_SERVERS: "Nenhum servidor OPDS configurado"
STR_DELETE_SERVER: "Excluir Servidor" STR_DELETE_SERVER: "Excluir Servidor"
STR_OPDS_SERVERS: "Servidores OPDS" STR_OPDS_SERVERS: "Servidores OPDS"
STR_OPDS_DOWNLOAD_FOLDER: "Pasta de downloads"
STR_OPDS_FILENAME_FORMAT: "Formato do nome do arquivo"
STR_FMT_AUTHOR_TITLE: "Autor - Título"
STR_FMT_TITLE_AUTHOR: "Título - Autor"
STR_FMT_TITLE: "Título"
STR_AUTO_TURN_ENABLED: "Virada automática ativada: " STR_AUTO_TURN_ENABLED: "Virada automática ativada: "
STR_AUTO_TURN_PAGES_PER_MIN: "Virada automática (páginas por minuto)" STR_AUTO_TURN_PAGES_PER_MIN: "Virada automática (páginas por minuto)"
STR_MANAGE_FONTS: "Gerenciar fontes" STR_MANAGE_FONTS: "Gerenciar fontes"
+5
View File
@@ -300,6 +300,11 @@ STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Niciodată" STR_SLEEP_NEVER: "Niciodată"
STR_STEP_HINT_FRONT: "Butoane frontale:" STR_STEP_HINT_FRONT: "Butoane frontale:"
STR_STEP_HINT_SIDE: "Butoane laterale:" STR_STEP_HINT_SIDE: "Butoane laterale:"
STR_OPDS_DOWNLOAD_FOLDER: "Dosar descărcări"
STR_OPDS_FILENAME_FORMAT: "Format nume fișier"
STR_FMT_AUTHOR_TITLE: "Autor - Titlu"
STR_FMT_TITLE_AUTHOR: "Titlu - Autor"
STR_FMT_TITLE: "Titlu"
STR_SCREENSHOT_BUTTON: "Captură ecran" STR_SCREENSHOT_BUTTON: "Captură ecran"
STR_AUTO_TURN_ENABLED: "Răsfoire automată: " STR_AUTO_TURN_ENABLED: "Răsfoire automată: "
STR_AUTO_TURN_PAGES_PER_MIN: "Pagini pe minut" STR_AUTO_TURN_PAGES_PER_MIN: "Pagini pe minut"
+5
View File
@@ -339,6 +339,11 @@ STR_SERVER_NAME: "Имя сервера"
STR_NO_SERVERS: "Нет настроенных серверов OPDS" STR_NO_SERVERS: "Нет настроенных серверов OPDS"
STR_DELETE_SERVER: "Удалить сервер" STR_DELETE_SERVER: "Удалить сервер"
STR_OPDS_SERVERS: "Серверы OPDS" STR_OPDS_SERVERS: "Серверы OPDS"
STR_OPDS_DOWNLOAD_FOLDER: "Папка загрузок"
STR_OPDS_FILENAME_FORMAT: "Формат имени файла"
STR_FMT_AUTHOR_TITLE: "Автор - Название"
STR_FMT_TITLE_AUTHOR: "Название - Автор"
STR_FMT_TITLE: "Название"
STR_AUTO_TURN_ENABLED: "Автоперелистывание: " STR_AUTO_TURN_ENABLED: "Автоперелистывание: "
STR_AUTO_TURN_PAGES_PER_MIN: "Автоперелистывание (стр./мин)" STR_AUTO_TURN_PAGES_PER_MIN: "Автоперелистывание (стр./мин)"
STR_MANAGE_FONTS: "Управление шрифтами" STR_MANAGE_FONTS: "Управление шрифтами"
+5
View File
@@ -335,6 +335,11 @@ STR_SERVER_NAME: "Názov servera"
STR_NO_SERVERS: "Nie sú nakonfigurované žiadne OPDS servery" STR_NO_SERVERS: "Nie sú nakonfigurované žiadne OPDS servery"
STR_DELETE_SERVER: "Odstrániť server" STR_DELETE_SERVER: "Odstrániť server"
STR_OPDS_SERVERS: "OPDS servery" STR_OPDS_SERVERS: "OPDS servery"
STR_OPDS_DOWNLOAD_FOLDER: "Priečinok sťahovania"
STR_OPDS_FILENAME_FORMAT: "Formát názvu súboru"
STR_FMT_AUTHOR_TITLE: "Autor - Názov"
STR_FMT_TITLE_AUTHOR: "Názov - Autor"
STR_FMT_TITLE: "Názov"
STR_AUTO_TURN_ENABLED: "Automatické otáčanie strán zapnuté: " STR_AUTO_TURN_ENABLED: "Automatické otáčanie strán zapnuté: "
STR_AUTO_TURN_PAGES_PER_MIN: "Automatické otáčanie (strán za minútu)" STR_AUTO_TURN_PAGES_PER_MIN: "Automatické otáčanie (strán za minútu)"
STR_MANAGE_FONTS: "Správa písiem" STR_MANAGE_FONTS: "Správa písiem"
+5
View File
@@ -297,6 +297,11 @@ STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Nikoli" STR_SLEEP_NEVER: "Nikoli"
STR_STEP_HINT_FRONT: "Sprednji gumbi:" STR_STEP_HINT_FRONT: "Sprednji gumbi:"
STR_STEP_HINT_SIDE: "Stranski gumbi:" STR_STEP_HINT_SIDE: "Stranski gumbi:"
STR_OPDS_DOWNLOAD_FOLDER: "Mapa za prenose"
STR_OPDS_FILENAME_FORMAT: "Oblika imena datoteke"
STR_FMT_AUTHOR_TITLE: "Avtor - Naslov"
STR_FMT_TITLE_AUTHOR: "Naslov - Avtor"
STR_FMT_TITLE: "Naslov"
STR_SCREENSHOT_BUTTON: "Naredi posnetek zaslona" STR_SCREENSHOT_BUTTON: "Naredi posnetek zaslona"
STR_AUTO_TURN_ENABLED: "Samodejno obračanje: " STR_AUTO_TURN_ENABLED: "Samodejno obračanje: "
STR_AUTO_TURN_PAGES_PER_MIN: "Samodejno obračanje (strani na minuto)" STR_AUTO_TURN_PAGES_PER_MIN: "Samodejno obračanje (strani na minuto)"
+5
View File
@@ -345,6 +345,11 @@ STR_SERVER_NAME: "Nombre de servidor"
STR_NO_SERVERS: "No se configuraron servidores OPDS" STR_NO_SERVERS: "No se configuraron servidores OPDS"
STR_DELETE_SERVER: "Borrar servidor" STR_DELETE_SERVER: "Borrar servidor"
STR_OPDS_SERVERS: "Servidores OPDS" STR_OPDS_SERVERS: "Servidores OPDS"
STR_OPDS_DOWNLOAD_FOLDER: "Carpeta de descargas"
STR_OPDS_FILENAME_FORMAT: "Formato del nombre de archivo"
STR_FMT_AUTHOR_TITLE: "Autor - Título"
STR_FMT_TITLE_AUTHOR: "Título - Autor"
STR_FMT_TITLE: "Título"
STR_AUTO_TURN_ENABLED: "Avance activado: " STR_AUTO_TURN_ENABLED: "Avance activado: "
STR_AUTO_TURN_PAGES_PER_MIN: "Avance auto. (pág./min)" STR_AUTO_TURN_PAGES_PER_MIN: "Avance auto. (pág./min)"
STR_MANAGE_FONTS: "Gestionar tipografías" STR_MANAGE_FONTS: "Gestionar tipografías"
+5
View File
@@ -342,6 +342,11 @@ STR_SERVER_NAME: "Servernamn"
STR_NO_SERVERS: "Inga OPDS-servrar konfigurerade" STR_NO_SERVERS: "Inga OPDS-servrar konfigurerade"
STR_DELETE_SERVER: "Ta bort server" STR_DELETE_SERVER: "Ta bort server"
STR_OPDS_SERVERS: "OPDS-servrar" STR_OPDS_SERVERS: "OPDS-servrar"
STR_OPDS_DOWNLOAD_FOLDER: "Hämtningsmapp"
STR_OPDS_FILENAME_FORMAT: "Filnamnsformat"
STR_FMT_AUTHOR_TITLE: "Författare - Titel"
STR_FMT_TITLE_AUTHOR: "Titel - Författare"
STR_FMT_TITLE: "Titel"
STR_AUTO_TURN_ENABLED: "Automatisk vändning aktiverad: " STR_AUTO_TURN_ENABLED: "Automatisk vändning aktiverad: "
STR_AUTO_TURN_PAGES_PER_MIN: "Automatisk vändning (sidor per minut)" STR_AUTO_TURN_PAGES_PER_MIN: "Automatisk vändning (sidor per minut)"
STR_MANAGE_FONTS: "Hantera teckensnitt" STR_MANAGE_FONTS: "Hantera teckensnitt"
+95 -4
View File
@@ -68,10 +68,10 @@ STR_SHORT_PWR_BTN: "Kısa Güç Tuşu Tıklaması"
STR_ORIENTATION: "Okuma Yönü" STR_ORIENTATION: "Okuma Yönü"
STR_SIDE_BTN_LAYOUT: "Yan Tuş Dizilimi (okuyucu)" STR_SIDE_BTN_LAYOUT: "Yan Tuş Dizilimi (okuyucu)"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Ön düğmeleri yönlendir" STR_FRONT_BTN_FOLLOW_ORIENTATION: "Ön düğmeleri yönlendir"
STR_LONG_PRESS_BEHAVIOR: "Long-press button behavior" STR_LONG_PRESS_BEHAVIOR: "Uzun basma tuş davranışı"
STR_LONG_PRESS_BEHAVIOR_OFF: "OFF" STR_LONG_PRESS_BEHAVIOR_OFF: "KAPALI"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Chapter skip" STR_LONG_PRESS_BEHAVIOR_SKIP: "Bölüm atlama"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Orientation change" STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Yön değiştirme"
STR_FONT_PREVIEW_TEXT: "Pijamalı hasta yağız şoföre çabucak güvendi" STR_FONT_PREVIEW_TEXT: "Pijamalı hasta yağız şoföre çabucak güvendi"
STR_FONT_FAMILY: "Okuyucu Yazı Tipi Ailesi" STR_FONT_FAMILY: "Okuyucu Yazı Tipi Ailesi"
STR_FONT_SIZE: "Arayüz Yazı Boyutu" STR_FONT_SIZE: "Arayüz Yazı Boyutu"
@@ -291,6 +291,11 @@ STR_SLEEP_TIMER_VALUE_FORMAT: "%u dak"
STR_SLEEP_NEVER: "Asla" STR_SLEEP_NEVER: "Asla"
STR_STEP_HINT_FRONT: "Ön tuşlar:" STR_STEP_HINT_FRONT: "Ön tuşlar:"
STR_STEP_HINT_SIDE: "Yan tuşlar:" STR_STEP_HINT_SIDE: "Yan tuşlar:"
STR_OPDS_DOWNLOAD_FOLDER: "İndirme klasörü"
STR_OPDS_FILENAME_FORMAT: "Dosya adı biçimi"
STR_FMT_AUTHOR_TITLE: "Yazar - Başlık"
STR_FMT_TITLE_AUTHOR: "Başlık - Yazar"
STR_FMT_TITLE: "Başlık"
STR_NO_FILES_FOUND: "Dosya bulunamadı" STR_NO_FILES_FOUND: "Dosya bulunamadı"
STR_NO_FOOTNOTES: "Bu sayfada dipnot yok" STR_NO_FOOTNOTES: "Bu sayfada dipnot yok"
STR_PREVIEW: "Önizleme" STR_PREVIEW: "Önizleme"
@@ -306,3 +311,89 @@ STR_TITLE: "Başlık"
STR_TILT_PAGE_TURN: "Eğerek sayfa çevirme" STR_TILT_PAGE_TURN: "Eğerek sayfa çevirme"
STR_ADD_HIDDEN_NETWORK: "Gizli ağ ekle..." STR_ADD_HIDDEN_NETWORK: "Gizli ağ ekle..."
STR_ENTER_WIFI_SSID: "Ağ adını girin (SSID)" STR_ENTER_WIFI_SSID: "Ağ adını girin (SSID)"
STR_ADD_SERVER: "Sunucu Ekle"
STR_BOOKMARKS: "Yer İmleri"
STR_BOOKMARK_ADDED: "Yer imi eklendi."
STR_BOOKMARK_OPTION: "Yer İmi"
STR_BOTTOM: "Alt"
STR_CLOCK: "Saat"
STR_CLOCK_FORMAT: "Saat Biçimi"
STR_CLOCK_FORMAT_12H: "12 saat"
STR_CLOCK_FORMAT_24H: "24 saat"
STR_CLOCK_SYNC: "Saati Eşitle"
STR_CLOCK_SYNCED: "Saat Eşitlendi"
STR_CLOCK_SYNCING: "NTP'den eşitleniyor..."
STR_CLOCK_SYNC_FAIL: "Eşitleme başarısız"
STR_CLOCK_SYNC_NOW: "Saati şimdi eşitle"
STR_CLOCK_SYNC_NO_WIFI: "Wi-Fi bağlı değil"
STR_CLOCK_SYNC_NO_WIFI_HINT: "Önce Wi-Fi'ye bağlanın, sonra tekrar deneyin."
STR_CLOCK_SYNC_OK: "Saat eşitlendi"
STR_CLOCK_UTC_OFFSET: "Saat UTC Farkı"
STR_CONFIRM_DELETE_BOOKMARK: "Bu yer imi silinsin mi?"
STR_CONNECTING_SAVED_WIFI: "Kayıtlı Wi-Fi'ye bağlanılıyor..."
STR_CRASH_DESCRIPTION: "Ayrıntılı rapor crash_report.txt dosyasına kaydedildi. Lütfen hata bildiriminize bu dosyayı ekleyin."
STR_CRASH_NO_REASON: "(Neden kaydedilmedi)"
STR_CRASH_REASON: "Çökme nedeni:"
STR_CRASH_TITLE: "Sistem Çökmesi"
STR_CURRENT_TIME: "Şu anki saat:"
STR_DELETE_SERVER: "Sunucuyu Sil"
STR_DISABLED: "Devre dışı"
STR_DOWNLOAD_ALL: "Tümünü İndir"
STR_EOB_CONTINUE_WITH: "Şununla devam et"
STR_EOB_HOME: "Ana Ekran"
STR_FINDING_SAVED_WIFI: "Kayıtlı Wi-Fi aranıyor..."
STR_FIRMWARE_FILE_OPEN_FAILED: "Dosya açılamıyor"
STR_FIRMWARE_TOO_LARGE: "Firmware, bölümlemeye sığmıyor"
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_FONT_BROWSER: "Yazı Tipi Tarayıcısı"
STR_FONT_INSTALLED: "Yazı tipi yüklendi!"
STR_FONT_INSTALL_FAILED: "Yazı tipi yüklenemedi"
STR_FORCE_REFRESH: "Ekranı Yenile"
STR_INDEX_FAILED: "Endeksleme başarısız - geçersiz kitap"
STR_INSTALLED: "Yüklü"
STR_INVALID_FIRMWARE: "Geçersiz firmware dosyası"
STR_KB_HINT_CLEAR_TEXT: "Tüm metni silmek için DEL tuşunu basılı tutun"
STR_KB_HINT_EDIT_ENTRY: "Girdiyi düzenlemek için UP tuşunu basılı tutun"
STR_KB_HINT_EXIT_URL_MODE: "URL modundan çıkmak için ABC'ye basın"
STR_KB_HINT_HIDE_PASSWORD: "Şifreyi gizlemek için RIGHT'ı basılı tutup [***] tuşuna basın"
STR_KB_HINT_LOWER_SECONDARY: "Küçük harf veya ikincil karakter için SELECT'i basılı tutun"
STR_KB_HINT_MOVE_CURSOR: "İmleci taşımak için LEFT veya RIGHT'a basın"
STR_KB_HINT_RETURN_CURSOR: "İmleç konumuna dönmek için LEFT'e basın"
STR_KB_HINT_RETURN_KEYBOARD: "Klavyeye dönmek için DOWN'a basın"
STR_KB_HINT_SECONDARY_CHAR: "İkincil karakter için SELECT'i basılı tutun"
STR_KB_HINT_SHOW_PASSWORD: "Şifreyi göstermek için RIGHT'ı basılı tutup [abc] tuşuna basın"
STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Şifreyi gizlemek için [***] tuşuna basın"
STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Şifreyi göstermek için [abc] tuşuna basın"
STR_KB_HINT_UPPER_SECONDARY: "BÜYÜK harf veya ikincil karakter için SELECT'i basılı tutun"
STR_KB_HINT_URL_SNIPPETS: "Hazır kalıplar için URL'ye basın"
STR_KB_TIPS: "İpuçları:"
STR_KOSYNC: "KOSync"
STR_LOADING_FONT_LIST: "Yazı tipi listesi yükleniyor..."
STR_LONG_PRESS_MENU: "Uzun Basma Menüsü"
STR_MANAGE_FONTS: "Yazı Tiplerini Yönet"
STR_NEXT_FIELD: "Sonraki"
STR_NEXT_PAGE: "Sonraki Sayfa »"
STR_NO_BIN_FILES: ".bin dosyası bulunamadı"
STR_NO_FONTS_AVAILABLE: "Kullanılabilir yazı tipi yok"
STR_NO_SERVERS: "Yapılandırılmış OPDS sunucusu yok"
STR_OPDS_SERVERS: "OPDS Sunucuları"
STR_PREV_PAGE: "« Önceki Sayfa"
STR_PWR_BTN_FOOTNOTE_BACK: "Dipnottan hızlı dönüş"
STR_RECOVERY_MODE: "Kurtarma Modu"
STR_RECOVERY_MODE_HINT: "firmware.bin dosyasını SD kartın köküne koyup seçin"
STR_RESTARTING_HINT: "Yeniden başlatılıyor... Cihaz yeniden başlamazsa güç düğmesini birkaç saniye basılı tutun."
STR_SD_FIRMWARE_UPDATE: "SD Karttan Firmware Güncelleme"
STR_SEARCH: "Ara"
STR_SELECT_FIRMWARE_FILE: "Firmware dosyası seçin (.bin)"
STR_SERVER_NAME: "Sunucu Adı"
STR_SET_SLEEP_COVER: "Kapak Ayarla"
STR_SHOW_NETWORKS: "Göster"
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
STR_TOP: "Üst"
STR_UPDATE_ALL: "Tümünü Güncelle"
STR_UPDATE_AVAILABLE: "Güncelle"
STR_VALIDATING_FIRMWARE: "Firmware doğrulanıyor..."
STR_XTC_STATUS_BAR: "XTC Durum Çubuğu"
+5
View File
@@ -336,6 +336,11 @@ STR_SERVER_NAME: "Назва сервера"
STR_NO_SERVERS: "Не налаштовано жодного сервера OPDS" STR_NO_SERVERS: "Не налаштовано жодного сервера OPDS"
STR_DELETE_SERVER: "Видалити сервер" STR_DELETE_SERVER: "Видалити сервер"
STR_OPDS_SERVERS: "Сервери OPDS" STR_OPDS_SERVERS: "Сервери OPDS"
STR_OPDS_DOWNLOAD_FOLDER: "Папка завантажень"
STR_OPDS_FILENAME_FORMAT: "Формат імені файлу"
STR_FMT_AUTHOR_TITLE: "Автор - Назва"
STR_FMT_TITLE_AUTHOR: "Назва - Автор"
STR_FMT_TITLE: "Назва"
STR_AUTO_TURN_ENABLED: "Автоперегортання увімк: " STR_AUTO_TURN_ENABLED: "Автоперегортання увімк: "
STR_AUTO_TURN_PAGES_PER_MIN: "Автоперегортання (ст/хв)" STR_AUTO_TURN_PAGES_PER_MIN: "Автоперегортання (ст/хв)"
STR_MANAGE_FONTS: "Керування шрифтами" STR_MANAGE_FONTS: "Керування шрифтами"
+5
View File
@@ -348,6 +348,11 @@ STR_SERVER_NAME: "Nom del servidor"
STR_NO_SERVERS: "No hi ha servidors OPDS configurats" STR_NO_SERVERS: "No hi ha servidors OPDS configurats"
STR_DELETE_SERVER: "Elimina el servidor" STR_DELETE_SERVER: "Elimina el servidor"
STR_OPDS_SERVERS: "Servidors OPDS" STR_OPDS_SERVERS: "Servidors OPDS"
STR_OPDS_DOWNLOAD_FOLDER: "Carpeta de baixades"
STR_OPDS_FILENAME_FORMAT: "Format del nom de fitxer"
STR_FMT_AUTHOR_TITLE: "Autor - Títol"
STR_FMT_TITLE_AUTHOR: "Títol - Autor"
STR_FMT_TITLE: "Títol"
STR_MANAGE_FONTS: "Gestiona les fonts" STR_MANAGE_FONTS: "Gestiona les fonts"
STR_FONT_BROWSER: "Navegador de fonts" STR_FONT_BROWSER: "Navegador de fonts"
STR_LOADING_FONT_LIST: "S'està carregant la llista de fonts..." STR_LOADING_FONT_LIST: "S'està carregant la llista de fonts..."
+5
View File
@@ -335,6 +335,11 @@ STR_SERVER_NAME: "Tên máy chủ"
STR_NO_SERVERS: "Chưa cấu hình máy chủ OPDS" STR_NO_SERVERS: "Chưa cấu hình máy chủ OPDS"
STR_DELETE_SERVER: "Xóa máy chủ" STR_DELETE_SERVER: "Xóa máy chủ"
STR_OPDS_SERVERS: "Máy chủ OPDS" STR_OPDS_SERVERS: "Máy chủ OPDS"
STR_OPDS_DOWNLOAD_FOLDER: "Thư mục tải xuống"
STR_OPDS_FILENAME_FORMAT: "Định dạng tên tệp"
STR_FMT_AUTHOR_TITLE: "Tác giả - Tựa đề"
STR_FMT_TITLE_AUTHOR: "Tựa đề - Tác giả"
STR_FMT_TITLE: "Tựa đề"
STR_AUTO_TURN_ENABLED: "Tự lật trang: " STR_AUTO_TURN_ENABLED: "Tự lật trang: "
STR_AUTO_TURN_PAGES_PER_MIN: "Tự lật (số trang mỗi phút)" STR_AUTO_TURN_PAGES_PER_MIN: "Tự lật (số trang mỗi phút)"
STR_MANAGE_FONTS: "Quản lý phông chữ" STR_MANAGE_FONTS: "Quản lý phông chữ"
+46 -2
View File
@@ -5,16 +5,27 @@
#include <ObfuscationUtils.h> #include <ObfuscationUtils.h>
namespace { namespace {
// Default sync server URL // Default sync server URL. crosspoint-sync speaks the full KOSync protocol, so
constexpr char DEFAULT_SERVER_URL[] = "https://sync.koreader.rocks:443"; // pointing at any other kosync server (e.g. https://sync.koreader.rocks:443)
// still works via the custom server URL setting.
constexpr char DEFAULT_SERVER_URL[] = "https://sync.crosspointreader.com";
// Default before config version 2. Configs saved without a version stamp and an
// empty serverUrl were implicitly syncing here — they get pinned on upgrade.
constexpr char LEGACY_DEFAULT_SERVER_URL[] = "https://sync.koreader.rocks:443";
// Bumped when a change to defaults would alter behavior for existing configs.
constexpr uint8_t CONFIG_VERSION = 2;
} // namespace } // namespace
void KOReaderCredentialStore::toJson(JsonDocument& doc) const { void KOReaderCredentialStore::toJson(JsonDocument& doc) const {
doc["cfgVersion"] = CONFIG_VERSION;
doc["username"] = getUsername(); doc["username"] = getUsername();
doc["password_obf"] = obfuscation::obfuscateToBase64(getPassword()); doc["password_obf"] = obfuscation::obfuscateToBase64(getPassword());
doc["serverUrl"] = getServerUrl(); doc["serverUrl"] = getServerUrl();
doc["matchMethod"] = static_cast<uint8_t>(getMatchMethod()); doc["matchMethod"] = static_cast<uint8_t>(getMatchMethod());
doc["sendMetadata"] = getSendMetadata(); doc["sendMetadata"] = getSendMetadata();
doc["syncBehavior"] = static_cast<uint8_t>(getSyncBehavior());
} }
bool KOReaderCredentialStore::fromJson(JsonVariantConst doc) { bool KOReaderCredentialStore::fromJson(JsonVariantConst doc) {
@@ -26,6 +37,19 @@ bool KOReaderCredentialStore::fromJson(JsonVariantConst doc) {
setCredentials(user, pass); setCredentials(user, pass);
setServerUrl(doc["serverUrl"] | ""); setServerUrl(doc["serverUrl"] | "");
// The default server changed in config v2 (sync.koreader.rocks -> crosspoint-sync).
// A pre-v2 config with credentials and no explicit URL was actively syncing
// against the old default — pin that URL so the upgrade doesn't switch servers
// out from under the user. Fresh setups get the new default.
const uint8_t cfgVersion = doc["cfgVersion"] | (uint8_t)1;
if (cfgVersion < CONFIG_VERSION) {
if (getServerUrl().empty() && hasCredentials()) {
LOG_DBG("KRS", "Pre-v2 config used the old default server; pinning %s", LEGACY_DEFAULT_SERVER_URL);
setServerUrl(LEGACY_DEFAULT_SERVER_URL);
}
needsResave = true; // stamp cfgVersion so this migration runs once
}
uint8_t method = doc["matchMethod"] | (uint8_t)0; uint8_t method = doc["matchMethod"] | (uint8_t)0;
if (method <= static_cast<uint8_t>(DocumentMatchMethod::BINARY)) { if (method <= static_cast<uint8_t>(DocumentMatchMethod::BINARY)) {
setMatchMethod(static_cast<DocumentMatchMethod>(method)); setMatchMethod(static_cast<DocumentMatchMethod>(method));
@@ -35,6 +59,18 @@ bool KOReaderCredentialStore::fromJson(JsonVariantConst doc) {
} }
setSendMetadata(doc["sendMetadata"] | false); setSendMetadata(doc["sendMetadata"] | false);
const JsonVariantConst behaviorValue = doc["syncBehavior"];
const bool missingBehavior = behaviorValue.isNull();
uint8_t behavior = behaviorValue | static_cast<uint8_t>(KOReaderSyncBehavior::ASK_EVERY_TIME);
if (behavior <= static_cast<uint8_t>(KOReaderSyncBehavior::SMART)) {
setSyncBehavior(static_cast<KOReaderSyncBehavior>(behavior));
needsResave = needsResave || missingBehavior;
} else {
LOG_DBG("KRS", "Invalid syncBehavior %u in JSON, resetting to ASK_EVERY_TIME", behavior);
setSyncBehavior(KOReaderSyncBehavior::ASK_EVERY_TIME);
needsResave = true;
}
if (needsResave) { if (needsResave) {
LOG_DBG("KRS", "Resaved KOReader credentials to update format"); LOG_DBG("KRS", "Resaved KOReader credentials to update format");
saveToFile(); saveToFile();
@@ -105,3 +141,11 @@ void KOReaderCredentialStore::setSendMetadata(bool enabled) {
sendMetadata = enabled; sendMetadata = enabled;
LOG_DBG("KRS", "Set send metadata: %s", enabled ? "true" : "false"); LOG_DBG("KRS", "Set send metadata: %s", enabled ? "true" : "false");
} }
void KOReaderCredentialStore::setSyncBehavior(KOReaderSyncBehavior behavior) {
if (static_cast<uint8_t>(behavior) > static_cast<uint8_t>(KOReaderSyncBehavior::SMART)) {
behavior = KOReaderSyncBehavior::ASK_EVERY_TIME;
}
syncBehavior = behavior;
LOG_DBG("KRS", "Set sync behavior: %s", behavior == KOReaderSyncBehavior::SMART ? "Smart" : "Ask");
}
@@ -11,6 +11,12 @@ enum class DocumentMatchMethod : uint8_t {
BINARY = 1, // Match by partial MD5 of file content (more accurate, but files must be identical) BINARY = 1, // Match by partial MD5 of file content (more accurate, but files must be identical)
}; };
// How manual "Sync Progress" resolves differences after fetching remote progress.
enum class KOReaderSyncBehavior : uint8_t {
ASK_EVERY_TIME = 0, // Preserve legacy behavior: always show Apply/Upload choices.
SMART = 1, // Auto-resolve simple cases using furthest progress.
};
/** /**
* Singleton class for storing KOReader sync credentials on the SD card. * Singleton class for storing KOReader sync credentials on the SD card.
* Passwords are XOR-obfuscated with the device's unique hardware MAC address * Passwords are XOR-obfuscated with the device's unique hardware MAC address
@@ -25,6 +31,7 @@ class KOReaderCredentialStore : public PersistableStore<KOReaderCredentialStore>
std::string serverUrl; // Custom sync server URL (empty = default) std::string serverUrl; // Custom sync server URL (empty = default)
DocumentMatchMethod matchMethod = DocumentMatchMethod::FILENAME; // Default to filename for compatibility DocumentMatchMethod matchMethod = DocumentMatchMethod::FILENAME; // Default to filename for compatibility
bool sendMetadata = false; // Send document metadata with progress sync bool sendMetadata = false; // Send document metadata with progress sync
KOReaderSyncBehavior syncBehavior = KOReaderSyncBehavior::SMART;
// Private constructor for singleton // Private constructor for singleton
KOReaderCredentialStore() = default; KOReaderCredentialStore() = default;
@@ -65,6 +72,10 @@ class KOReaderCredentialStore : public PersistableStore<KOReaderCredentialStore>
// Send metadata setting // Send metadata setting
void setSendMetadata(bool enabled); void setSendMetadata(bool enabled);
bool getSendMetadata() const { return sendMetadata; } bool getSendMetadata() const { return sendMetadata; }
// Sync behavior
void setSyncBehavior(KOReaderSyncBehavior behavior);
KOReaderSyncBehavior getSyncBehavior() const { return syncBehavior; }
}; };
// Helper macro to access credential store // Helper macro to access credential store
+67
View File
@@ -78,6 +78,43 @@ KOReaderSyncClient::Error KOReaderSyncClient::authenticate() {
return SERVER_ERROR; return SERVER_ERROR;
} }
KOReaderSyncClient::Error KOReaderSyncClient::createUser() {
lastHttpCode = 0;
if (!KOREADER_STORE.hasCredentials()) {
LOG_DBG("KOSync", "No credentials configured");
return NO_CREDENTIALS;
}
const std::string url = KOREADER_STORE.getBaseUrl() + "/users/create";
LOG_DBG("KOSync", "Creating account: %s (heap: %u)", url.c_str(), (unsigned)ESP.getFreeHeap());
if (insufficientHeap()) return LOW_MEMORY;
JsonDocument doc;
doc["username"] = KOREADER_STORE.getUsername();
doc["password"] = KOREADER_STORE.getMd5Password();
std::string body;
serializeJson(doc, body);
freeink::SecureHttpClient http;
http.setInsecure();
if (!http.begin(url)) {
LOG_ERR("KOSync", "Bad URL: %s", url.c_str());
return NETWORK_ERROR;
}
http.addHeader("Accept", "application/vnd.koreader.v1+json");
http.addHeader("Content-Type", "application/json");
const int httpCode = http.sendRequest("POST", body);
http.end();
lastHttpCode = httpCode;
LOG_DBG("KOSync", "Create user response: %d", httpCode);
if (httpCode <= 0) return NETWORK_ERROR;
if (httpCode == 200 || httpCode == 201) return OK;
if (httpCode == 402) return USER_EXISTS;
return SERVER_ERROR;
}
KOReaderSyncClient::Error KOReaderSyncClient::getProgress(const std::string& documentHash, KOReaderSyncClient::Error KOReaderSyncClient::getProgress(const std::string& documentHash,
KOReaderProgress& outProgress) { KOReaderProgress& outProgress) {
lastHttpCode = 0; lastHttpCode = 0;
@@ -124,6 +161,24 @@ KOReaderSyncClient::Error KOReaderSyncClient::getProgress(const std::string& doc
outProgress.deviceId = doc["device_id"].as<std::string>(); outProgress.deviceId = doc["device_id"].as<std::string>();
outProgress.timestamp = doc["timestamp"].as<int64_t>(); outProgress.timestamp = doc["timestamp"].as<int64_t>();
// Extended crosspoint-sync field; absent on plain kosync servers.
outProgress.position.reset();
const JsonObjectConst pos = doc["position"].as<JsonObjectConst>();
if (!pos.isNull()) {
KOReaderRichPosition rich;
rich.pctQ = pos["pctQ"].as<uint32_t>();
rich.spineIndex = pos["spine"].as<uint16_t>();
rich.pageNumber = pos["page"].as<uint16_t>();
const uint16_t pages = pos["pages"].as<uint16_t>();
rich.totalPages = pages > 0 ? pages : 1;
const uint16_t para = pos["para"].as<uint16_t>();
if (para > 0) rich.paragraphIndex = para;
rich.xpath = pos["xpath"].as<const char*>() ? pos["xpath"].as<const char*>() : "";
LOG_DBG("KOSync", "Got rich position: spine=%u page=%u/%u para=%u", rich.spineIndex, rich.pageNumber,
rich.totalPages, para);
outProgress.position = std::move(rich);
}
LOG_DBG("KOSync", "Got progress: %.2f%% at %s", outProgress.percentage * 100, outProgress.progress.c_str()); LOG_DBG("KOSync", "Got progress: %.2f%% at %s", outProgress.percentage * 100, outProgress.progress.c_str());
return OK; return OK;
} }
@@ -158,6 +213,18 @@ KOReaderSyncClient::Error KOReaderSyncClient::updateProgress(const KOReaderProgr
doc["percentage"] = progress.percentage; doc["percentage"] = progress.percentage;
doc["device"] = DEVICE_NAME; doc["device"] = DEVICE_NAME;
doc["device_id"] = DEVICE_ID; doc["device_id"] = DEVICE_ID;
if (progress.position.has_value()) {
// Extended crosspoint-sync field; kosync servers ignore unknown keys.
const auto& p = *progress.position;
auto pos = doc["position"].to<JsonObject>();
pos["pctQ"] = p.pctQ;
pos["spine"] = p.spineIndex;
pos["page"] = p.pageNumber;
pos["pages"] = p.totalPages;
if (p.paragraphIndex.has_value()) pos["para"] = *p.paragraphIndex;
// Server rejects the whole position object if xpath exceeds 120 bytes.
if (!p.xpath.empty() && p.xpath.size() <= 120) pos["xpath"] = p.xpath;
}
std::string body; std::string body;
serializeJson(doc, body); serializeJson(doc, body);
+42 -8
View File
@@ -14,17 +14,33 @@ struct KOReaderMetadata {
std::string authors; // Author(s) from EPUB metadata std::string authors; // Author(s) from EPUB metadata
}; };
/**
* Rich CrossPoint position sent alongside progress uploads. Maps 1:1 onto the
* crosspoint-sync extended `position` object (see crosspoint-sync docs/API.md).
* The official KOSync server ignores unknown fields; crosspoint-sync stores it
* so CrossPoint<->CrossPoint sync is lossless instead of xpath-approximated.
*/
struct KOReaderRichPosition {
uint32_t pctQ = 0; // Percentage quantized 0..1,000,000 (authoritative)
uint16_t spineIndex = 0; // Spine (chapter) index
uint16_t pageNumber = 0; // Page within spine (layout-dependent hint)
uint16_t totalPages = 1; // Spine page count (layout-dependent hint)
std::optional<uint16_t> paragraphIndex; // Synthetic 1-based paragraph index
std::string xpath; // KOReader-style xpath (server cap: 120 bytes)
};
/** /**
* Progress data from KOReader sync server. * Progress data from KOReader sync server.
*/ */
struct KOReaderProgress { struct KOReaderProgress {
std::string document; // Document hash std::string document; // Document hash
std::string progress; // XPath-like progress string std::string progress; // XPath-like progress string
float percentage; // Progress percentage (0.0 to 1.0) float percentage; // Progress percentage (0.0 to 1.0)
std::string device; // Device name std::string device; // Device name
std::string deviceId; // Device ID std::string deviceId; // Device ID
int64_t timestamp; // Unix timestamp of last update int64_t timestamp; // Unix timestamp of last update
std::optional<KOReaderMetadata> metadata; // Optional document metadata std::optional<KOReaderMetadata> metadata; // Optional document metadata
std::optional<KOReaderRichPosition> position; // Optional rich position (crosspoint-sync servers only)
}; };
/** /**
@@ -43,7 +59,17 @@ struct KOReaderProgress {
*/ */
class KOReaderSyncClient { class KOReaderSyncClient {
public: public:
enum Error { OK = 0, NO_CREDENTIALS, NETWORK_ERROR, AUTH_FAILED, SERVER_ERROR, JSON_ERROR, NOT_FOUND, LOW_MEMORY }; enum Error {
OK = 0,
NO_CREDENTIALS,
NETWORK_ERROR,
AUTH_FAILED,
SERVER_ERROR,
JSON_ERROR,
NOT_FOUND,
LOW_MEMORY,
USER_EXISTS
};
/** /**
* Authenticate with the sync server (validate credentials). * Authenticate with the sync server (validate credentials).
@@ -51,6 +77,14 @@ class KOReaderSyncClient {
*/ */
static Error authenticate(); static Error authenticate();
/**
* Register a new account on the sync server using the stored credentials
* (POST /users/create with the MD5 auth key — the server never sees the
* plain password).
* @return OK on success, USER_EXISTS if the username is taken
*/
static Error createUser();
/** /**
* Get reading progress for a document. * Get reading progress for a document.
* @param documentHash The document hash (from KOReaderDocumentId) * @param documentHash The document hash (from KOReaderDocumentId)
+53
View File
@@ -724,6 +724,59 @@ SavedProgressPosition ProgressMapper::toSavedProgress(const std::shared_ptr<Epub
return result; return result;
} }
std::optional<CrossPointPosition> ProgressMapper::fromRichPosition(const std::shared_ptr<Epub>& epub,
const KOReaderRichPosition& rich,
GfxRenderer& renderer) {
const int spineCount = epub->getSpineItemsCount();
if (static_cast<int>(rich.spineIndex) >= spineCount) {
LOG_DBG("PM", "Rich position spine %u out of range (%d spine items)", rich.spineIndex, spineCount);
return std::nullopt;
}
CrossPointPosition result{};
result.spineIndex = rich.spineIndex;
Section tempSection(epub, result.spineIndex, renderer);
const auto cachedCount = tempSection.getCachedPageCount();
if (!cachedCount || *cachedCount <= 0) {
// No local layout for the target spine yet; the percentage/xpath mapping
// handles density estimation better than a blind copy of remote pages.
LOG_DBG("PM", "Rich position spine %u has no cached page count", rich.spineIndex);
return std::nullopt;
}
result.totalPages = *cachedCount;
const int remotePages = rich.totalPages > 0 ? rich.totalPages : 1;
if (result.totalPages == remotePages) {
// Identical layout (same render settings) — the page transfers losslessly.
result.pageNumber = std::min<int>(rich.pageNumber, result.totalPages - 1);
LOG_DBG("PM", "Rich position exact: spine=%d page=%d/%d", result.spineIndex, result.pageNumber, result.totalPages);
return result;
}
// Layout differs; the paragraph LUT is the most accurate anchor we have.
if (rich.paragraphIndex.has_value()) {
const auto lutPage = tempSection.getPageForParagraphIndex(*rich.paragraphIndex);
if (lutPage.has_value()) {
result.paragraphIndex = *rich.paragraphIndex;
result.hasParagraphIndex = true;
result.pageNumber = std::min<int>(*lutPage, result.totalPages - 1);
LOG_DBG("PM", "Rich position para %u -> spine=%d page=%d/%d", *rich.paragraphIndex, result.spineIndex,
result.pageNumber, result.totalPages);
return result;
}
}
// Fall back to the intra-spine page fraction.
const float intra =
(remotePages > 1) ? static_cast<float>(rich.pageNumber) / static_cast<float>(remotePages - 1) : 0.0f;
result.pageNumber = std::max(
0, std::min(static_cast<int>(intra * static_cast<float>(result.totalPages - 1) + 0.5f), result.totalPages - 1));
LOG_DBG("PM", "Rich position scaled: spine=%d remote %u/%d -> page=%d/%d", result.spineIndex, rich.pageNumber,
remotePages, result.pageNumber, result.totalPages);
return result;
}
CrossPointPosition ProgressMapper::toCrossPoint(const std::shared_ptr<Epub>& epub, const SavedProgressPosition& koPos, CrossPointPosition ProgressMapper::toCrossPoint(const std::shared_ptr<Epub>& epub, const SavedProgressPosition& koPos,
GfxRenderer& renderer, int currentSpineIndex, GfxRenderer& renderer, int currentSpineIndex,
int totalPagesInCurrentSpine, int fallbackTotalPages) { int totalPagesInCurrentSpine, int fallbackTotalPages) {
+17
View File
@@ -3,8 +3,11 @@
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <memory> #include <memory>
#include <optional>
#include <string> #include <string>
#include "KOReaderSyncClient.h"
/** /**
* CrossPoint position representation. * CrossPoint position representation.
*/ */
@@ -65,6 +68,20 @@ class ProgressMapper {
GfxRenderer& renderer, int currentSpineIndex = -1, GfxRenderer& renderer, int currentSpineIndex = -1,
int totalPagesInCurrentSpine = 0, int fallbackTotalPages = 0); int totalPagesInCurrentSpine = 0, int fallbackTotalPages = 0);
/**
* Convert a rich CrossPoint position (downloaded from a crosspoint-sync
* server) directly to a CrossPoint position, without XPath approximation.
* When the local layout matches the uploader's (same spine page count) the
* page transfers losslessly; otherwise the paragraph LUT or the intra-spine
* page fraction is used.
*
* @return The position, or std::nullopt when the rich position cannot be
* applied (spine out of range, no section cache) and the caller
* should fall back to toCrossPoint().
*/
static std::optional<CrossPointPosition> fromRichPosition(const std::shared_ptr<Epub>& epub,
const KOReaderRichPosition& rich, GfxRenderer& renderer);
private: private:
/** /**
* Generate a fallback XPath by streaming the spine item's XHTML and resolving * Generate a fallback XPath by streaming the spine item's XHTML and resolving
+8 -19
View File
@@ -225,29 +225,17 @@ unsigned long HalGPIO::getHeldTime() const { return inputMgr.getHeldTime(); }
unsigned long HalGPIO::getPowerButtonHeldTime() const { return inputMgr.getPowerButtonHeldTime(); } unsigned long HalGPIO::getPowerButtonHeldTime() const { return inputMgr.getPowerButtonHeldTime(); }
void HalGPIO::startDeepSleep() { bool HalGPIO::verifyPowerButtonWakeup(uint16_t requiredDurationMs, bool shortPressAllowed) {
// Ensure that the power button has been released to avoid immediately turning back on if you're holding it
while (inputMgr.isPressed(BTN_POWER)) {
delay(50);
inputMgr.update();
}
// Arm the wakeup trigger *after* the button is released
esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW);
// Enter Deep Sleep
esp_deep_sleep_start();
}
void HalGPIO::verifyPowerButtonWakeup(uint16_t requiredDurationMs, bool shortPressAllowed) {
if (shortPressAllowed) { if (shortPressAllowed) {
// Fast path - no duration check needed // Fast path - no duration check needed
return; return true;
} }
// TODO: Intermittent edge case remains: a single tap followed by another single tap // TODO: Intermittent edge case remains: a single tap followed by another single tap
// can still power on the device. Tighten wake debounce/state handling here. // can still power on the device. Tighten wake debounce/state handling here.
// Calibrate: subtract boot time already elapsed, assuming button held since boot // Calibrate: subtract boot time already elapsed, assuming button held since boot.
const uint16_t calibration = millis(); const unsigned long calibration = millis();
const uint16_t calibratedDuration = (calibration < requiredDurationMs) ? (requiredDurationMs - calibration) : 1; const unsigned long calibratedDuration = (calibration < requiredDurationMs) ? (requiredDurationMs - calibration) : 1;
const auto start = millis(); const auto start = millis();
inputMgr.update(); inputMgr.update();
@@ -262,11 +250,12 @@ void HalGPIO::verifyPowerButtonWakeup(uint16_t requiredDurationMs, bool shortPre
inputMgr.update(); inputMgr.update();
} while (inputMgr.isPressed(BTN_POWER) && inputMgr.getPowerButtonHeldTime() < calibratedDuration); } while (inputMgr.isPressed(BTN_POWER) && inputMgr.getPowerButtonHeldTime() < calibratedDuration);
if (inputMgr.getPowerButtonHeldTime() < calibratedDuration) { if (inputMgr.getPowerButtonHeldTime() < calibratedDuration) {
startDeepSleep(); return false;
} }
} else { } else {
startDeepSleep(); return false;
} }
return true;
} }
bool HalGPIO::isUsbConnected() const { bool HalGPIO::isUsbConnected() const {
+2 -5
View File
@@ -72,13 +72,10 @@ class HalGPIO {
unsigned long getHeldTime() const; unsigned long getHeldTime() const;
unsigned long getPowerButtonHeldTime() const; unsigned long getPowerButtonHeldTime() const;
// Setup wake up GPIO and enter deep sleep
void startDeepSleep();
// Verify power button was held long enough after wakeup. // Verify power button was held long enough after wakeup.
// If verification fails, enters deep sleep and does not return. // Returns true if verification succeeded, false if device should return to sleep.
// Should only be called when wakeup reason is PowerButton. // Should only be called when wakeup reason is PowerButton.
void verifyPowerButtonWakeup(uint16_t requiredDurationMs, bool shortPressAllowed); bool verifyPowerButtonWakeup(uint16_t requiredDurationMs, bool shortPressAllowed);
// Check if USB is connected // Check if USB is connected
bool isUsbConnected() const; bool isUsbConnected() const;
+12 -2
View File
@@ -29,8 +29,8 @@ class CrossPointSettings {
LIGHT = 1, LIGHT = 1,
CUSTOM = 2, CUSTOM = 2,
COVER = 3, COVER = 3,
BLANK = 4, COVER_CUSTOM = 4,
COVER_CUSTOM = 5, BLANK = 5,
QUICK_RESUME = 6, QUICK_RESUME = 6,
SLEEP_SCREEN_MODE_COUNT SLEEP_SCREEN_MODE_COUNT
}; };
@@ -238,6 +238,14 @@ class CrossPointSettings {
// Reader screen margin settings // Reader screen margin settings
uint8_t screenMargin = 5; uint8_t screenMargin = 5;
// OPDS download destination folder ("" = SD root). Global; edited from the
// OPDS server list. Persisted via a category-less SettingInfo::String in
// SettingsList.h, so it stays out of the on-device Settings screen.
char opdsDownloadFolder[64] = "";
// On-disk filename format for OPDS downloads (0=Author-Title default, 1=Title-Author,
// 2=Title). See OpdsFilenameFormat. Persisted via a category-less SettingInfo::Enum,
// edited from the OPDS server list; hidden from the on-device Settings screen.
uint8_t opdsFilenameFormat = 0;
// Hide battery percentage // Hide battery percentage
uint8_t hideBatteryPercentage = HIDE_NEVER; uint8_t hideBatteryPercentage = HIDE_NEVER;
// Long-press page turn button behavior // Long-press page turn button behavior
@@ -263,6 +271,8 @@ class CrossPointSettings {
uint8_t removeReadBooksFromRecents = 0; uint8_t removeReadBooksFromRecents = 0;
// Move epub to /Read/ folder on SD card when finished (0 = disabled, 1 = enabled) // Move epub to /Read/ folder on SD card when finished (0 = disabled, 1 = enabled)
uint8_t moveFinishedToReadFolder = 0; uint8_t moveFinishedToReadFolder = 0;
// Short press Back goes to file browser instead of home (0 = disabled, 1 = enabled)
uint8_t backShortToFileBrowser = 0;
// Image rendering mode in EPUB reader // Image rendering mode in EPUB reader
uint8_t imageRendering = IMAGES_DISPLAY; uint8_t imageRendering = IMAGES_DISPLAY;
// Tilt-based page turning (X3 only — requires QMI8658 IMU) // Tilt-based page turning (X3 only — requires QMI8658 IMU)
+20
View File
@@ -181,6 +181,8 @@ inline std::vector<SettingInfo> getSettingsList(const SdCardFontRegistry* regist
"shortPwrBtn", StrId::STR_CAT_CONTROLS), "shortPwrBtn", StrId::STR_CAT_CONTROLS),
SettingInfo::Toggle(StrId::STR_PWR_BTN_FOOTNOTE_BACK, &CrossPointSettings::pwrBtnFootnoteBack, SettingInfo::Toggle(StrId::STR_PWR_BTN_FOOTNOTE_BACK, &CrossPointSettings::pwrBtnFootnoteBack,
"pwrBtnFootnoteBack", StrId::STR_CAT_CONTROLS), "pwrBtnFootnoteBack", StrId::STR_CAT_CONTROLS),
SettingInfo::Toggle(StrId::STR_BACK_SHORT_TO_FILE_BROWSER, &CrossPointSettings::backShortToFileBrowser,
"backShortToFileBrowser", StrId::STR_CAT_CONTROLS),
// --- System --- // --- System ---
SettingInfo::Value( SettingInfo::Value(
@@ -194,6 +196,16 @@ inline std::vector<SettingInfo> getSettingsList(const SdCardFontRegistry* regist
SettingInfo::Toggle(StrId::STR_MOVE_FINISHED_TO_READ, &CrossPointSettings::moveFinishedToReadFolder, SettingInfo::Toggle(StrId::STR_MOVE_FINISHED_TO_READ, &CrossPointSettings::moveFinishedToReadFolder,
"moveFinishedToReadFolder", StrId::STR_CAT_SYSTEM), "moveFinishedToReadFolder", StrId::STR_CAT_SYSTEM),
// OPDS download folder: persisted + web-exposed, but category-less so it
// is hidden from the on-device Settings screen (edited via OPDS UI).
SettingInfo::String(StrId::STR_OPDS_DOWNLOAD_FOLDER, &SETTINGS.opdsDownloadFolder[0],
sizeof(SETTINGS.opdsDownloadFolder), "opdsDownloadFolder"),
// OPDS download filename format: persisted + web-exposed, category-less so it
// is hidden from the on-device Settings screen (cycled from the OPDS UI).
SettingInfo::Enum(StrId::STR_OPDS_FILENAME_FORMAT, &CrossPointSettings::opdsFilenameFormat,
{StrId::STR_FMT_AUTHOR_TITLE, StrId::STR_FMT_TITLE_AUTHOR, StrId::STR_FMT_TITLE},
"opdsFilenameFormat"),
// --- KOReader Sync (web-only, uses KOReaderCredentialStore) --- // --- KOReader Sync (web-only, uses KOReaderCredentialStore) ---
SettingInfo::DynamicString( SettingInfo::DynamicString(
StrId::STR_KOREADER_USERNAME, [] { return KOREADER_STORE.getUsername(); }, StrId::STR_KOREADER_USERNAME, [] { return KOREADER_STORE.getUsername(); },
@@ -232,6 +244,14 @@ inline std::vector<SettingInfo> getSettingsList(const SdCardFontRegistry* regist
KOREADER_STORE.saveToFile(); KOREADER_STORE.saveToFile();
}, },
"koSendMetadata", StrId::STR_KOREADER_SYNC), "koSendMetadata", StrId::STR_KOREADER_SYNC),
SettingInfo::DynamicEnum(
StrId::STR_SYNC_BEHAVIOR, {StrId::STR_ASK_EVERY_TIME, StrId::STR_SMART_SYNC},
[] { return static_cast<uint8_t>(KOREADER_STORE.getSyncBehavior()); },
[](uint8_t v) {
KOREADER_STORE.setSyncBehavior(static_cast<KOReaderSyncBehavior>(v));
KOREADER_STORE.saveToFile();
},
"koSyncBehavior", StrId::STR_KOREADER_SYNC),
// --- Status Bar Settings (web-only, uses StatusBarSettingsActivity) --- // --- Status Bar Settings (web-only, uses StatusBarSettingsActivity) ---
SettingInfo::Toggle(StrId::STR_CHAPTER_PAGE_COUNT, &CrossPointSettings::statusBarChapterPageCount, SettingInfo::Toggle(StrId::STR_CHAPTER_PAGE_COUNT, &CrossPointSettings::statusBarChapterPageCount,
"statusBarChapterPageCount", StrId::STR_CUSTOMISE_STATUS_BAR), "statusBarChapterPageCount", StrId::STR_CUSTOMISE_STATUS_BAR),
@@ -2,11 +2,13 @@
#include <Arduino.h> #include <Arduino.h>
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <HalStorage.h>
#include <I18n.h> #include <I18n.h>
#include <Logging.h> #include <Logging.h>
#include <OpdsStream.h> #include <OpdsStream.h>
#include <WiFi.h> #include <WiFi.h>
#include "CrossPointSettings.h"
#include "MappedInputManager.h" #include "MappedInputManager.h"
#include "SilentRestart.h" #include "SilentRestart.h"
#include "activities/network/WifiSelectionActivity.h" #include "activities/network/WifiSelectionActivity.h"
@@ -15,6 +17,7 @@
#include "fontIds.h" #include "fontIds.h"
#include "network/HttpDownloader.h" #include "network/HttpDownloader.h"
#include "util/BookCacheUtils.h" #include "util/BookCacheUtils.h"
#include "util/OpdsFilename.h"
#include "util/StringUtils.h" #include "util/StringUtils.h"
#include "util/UrlUtils.h" #include "util/UrlUtils.h"
@@ -279,8 +282,26 @@ void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book) {
// Build full download URL relative to the current feed, not the root server URL // Build full download URL relative to the current feed, not the root server URL
const std::string feedUrl = UrlUtils::buildUrl(server.url, currentPath); const std::string feedUrl = UrlUtils::buildUrl(server.url, currentPath);
std::string downloadUrl = UrlUtils::buildUrl(feedUrl, book.href); std::string downloadUrl = UrlUtils::buildUrl(feedUrl, book.href);
std::string filename = // opdsDownloadFolder is already a null-terminated char[64]; use it directly —
"/" + StringUtils::sanitizeFilename((book.author.empty() ? "" : book.author + " - ") + book.title) + ".epub"; // no std::string copy. exists()/mkdir() take const char*.
const char* folder = SETTINGS.opdsDownloadFolder; // "" => SD root
bool haveFolder = folder[0] != '\0';
if (haveFolder && !Storage.exists(folder) && !Storage.mkdir(folder)) {
// exists()-guard first: mkdir's return-on-existing is unconfirmed, and every
// existing caller checks exists() before mkdir. On real failure, fall back
// to SD root so the download is never lost.
LOG_ERR("OPDS", "mkdir failed for %s, using SD root", folder);
haveFolder = false;
}
// downloadToFile() needs a std::string, and titles are unbounded (a fixed
// char[] would truncate). Cold path (a multi-second download follows), so one
// reserve'd, in-place-appended owning string is the right call.
std::string filename;
filename.reserve(96);
if (haveFolder) filename += folder;
filename += '/';
filename += opdsBookFilename(book.author, book.title, static_cast<OpdsFilenameFormat>(SETTINGS.opdsFilenameFormat));
LOG_DBG("OPDS", "Downloading: %s -> %s", downloadUrl.c_str(), filename.c_str()); LOG_DBG("OPDS", "Downloading: %s -> %s", downloadUrl.c_str(), filename.c_str());
int lastRenderedPercent = -1; int lastRenderedPercent = -1;
+13 -1
View File
@@ -179,6 +179,17 @@ void HomeActivity::loop() {
requestUpdate(); requestUpdate();
}); });
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) backPressSeen = true;
// Back is otherwise unused on the home menu: open the most recently read
// book directly (recentBooks is most-recent-first and already pruned of
// files missing from the SD card). backPressSeen guards against the stale
// release of the Back press that closed the previous activity.
if (mappedInput.wasReleased(MappedInputManager::Button::Back) && backPressSeen && !recentBooks.empty()) {
onSelectBook(recentBooks[0].path);
return;
}
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
if (selectorIndex < recentBooks.size()) { if (selectorIndex < recentBooks.size()) {
onSelectBook(recentBooks[selectorIndex].path); onSelectBook(recentBooks[selectorIndex].path);
@@ -256,7 +267,8 @@ void HomeActivity::render(RenderLock&&) {
[&menuItems](int index) { return std::string(menuItems[index]); }, [&menuItems](int index) { return std::string(menuItems[index]); },
[&menuIcons](int index) { return menuIcons[index]; }); [&menuIcons](int index) { return menuIcons[index]; });
const auto labels = mappedInput.mapLabels("", tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); const auto labels = mappedInput.mapLabels(recentBooks.empty() ? "" : tr(STR_RESUME), tr(STR_SELECT), tr(STR_DIR_UP),
tr(STR_DIR_DOWN));
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();
+3
View File
@@ -18,6 +18,9 @@ class HomeActivity final : public Activity {
bool hasOpdsServers = false; bool hasOpdsServers = false;
bool coverRendered = false; // Track if cover has been rendered once bool coverRendered = false; // Track if cover has been rendered once
bool coverBufferStored = false; // Track if cover buffer is stored bool coverBufferStored = false; // Track if cover buffer is stored
// Home can be entered while Back is still held (e.g. leaving Settings with
// Back): ignore that stale release until a fresh press is seen here.
bool backPressSeen = false;
uint8_t* coverBuffer = nullptr; // HomeActivity's own buffer for cover image uint8_t* coverBuffer = nullptr; // HomeActivity's own buffer for cover image
size_t coverBufferSize = 0; // Bytes allocated to coverBuffer size_t coverBufferSize = 0; // Bytes allocated to coverBuffer
// Logical rect last passed to drawRecentBookCover. The cover snapshot only // Logical rect last passed to drawRecentBookCover. The cover snapshot only
@@ -6,6 +6,10 @@
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
#include "ReaderUtils.h" #include "ReaderUtils.h"
// ReaderUtils.h pulls in ActivityManager.h, which only forward-declares Activity while holding
// std::unique_ptr<Activity> members. Destroying that unique_ptr needs the complete type, so the
// definition must be visible here.
#include "activities/Activity.h"
#include "components/UITheme.h" #include "components/UITheme.h"
#include "fontIds.h" #include "fontIds.h"
#include "util/ButtonNavigator.h" #include "util/ButtonNavigator.h"
+6 -11
View File
@@ -448,20 +448,15 @@ void EpubReaderActivity::loop() {
} }
} }
// Long press BACK (1s+) goes to file selection // Short press Back restores position when viewing a footnote (takes priority over navigation)
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= ReaderUtils::GO_HOME_MS) { if (footnoteDepth > 0 && mappedInput.wasReleased(MappedInputManager::Button::Back) &&
activityManager.goToFileBrowser(epub ? epub->getPath() : ""); mappedInput.getHeldTime() < ReaderUtils::GO_BACK_OR_HOME_MS) {
restoreSavedPosition();
return; return;
} }
// Short press BACK goes directly to home (or restores position if viewing footnote) if (ReaderUtils::handleBackNavigation(mappedInput, activityManager, epub ? epub->getPath().c_str() : "",
if (mappedInput.wasReleased(MappedInputManager::Button::Back) && {this, [](void* ctx) { static_cast<EpubReaderActivity*>(ctx)->onGoHome(); }})) {
mappedInput.getHeldTime() < ReaderUtils::GO_HOME_MS) {
if (footnoteDepth > 0) {
restoreSavedPosition();
return;
}
onGoHome();
return; return;
} }
+147 -19
View File
@@ -10,6 +10,7 @@
#include <algorithm> #include <algorithm>
#include <cassert> #include <cassert>
#include <cmath>
#include "Epub/Section.h" #include "Epub/Section.h"
#include "EpubReaderUtils.h" #include "EpubReaderUtils.h"
@@ -24,6 +25,19 @@
#include "fontIds.h" #include "fontIds.h"
namespace { namespace {
std::string calculateDocumentHashForMethod(const std::string& path, const DocumentMatchMethod method) {
return method == DocumentMatchMethod::FILENAME ? KOReaderDocumentId::calculateFromFilename(path)
: KOReaderDocumentId::calculate(path);
}
DocumentMatchMethod alternateMatchMethod(const DocumentMatchMethod method) {
return method == DocumentMatchMethod::FILENAME ? DocumentMatchMethod::BINARY : DocumentMatchMethod::FILENAME;
}
const char* matchMethodName(const DocumentMatchMethod method) {
return method == DocumentMatchMethod::FILENAME ? "filename" : "binary";
}
void syncTimeWithNTP() { void syncTimeWithNTP() {
// Stop SNTP if already running (can't reconfigure while running) // Stop SNTP if already running (can't reconfigure while running)
if (esp_sntp_enabled()) { if (esp_sntp_enabled()) {
@@ -84,6 +98,21 @@ void KOReaderSyncActivity::saveProgressAndReturn(int spineIndex, int page) {
void KOReaderSyncActivity::returnToReader() { activityManager.goToReader(epubPath); } void KOReaderSyncActivity::returnToReader() { activityManager.goToReader(epubPath); }
bool KOReaderSyncActivity::smartSyncEnabled() const {
return KOREADER_STORE.getSyncBehavior() == KOReaderSyncBehavior::SMART;
}
void KOReaderSyncActivity::markAutoReturn() { autoReturnAt = millis() + AUTO_RETURN_DELAY_MS; }
void KOReaderSyncActivity::completeAlreadySynced() {
{
RenderLock lock(*this);
state = SYNC_COMPLETE;
}
markAutoReturn();
requestUpdate(true);
}
void KOReaderSyncActivity::onWifiSelectionComplete(const bool success) { void KOReaderSyncActivity::onWifiSelectionComplete(const bool success) {
if (!success) { if (!success) {
LOG_DBG("KOSync", "WiFi connection failed, exiting"); LOG_DBG("KOSync", "WiFi connection failed, exiting");
@@ -113,12 +142,8 @@ void KOReaderSyncActivity::onWifiSelectionComplete(const bool success) {
} }
void KOReaderSyncActivity::performSync() { void KOReaderSyncActivity::performSync() {
// Calculate document hash based on user's preferred method const DocumentMatchMethod primaryMethod = KOREADER_STORE.getMatchMethod();
if (KOREADER_STORE.getMatchMethod() == DocumentMatchMethod::FILENAME) { documentHash = calculateDocumentHashForMethod(epubPath, primaryMethod);
documentHash = KOReaderDocumentId::calculateFromFilename(epubPath);
} else {
documentHash = KOReaderDocumentId::calculate(epubPath);
}
if (documentHash.empty()) { if (documentHash.empty()) {
{ {
RenderLock lock(*this); RenderLock lock(*this);
@@ -128,8 +153,9 @@ void KOReaderSyncActivity::performSync() {
requestUpdate(true); requestUpdate(true);
return; return;
} }
const std::string primaryHash = documentHash;
LOG_DBG("KOSync", "Document hash: %s", documentHash.c_str()); LOG_DBG("KOSync", "Document hash (%s): %s", matchMethodName(primaryMethod), documentHash.c_str());
{ {
RenderLock lock(*this); RenderLock lock(*this);
@@ -137,10 +163,42 @@ void KOReaderSyncActivity::performSync() {
} }
requestUpdateAndWait(); requestUpdateAndWait();
// Fetch remote progress // Fetch remote progress. In smart mode, also probe the alternate document-id
const auto result = KOReaderSyncClient::getProgress(documentHash, remoteProgress); // method and use the furthest remote state we can find. This avoids a stale
// local upload when another KOReader device synced the same book with a
// different document matching method.
auto result = KOReaderSyncClient::getProgress(documentHash, remoteProgress);
LOG_DBG("KOSync", "Primary remote (%s): result=%d http=%d doc=%s local=%.6f remote=%.6f xpath=%s",
matchMethodName(primaryMethod), result, KOReaderSyncClient::lastHttpCode, documentHash.c_str(),
localProgress.percentage, remoteProgress.percentage, remoteProgress.progress.c_str());
if (smartSyncEnabled()) {
const DocumentMatchMethod altMethod = alternateMatchMethod(primaryMethod);
const std::string altHash = calculateDocumentHashForMethod(epubPath, altMethod);
if (!altHash.empty() && altHash != documentHash) {
KOReaderProgress altProgress;
const auto altResult = KOReaderSyncClient::getProgress(altHash, altProgress);
LOG_DBG("KOSync", "Alternate remote (%s): result=%d http=%d doc=%s local=%.6f remote=%.6f xpath=%s",
matchMethodName(altMethod), altResult, KOReaderSyncClient::lastHttpCode, altHash.c_str(),
localProgress.percentage, altProgress.percentage, altProgress.progress.c_str());
if (altResult == KOReaderSyncClient::OK &&
(result == KOReaderSyncClient::NOT_FOUND || altProgress.percentage > remoteProgress.percentage)) {
documentHash = altHash;
remoteProgress = std::move(altProgress);
result = KOReaderSyncClient::OK;
}
}
}
if (result == KOReaderSyncClient::NOT_FOUND) { if (result == KOReaderSyncClient::NOT_FOUND) {
if (smartSyncEnabled()) {
LOG_DBG("KOSync", "Smart sync: no remote progress found for known document hashes; uploading local %.6f",
localProgress.percentage);
performUpload();
return;
}
// No remote progress - offer to upload // No remote progress - offer to upload
{ {
RenderLock lock(*this); RenderLock lock(*this);
@@ -174,8 +232,42 @@ void KOReaderSyncActivity::performSync() {
return; return;
} }
SavedProgressPosition koPos = {remoteProgress.progress, remoteProgress.percentage}; // Prefer the exact spine/page from a crosspoint-sync rich position (lossless
remotePosition = ProgressMapper::toCrossPoint(epub, koPos, renderer, currentSpineIndex, totalPagesInSpine); // CrossPoint<->CrossPoint sync); fall back to the approximate XPath mapping
// for plain kosync servers or when the rich position cannot be applied.
std::optional<CrossPointPosition> richMapped;
if (remoteProgress.position.has_value()) {
richMapped = ProgressMapper::fromRichPosition(epub, *remoteProgress.position, renderer);
}
if (richMapped.has_value()) {
remotePosition = *richMapped;
} else {
SavedProgressPosition koPos = {remoteProgress.progress, remoteProgress.percentage};
remotePosition = ProgressMapper::toCrossPoint(epub, koPos, renderer, currentSpineIndex, totalPagesInSpine);
}
if (smartSyncEnabled()) {
static constexpr float SAME_PROGRESS_EPSILON = 0.001f; // 0.1 percentage points
const float delta = localProgress.percentage - remoteProgress.percentage;
LOG_DBG("KOSync", "Smart decision: doc=%s local=%.6f remote=%.6f delta=%.6f remoteXpath=%s mapped=%d/%d",
documentHash.c_str(), localProgress.percentage, remoteProgress.percentage, delta,
remoteProgress.progress.c_str(), remotePosition.spineIndex, remotePosition.pageNumber);
if (std::fabs(delta) <= SAME_PROGRESS_EPSILON) {
completeAlreadySynced();
return;
}
if (delta > 0) {
// Alternate hashes are only probes for newer remote state. Keep uploads
// on the user's configured matching method so its primary record heals.
documentHash = primaryHash;
performUpload();
return;
}
saveProgressAndReturn(remotePosition.spineIndex, remotePosition.pageNumber);
return;
}
// localProgress was pre-computed in EpubReaderActivity before the Epub was released. // localProgress was pre-computed in EpubReaderActivity before the Epub was released.
{ {
@@ -206,17 +298,45 @@ void KOReaderSyncActivity::performUpload() {
progress.progress = localProgress.xpath; progress.progress = localProgress.xpath;
progress.percentage = localProgress.percentage; progress.percentage = localProgress.percentage;
// Rich CrossPoint position for crosspoint-sync servers (lossless
// CrossPoint<->CrossPoint sync); plain kosync servers ignore the extra field.
{
KOReaderRichPosition pos;
const float pct = localProgress.percentage < 0.0f ? 0.0f
: localProgress.percentage > 1.0f ? 1.0f
: localProgress.percentage;
pos.pctQ = static_cast<uint32_t>(pct * 1000000.0f + 0.5f);
pos.spineIndex = static_cast<uint16_t>(currentSpineIndex);
pos.pageNumber = static_cast<uint16_t>(currentPage);
pos.totalPages = static_cast<uint16_t>(totalPagesInSpine > 0 ? totalPagesInSpine : 1);
pos.paragraphIndex = currentParagraphIndex;
pos.xpath = localProgress.xpath;
progress.position = std::move(pos);
}
// Optionally include document metadata (KOReader PR #15306) // Optionally include document metadata (KOReader PR #15306)
if (KOREADER_STORE.getSendMetadata()) { if (KOREADER_STORE.getSendMetadata()) {
// The Epub is released before the sync network calls and is only reloaded on the
// remote-progress path (performSync). When uploading from NO_REMOTE_PROGRESS the
// Epub is still null, so reload it here and guard the title/author reads to avoid
// dereferencing a null Epub. Filename is derived from the path and is always safe.
ensureEpubLoaded();
KOReaderMetadata meta; KOReaderMetadata meta;
// Extract filename from path
const auto lastSlash = epubPath.rfind('/'); const auto lastSlash = epubPath.rfind('/');
meta.filename = (lastSlash != std::string::npos) ? epubPath.substr(lastSlash + 1) : epubPath; meta.filename = (lastSlash != std::string::npos) ? epubPath.substr(lastSlash + 1) : epubPath;
meta.title = epub->getTitle(); if (epub) {
meta.authors = epub->getAuthor(); meta.title = epub->getTitle();
meta.authors = epub->getAuthor();
} else {
LOG_ERR("KOSync", "Epub unavailable for metadata; sending filename only");
}
progress.metadata = std::move(meta); progress.metadata = std::move(meta);
} }
// Release the Epub before the network call so the TLS handshake has enough free heap
// (consistent with the release-before-sync pattern in performSync); nothing below needs it.
epub.reset();
const auto result = KOReaderSyncClient::updateProgress(progress); const auto result = KOReaderSyncClient::updateProgress(progress);
// Drop the radio while user reads the result; full teardown happens at silent reboot. // Drop the radio while user reads the result; full teardown happens at silent reboot.
@@ -236,6 +356,7 @@ void KOReaderSyncActivity::performUpload() {
RenderLock lock(*this); RenderLock lock(*this);
state = UPLOAD_COMPLETE; state = UPLOAD_COMPLETE;
} }
markAutoReturn();
requestUpdate(true); requestUpdate(true);
} }
@@ -379,10 +500,12 @@ void KOReaderSyncActivity::render(RenderLock&&) {
return; return;
} }
if (state == UPLOAD_COMPLETE) { if (state == UPLOAD_COMPLETE || state == SYNC_COMPLETE) {
UITheme::drawCenteredText(renderer, screen, UI_10_FONT_ID, top, tr(STR_UPLOAD_SUCCESS), true, EpdFontFamily::BOLD); UITheme::drawCenteredText(renderer, screen, UI_10_FONT_ID, top,
state == UPLOAD_COMPLETE ? tr(STR_UPLOAD_SUCCESS) : tr(STR_ALREADY_SYNCED), true,
EpdFontFamily::BOLD);
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_DONE), "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
@@ -400,8 +523,13 @@ void KOReaderSyncActivity::render(RenderLock&&) {
} }
void KOReaderSyncActivity::loop() { void KOReaderSyncActivity::loop() {
if (state == NO_CREDENTIALS || state == SYNC_FAILED || state == UPLOAD_COMPLETE) { if (state == NO_CREDENTIALS || state == SYNC_FAILED || state == UPLOAD_COMPLETE || state == SYNC_COMPLETE) {
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { if (autoReturnAt != 0 && millis() >= autoReturnAt) {
returnToReader();
return;
}
if (mappedInput.wasReleased(MappedInputManager::Button::Back) ||
mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
returnToReader(); returnToReader();
} }
return; return;
+9 -1
View File
@@ -40,7 +40,7 @@ class KOReaderSyncActivity final : public Activity {
void onExit() override; void onExit() override;
void loop() override; void loop() override;
void render(RenderLock&&) override; void render(RenderLock&&) override;
bool preventAutoSleep() override { return state == CONNECTING || state == SYNCING; } bool preventAutoSleep() override { return state == CONNECTING || state == SYNCING || state == UPLOADING; }
private: private:
enum State { enum State {
@@ -50,6 +50,7 @@ class KOReaderSyncActivity final : public Activity {
SHOWING_RESULT, SHOWING_RESULT,
UPLOADING, UPLOADING,
UPLOAD_COMPLETE, UPLOAD_COMPLETE,
SYNC_COMPLETE,
NO_REMOTE_PROGRESS, NO_REMOTE_PROGRESS,
SYNC_FAILED, SYNC_FAILED,
NO_CREDENTIALS NO_CREDENTIALS
@@ -78,6 +79,10 @@ class KOReaderSyncActivity final : public Activity {
// Selection in result screen (0=Apply, 1=Upload) // Selection in result screen (0=Apply, 1=Upload)
int selectedOption = 0; int selectedOption = 0;
// Timed return for successful smart-sync terminal states.
unsigned long autoReturnAt = 0;
static constexpr unsigned long AUTO_RETURN_DELAY_MS = 1200;
// Tracks whether this session activated WiFi. Set in onEnter past the credentials // Tracks whether this session activated WiFi. Set in onEnter past the credentials
// check; checked in onExit to decide whether to silent-reboot. Can't rely on // check; checked in onExit to decide whether to silent-reboot. Can't rely on
// WiFi.getMode() because performUpload() calls esp_wifi_stop() on the way out, // WiFi.getMode() because performUpload() calls esp_wifi_stop() on the way out,
@@ -87,6 +92,9 @@ class KOReaderSyncActivity final : public Activity {
void onWifiSelectionComplete(bool success); void onWifiSelectionComplete(bool success);
void performSync(); void performSync();
void performUpload(); void performUpload();
bool smartSyncEnabled() const;
void markAutoReturn();
void completeAlreadySynced();
void ensureEpubLoaded(); void ensureEpubLoaded();
void saveProgressAndReturn(int spineIndex, int page); void saveProgressAndReturn(int spineIndex, int page);
void returnToReader(); void returnToReader();
+35
View File
@@ -6,10 +6,12 @@
#include <Logging.h> #include <Logging.h>
#include "MappedInputManager.h" #include "MappedInputManager.h"
#include "activities/ActivityManager.h"
namespace ReaderUtils { namespace ReaderUtils {
constexpr unsigned long GO_HOME_MS = 1000; constexpr unsigned long GO_HOME_MS = 1000;
constexpr unsigned long GO_BACK_OR_HOME_MS = GO_HOME_MS;
constexpr unsigned long SKIP_HOLD_MS = 700; constexpr unsigned long SKIP_HOLD_MS = 700;
constexpr unsigned long BOOKMARK_HOLD_MS = 400; constexpr unsigned long BOOKMARK_HOLD_MS = 400;
constexpr unsigned long BOOKMARK_MESSAGE_DURATION_MS = 2500; constexpr unsigned long BOOKMARK_MESSAGE_DURATION_MS = 2500;
@@ -96,4 +98,37 @@ void renderAntiAliased(GfxRenderer& renderer, RenderFn&& renderFn) {
renderer.restoreBwBuffer(); renderer.restoreBwBuffer();
} }
struct BackNavCallback {
void* ctx;
void (*fn)(void*);
};
// Returns true if the back button was consumed (caller should return).
// Long press (>= GO_BACK_OR_HOME_MS):
// - default: go to file browser
// - with backShortToFileBrowser: go home
// Short press (< GO_BACK_OR_HOME_MS):
// - default: go home
// - with backShortToFileBrowser: go to file browser.
inline bool handleBackNavigation(const MappedInputManager& mappedInput, ActivityManager& activityManager,
const char* filePath, BackNavCallback goHome) {
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= GO_BACK_OR_HOME_MS) {
if (SETTINGS.backShortToFileBrowser) {
goHome.fn(goHome.ctx);
} else {
activityManager.goToFileBrowser(filePath);
}
return true;
}
if (mappedInput.wasReleased(MappedInputManager::Button::Back) && mappedInput.getHeldTime() < GO_BACK_OR_HOME_MS) {
if (SETTINGS.backShortToFileBrowser) {
activityManager.goToFileBrowser(filePath);
} else {
goHome.fn(goHome.ctx);
}
return true;
}
return false;
}
} // namespace ReaderUtils } // namespace ReaderUtils
+2 -10
View File
@@ -60,16 +60,8 @@ void TxtReaderActivity::onExit() {
} }
void TxtReaderActivity::loop() { void TxtReaderActivity::loop() {
// Long press BACK (1s+) goes to file selection if (ReaderUtils::handleBackNavigation(mappedInput, activityManager, txt ? txt->getPath().c_str() : "",
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= ReaderUtils::GO_HOME_MS) { {this, [](void* ctx) { static_cast<TxtReaderActivity*>(ctx)->onGoHome(); }})) {
activityManager.goToFileBrowser(txt ? txt->getPath() : "");
return;
}
// Short press BACK goes directly to home
if (mappedInput.wasReleased(MappedInputManager::Button::Back) &&
mappedInput.getHeldTime() < ReaderUtils::GO_HOME_MS) {
onGoHome();
return; return;
} }
+2 -10
View File
@@ -101,16 +101,8 @@ void XtcReaderActivity::loop() {
openChapterSelection(); openChapterSelection();
} }
// Long press BACK (1s+) goes to file selection if (ReaderUtils::handleBackNavigation(mappedInput, activityManager, xtc ? xtc->getPath().c_str() : "",
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= ReaderUtils::GO_HOME_MS) { {this, [](void* ctx) { static_cast<XtcReaderActivity*>(ctx)->onGoHome(); }})) {
activityManager.goToFileBrowser(xtc ? xtc->getPath() : "");
return;
}
// Short press BACK goes directly to home
if (mappedInput.wasReleased(MappedInputManager::Button::Back) &&
mappedInput.getHeldTime() < ReaderUtils::GO_HOME_MS) {
onGoHome();
return; return;
} }
@@ -26,7 +26,7 @@ void KOReaderAuthActivity::onWifiSelectionComplete(const bool success) {
{ {
RenderLock lock(*this); RenderLock lock(*this);
state = AUTHENTICATING; state = AUTHENTICATING;
statusMessage = tr(STR_AUTHENTICATING); statusMessage = mode == Mode::SIGN_UP ? tr(STR_CREATING_ACCOUNT) : tr(STR_AUTHENTICATING);
} }
requestUpdate(); requestUpdate();
@@ -34,16 +34,17 @@ void KOReaderAuthActivity::onWifiSelectionComplete(const bool success) {
} }
void KOReaderAuthActivity::performAuthentication() { void KOReaderAuthActivity::performAuthentication() {
const auto result = KOReaderSyncClient::authenticate(); const auto result = mode == Mode::SIGN_UP ? KOReaderSyncClient::createUser() : KOReaderSyncClient::authenticate();
{ {
RenderLock lock(*this); RenderLock lock(*this);
if (result == KOReaderSyncClient::OK) { if (result == KOReaderSyncClient::OK) {
state = SUCCESS; state = SUCCESS;
statusMessage = tr(STR_AUTH_SUCCESS); statusMessage = mode == Mode::SIGN_UP ? tr(STR_ACCOUNT_CREATED) : tr(STR_AUTH_SUCCESS);
} else { } else {
state = FAILED; state = FAILED;
errorMessage = KOReaderSyncClient::errorString(result); errorMessage =
result == KOReaderSyncClient::USER_EXISTS ? tr(STR_USERNAME_TAKEN) : KOReaderSyncClient::errorString(result);
} }
} }
requestUpdate(); requestUpdate();
@@ -80,17 +81,21 @@ void KOReaderAuthActivity::render(RenderLock&&) {
const auto pageWidth = renderer.getScreenWidth(); const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight(); const auto pageHeight = renderer.getScreenHeight();
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_KOREADER_AUTH)); GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight},
mode == Mode::SIGN_UP ? tr(STR_SIGN_UP) : tr(STR_KOREADER_AUTH));
const auto height = renderer.getLineHeight(UI_10_FONT_ID); const auto height = renderer.getLineHeight(UI_10_FONT_ID);
const auto top = (pageHeight - height) / 2; const auto top = (pageHeight - height) / 2;
if (state == AUTHENTICATING) { if (state == AUTHENTICATING) {
renderer.drawCenteredText(UI_10_FONT_ID, top, statusMessage.c_str()); renderer.drawCenteredText(UI_10_FONT_ID, top, statusMessage.c_str());
} else if (state == SUCCESS) { } else if (state == SUCCESS) {
renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_AUTH_SUCCESS), true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, top,
mode == Mode::SIGN_UP ? tr(STR_ACCOUNT_CREATED) : tr(STR_AUTH_SUCCESS), true,
EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, top + height + 10, tr(STR_SYNC_READY)); renderer.drawCenteredText(UI_10_FONT_ID, top + height + 10, tr(STR_SYNC_READY));
} else if (state == FAILED) { } else if (state == FAILED) {
renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_AUTH_FAILED), true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, top, mode == Mode::SIGN_UP ? tr(STR_SIGNUP_FAILED) : tr(STR_AUTH_FAILED),
true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, top + height + 10, errorMessage.c_str()); renderer.drawCenteredText(UI_10_FONT_ID, top + height + 10, errorMessage.c_str());
} }
@@ -5,13 +5,16 @@
#include "activities/Activity.h" #include "activities/Activity.h"
/** /**
* Activity for testing KOReader credentials. * Activity for testing KOReader credentials, or — in sign-up mode — creating a
* Connects to WiFi and authenticates with the KOReader sync server. * new account on the sync server with the entered username/password.
* Connects to WiFi, then authenticates or registers.
*/ */
class KOReaderAuthActivity final : public Activity { class KOReaderAuthActivity final : public Activity {
public: public:
explicit KOReaderAuthActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) enum class Mode { AUTHENTICATE, SIGN_UP };
: Activity("KOReaderAuth", renderer, mappedInput) {}
explicit KOReaderAuthActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, Mode mode = Mode::AUTHENTICATE)
: Activity("KOReaderAuth", renderer, mappedInput), mode(mode) {}
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;
@@ -22,6 +25,7 @@ class KOReaderAuthActivity final : public Activity {
private: private:
enum State { WIFI_SELECTION, CONNECTING, AUTHENTICATING, SUCCESS, FAILED }; enum State { WIFI_SELECTION, CONNECTING, AUTHENTICATING, SUCCESS, FAILED };
Mode mode = Mode::AUTHENTICATE;
State state = WIFI_SELECTION; State state = WIFI_SELECTION;
std::string statusMessage; std::string statusMessage;
std::string errorMessage; std::string errorMessage;
@@ -13,9 +13,10 @@
#include "fontIds.h" #include "fontIds.h"
namespace { namespace {
constexpr int MENU_ITEMS = 6; constexpr int MENU_ITEMS = 8;
const StrId menuNames[MENU_ITEMS] = {StrId::STR_USERNAME, StrId::STR_PASSWORD, StrId::STR_SYNC_SERVER_URL, const StrId menuNames[MENU_ITEMS] = {StrId::STR_USERNAME, StrId::STR_PASSWORD, StrId::STR_SYNC_SERVER_URL,
StrId::STR_DOCUMENT_MATCHING, StrId::STR_SEND_METADATA, StrId::STR_AUTHENTICATE}; StrId::STR_DOCUMENT_MATCHING, StrId::STR_SEND_METADATA, StrId::STR_SYNC_BEHAVIOR,
StrId::STR_SIGN_UP, StrId::STR_AUTHENTICATE};
} // namespace } // namespace
void KOReaderSettingsActivity::onEnter() { void KOReaderSettingsActivity::onEnter() {
@@ -103,6 +104,22 @@ void KOReaderSettingsActivity::handleSelection() {
KOREADER_STORE.saveToFile(); KOREADER_STORE.saveToFile();
requestUpdate(); requestUpdate();
} else if (selectedIndex == 5) { } else if (selectedIndex == 5) {
// Sync behavior - toggle between Ask and Smart
const auto current = KOREADER_STORE.getSyncBehavior();
const auto newBehavior = (current == KOReaderSyncBehavior::ASK_EVERY_TIME) ? KOReaderSyncBehavior::SMART
: KOReaderSyncBehavior::ASK_EVERY_TIME;
KOREADER_STORE.setSyncBehavior(newBehavior);
KOREADER_STORE.saveToFile();
requestUpdate();
} else if (selectedIndex == 6) {
// Sign Up - create a new account on the sync server with the entered credentials
if (!KOREADER_STORE.hasCredentials()) {
return;
}
startActivityForResult(
std::make_unique<KOReaderAuthActivity>(renderer, mappedInput, KOReaderAuthActivity::Mode::SIGN_UP),
[](const ActivityResult&) {});
} else if (selectedIndex == 7) {
// Authenticate // Authenticate
if (!KOREADER_STORE.hasCredentials()) { if (!KOREADER_STORE.hasCredentials()) {
// Can't authenticate without credentials - just show message briefly // Can't authenticate without credentials - just show message briefly
@@ -136,13 +153,25 @@ void KOReaderSettingsActivity::render(RenderLock&&) {
return KOREADER_STORE.getPassword().empty() ? std::string(tr(STR_NOT_SET)) : std::string("******"); return KOREADER_STORE.getPassword().empty() ? std::string(tr(STR_NOT_SET)) : std::string("******");
} else if (index == 2) { } else if (index == 2) {
auto serverUrl = KOREADER_STORE.getServerUrl(); auto serverUrl = KOREADER_STORE.getServerUrl();
return serverUrl.empty() ? std::string(tr(STR_DEFAULT_VALUE)) : serverUrl; if (!serverUrl.empty()) {
return serverUrl;
}
// Show which server the default actually is, scheme stripped for space
std::string defaultUrl = KOREADER_STORE.getBaseUrl();
const auto schemeEnd = defaultUrl.find("://");
if (schemeEnd != std::string::npos) {
defaultUrl.erase(0, schemeEnd + 3);
}
return std::string(tr(STR_DEFAULT_VALUE)) + ": " + defaultUrl;
} else if (index == 3) { } else if (index == 3) {
return KOREADER_STORE.getMatchMethod() == DocumentMatchMethod::FILENAME ? std::string(tr(STR_FILENAME)) return KOREADER_STORE.getMatchMethod() == DocumentMatchMethod::FILENAME ? std::string(tr(STR_FILENAME))
: std::string(tr(STR_BINARY)); : std::string(tr(STR_BINARY));
} else if (index == 4) { } else if (index == 4) {
return KOREADER_STORE.getSendMetadata() ? std::string(tr(STR_STATE_ON)) : std::string(tr(STR_STATE_OFF)); return KOREADER_STORE.getSendMetadata() ? std::string(tr(STR_STATE_ON)) : std::string(tr(STR_STATE_OFF));
} else if (index == 5) { } else if (index == 5) {
return KOREADER_STORE.getSyncBehavior() == KOReaderSyncBehavior::SMART ? std::string(tr(STR_SMART_SYNC))
: std::string(tr(STR_ASK_EVERY_TIME));
} else if (index == 6 || index == 7) {
return KOREADER_STORE.hasCredentials() ? "" : std::string("[") + tr(STR_SET_CREDENTIALS_FIRST) + "]"; return KOREADER_STORE.hasCredentials() ? "" : std::string("[") + tr(STR_SET_CREDENTIALS_FIRST) + "]";
} }
return std::string(tr(STR_NOT_SET)); return std::string(tr(STR_NOT_SET));
@@ -3,19 +3,51 @@
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <I18n.h> #include <I18n.h>
#include <cstring>
#include "CrossPointSettings.h"
#include "MappedInputManager.h" #include "MappedInputManager.h"
#include "OpdsServerStore.h" #include "OpdsServerStore.h"
#include "OpdsSettingsActivity.h" #include "OpdsSettingsActivity.h"
#include "activities/ActivityManager.h" #include "activities/ActivityManager.h"
#include "activities/browser/OpdsBookBrowserActivity.h" #include "activities/browser/OpdsBookBrowserActivity.h"
#include "activities/util/KeyboardEntryActivity.h"
#include "components/UITheme.h" #include "components/UITheme.h"
#include "fontIds.h" #include "fontIds.h"
#include "util/OpdsFilename.h"
namespace {
// Normalizes a user-typed folder: trims spaces, "" => SD root, otherwise a
// single leading '/' and no trailing '/'. Cold path (runs once per edit).
std::string normalizeFolder(std::string v) {
while (!v.empty() && (v.front() == ' ' || v.front() == '\t')) v.erase(v.begin());
while (!v.empty() && (v.back() == ' ' || v.back() == '\t')) v.pop_back();
if (v.empty()) return "";
if (v.front() != '/') v.insert(v.begin(), '/');
while (v.size() > 1 && v.back() == '/') v.pop_back();
if (v == "/") return ""; // a bare slash is SD root, same as empty
return v;
}
// Label shown for the current OPDS filename format in the list subtitle.
StrId opdsFormatLabel(uint8_t format) {
switch (format) {
case static_cast<uint8_t>(OpdsFilenameFormat::TitleAuthor):
return StrId::STR_FMT_TITLE_AUTHOR;
case static_cast<uint8_t>(OpdsFilenameFormat::TitleOnly):
return StrId::STR_FMT_TITLE;
default:
return StrId::STR_FMT_AUTHOR_TITLE;
}
}
} // namespace
int OpdsServerListActivity::getItemCount() const { int OpdsServerListActivity::getItemCount() const {
int count = static_cast<int>(OPDS_STORE.getCount()); int count = static_cast<int>(OPDS_STORE.getCount());
// In settings mode, append a virtual "Add Server" item; in picker mode, only show real servers // Settings mode appends three virtual items: "Add Server", "Download folder"
// and "Filename format".
if (!pickerMode) { if (!pickerMode) {
count++; count += 3;
} }
return count; return count;
} }
@@ -74,6 +106,34 @@ void OpdsServerListActivity::handleSelection() {
return; return;
} }
// Index layout: [servers 0..serverCount-1], [Add Server], [Download folder], [Filename format].
if (selectedIndex == serverCount + 1) {
auto folderHandler = [this](const ActivityResult& result) {
if (!result.isCancelled) {
const auto& kb = std::get<KeyboardResult>(result.data);
const std::string norm = normalizeFolder(kb.text);
strncpy(SETTINGS.opdsDownloadFolder, norm.c_str(), sizeof(SETTINGS.opdsDownloadFolder) - 1);
SETTINGS.opdsDownloadFolder[sizeof(SETTINGS.opdsDownloadFolder) - 1] = '\0';
SETTINGS.saveToFile();
requestUpdate();
}
};
startActivityForResult(
std::make_unique<KeyboardEntryActivity>(renderer, mappedInput, tr(STR_OPDS_DOWNLOAD_FOLDER),
std::string(SETTINGS.opdsDownloadFolder), 63, InputType::Text),
folderHandler);
return;
}
// "Filename format": tap cycles through the available formats.
if (selectedIndex == serverCount + 2) {
SETTINGS.opdsFilenameFormat =
static_cast<uint8_t>((SETTINGS.opdsFilenameFormat + 1) % static_cast<uint8_t>(OpdsFilenameFormat::Count));
SETTINGS.saveToFile();
requestUpdate();
return;
}
// Settings mode: open editor for selected server, or create a new one // Settings mode: open editor for selected server, or create a new one
auto resultHandler = [this](const ActivityResult&) { auto resultHandler = [this](const ActivityResult&) {
// Reload server list when returning from editor // Reload server list when returning from editor
@@ -111,17 +171,30 @@ void OpdsServerListActivity::render(RenderLock&&) {
// Secondary label: server URL (shown as subtitle when name is set). // Secondary label: server URL (shown as subtitle when name is set).
GUI.drawList( GUI.drawList(
renderer, Rect{0, contentTop, pageWidth, contentHeight}, itemCount, selectedIndex, renderer, Rect{0, contentTop, pageWidth, contentHeight}, itemCount, selectedIndex,
[&servers, serverCount](int index) { [&servers, serverCount](int index) -> std::string {
if (index < serverCount) { if (index < serverCount) {
const auto& server = servers[index]; const auto& server = servers[index];
return server.name.empty() ? server.url : server.name; return server.name.empty() ? server.url : server.name;
} }
return std::string(I18n::getInstance().get(StrId::STR_ADD_SERVER)); if (index == serverCount) {
return std::string(I18n::getInstance().get(StrId::STR_ADD_SERVER));
}
if (index == serverCount + 1) {
return std::string(I18n::getInstance().get(StrId::STR_OPDS_DOWNLOAD_FOLDER));
}
return std::string(I18n::getInstance().get(StrId::STR_OPDS_FILENAME_FORMAT));
}, },
[&servers, serverCount](int index) { [&servers, serverCount](int index) -> std::string {
if (index < serverCount && !servers[index].name.empty()) { if (index < serverCount && !servers[index].name.empty()) {
return servers[index].url; return servers[index].url;
} }
if (index == serverCount + 1) {
const char* f = SETTINGS.opdsDownloadFolder;
return f[0] ? std::string(f) : std::string(I18n::getInstance().get(StrId::STR_OPDS_SD_ROOT));
}
if (index == serverCount + 2) {
return std::string(I18n::getInstance().get(opdsFormatLabel(SETTINGS.opdsFilenameFormat)));
}
return std::string(""); return std::string("");
}); });
} }
+4 -45
View File
@@ -159,49 +159,6 @@ void silentRestartToReader() {
ESP.restart(); ESP.restart();
} }
// Verify power button press duration on wake-up from deep sleep
// Pre-condition: isWakeupByPowerButton() == true
void verifyPowerButtonDuration() {
if (SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::SLEEP) {
// Fast path for short press
// Needed because inputManager.isPressed() may take up to ~500ms to return the correct state
return;
}
// Give the user up to 1000ms to start holding the power button, and must hold for SETTINGS.getPowerButtonDuration()
const auto start = millis();
bool abort = false;
// Subtract the current time, because inputManager only starts counting the HeldTime from the first update()
// This way, we remove the time we already took to reach here from the duration,
// assuming the button was held until now from millis()==0 (i.e. device start time).
const uint16_t calibration = start;
const uint16_t calibratedPressDuration =
(calibration < SETTINGS.getPowerButtonDuration()) ? SETTINGS.getPowerButtonDuration() - calibration : 1;
gpio.update();
// Needed because inputManager.isPressed() may take up to ~500ms to return the correct state
while (!gpio.isPressed(HalGPIO::BTN_POWER) && millis() - start < 1000) {
delay(10); // only wait 10ms each iteration to not delay too much in case of short configured duration.
gpio.update();
}
t2 = millis();
if (gpio.isPressed(HalGPIO::BTN_POWER)) {
do {
delay(10);
gpio.update();
} while (gpio.isPressed(HalGPIO::BTN_POWER) && gpio.getPowerButtonHeldTime() < calibratedPressDuration);
abort = gpio.getPowerButtonHeldTime() < calibratedPressDuration;
} else {
abort = true;
}
if (abort) {
// Button released too early. Returning to sleep.
// IMPORTANT: Re-arm the wakeup trigger before sleeping again
powerManager.startDeepSleep(gpio);
}
}
void waitForPowerRelease() { void waitForPowerRelease() {
gpio.update(); gpio.update();
while (gpio.isPressed(HalGPIO::BTN_POWER)) { while (gpio.isPressed(HalGPIO::BTN_POWER)) {
@@ -357,8 +314,10 @@ void setup() {
switch (wakeupReason) { switch (wakeupReason) {
case HalGPIO::WakeupReason::PowerButton: case HalGPIO::WakeupReason::PowerButton:
LOG_DBG("MAIN", "Verifying power button press duration"); LOG_DBG("MAIN", "Verifying power button press duration");
gpio.verifyPowerButtonWakeup(SETTINGS.getPowerButtonDuration(), if (!gpio.verifyPowerButtonWakeup(SETTINGS.getPowerButtonDuration(),
SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::SLEEP); SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::SLEEP)) {
powerManager.startDeepSleep(gpio);
}
break; break;
case HalGPIO::WakeupReason::AfterUSBPower: case HalGPIO::WakeupReason::AfterUSBPower:
// If USB power caused a cold boot, go back to sleep // If USB power caused a cold boot, go back to sleep
+23 -13
View File
@@ -132,6 +132,11 @@ void CrossPointWebServer::begin() {
return; return;
} }
// Add Access-Control-Allow-* headers to every response so web-based clients
// and PWAs on other origins can use the HTTP API. Preflight OPTIONS requests
// are answered in handleNotFound().
server->enableCORS(true);
// Setup routes // Setup routes
LOG_DBG("WEB", "Setting up routes..."); LOG_DBG("WEB", "Setting up routes...");
server->on("/", HTTP_GET, [this] { handleRoot(); }); server->on("/", HTTP_GET, [this] { handleRoot(); });
@@ -353,6 +358,13 @@ void CrossPointWebServer::handleJszip() const {
} }
void CrossPointWebServer::handleNotFound() const { void CrossPointWebServer::handleNotFound() const {
// CORS preflight: routes are registered per-method, so OPTIONS requests land
// here. The Access-Control-Allow-* headers are added by enableCORS().
if (server->method() == HTTP_OPTIONS) {
server->send(204, "text/plain", "");
return;
}
// in AP mode, redirect unmatched browser/captive-portal requests to "/" so the OS auto-opens the browser // in AP mode, redirect unmatched browser/captive-portal requests to "/" so the OS auto-opens the browser
// API requests (/api/*) still return 404 so XHR errors surface correctly // API requests (/api/*) still return 404 so XHR errors surface correctly
// see https://en.wikipedia.org/wiki/Captive_portal#Detection // see https://en.wikipedia.org/wiki/Captive_portal#Detection
@@ -671,17 +683,15 @@ void CrossPointWebServer::handleUpload(UploadState& state) const {
LOG_DBG("WEB", "[UPLOAD] START: %s to path: %s", state.fileName.c_str(), state.path.c_str()); LOG_DBG("WEB", "[UPLOAD] START: %s to path: %s", state.fileName.c_str(), state.path.c_str());
LOG_DBG("WEB", "[UPLOAD] Free heap: %d bytes", ESP.getFreeHeap()); LOG_DBG("WEB", "[UPLOAD] Free heap: %d bytes", ESP.getFreeHeap());
// Create file path
String filePath = state.path; String filePath = state.path;
if (!filePath.endsWith("/")) filePath += "/"; if (!filePath.endsWith("/")) filePath += "/";
filePath += state.fileName; filePath += state.fileName;
// Check if file already exists - SD operations can be slow
esp_task_wdt_reset(); esp_task_wdt_reset();
if (Storage.exists(filePath.c_str())) { if (Storage.exists(filePath.c_str())) {
LOG_DBG("WEB", "[UPLOAD] Overwriting existing file: %s", filePath.c_str()); state.error = "File already exists: " + state.fileName;
esp_task_wdt_reset(); LOG_DBG("WEB", "[UPLOAD] Collision: %s", filePath.c_str());
Storage.remove(filePath.c_str()); return;
} }
// Open file for writing - this can be slow due to FAT cluster allocation // Open file for writing - this can be slow due to FAT cluster allocation
@@ -749,7 +759,7 @@ void CrossPointWebServer::handleUpload(UploadState& state) const {
LOG_DBG("WEB", "[UPLOAD] Diagnostics: %d writes, total write time: %lu ms (%.1f%%)", writeCount, totalWriteTime, LOG_DBG("WEB", "[UPLOAD] Diagnostics: %d writes, total write time: %lu ms (%.1f%%)", writeCount, totalWriteTime,
writePercent); writePercent);
// Clear epub cache to prevent stale metadata issues when overwriting files // Clear epub cache after uploading the file
String filePath = state.path; String filePath = state.path;
if (!filePath.endsWith("/")) filePath += "/"; if (!filePath.endsWith("/")) filePath += "/";
filePath += state.fileName; filePath += state.fileName;
@@ -1624,20 +1634,20 @@ void CrossPointWebServer::onWebSocketEvent(uint8_t num, WStype_t type, uint8_t*
wsUploadPath = wsUploadPath.substring(0, wsUploadPath.length() - 1); wsUploadPath = wsUploadPath.substring(0, wsUploadPath.length() - 1);
} }
// Build file path
String filePath = wsUploadPath; String filePath = wsUploadPath;
if (!filePath.endsWith("/")) filePath += "/"; if (!filePath.endsWith("/")) filePath += "/";
filePath += wsUploadFileName; filePath += wsUploadFileName;
LOG_DBG("WS", "Starting upload: %s (%d bytes) to %s", wsUploadFileName.c_str(), wsUploadSize,
filePath.c_str());
// Check if file exists and remove it
esp_task_wdt_reset(); esp_task_wdt_reset();
if (Storage.exists(filePath.c_str())) { if (Storage.exists(filePath.c_str())) {
Storage.remove(filePath.c_str()); LOG_DBG("WS", "Upload collision: %s", filePath.c_str());
wsServer->sendTXT(num, "ERROR:File already exists: " + wsUploadFileName);
return;
} }
LOG_DBG("WS", "Starting upload: %s (%d bytes) to %s", wsUploadFileName.c_str(), wsUploadSize,
filePath.c_str());
// Open file for writing // Open file for writing
esp_task_wdt_reset(); esp_task_wdt_reset();
if (!Storage.openFileForWrite("WS", filePath, wsUploadFile)) { if (!Storage.openFileForWrite("WS", filePath, wsUploadFile)) {
@@ -1721,7 +1731,7 @@ void CrossPointWebServer::onWebSocketEvent(uint8_t num, WStype_t type, uint8_t*
LOG_DBG("WS", "Upload complete: %s (%d bytes in %lu ms, %.1f KB/s)", wsUploadFileName.c_str(), wsUploadSize, LOG_DBG("WS", "Upload complete: %s (%d bytes in %lu ms, %.1f KB/s)", wsUploadFileName.c_str(), wsUploadSize,
elapsed, kbps); elapsed, kbps);
// Clear epub cache to prevent stale metadata issues when overwriting files // Clear epub cache after uploading the file
String filePath = wsUploadPath; String filePath = wsUploadPath;
if (!filePath.endsWith("/")) filePath += "/"; if (!filePath.endsWith("/")) filePath += "/";
filePath += wsUploadFileName; filePath += wsUploadFileName;
+244 -35
View File
@@ -1579,6 +1579,19 @@
<div id="convertInfo" style="display:none; margin-bottom: 10px; font-size: 0.9em;"> <div id="convertInfo" style="display:none; margin-bottom: 10px; font-size: 0.9em;">
</div> </div>
<div class="advanced-setting-row">
<div class="setting-label">
<div class="setting-title">Rename from Book Metadata</div>
<div class="setting-desc">Use Title - Author.epub when available</div>
</div>
<div class="setting-controls">
<label class="toggle-switch">
<input type="checkbox" id="renameFromMetadataToggle" onchange="updateUploadSettingsPersistence()">
<span class="toggle-slider"></span>
</label>
</div>
</div>
<div class="convert-settings" id="convertSettings" style="display:block;"> <div class="convert-settings" id="convertSettings" style="display:block;">
<span>⚫ True-Grayscale</span> <span>⚫ True-Grayscale</span>
<span id="convertSizeSummary">📏 Max 480×800px</span> <span id="convertSizeSummary">📏 Max 480×800px</span>
@@ -1675,7 +1688,19 @@
</div> </div>
<div class="setting-controls"> <div class="setting-controls">
<label class="toggle-switch"> <label class="toggle-switch">
<input type="checkbox" id="export-log-checkbox"> <input type="checkbox" id="export-log-checkbox" onchange="updateUploadSettingsPersistence()">
<span class="toggle-slider"></span>
</label>
</div>
</div>
<div class="advanced-setting-row">
<div class="setting-label">
<div class="setting-title">Remember Settings</div>
<div class="setting-desc">Store these upload options in this browser</div>
</div>
<div class="setting-controls">
<label class="toggle-switch">
<input type="checkbox" id="rememberUploadSettings" onchange="updateUploadSettingsPersistence()">
<span class="toggle-slider"></span> <span class="toggle-slider"></span>
</label> </label>
</div> </div>
@@ -2085,12 +2110,8 @@
// Modal functions // Modal functions
function openUploadModal() { function openUploadModal() {
// Reset converter variables to defaults
ENABLE_GRAYSCALE = true;
JPEG_QUALITY = 85;
HANDEDNESS = 'right';
OVERLAP_PERCENT = 5;
imageStates = {}; imageStates = {};
restoreUploadSettingsFromStorage();
// Hide convert options when opening modal (no files selected initially) // Hide convert options when opening modal (no files selected initially)
const convertOptions = document.getElementById('convertOptions'); const convertOptions = document.getElementById('convertOptions');
@@ -2098,10 +2119,6 @@
convertOptions.style.display = 'none'; convertOptions.style.display = 'none';
} }
// Reset rotation and overlap UI
setHandedness('right');
setOverlap(5);
// Hide log section from previous session // Hide log section from previous session
const logSection = document.getElementById('log-section'); const logSection = document.getElementById('log-section');
if (logSection) logSection.classList.remove('visible'); if (logSection) logSection.classList.remove('visible');
@@ -2160,7 +2177,8 @@
document.getElementById('progress-container').style.display = 'none'; document.getElementById('progress-container').style.display = 'none';
document.getElementById('progress-fill').style.width = '0%'; document.getElementById('progress-fill').style.width = '0%';
document.getElementById('progress-fill').style.backgroundColor = '#27ae60'; document.getElementById('progress-fill').style.backgroundColor = '#27ae60';
document.getElementById('convertBeforeUpload').checked = false; const convertOptions = document.getElementById('convertOptions');
if (convertOptions) convertOptions.style.display = 'none';
document.getElementById('convertInfo').style.display = 'none'; document.getElementById('convertInfo').style.display = 'none';
document.getElementById('convertWarning').style.display = 'none'; document.getElementById('convertWarning').style.display = 'none';
// Clear image picker cache and reset layout // Clear image picker cache and reset layout
@@ -2183,15 +2201,7 @@
advancedOptionsToggle.style.opacity = '0.5'; advancedOptionsToggle.style.opacity = '0.5';
advancedOptionsToggle.style.pointerEvents = 'none'; advancedOptionsToggle.style.pointerEvents = 'none';
} }
// Reset to defaults applyUploadSettings();
document.getElementById('qualitySlider').value = 85;
document.getElementById('qualityInput').value = 85;
const autoCropToggle = document.getElementById('autoCropToggle');
if (autoCropToggle) autoCropToggle.checked = false;
setHandedness('right');
setOverlap(5);
// Update converter variables
updateQualitySettings();
} }
function updateBatchModeUI(isBatch) { function updateBatchModeUI(isBatch) {
@@ -2228,6 +2238,7 @@
advancedOptionsToggle.style.opacity = checked ? '1' : '0.5'; advancedOptionsToggle.style.opacity = checked ? '1' : '0.5';
advancedOptionsToggle.style.pointerEvents = checked ? 'auto' : 'none'; advancedOptionsToggle.style.pointerEvents = checked ? 'auto' : 'none';
} }
updateUploadSettingsPersistence();
} }
function toggleAdvancedOptions() { function toggleAdvancedOptions() {
@@ -2265,12 +2276,8 @@
function setQualityPreset(value) { function setQualityPreset(value) {
document.getElementById('qualitySlider').value = value; document.getElementById('qualitySlider').value = value;
document.getElementById('qualityInput').value = value; document.getElementById('qualityInput').value = value;
// Update active preset
document.querySelectorAll('.quality-preset').forEach(btn => { document.querySelectorAll('.quality-preset').forEach(btn => {
btn.classList.remove('active'); btn.classList.toggle('active', parseInt(btn.dataset.value, 10) === value);
if (parseInt(btn.dataset.value, 10) === value) {
btn.classList.add('active');
}
}); });
updateQualitySettings(); updateQualitySettings();
} }
@@ -2292,6 +2299,7 @@
if (isImagePickerVisible()) { if (isImagePickerVisible()) {
renderImageGrid(); renderImageGrid();
} }
updateUploadSettingsPersistence();
} }
function setHandedness(value) { function setHandedness(value) {
@@ -2304,6 +2312,7 @@
if (isImagePickerVisible()) { if (isImagePickerVisible()) {
renderImageGrid(); renderImageGrid();
} }
updateUploadSettingsPersistence();
} }
function setOverlap(value) { function setOverlap(value) {
@@ -2312,6 +2321,7 @@
document.querySelectorAll('.overlap-btn').forEach(btn => { document.querySelectorAll('.overlap-btn').forEach(btn => {
btn.classList.toggle('active', parseInt(btn.dataset.value) === value); btn.classList.toggle('active', parseInt(btn.dataset.value) === value);
}); });
updateUploadSettingsPersistence();
} }
function isImagePickerVisible() { function isImagePickerVisible() {
@@ -2980,7 +2990,12 @@
if (qualitySlider && qualityInput) { if (qualitySlider && qualityInput) {
// Initialize converter variables with UI default values // Initialize converter variables with UI default values
updateQualitySettings(); suppressUploadSettingsSave = true;
try {
updateQualitySettings();
} finally {
suppressUploadSettingsSave = false;
}
// Deselect all presets when slider is manually changed // Deselect all presets when slider is manually changed
const deselectPresets = function() { const deselectPresets = function() {
@@ -3213,15 +3228,13 @@
const hasEpub = Array.from(files).some(f => f.name.toLowerCase().endsWith('.epub')); const hasEpub = Array.from(files).some(f => f.name.toLowerCase().endsWith('.epub'));
if (files.length > 0 && hasEpub) { if (files.length > 0 && hasEpub) {
convertOptions.style.display = 'block'; convertOptions.style.display = 'block';
toggleConvertOptions();
} else { } else {
convertOptions.style.display = 'none'; convertOptions.style.display = 'none';
// Clear stale checkbox state so the "Optimize & Upload" button doesn't linger uploadBtn.textContent = 'Upload';
// when the user re-picks a non-EPUB after having ticked Optimize for an EPUB. uploadBtn.classList.remove('optimize');
const cb = document.getElementById('convertBeforeUpload'); document.getElementById('convertInfo').style.display = 'none';
if (cb && cb.checked) { document.getElementById('convertWarning').style.display = 'none';
cb.checked = false;
toggleConvertOptions();
}
if (files.length === 0) clearImagePicker(); if (files.length === 0) clearImagePicker();
} }
@@ -3303,6 +3316,75 @@ let ENABLE_GRAYSCALE = DEFAULT_ENABLE_GRAYSCALE;
let ENABLE_AUTO_CROP = DEFAULT_ENABLE_AUTO_CROP; let ENABLE_AUTO_CROP = DEFAULT_ENABLE_AUTO_CROP;
let HANDEDNESS = 'right'; // 'right' = clockwise (right-handed), 'left' = counter-clockwise (left-handed) let HANDEDNESS = 'right'; // 'right' = clockwise (right-handed), 'left' = counter-clockwise (left-handed)
let OVERLAP_PERCENT = 5; // Minimum overlap percentage for splits (5%, 10%, 15%) let OVERLAP_PERCENT = 5; // Minimum overlap percentage for splits (5%, 10%, 15%)
const UPLOAD_SETTINGS_STORAGE_KEY = 'crosspoint.files.uploadSettings.v1';
const DEFAULT_UPLOAD_SETTINGS = Object.freeze({
convertBeforeUpload: false,
renameFromMetadata: false,
quality: DEFAULT_JPEG_QUALITY,
autoCrop: DEFAULT_ENABLE_AUTO_CROP,
deviceTarget: 'auto',
handedness: 'right',
overlap: 5,
exportLog: false
});
let suppressUploadSettingsSave = false;
function getCurrentUploadSettings() {
return {
convertBeforeUpload: !!document.getElementById('convertBeforeUpload')?.checked,
renameFromMetadata: !!document.getElementById('renameFromMetadataToggle')?.checked,
quality: parseInt(document.getElementById('qualitySlider')?.value || JPEG_QUALITY, 10),
autoCrop: !!document.getElementById('autoCropToggle')?.checked,
deviceTarget: DEVICE_TARGET,
handedness: HANDEDNESS,
overlap: OVERLAP_PERCENT,
exportLog: !!document.getElementById('export-log-checkbox')?.checked
};
}
function applyUploadSettings(settings = {}) {
const merged = { ...DEFAULT_UPLOAD_SETTINGS, ...settings };
suppressUploadSettingsSave = true;
try {
document.getElementById('convertBeforeUpload').checked = !!merged.convertBeforeUpload;
document.getElementById('renameFromMetadataToggle').checked = !!merged.renameFromMetadata;
document.getElementById('autoCropToggle').checked = !!merged.autoCrop;
document.getElementById('export-log-checkbox').checked = !!merged.exportLog;
document.getElementById('rememberUploadSettings').checked = !!settings.rememberSettings;
setQualityPreset(Math.max(1, Math.min(95, parseInt(merged.quality, 10) || DEFAULT_JPEG_QUALITY)));
setDeviceTarget(['auto', 'X3', 'X4'].includes(merged.deviceTarget) ? merged.deviceTarget : 'auto');
setHandedness(merged.handedness === 'left' ? 'left' : 'right');
setOverlap([5, 10, 15].includes(Number(merged.overlap)) ? Number(merged.overlap) : 5);
toggleConvertOptions();
} finally {
suppressUploadSettingsSave = false;
}
}
function restoreUploadSettingsFromStorage() {
try {
const saved = JSON.parse(localStorage.getItem(UPLOAD_SETTINGS_STORAGE_KEY) || 'null');
applyUploadSettings(saved?.rememberSettings ? saved : undefined);
} catch (e) {
console.warn('Could not read remembered upload settings:', e);
applyUploadSettings();
}
}
function updateUploadSettingsPersistence() {
if (suppressUploadSettingsSave) return;
try {
if (!document.getElementById('rememberUploadSettings')?.checked) {
localStorage.removeItem(UPLOAD_SETTINGS_STORAGE_KEY);
return;
}
const settings = { ...getCurrentUploadSettings(), rememberSettings: true };
localStorage.setItem(UPLOAD_SETTINGS_STORAGE_KEY, JSON.stringify(settings));
} catch (e) {
console.warn('Could not save upload settings:', e);
}
}
// ============================================================================ // ============================================================================
// Image Picker State Management // Image Picker State Management
@@ -3380,6 +3462,7 @@ function applyDeviceTarget() {
function setDeviceTarget(value) { function setDeviceTarget(value) {
DEVICE_TARGET = value; DEVICE_TARGET = value;
applyDeviceTarget(); applyDeviceTarget();
updateUploadSettingsPersistence();
} }
// Batch logging system for multiple files // Batch logging system for multiple files
@@ -3788,6 +3871,104 @@ async function findOPFPath(zip) {
return fallback; return fallback;
} }
function sanitizeMetadataFilenamePart(value) {
const text = String(value || '').replace(/\s+/g, ' ').trim();
return (text.normalize ? text.normalize('NFC') : text)
.replace(/[<>:"/\\|?*\x00-\x1F]/g, ' ')
.replace(/\s+/g, ' ')
.replace(/^[. ]+/g, '')
.replace(/[. ]+$/g, '')
.trim();
}
function buildMetadataFilename(title, author) {
title = sanitizeMetadataFilenamePart(title);
author = sanitizeMetadataFilenamePart(author);
if (!title) return '';
let base = author ? `${title} - ${author}` : title;
if (base.length > 180) base = base.substring(0, 180).replace(/\s+\S*$/g, '').trim() || base.substring(0, 180).trim();
return `${base}.epub`;
}
async function getMetadataFilenameForEpub(file) {
if (typeof JSZip === 'undefined') return '';
const zip = await JSZip.loadAsync(file);
const opfPath = await findOPFPath(zip);
if (!opfPath || !zip.files[opfPath]) return '';
const doc = new DOMParser().parseFromString(await safeReadText(zip.files[opfPath]), 'application/xml');
if (doc.querySelector('parsererror')) return '';
const title = doc.getElementsByTagNameNS('*', 'title')[0]?.textContent || '';
const creators = Array.from(doc.getElementsByTagNameNS('*', 'creator'));
const getCreatorRole = el => (
el.getAttribute('role') ||
el.getAttribute('opf:role') ||
el.getAttributeNS('http://www.idpf.org/2007/opf', 'role') ||
''
).toLowerCase();
const authorEl = creators.find(el => getCreatorRole(el) === 'aut') ||
creators.find(el => !getCreatorRole(el));
const authorText = authorEl?.textContent?.trim();
const author = authorText ||
authorEl?.getAttribute('file-as') ||
authorEl?.getAttribute('opf:file-as') ||
authorEl?.getAttributeNS('http://www.idpf.org/2007/opf', 'file-as') ||
'';
return buildMetadataFilename(title, author);
}
async function maybeRenameEbookFile(file) {
const renameToggle = document.getElementById('renameFromMetadataToggle');
if (!renameToggle || !renameToggle.checked || !file.name.toLowerCase().endsWith('.epub')) {
return file;
}
try {
const metadataName = await getMetadataFilenameForEpub(file);
if (!metadataName || metadataName === file.name) return file;
return new File([file], metadataName, {
type: file.type || 'application/epub+zip',
lastModified: file.lastModified
});
} catch (e) {
console.warn('Could not rename EPUB from metadata:', e);
return file;
}
}
function reserveAvailableUploadFilename(fileName, usedFileNames) {
const normalize = name => name.toLowerCase();
if (!usedFileNames.has(normalize(fileName))) {
usedFileNames.add(normalize(fileName));
return fileName;
}
const dotIndex = fileName.lastIndexOf('.');
const extensionIndex = dotIndex > 0 ? dotIndex : fileName.length;
const baseName = fileName.substring(0, extensionIndex);
const extension = fileName.substring(extensionIndex);
let nextSuffix = 2;
let candidateName;
do {
candidateName = `${baseName} (${nextSuffix})${extension}`;
nextSuffix++;
} while (usedFileNames.has(normalize(candidateName)));
usedFileNames.add(normalize(candidateName));
return candidateName;
}
async function fetchExistingUploadNames() {
const response = await fetch('/api/files?path=' + encodeURIComponent(currentPath) + '&_=' + Date.now());
if (!response.ok) {
throw new Error(response.status + ' ' + response.statusText);
}
const entries = await response.json();
return new Set(entries.map(entry => entry.name.toLowerCase()));
}
/** /**
* Resolve a relative href against a base file path. * Resolve a relative href against a base file path.
* Handles multiple ../, ./, absolute /, and bare relative paths. * Handles multiple ../, ./, absolute /, and bare relative paths.
@@ -5219,7 +5400,7 @@ function uploadFileHTTP(file, onProgress, onComplete, onError) {
}); });
} }
function uploadFile() { async function uploadFile() {
if (isUploadInProgress) return; if (isUploadInProgress) return;
const fileInput = document.getElementById('fileInput'); const fileInput = document.getElementById('fileInput');
@@ -5231,8 +5412,17 @@ function uploadFile() {
return; return;
} }
// Prevent modal close during upload
isUploadInProgress = true; isUploadInProgress = true;
let usedFileNames;
try {
usedFileNames = await fetchExistingUploadNames();
} catch (error) {
isUploadInProgress = false;
alert('Failed to check existing files: ' + error.message);
return;
}
// Prevent modal close during upload
uploadGeneration++; uploadGeneration++;
const myGeneration = uploadGeneration; const myGeneration = uploadGeneration;
document.getElementById('uploadModalClose').classList.add('disabled'); document.getElementById('uploadModalClose').classList.add('disabled');
@@ -5323,6 +5513,25 @@ function uploadFile() {
let convOriginalSize = 0; // Picked-file size; 0 unless conversion succeeded let convOriginalSize = 0; // Picked-file size; 0 unless conversion succeeded
let convNewSize = 0; // Generated blob size; 0 unless conversion succeeded let convNewSize = 0; // Generated blob size; 0 unless conversion succeeded
if (isEpub && document.getElementById('renameFromMetadataToggle').checked) {
const originalName = file.name;
progressText.style.color = '';
progressText.textContent = `Reading metadata for ${file.name} (${currentIndex + 1}/${files.length})...`;
file = await maybeRenameEbookFile(file);
if (file.name !== originalName) {
console.log(`[Upload] Renamed from metadata: ${originalName} -> ${file.name}`);
}
}
const availableName = reserveAvailableUploadFilename(file.name, usedFileNames);
if (availableName !== file.name) {
console.log(`[Upload] Renamed to avoid collision: ${file.name} -> ${availableName}`);
file = new File([file], availableName, {
type: file.type,
lastModified: file.lastModified
});
}
const methodText = useWebSocket ? ' [WS]' : ' [HTTP]'; const methodText = useWebSocket ? ' [WS]' : ' [HTTP]';
const stageText = needsConversion ? 'Converting & uploading' : 'Uploading'; const stageText = needsConversion ? 'Converting & uploading' : 'Uploading';
progressText.style.color = ''; progressText.style.color = '';
+23
View File
@@ -0,0 +1,23 @@
#include "OpdsFilename.h"
#include "StringUtils.h"
std::string opdsBookFilename(const std::string& author, const std::string& title, OpdsFilenameFormat format) {
std::string base;
switch (format) {
case OpdsFilenameFormat::TitleAuthor:
base = author.empty() ? title : title + " - " + author;
break;
case OpdsFilenameFormat::TitleOnly:
base = title;
break;
case OpdsFilenameFormat::AuthorTitle:
default:
base = author.empty() ? title : author + " - " + title;
break;
}
// sanitizeFilename caps at 100 bytes and never returns empty (falls back to
// "book"); ".epub" is appended after so the extension is never truncated —
// identical treatment to the previous inline construction.
return StringUtils::sanitizeFilename(base) + ".epub";
}
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#include <cstdint>
#include <string>
// On-disk filename format for books downloaded from an OPDS server. Stored as a
// uint8_t in CrossPointSettings; cast to this enum at the call sites. `Count` is
// the number of selectable formats (used to cycle the setting in the UI).
enum class OpdsFilenameFormat : uint8_t {
AuthorTitle = 0, // "Author - Title.epub" (default; matches legacy behaviour)
TitleAuthor = 1, // "Title - Author.epub"
TitleOnly = 2, // "Title.epub"
Count = 3,
};
// Composes and sanitizes the on-disk filename (including the ".epub" extension)
// for a downloaded OPDS book, according to `format`. When the author is empty,
// every format collapses to just the sanitized title. Pure: no I/O, no globals.
std::string opdsBookFilename(const std::string& author, const std::string& title, OpdsFilenameFormat format);
+1
View File
@@ -43,5 +43,6 @@ add_subdirectory(release_json_parser)
add_subdirectory(differential_rounding) add_subdirectory(differential_rounding)
add_subdirectory(hyphenation_eval) add_subdirectory(hyphenation_eval)
add_subdirectory(utf8_compose) add_subdirectory(utf8_compose)
add_subdirectory(opds_filename)
add_subdirectory(minibidi_arabic) add_subdirectory(minibidi_arabic)
add_subdirectory(combining_marks) add_subdirectory(combining_marks)
+18
View File
@@ -0,0 +1,18 @@
add_executable(OpdsFilenameTest
OpdsFilenameTest.cpp
${REPO_ROOT}/src/util/OpdsFilename.cpp
${REPO_ROOT}/src/util/StringUtils.cpp
${REPO_ROOT}/lib/Utf8/Utf8.cpp
)
target_include_directories(OpdsFilenameTest PRIVATE
${REPO_ROOT}/src/util
${REPO_ROOT}/lib/Utf8
)
target_link_libraries(OpdsFilenameTest PRIVATE
crosspoint_test_common
GTest::gtest_main
)
gtest_discover_tests(OpdsFilenameTest)
+52
View File
@@ -0,0 +1,52 @@
#include <gtest/gtest.h>
#include <string>
#include "OpdsFilename.h"
namespace {
TEST(OpdsFilename, AuthorTitleIsDefaultOrder) {
EXPECT_EQ(opdsBookFilename("J. Doe", "My Book", OpdsFilenameFormat::AuthorTitle), "J. Doe - My Book.epub");
}
TEST(OpdsFilename, TitleAuthorSwapsOrder) {
EXPECT_EQ(opdsBookFilename("J. Doe", "My Book", OpdsFilenameFormat::TitleAuthor), "My Book - J. Doe.epub");
}
TEST(OpdsFilename, TitleOnlyIgnoresAuthor) {
EXPECT_EQ(opdsBookFilename("J. Doe", "My Book", OpdsFilenameFormat::TitleOnly), "My Book.epub");
}
TEST(OpdsFilename, EmptyAuthorCollapsesToTitleForEveryFormat) {
EXPECT_EQ(opdsBookFilename("", "My Book", OpdsFilenameFormat::AuthorTitle), "My Book.epub");
EXPECT_EQ(opdsBookFilename("", "My Book", OpdsFilenameFormat::TitleAuthor), "My Book.epub");
EXPECT_EQ(opdsBookFilename("", "My Book", OpdsFilenameFormat::TitleOnly), "My Book.epub");
}
TEST(OpdsFilename, IllegalCharactersAreSanitized) {
// '/' ':' '*' '?' etc. are replaced with '_' by sanitizeFilename.
EXPECT_EQ(opdsBookFilename("A/B", "C:D*E?", OpdsFilenameFormat::AuthorTitle), "A_B - C_D_E_.epub");
}
TEST(OpdsFilename, EmptyAuthorAndTitleFallsBackToBook) {
// sanitizeFilename returns "book" when nothing usable remains.
EXPECT_EQ(opdsBookFilename("", "", OpdsFilenameFormat::AuthorTitle), "book.epub");
EXPECT_EQ(opdsBookFilename("", "", OpdsFilenameFormat::TitleOnly), "book.epub");
}
TEST(OpdsFilename, LongNameIsTruncatedToByteBudgetBeforeExtension) {
// sanitizeFilename caps the base at 100 bytes; ".epub" is appended after.
const std::string longTitle(200, 'a');
const std::string result = opdsBookFilename("", longTitle, OpdsFilenameFormat::TitleOnly);
EXPECT_EQ(result, std::string(100, 'a') + ".epub");
EXPECT_EQ(result.size(), 105u);
}
TEST(OpdsFilename, UnknownFormatValueFallsBackToAuthorTitle) {
// Defensive: a persisted value outside the enum still yields a valid name.
const auto bogus = static_cast<OpdsFilenameFormat>(99);
EXPECT_EQ(opdsBookFilename("J. Doe", "My Book", bogus), "J. Doe - My Book.epub");
}
} // namespace