feat: configurable OPDS download folder and filename format (#2571)

This commit is contained in:
Oscar Nogueira Neto
2026-07-15 10:53:27 -04:00
committed by GitHub
parent b9867b0d09
commit 95d8cb712b
35 changed files with 362 additions and 7 deletions
+5
View File
@@ -298,6 +298,11 @@ STR_SLEEP_TIMER_VALUE_FORMAT: "%u хв"
STR_SLEEP_NEVER: "Ніколі"
STR_STEP_HINT_FRONT: "Пярэднія кнопкі:"
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_PAGES_PER_MIN: "Аўтаперагортванне (старонак за хвіліну)"
STR_TILT_PAGE_TURN: "Перагортванне нахілам"
+5
View File
@@ -347,6 +347,11 @@ STR_SERVER_NAME: "Nom del servidor"
STR_NO_SERVERS: "No hi ha servidors OPDS configurats"
STR_DELETE_SERVER: "Suprimeix el servidor"
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_FONT_BROWSER: "Navegador 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_FOCUS_READING: "Soustředěné čtení"
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_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Nikdy"
+5
View File
@@ -300,6 +300,11 @@ STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Aldrig"
STR_STEP_HINT_FRONT: "Frontknapper:"
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_AUTO_TURN_ENABLED: "Automatisk sidevendning aktiveret: "
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_STEP_HINT_FRONT: "Voorknoppen:"
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_AUTO_TURN_ENABLED: "Automatisch omslaan ingeschakeld: "
STR_AUTO_TURN_PAGES_PER_MIN: "Autom. omslaan (pagina's per minuut)"
+6
View File
@@ -347,6 +347,12 @@ STR_SERVER_NAME: "Server Name"
STR_NO_SERVERS: "No OPDS servers configured"
STR_DELETE_SERVER: "Delete Server"
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_PAGES_PER_MIN: "Auto Turn (Pages Per Minute)"
STR_MANAGE_FONTS: "Manage Fonts"
+5
View File
@@ -268,6 +268,11 @@ STR_BOOK_S_STYLE: "Kirjan tyyli"
STR_EMBEDDED_STYLE: "Upotettu tyyli"
STR_FOCUS_READING: "Keskittynyt lukeminen"
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_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Ei koskaan"
+5
View File
@@ -301,6 +301,11 @@ STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Jamais"
STR_STEP_HINT_FRONT: "Boutons avant :"
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_AUTO_TURN_ENABLED: "Tourne-page auto : "
STR_AUTO_TURN_PAGES_PER_MIN: "Tourne-page auto (pages par minute)"
+5
View File
@@ -329,6 +329,11 @@ STR_SERVER_NAME: "Servername"
STR_NO_SERVERS: "Keine OPDS-Server konfiguriert"
STR_DELETE_SERVER: "Server entfernen"
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_PAGES_PER_MIN: "Auto-Umblättern (Seiten/Min.)"
STR_IMAGES: "Bilder"
+5
View File
@@ -306,6 +306,11 @@ STR_NO_SERVERS: "לא הוגדרו שרתי OPDS"
STR_DELETE_SERVER: "מחק שרת"
STR_DELETE_CONFIRM: "למחוק שרת זה?"
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_PAGES_PER_MIN: "דפדוף אוטומטי (דפים בדקה)"
STR_MANAGE_FONTS: "ניהול גופנים"
+5
View File
@@ -299,6 +299,11 @@ STR_SLEEP_TIMER_VALUE_FORMAT: "%u perc"
STR_SLEEP_NEVER: "Soha"
STR_STEP_HINT_FRONT: "Elülső 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_AUTO_TURN_ENABLED: "Automatikus lapozás bekapcsolva: "
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_DELETE_SERVER: "Elimina server"
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_PAGES_PER_MIN: "Volta pagina automatico (pag/min)"
STR_MANAGE_FONTS: "Gestisci font"
+5
View File
@@ -296,6 +296,11 @@ STR_SLEEP_TIMER_VALUE_FORMAT: "%u мин"
STR_SLEEP_NEVER: "Ешқашан"
STR_STEP_HINT_FRONT: "Алдыңғы түймелер:"
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_AUTO_TURN_ENABLED: "Автоматты бет аудару қосулы: "
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_STEP_HINT_FRONT: "Priekiniai 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_AUTO_TURN_ENABLED: "Auto-vertimas: "
STR_AUTO_TURN_PAGES_PER_MIN: "Auto-vertimas (psl/min)"
+5
View File
@@ -316,6 +316,11 @@ STR_SERVER_NAME: "Nazwa serwera"
STR_NO_SERVERS: "Brak skonfigurowanych serwerów OPDS"
STR_DELETE_SERVER: "Usuń serwer"
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_PAGES_PER_MIN: "Auto-kartkowanie (str./min)"
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_DELETE_SERVER: "Excluir Servidor"
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_PAGES_PER_MIN: "Virada automática (páginas por minuto)"
STR_MANAGE_FONTS: "Gerenciar fontes"
+5
View File
@@ -300,6 +300,11 @@ STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Niciodată"
STR_STEP_HINT_FRONT: "Butoane frontale:"
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_AUTO_TURN_ENABLED: "Răsfoire automată: "
STR_AUTO_TURN_PAGES_PER_MIN: "Pagini pe minut"
+5
View File
@@ -339,6 +339,11 @@ STR_SERVER_NAME: "Имя сервера"
STR_NO_SERVERS: "Нет настроенных серверов OPDS"
STR_DELETE_SERVER: "Удалить сервер"
STR_OPDS_SERVERS: "Серверы OPDS"
STR_OPDS_DOWNLOAD_FOLDER: "Папка загрузок"
STR_OPDS_FILENAME_FORMAT: "Формат имени файла"
STR_FMT_AUTHOR_TITLE: "Автор - Название"
STR_FMT_TITLE_AUTHOR: "Название - Автор"
STR_FMT_TITLE: "Название"
STR_AUTO_TURN_ENABLED: "Автоперелистывание: "
STR_AUTO_TURN_PAGES_PER_MIN: "Автоперелистывание (стр./мин)"
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_DELETE_SERVER: "Odstrániť server"
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_PAGES_PER_MIN: "Automatické otáčanie (strán za minútu)"
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_STEP_HINT_FRONT: "Sprednji 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_AUTO_TURN_ENABLED: "Samodejno obračanje: "
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_DELETE_SERVER: "Borrar servidor"
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_PAGES_PER_MIN: "Avance auto. (pág./min)"
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_DELETE_SERVER: "Ta bort server"
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_PAGES_PER_MIN: "Automatisk vändning (sidor per minut)"
STR_MANAGE_FONTS: "Hantera teckensnitt"
+5
View File
@@ -291,6 +291,11 @@ STR_SLEEP_TIMER_VALUE_FORMAT: "%u dak"
STR_SLEEP_NEVER: "Asla"
STR_STEP_HINT_FRONT: "Ön 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_FOOTNOTES: "Bu sayfada dipnot yok"
STR_PREVIEW: "Önizleme"
+5
View File
@@ -336,6 +336,11 @@ STR_SERVER_NAME: "Назва сервера"
STR_NO_SERVERS: "Не налаштовано жодного сервера OPDS"
STR_DELETE_SERVER: "Видалити сервер"
STR_OPDS_SERVERS: "Сервери OPDS"
STR_OPDS_DOWNLOAD_FOLDER: "Папка завантажень"
STR_OPDS_FILENAME_FORMAT: "Формат імені файлу"
STR_FMT_AUTHOR_TITLE: "Автор - Назва"
STR_FMT_TITLE_AUTHOR: "Назва - Автор"
STR_FMT_TITLE: "Назва"
STR_AUTO_TURN_ENABLED: "Автоперегортання увімк: "
STR_AUTO_TURN_PAGES_PER_MIN: "Автоперегортання (ст/хв)"
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_DELETE_SERVER: "Elimina el servidor"
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_FONT_BROWSER: "Navegador 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_DELETE_SERVER: "Xóa máy chủ"
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_PAGES_PER_MIN: "Tự lật (số trang mỗi phút)"
STR_MANAGE_FONTS: "Quản lý phông chữ"
+8
View File
@@ -238,6 +238,14 @@ class CrossPointSettings {
// Reader screen margin settings
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
uint8_t hideBatteryPercentage = HIDE_NEVER;
// Long-press page turn button behavior
+10
View File
@@ -194,6 +194,16 @@ inline std::vector<SettingInfo> getSettingsList(const SdCardFontRegistry* regist
SettingInfo::Toggle(StrId::STR_MOVE_FINISHED_TO_READ, &CrossPointSettings::moveFinishedToReadFolder,
"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) ---
SettingInfo::DynamicString(
StrId::STR_KOREADER_USERNAME, [] { return KOREADER_STORE.getUsername(); },
@@ -2,11 +2,13 @@
#include <Arduino.h>
#include <GfxRenderer.h>
#include <HalStorage.h>
#include <I18n.h>
#include <Logging.h>
#include <OpdsStream.h>
#include <WiFi.h>
#include "CrossPointSettings.h"
#include "MappedInputManager.h"
#include "SilentRestart.h"
#include "activities/network/WifiSelectionActivity.h"
@@ -15,6 +17,7 @@
#include "fontIds.h"
#include "network/HttpDownloader.h"
#include "util/BookCacheUtils.h"
#include "util/OpdsFilename.h"
#include "util/StringUtils.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
const std::string feedUrl = UrlUtils::buildUrl(server.url, currentPath);
std::string downloadUrl = UrlUtils::buildUrl(feedUrl, book.href);
std::string filename =
"/" + StringUtils::sanitizeFilename((book.author.empty() ? "" : book.author + " - ") + book.title) + ".epub";
// opdsDownloadFolder is already a null-terminated char[64]; use it directly —
// 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());
int lastRenderedPercent = -1;
@@ -3,19 +3,51 @@
#include <GfxRenderer.h>
#include <I18n.h>
#include <cstring>
#include "CrossPointSettings.h"
#include "MappedInputManager.h"
#include "OpdsServerStore.h"
#include "OpdsSettingsActivity.h"
#include "activities/ActivityManager.h"
#include "activities/browser/OpdsBookBrowserActivity.h"
#include "activities/util/KeyboardEntryActivity.h"
#include "components/UITheme.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 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) {
count++;
count += 3;
}
return count;
}
@@ -74,6 +106,34 @@ void OpdsServerListActivity::handleSelection() {
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
auto resultHandler = [this](const ActivityResult&) {
// 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).
GUI.drawList(
renderer, Rect{0, contentTop, pageWidth, contentHeight}, itemCount, selectedIndex,
[&servers, serverCount](int index) {
[&servers, serverCount](int index) -> std::string {
if (index < serverCount) {
const auto& server = servers[index];
return server.name.empty() ? server.url : server.name;
}
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()) {
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("");
});
}
+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(hyphenation_eval)
add_subdirectory(utf8_compose)
add_subdirectory(opds_filename)
add_subdirectory(minibidi_arabic)
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