Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions lib/microreader/Application.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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:
Expand Down
6 changes: 6 additions & 0 deletions lib/microreader/Application.h
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -29,6 +30,7 @@ enum class ScreenId : uint8_t {
ReaderOptions,
ChapterSelect,
Links,
DeleteConfirm,
BouncingBall,
GrayscaleDemo,
};
Expand Down Expand Up @@ -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_;
Expand Down Expand Up @@ -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_;
Expand Down
5 changes: 5 additions & 0 deletions lib/microreader/content/BookIndex.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<int>(entries_.size()))
entries_.erase(entries_.begin() + index);
}

void BookIndex::build_index(const std::string& root_dir, DrawBuffer& buf) {
entries_.clear();
pool_.reset();
Expand Down
4 changes: 4 additions & 0 deletions lib/microreader/content/BookIndex.h
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
148 changes: 148 additions & 0 deletions lib/microreader/screens/DeleteConfirmScreen.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
#include "DeleteConfirmScreen.h"

#include <cstdio>
#include <cstring>
#include <vector>

#include "../Application.h"
#include "../content/BookIndex.h"

#ifdef ESP_PLATFORM
#include <dirent.h>
#include <sys/stat.h>
#else
#include <filesystem>
namespace fs = std::filesystem;
#endif

namespace microreader {

static std::vector<std::string> wrap_text(const std::string& text,
const BitmapFont& font,
int max_w) {
std::vector<std::string> 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<uint8_t>(*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<int>(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: <data_dir>/cache/<filename_without_ext>/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<size_t>(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<int>(i));
break;
}
}
index.save(index_path);
}
}

} // namespace microreader
48 changes: 48 additions & 0 deletions lib/microreader/screens/DeleteConfirmScreen.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#pragma once

#include <string>

#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
69 changes: 52 additions & 17 deletions lib/microreader/screens/ListMenuScreen.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
#include <algorithm>
#include <cstring>

#include "../HeapLog.h"

#include "../Application.h"
#include "../display/ui_font_header.h"
#include "../display/ui_font_large.h"
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -444,35 +444,42 @@ 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;
}
} 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;
Expand All @@ -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_);
Expand All @@ -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();
Expand Down
Loading