feat(hardcover): auto-connect to WiFi before syncing
This commit is contained in:
@@ -2,36 +2,66 @@
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <I18n.h>
|
||||
#include <WiFi.h>
|
||||
|
||||
#include "HardcoverPlugin.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "activities/network/WifiSelectionActivity.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
|
||||
void HardcoverSyncActivity::onEnter() {
|
||||
Activity::onEnter();
|
||||
state = SYNCING;
|
||||
result = HardcoverPlugin::SyncResult::OK;
|
||||
requestUpdate();
|
||||
result = HardcoverPlugin::SyncResult::OK;
|
||||
startWifiThenSync();
|
||||
}
|
||||
|
||||
void HardcoverSyncActivity::onExit() { Activity::onExit(); }
|
||||
|
||||
void HardcoverSyncActivity::startWifiThenSync() {
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
state = SYNCING;
|
||||
requestUpdate();
|
||||
return;
|
||||
}
|
||||
|
||||
state = CONNECTING;
|
||||
requestUpdate();
|
||||
|
||||
startActivityForResult(
|
||||
std::make_unique<WifiSelectionActivity>(renderer, mappedInput),
|
||||
[this](const ActivityResult&) {
|
||||
if (WiFi.status() != WL_CONNECTED) {
|
||||
result = HardcoverPlugin::SyncResult::NO_WIFI;
|
||||
state = DONE;
|
||||
requestUpdate();
|
||||
return;
|
||||
}
|
||||
state = SYNCING;
|
||||
requestUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
void HardcoverSyncActivity::render(RenderLock&&) {
|
||||
const auto& metrics = UITheme::getInstance().getMetrics();
|
||||
const auto& metrics = UITheme::getInstance().getMetrics();
|
||||
const auto pageWidth = renderer.getScreenWidth();
|
||||
const auto pageHeight = renderer.getScreenHeight();
|
||||
|
||||
renderer.clearScreen();
|
||||
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, "Hardcover");
|
||||
|
||||
if (state == CONNECTING) {
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, "Connecting to WiFi...");
|
||||
renderer.displayBuffer();
|
||||
return;
|
||||
}
|
||||
|
||||
if (state == SYNCING) {
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, "Syncing...");
|
||||
renderer.displayBuffer();
|
||||
return;
|
||||
}
|
||||
|
||||
// DONE state - show result
|
||||
const char* line1 = nullptr;
|
||||
const char* line2 = nullptr;
|
||||
|
||||
@@ -60,9 +90,8 @@ void HardcoverSyncActivity::render(RenderLock&&) {
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - (line2 ? 15 : 0), line1, true,
|
||||
result == HardcoverPlugin::SyncResult::OK ? EpdFontFamily::BOLD
|
||||
: EpdFontFamily::REGULAR);
|
||||
if (line2) {
|
||||
if (line2)
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 15, line2);
|
||||
}
|
||||
|
||||
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
|
||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
@@ -86,9 +115,8 @@ void HardcoverSyncActivity::loop() {
|
||||
}
|
||||
|
||||
if (state == DONE) {
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Back))
|
||||
finish();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,8 +15,9 @@ class HardcoverSyncActivity final : public Activity {
|
||||
void render(RenderLock&&) override;
|
||||
|
||||
private:
|
||||
enum State { SYNCING, DONE };
|
||||
State state = SYNCING;
|
||||
enum State { CONNECTING, SYNCING, DONE };
|
||||
State state = CONNECTING;
|
||||
HardcoverPlugin::SyncResult result = HardcoverPlugin::SyncResult::OK;
|
||||
void doSync();
|
||||
void startWifiThenSync();
|
||||
};
|
||||
|
||||
+35
-107
@@ -1,31 +1,10 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Hardcover Progress plugin patch.
|
||||
|
||||
Adds a "Hardcover Progress" entry to the Plugins settings tab.
|
||||
When the user presses Sync, the device:
|
||||
|
||||
1. Recursively scans the SD card for .epub files.
|
||||
2. For each file whose progress.bin shows 1-99% read, parses the
|
||||
epub OPF metadata to extract the ISBN-13.
|
||||
3. POSTs a GraphQL mutation to https://api.hardcover.app/v1/graphql,
|
||||
setting that book's status to "Currently Reading" with the current
|
||||
page number.
|
||||
|
||||
The Hardcover API bearer token is stored in CrossPointSettings as a
|
||||
char[256] field (hardcoverApiToken) and exposed in the web UI.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import glob
|
||||
import shutil
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def read_file(path):
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
@@ -40,11 +19,6 @@ def find_first(filename, repo_dir):
|
||||
sys.exit(f"ERROR: Could not locate {filename} in {repo_dir}")
|
||||
return results[0]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Copy plugin sources into the repo
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def copy_plugin_sources(plugin_dir, repo_dir):
|
||||
dest = os.path.join(repo_dir, "src", "activities", "settings")
|
||||
os.makedirs(dest, exist_ok=True)
|
||||
@@ -55,11 +29,6 @@ def copy_plugin_sources(plugin_dir, repo_dir):
|
||||
shutil.copy2(src, dst)
|
||||
print(f" \u2713 {fname}")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CrossPointSettings.h - add hardcoverApiToken char array with default token
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def patch_cross_point_settings_h(repo_dir, token=""):
|
||||
path = find_first("CrossPointSettings.h", repo_dir)
|
||||
content = read_file(path)
|
||||
@@ -68,8 +37,6 @@ def patch_cross_point_settings_h(repo_dir, token=""):
|
||||
print(" CrossPointSettings.h already patched, skipping.")
|
||||
return
|
||||
|
||||
# Escape token for use in a C string literal (tokens are JWTs, no special chars needed
|
||||
# beyond basic safety)
|
||||
safe_token = token.replace('\\', '\\\\').replace('"', '\\"')
|
||||
|
||||
content = content.replace(
|
||||
@@ -80,11 +47,6 @@ def patch_cross_point_settings_h(repo_dir, token=""):
|
||||
write_file(path, content)
|
||||
print(f" CrossPointSettings.h patched{' (token embedded)' if token else ' (no token)'}.")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SettingsList.h - add hardcoverApiToken String for web UI exposure
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def patch_settings_list_h(repo_dir):
|
||||
path = find_first("SettingsList.h", repo_dir)
|
||||
content = read_file(path)
|
||||
@@ -104,12 +66,6 @@ def patch_settings_list_h(repo_dir):
|
||||
write_file(path, content)
|
||||
print(" SettingsList.h patched.")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SettingAction enum - add HardcoverSync value
|
||||
# The enum lives in SettingsActivity.h.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def patch_setting_action_enum(repo_dir):
|
||||
path = find_first("SettingsActivity.h", repo_dir)
|
||||
content = read_file(path)
|
||||
@@ -135,12 +91,6 @@ def patch_setting_action_enum(repo_dir):
|
||||
write_file(path, content)
|
||||
print(" SettingAction::HardcoverSync added.")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SettingsActivity.h - add pluginsSettings vector (if not already done by
|
||||
# darkmode/smallerfonts) and categoryCount = 5
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def patch_settings_h(repo_dir):
|
||||
path = find_first("SettingsActivity.h", repo_dir)
|
||||
content = read_file(path)
|
||||
@@ -167,11 +117,6 @@ def patch_settings_h(repo_dir):
|
||||
write_file(path, content)
|
||||
print(" SettingsActivity.h patched.")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SettingsActivity.cpp - wire up Plugins tab + Hardcover action
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def patch_settings_cpp(repo_dir):
|
||||
path = find_first("SettingsActivity.cpp", repo_dir)
|
||||
content = read_file(path)
|
||||
@@ -180,13 +125,11 @@ def patch_settings_cpp(repo_dir):
|
||||
print(" SettingsActivity.cpp already patched, skipping.")
|
||||
return
|
||||
|
||||
# ---- include HardcoverPlugin.h and HardcoverSyncActivity.h ------------
|
||||
content = content.replace(
|
||||
'#include "SettingsList.h"',
|
||||
'#include "SettingsList.h"\n#include "HardcoverPlugin.h"\n#include "HardcoverSyncActivity.h"'
|
||||
)
|
||||
|
||||
# ---- categoryNames: add STR_NONE_OPT for Plugins tab (if not done yet) -
|
||||
if 'StrId::STR_NONE_OPT};' not in content and 'STR_NONE_OPT' not in content.split('categoryNames')[1].split(';')[0]:
|
||||
content = content.replace(
|
||||
'const StrId SettingsActivity::categoryNames[categoryCount] = {StrId::STR_CAT_DISPLAY, StrId::STR_CAT_READER,\n'
|
||||
@@ -196,16 +139,12 @@ def patch_settings_cpp(repo_dir):
|
||||
' StrId::STR_NONE_OPT};'
|
||||
)
|
||||
|
||||
# ---- pluginsSettings.clear() -------------------------------------------
|
||||
if "pluginsSettings.clear();" not in content:
|
||||
content = content.replace(
|
||||
' systemSettings.clear();',
|
||||
' systemSettings.clear();\n pluginsSettings.clear();'
|
||||
)
|
||||
|
||||
# ---- push Hardcover action into pluginsSettings -------------------------
|
||||
# Darkmode pushes darkModeState and smallerFontsMode here. We append after.
|
||||
# Handle three cases: both other plugins present, only darkmode, or neither.
|
||||
hardcover_push = (
|
||||
' pluginsSettings.push_back(SettingInfo::Action(\n'
|
||||
' StrId::STR_NONE_OPT, SettingAction::HardcoverSync\n'
|
||||
@@ -214,17 +153,17 @@ def patch_settings_cpp(repo_dir):
|
||||
|
||||
if 'SettingAction::HardcoverSync' not in content:
|
||||
anchors = [
|
||||
# After smallerFontsMode push (both other plugins installed)
|
||||
|
||||
' pluginsSettings.push_back(SettingInfo::Enum(\n'
|
||||
' StrId::STR_NONE_OPT, &CrossPointSettings::smallerFontsMode,\n'
|
||||
' {StrId::STR_NONE_OPT, StrId::STR_NONE_OPT, StrId::STR_NONE_OPT}, "smallerFontsMode"\n'
|
||||
' ));\n',
|
||||
# After darkModeState push (only darkmode installed)
|
||||
|
||||
' pluginsSettings.push_back(SettingInfo::Enum(\n'
|
||||
' StrId::STR_NONE_OPT, &CrossPointSettings::darkModeState,\n'
|
||||
' {StrId::STR_NONE_OPT, StrId::STR_NONE_OPT}, "darkModeState"\n'
|
||||
' ));\n',
|
||||
# No other plugins: append after CustomiseStatusBar push
|
||||
|
||||
' readerSettings.push_back(SettingInfo::Action(StrId::STR_CUSTOMISE_STATUS_BAR, SettingAction::CustomiseStatusBar));\n',
|
||||
]
|
||||
for anchor in anchors:
|
||||
@@ -232,7 +171,6 @@ def patch_settings_cpp(repo_dir):
|
||||
content = content.replace(anchor, anchor + hardcover_push)
|
||||
break
|
||||
|
||||
# ---- case 4 -> pluginsSettings (if not done yet) -----------------------
|
||||
if 'case 4:\n currentSettings = &pluginsSettings;' not in content:
|
||||
content = content.replace(
|
||||
' case 3:\n currentSettings = &systemSettings;\n break;',
|
||||
@@ -240,7 +178,6 @@ def patch_settings_cpp(repo_dir):
|
||||
' case 4:\n currentSettings = &pluginsSettings;\n break;'
|
||||
)
|
||||
|
||||
# ---- "Plugins" tab label (if not done yet) -----------------------------
|
||||
if '"Plugins"' not in content:
|
||||
content = content.replace(
|
||||
' std::vector<TabInfo> tabs;\n'
|
||||
@@ -256,7 +193,6 @@ def patch_settings_cpp(repo_dir):
|
||||
' }'
|
||||
)
|
||||
|
||||
# ---- nextCatLabel fix (if not done yet) --------------------------------
|
||||
if 'nextCatLabel' not in content:
|
||||
content = content.replace(
|
||||
' const auto confirmLabel = (selectedSettingIndex == 0)\n'
|
||||
@@ -267,10 +203,6 @@ def patch_settings_cpp(repo_dir):
|
||||
' const auto confirmLabel = (selectedSettingIndex == 0) ? nextCatLabel : tr(STR_TOGGLE);'
|
||||
)
|
||||
|
||||
# ---- Row label lambda --------------------------------------------------
|
||||
# ACTION rows never have a key, so we must match on s.action, not s.key.
|
||||
# The darkmode patch already converted the lambda to the multi-line form
|
||||
# and added key-based checks. We extend it with our action-based check.
|
||||
hardcover_label_line = (
|
||||
' if (s.type == SettingType::ACTION &&\n'
|
||||
' s.action == SettingAction::HardcoverSync) return "Hardcover Progress";\n'
|
||||
@@ -278,7 +210,7 @@ def patch_settings_cpp(repo_dir):
|
||||
|
||||
if 'Hardcover Progress' not in content:
|
||||
if 'selectedCategoryIndex == 4' in content:
|
||||
# Lambda already patched by another plugin - insert before the closing return.
|
||||
|
||||
closing = (
|
||||
' }\n'
|
||||
' return std::string(I18N.get(settings[index].nameId));\n'
|
||||
@@ -309,8 +241,7 @@ def patch_settings_cpp(repo_dir):
|
||||
content = content.replace(cl, hardcover_label_line + cl, 1)
|
||||
break
|
||||
else:
|
||||
# No other plugin has patched the lambda yet - create it from scratch.
|
||||
# Exact line from real unpatched file (6 leading spaces):
|
||||
|
||||
old_lambda = (
|
||||
' [&settings](int index) { return std::string(I18N.get(settings[index].nameId)); }, nullptr, nullptr,'
|
||||
)
|
||||
@@ -325,39 +256,47 @@ def patch_settings_cpp(repo_dir):
|
||||
)
|
||||
content = content.replace(old_lambda, new_lambda)
|
||||
|
||||
# ---- Value text: "Sync" for the HardcoverSync action row ---------------
|
||||
hardcover_value_block = (
|
||||
' if (setting.type == SettingType::ACTION &&\n'
|
||||
' setting.action == SettingAction::HardcoverSync) {\n'
|
||||
' valueText = "Sync";\n'
|
||||
' } else '
|
||||
)
|
||||
if 'SettingAction::HardcoverSync) {\n valueText = "Sync"' not in content:
|
||||
content = content.replace(
|
||||
' if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) {',
|
||||
for value_anchor in [
|
||||
' if (setting.type == SettingType::ACTION &&\n'
|
||||
' setting.action == SettingAction::HardcoverSync) {\n'
|
||||
' setting.action == SettingAction::GoodreadsSync) {\n'
|
||||
' valueText = "Sync";\n'
|
||||
' } else if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) {'
|
||||
)
|
||||
' } else if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) {',
|
||||
' if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) {',
|
||||
]:
|
||||
if value_anchor in content:
|
||||
content = content.replace(value_anchor, hardcover_value_block + value_anchor)
|
||||
break
|
||||
|
||||
# ---- HardcoverSync case in toggleCurrentSetting switch -----------------
|
||||
content = content.replace(
|
||||
' case SettingAction::Language:\n'
|
||||
' startActivityForResult(std::make_unique<LanguageSelectActivity>(renderer, mappedInput), resultHandler);\n'
|
||||
' break;\n'
|
||||
' case SettingAction::None:',
|
||||
' case SettingAction::Language:\n'
|
||||
' startActivityForResult(std::make_unique<LanguageSelectActivity>(renderer, mappedInput), resultHandler);\n'
|
||||
' break;\n'
|
||||
hardcover_case = (
|
||||
' case SettingAction::HardcoverSync:\n'
|
||||
' startActivityForResult(std::make_unique<HardcoverSyncActivity>(renderer, mappedInput), resultHandler);\n'
|
||||
' break;\n'
|
||||
' case SettingAction::None:'
|
||||
)
|
||||
if 'case SettingAction::HardcoverSync:' not in content:
|
||||
for anchor in [
|
||||
' case SettingAction::GoodreadsSync:\n'
|
||||
' startActivityForResult(std::make_unique<GoodreadsSyncActivity>(renderer, mappedInput), resultHandler);\n'
|
||||
' break;\n'
|
||||
' case SettingAction::None:',
|
||||
' case SettingAction::Language:\n'
|
||||
' startActivityForResult(std::make_unique<LanguageSelectActivity>(renderer, mappedInput), resultHandler);\n'
|
||||
' break;\n'
|
||||
' case SettingAction::None:',
|
||||
]:
|
||||
if anchor in content:
|
||||
content = content.replace(anchor, hardcover_case + anchor)
|
||||
break
|
||||
|
||||
write_file(path, content)
|
||||
print(" SettingsActivity.cpp patched.")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CrossPointWebServer.cpp - expose hardcoverApiToken + Hardcover label
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def patch_web_server(repo_dir):
|
||||
path = find_first("CrossPointWebServer.cpp", repo_dir)
|
||||
content = read_file(path)
|
||||
@@ -373,7 +312,6 @@ def patch_web_server(repo_dir):
|
||||
' }\n'
|
||||
)
|
||||
|
||||
# Try to insert inside the existing s.key block added by darkmode patch
|
||||
if 'if (s.key) {' in content and 'darkModeState' in content:
|
||||
content = content.replace(
|
||||
' if (strcmp(s.key, "darkModeState") == 0) {\n',
|
||||
@@ -381,7 +319,7 @@ def patch_web_server(repo_dir):
|
||||
' if (strcmp(s.key, "darkModeState") == 0) {\n'
|
||||
)
|
||||
else:
|
||||
# No darkmode patch present - add our own block
|
||||
|
||||
content = content.replace(
|
||||
' doc["category"] = I18N.get(s.category);\n'
|
||||
'\n'
|
||||
@@ -398,17 +336,9 @@ def patch_web_server(repo_dir):
|
||||
write_file(path, content)
|
||||
print(" CrossPointWebServer.cpp patched.")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Entry point
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def patch(repo_dir: str, yes_all: bool = False):
|
||||
plugin_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
# ---- Resolve API token from ~/.hardcover or prompt ---------------------
|
||||
# The token prompt is always shown even with --yes, since it is required
|
||||
# input, not an optional confirmation.
|
||||
hardcover_file = os.path.expanduser("~/.hardcover")
|
||||
token = ""
|
||||
|
||||
@@ -459,7 +389,6 @@ def patch(repo_dir: str, yes_all: bool = False):
|
||||
print(" Patching CrossPointWebServer.cpp (web UI)...")
|
||||
patch_web_server(repo_dir)
|
||||
|
||||
# ---- Verification ------------------------------------------------------
|
||||
import glob as _glob
|
||||
cpp_results = _glob.glob(os.path.join(repo_dir, "**", "SettingsActivity.cpp"), recursive=True)
|
||||
if cpp_results:
|
||||
@@ -480,7 +409,6 @@ def patch(repo_dir: str, yes_all: bool = False):
|
||||
sys.exit("ERROR: One or more Hardcover patch checks failed.")
|
||||
print(" Hardcover plugin patch verified OK.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
sys.exit("Usage: python patch.py <path-to-crosspoint-repo>")
|
||||
|
||||
Reference in New Issue
Block a user