diff --git a/lib/microreader/Application.cpp b/lib/microreader/Application.cpp index 268d030..9372109 100644 --- a/lib/microreader/Application.cpp +++ b/lib/microreader/Application.cpp @@ -45,6 +45,7 @@ void Application::start(DrawBuffer& buf, IRuntime& runtime) { reader_options_.set_app(this); chapter_select_.set_app(this); links_screen_.set_app(this); + delete_confirm_.set_app(this); #ifdef MICROREADER_ENABLE_DEMOS bouncing_ball_.set_app(this); @@ -287,6 +288,8 @@ IScreen* microreader::Application::screen_for_(ScreenId id) { return &chapter_select_; case ScreenId::Links: return &links_screen_; + case ScreenId::DeleteConfirm: + return &delete_confirm_; #ifdef MICROREADER_ENABLE_DEMOS case ScreenId::BouncingBall: diff --git a/lib/microreader/Application.h b/lib/microreader/Application.h index 06fff23..86c0daf 100644 --- a/lib/microreader/Application.h +++ b/lib/microreader/Application.h @@ -9,6 +9,7 @@ #include "ScreenManager.h" #include "display/DrawBuffer.h" #include "screens/ChapterSelectScreen.h" +#include "screens/DeleteConfirmScreen.h" #include "screens/IScreen.h" #include "screens/LinksScreen.h" #include "screens/MainMenu.h" @@ -29,6 +30,7 @@ enum class ScreenId : uint8_t { ReaderOptions, ChapterSelect, Links, + DeleteConfirm, BouncingBall, GrayscaleDemo, }; @@ -106,6 +108,9 @@ class Application { MainMenu* main_menu() { return &menu_; } + DeleteConfirmScreen* delete_confirm() { + return &delete_confirm_; + } bool invert_menu_buttons() const { return invert_menu_buttons_; @@ -245,6 +250,7 @@ class Application { ReaderOptionsScreen reader_options_; ChapterSelectScreen chapter_select_; LinksScreen links_screen_; + DeleteConfirmScreen delete_confirm_; #ifdef MICROREADER_ENABLE_DEMOS BouncingBallDemo bouncing_ball_; diff --git a/lib/microreader/content/BookIndex.cpp b/lib/microreader/content/BookIndex.cpp index 5d3c99c..7cbbac7 100644 --- a/lib/microreader/content/BookIndex.cpp +++ b/lib/microreader/content/BookIndex.cpp @@ -112,6 +112,11 @@ void BookIndex::set_last_opened(std::string_view path, uint32_t order) { } } +void BookIndex::remove_entry(int index) { + if (index >= 0 && index < static_cast(entries_.size())) + entries_.erase(entries_.begin() + index); +} + void BookIndex::build_index(const std::string& root_dir, DrawBuffer& buf) { entries_.clear(); pool_.reset(); diff --git a/lib/microreader/content/BookIndex.h b/lib/microreader/content/BookIndex.h index 030d3a4..c2e2fe5 100644 --- a/lib/microreader/content/BookIndex.h +++ b/lib/microreader/content/BookIndex.h @@ -46,6 +46,10 @@ class BookIndex { // entry only; call save() afterwards to persist. void set_last_opened(std::string_view path, uint32_t order); + // Remove the entry at `index` from the in-memory entries vector. + // The StringPool is not compacted (individual strings cannot be freed). + void remove_entry(int index); + void clear_entries(); private: diff --git a/lib/microreader/screens/DeleteConfirmScreen.cpp b/lib/microreader/screens/DeleteConfirmScreen.cpp new file mode 100644 index 0000000..10b2a0e --- /dev/null +++ b/lib/microreader/screens/DeleteConfirmScreen.cpp @@ -0,0 +1,148 @@ +#include "DeleteConfirmScreen.h" + +#include +#include +#include + +#include "../Application.h" +#include "../content/BookIndex.h" + +#ifdef ESP_PLATFORM +#include +#include +#else +#include +namespace fs = std::filesystem; +#endif + +namespace microreader { + +static std::vector wrap_text(const std::string& text, + const BitmapFont& font, + int max_w) { + std::vector lines; + if (text.empty() || max_w <= 0 || !font.valid()) + return lines; + + const char* p = text.c_str(); + const char* start = p; + const char* last_break = nullptr; + int line_w = 0; + + while (*p) { + const uint8_t b = static_cast(*p); + const size_t cb = b < 0x80 ? 1u : b < 0xE0 ? 2u : b < 0xF0 ? 3u : 4u; + const int char_w = font.word_width(p, cb, FontStyle::Regular); + + if (line_w + char_w > max_w && p > start) { + const char* break_at = (last_break && last_break > start) ? last_break : p; + lines.push_back(std::string(start, break_at - start)); + start = break_at; + line_w = 0; + last_break = nullptr; + if (break_at < p) { + p = break_at; + continue; + } + } + + if (cb == 1 && (*p == ' ' || *p == '-' || *p == '_' || *p == '.')) + last_break = p + cb; + + line_w += char_w; + p += cb; + } + + if (p > start) + lines.push_back(std::string(start, p - start)); + + return lines; +} + +void DeleteConfirmScreen::on_start() { + title_ = "Delete Book?"; + + const int max_w = buffer_width() - 32; + auto lines = wrap_text(filename_, ui_font_, max_w); + filename_lines_ = static_cast(lines.size()); + + for (const auto& line : lines) + add_separator(line); + add_separator(""); + delete_idx_ = count(); + add_item("Delete"); + cancel_idx_ = count(); + add_item("Cancel"); + set_selected(cancel_idx_); +} + +void DeleteConfirmScreen::on_select(int index) { + if (index == delete_idx_) { + delete_book_(); + app_->pop_screen(); + } else { + app_->pop_screen(); + } +} + +void DeleteConfirmScreen::delete_book_() { + std::remove(book_path_.c_str()); + + const char* data_dir = app_->data_dir_; + if (data_dir) { + // Remove cache directory for this book + // Cache path: /cache//book.mrb + const char* name = book_path_.c_str(); + const char* sep = std::strrchr(name, '/'); +#ifdef _WIN32 + const char* bsep = std::strrchr(name, '\\'); + if (bsep && (!sep || bsep > sep)) + sep = bsep; +#endif + if (sep) + name = sep + 1; + const char* dot = std::strrchr(name, '.'); + size_t name_len = dot ? static_cast(dot - name) : std::strlen(name); + + std::string cache_dir = std::string(data_dir) + "/cache/" + std::string(name, name_len); + +#ifdef ESP_PLATFORM + DIR* cd = opendir(cache_dir.c_str()); + if (cd) { + struct dirent* ent; + char file_path[768]; + while ((ent = readdir(cd)) != nullptr) { + if (ent->d_name[0] == '.') + continue; + std::snprintf(file_path, sizeof(file_path), "%s/%s", cache_dir.c_str(), ent->d_name); + std::remove(file_path); + } + closedir(cd); + rmdir(cache_dir.c_str()); + } +#else + try { + fs::remove_all(cache_dir); + } catch (...) {} +#endif + + // Reload BookIndex from file because MainMenu::stop() calls + // clear_entries() when the menu is paused. + std::string index_path = std::string(data_dir) + "/book_index.dat"; + BookIndex::instance().load(index_path); + + // Remove entry from BookIndex and re-save + auto& index = BookIndex::instance(); + const StringPool& pool = index.pool(); + const auto& entries = index.entries(); + for (size_t i = 0; i < entries.size(); ++i) { + if (entries[i].path.view(pool) == book_path_) { + index.remove_entry(static_cast(i)); + break; + } + } + index.save(index_path); + } +} + +} // namespace microreader diff --git a/lib/microreader/screens/DeleteConfirmScreen.h b/lib/microreader/screens/DeleteConfirmScreen.h new file mode 100644 index 0000000..ceaf7dd --- /dev/null +++ b/lib/microreader/screens/DeleteConfirmScreen.h @@ -0,0 +1,48 @@ +#pragma once + +#include + +#include "../Input.h" +#include "../display/DrawBuffer.h" +#include "ListMenuScreen.h" + +namespace microreader { + +class DeleteConfirmScreen final : public ListMenuScreen { + public: + DeleteConfirmScreen() = default; + + void setup(const std::string& book_path) { + book_path_ = book_path; + // Extract filename from path + const char* name = book_path_.c_str(); + const char* sep = std::strrchr(name, '/'); +#ifdef _WIN32 + const char* bsep = std::strrchr(name, '\\'); + if (bsep && (!sep || bsep > sep)) + sep = bsep; +#endif + if (sep) + name = sep + 1; + filename_ = name; + } + + const char* name() const override { + return "DeleteConfirm"; + } + + protected: + void on_start() override; + void on_select(int index) override; + + private: + std::string book_path_; + std::string filename_; + int delete_idx_ = 0; + int cancel_idx_ = 0; + int filename_lines_ = 0; + + void delete_book_(); +}; + +} // namespace microreader diff --git a/lib/microreader/screens/ListMenuScreen.cpp b/lib/microreader/screens/ListMenuScreen.cpp index ec20c54..92579a7 100644 --- a/lib/microreader/screens/ListMenuScreen.cpp +++ b/lib/microreader/screens/ListMenuScreen.cpp @@ -3,8 +3,6 @@ #include #include -#include "../HeapLog.h" - #include "../Application.h" #include "../display/ui_font_header.h" #include "../display/ui_font_large.h" @@ -40,6 +38,8 @@ void ListMenuScreen::start(DrawBuffer& buf, IRuntime& runtime) { ui_font_.init(kFontData_ui_small_mbf, kFontData_ui_small_mbf_size); if (!header_font_.valid()) header_font_.init(kFontData_ui_header_mbf, kFontData_ui_header_mbf_size); + back_held_ = false; + back_hold_frames_ = 0; const int prev_selected = selected_; clear_items(); on_start_set_selection_ = false; @@ -444,13 +444,13 @@ void ListMenuScreen::update(const ButtonState& buttons, DrawBuffer& buf, IRuntim Button btn; while (buttons.next_press(btn)) { if (btn == logical_up_front || btn == logical_up_side) { - if (n > 0) { + if (n > 0 && !back_held_) { move_up(); moved = true; had_up_press = true; } } else if (btn == logical_down_front || btn == logical_down_side) { - if (n > 0) { + if (n > 0 && !back_held_) { move_down(); moved = true; had_down_press = true; @@ -458,21 +458,28 @@ void ListMenuScreen::update(const ButtonState& buttons, DrawBuffer& buf, IRuntim } else { switch (btn) { case Button::Button0: - // Flush any pending move before back so the screen redraws correctly - // if on_back() decides to stay. - if (moved) { - draw_all_(buf, runtime.battery_percentage()); - buf.refresh(); - moved = false; - } - on_back(); - if (app_ && app_->has_pending_transition()) { - return; + if (long_back_threshold_ > 0) { + if (!back_held_) { + back_held_ = true; + back_hold_frames_ = 0; + } + } else { + // Flush any pending move before back so the screen redraws correctly + // if on_back() decides to stay. + if (moved) { + draw_all_(buf, runtime.battery_percentage()); + buf.refresh(); + moved = false; + } + on_back(); + if (app_ && app_->has_pending_transition()) { + return; + } } break; case Button::Button1: // select - if (n > 0 && selected_ < n) { + if (n > 0 && selected_ < n && !back_held_) { on_select(selected_); if (app_ && app_->has_pending_transition()) { return; @@ -491,8 +498,8 @@ void ListMenuScreen::update(const ButtonState& buttons, DrawBuffer& buf, IRuntim // step size grows by 1 each frame: frame 0 = 1, frame 1 = 2, frame 2 = 3, … auto hold_step = [](int frames) -> int { return frames + 1; }; - const bool up_held = !had_up_press && (buttons.is_down(logical_up_front) || buttons.is_down(logical_up_side)); - const bool down_held = !had_down_press && (buttons.is_down(logical_down_front) || buttons.is_down(logical_down_side)); + const bool up_held = !had_up_press && !back_held_ && (buttons.is_down(logical_up_front) || buttons.is_down(logical_up_side)); + const bool down_held = !had_down_press && !back_held_ && (buttons.is_down(logical_down_front) || buttons.is_down(logical_down_side)); if (up_held && n > 0) { const int step = hold_step(hold_frames_up_); @@ -514,6 +521,34 @@ void ListMenuScreen::update(const ButtonState& buttons, DrawBuffer& buf, IRuntim hold_frames_down_ = 0; } + // Long-back hold tracking: count frames while Button0 is held. + if (back_held_) { + if (buttons.is_down(Button::Button0)) { + ++back_hold_frames_; + if (back_hold_frames_ >= long_back_threshold_) { + on_long_back(selected_); + back_held_ = false; + back_hold_frames_ = 0; + return; + } + } else { + // Released before threshold — normal tap + if (moved) { + draw_all_(buf, runtime.battery_percentage()); + buf.refresh(); + moved = false; + } + on_back(); + if (app_ && app_->has_pending_transition()) { + back_held_ = false; + back_hold_frames_ = 0; + return; + } + back_held_ = false; + back_hold_frames_ = 0; + } + } + if (moved || needs_draw || force_redraw_) { draw_all_(buf, runtime.battery_percentage()); buf.refresh(); diff --git a/lib/microreader/screens/ListMenuScreen.h b/lib/microreader/screens/ListMenuScreen.h index 1b82365..7240355 100644 --- a/lib/microreader/screens/ListMenuScreen.h +++ b/lib/microreader/screens/ListMenuScreen.h @@ -125,6 +125,16 @@ class ListMenuScreen : public IScreen { // Called when user presses back. virtual void on_back(); + // Called when user holds back for long_back_threshold_ frames (0 = disabled). + virtual void on_long_back(int index) {} + + void set_long_back_threshold(int frames) { + long_back_threshold_ = frames; + } + int long_back_threshold() const { + return long_back_threshold_; + } + protected: BitmapFont ui_font_; BitmapFont header_font_; @@ -134,6 +144,8 @@ class ListMenuScreen : public IScreen { force_redraw_ = true; } + int buffer_width() const { return buf_ ? buf_->width() : 0; } + // Re-run start() to rebuild items with updated settings (e.g. after font change). void restart() { if (buf_ && runtime_) @@ -155,6 +167,9 @@ class ListMenuScreen : public IScreen { int initial_selection_ = -1; int hold_frames_up_ = 0; int hold_frames_down_ = 0; + int long_back_threshold_ = 0; + int back_hold_frames_ = 0; + bool back_held_ = false; bool align_left_ = false; bool on_start_set_selection_ = false; diff --git a/lib/microreader/screens/MainMenu.cpp b/lib/microreader/screens/MainMenu.cpp index 65ea1b3..be1a87e 100644 --- a/lib/microreader/screens/MainMenu.cpp +++ b/lib/microreader/screens/MainMenu.cpp @@ -34,6 +34,7 @@ static std::string_view filename_sv(const std::string& path) { void MainMenu::on_start() { title_ = "Microreader"; + set_long_back_threshold(20); if (!app_->data_dir_) { needs_scan_ = false; @@ -86,6 +87,13 @@ void MainMenu::on_back() { app_->push_screen(ScreenId::Settings); } +void MainMenu::on_long_back(int index) { + if (index < 0 || index >= static_cast(entries_.size())) + return; + app_->delete_confirm()->setup(entries_[index].path); + app_->push_screen(ScreenId::DeleteConfirm); +} + void MainMenu::scan_directory_(DrawBuffer& buf) { if (!books_dir_ || !app_->data_dir_) return; diff --git a/lib/microreader/screens/MainMenu.h b/lib/microreader/screens/MainMenu.h index 8093b51..6e3249a 100644 --- a/lib/microreader/screens/MainMenu.h +++ b/lib/microreader/screens/MainMenu.h @@ -90,6 +90,7 @@ class MainMenu final : public ListMenuScreen { void on_start() override; void on_select(int index) override; void on_back() override; + void on_long_back(int index) override; private: const char* books_dir_ = nullptr; diff --git a/platforms/desktop/CMakeLists.txt b/platforms/desktop/CMakeLists.txt index 308edbd..a29b9fc 100644 --- a/platforms/desktop/CMakeLists.txt +++ b/platforms/desktop/CMakeLists.txt @@ -80,6 +80,7 @@ add_library(microreader_core STATIC ${CMAKE_CURRENT_SOURCE_DIR}/../../lib/microreader/screens/ReaderScreen.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../../lib/microreader/screens/SettingsScreen.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../../lib/microreader/screens/ChapterSelectScreen.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../lib/microreader/screens/DeleteConfirmScreen.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../../lib/microreader/screens/LinksScreen.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../../lib/microreader/screens/ReaderOptionsScreen.cpp ) diff --git a/platforms/esp32/CMakeLists.txt b/platforms/esp32/CMakeLists.txt index 0e06121..30c8d53 100644 --- a/platforms/esp32/CMakeLists.txt +++ b/platforms/esp32/CMakeLists.txt @@ -14,6 +14,7 @@ set(lib_sources ${LIB}/screens/ReaderScreen.cpp ${LIB}/screens/SettingsScreen.cpp ${LIB}/screens/ChapterSelectScreen.cpp + ${LIB}/screens/DeleteConfirmScreen.cpp ${LIB}/screens/LinksScreen.cpp ${LIB}/screens/ReaderOptionsScreen.cpp ${LIB}/content/ZipReader.cpp