ed8956e9ed
* feat(koplugin/library): data layer + busted harness + design doc - LibraryStore: per-user SQLite index merging cloud + local books by partial-md5 hash. listBooks/listBookshelfBooks/listBookshelfGroups, upsertBook with cloud_present/local_present OR-merge + _force/clear sentinels, parseSyncRow, getChangedBooks for tombstone push. - EXTS table mirroring web's document.ts. - busted harness with KOReader stubs (G_reader_settings, DataStorage, lua-ljsqlite3 against :memory:); spec_helper, exts_spec, smoke_spec, librarystore_spec covering schema, sort, group nesting, dedupe. - Library design doc + spec README. - pnpm test:lua wired through root + app package.json; lint-koplugin recurses into library/ + spec/. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(koplugin/library): cloud sync — push, pull, upload, delete, downloads - Spore methods: pullBooks (incremental /sync), getDownloadUrl, getUploadUrl, listFiles, deleteFile. - syncbooks.lua: pushBook + pushChangedBooks (advances watermark to max(updated_at, deleted_at)), syncBooks(opts, mode) for push/pull/both, downloadBook + downloadCover (sync socket.http with file sink; cover download via fork+poll with single-slot queue + visible-page filter + coalesced refresh), uploadBook (presigned-PUT flow + best-effort cover), deleteCloudFiles (list-then-delete-each, mirrors cloudService.deleteBook). - SyncAuth.withFreshToken wrapper resolves the ensureClient race; 401/403 unified across syncconfig + syncannotations. - Cloud + local book covers shared by partial-md5 hash; cover.png cached at <settings>/readest_covers/<hash>.png with sentinel for known 404s. - syncbooks_spec covers row-to-wire conversion + file_key shape. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(koplugin/library): local discovery — sidecar scanner + cover provider - localscanner.lightScan iterates ReadHistory entries, reads partial_md5_checksum from .sdr sidecars, and upserts local rows. Slow filesystem walks deferred to fullSidecarWalk (24h-gated). - coverprovider wraps BookInfoManager:getBookInfo for local books with graceful FakeCover fallback when coverbrowser is absent. - localscanner_spec + coverprovider_spec. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(koplugin/library): UI — widget, item, view menu, FileManager hooks - librarywidget: full-screen Menu mixed in with CoverMenu + Mosaic/List per zen_ui's group_view pattern. Title-bar tap → view menu, search via left icon, drill-in/back for grouping. Async cloud sync deferred via scheduleIn so the menu paints before HTTP fires. - libraryitem: BIM patch for cloud-only (readest-cloud://) and group (readest-group://) URIs; group-cover composer (2x2 mosaic for grid, same in list) with cache key derived from the actual first-N hashes for content-based invalidation; ListMenuItem update + paintTo patches for wider list-mode cover strip and cloud-up/down icon overlay. - libraryviewmenu: ButtonDialog with View/Group by/Sort by/Actions. Default Group by = Groups (parity with web), values authors/groups. - librarypaint: partial-page e-ink repaint shim adapted from zen_ui. - main.lua: Library menu entry, dispatcher actions (Open Library / Push / Pull as general; progress + annotations as reader-only), "Add to Readest" button in FileManager's long-press file dialog (dedupe by partial_md5; bumps updated_at when present, inserts a fresh local-only row otherwise; un-tombstones via _clear_fields). Push books on Library open when auto_sync is on, pull-only otherwise. - Long-press action sheet with Readest BookDetailView parity: Remove from Cloud & Device / Cloud Only / Device Only, Upload to Cloud, Download Book / Cover / All. - Cloud-down + cloud-up SVG icons (LiaCloudDownloadAltSolid / LiaCloudUploadAltSolid) painted in the right-side wpageinfo slot. - i18n catalog updated for new strings. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(koplugin/library): split libraryitem into focused modules libraryitem.lua had grown to 1018 lines mixing five unrelated concerns. Split along the natural seams: cloud_covers — readest-cloud:// URI scheme, on-disk <hash>.png cache, single-slot async download queue, visible-page filter group_covers — readest-group:// URI scheme, 2x2 mosaic composer with content-derived cache key (first-N hashes), cell layout table cloud_icons — bundled cloud-up/cloud-down SVG loader, IconWidget cache, paint-overlay positioning list_strip — list-mode group row builder (4-cover wider strip replacing ListMenu's square cover slot) bim_patch — BookInfoManager:getBookInfo router (cloud / group / local) + ListMenuItem update + paintTo patches; owns the _library_local_paths set and orig BIM reference libraryitem.lua is now 141 lines: just the entry-table constructors (entry_from_row, entry_from_group, entry_back) plus thin install / set_visible_hashes delegates. Each new module is 88-216 lines. No behavior change — same 113 specs pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(koplugin): co-locate dev tooling, ship-zip exclusions, CI job split - apps/readest.koplugin/scripts/build-koplugin.mjs: local sideloading build with the same exclusions the release workflow uses. - Move lint-koplugin + test-koplugin from apps/readest-app/scripts/ to apps/readest.koplugin/scripts/. All koplugin dev tooling now lives with the koplugin and is excluded from the published release zip. - Rename to .mjs so Node treats them as ESM without the reparse warning (the i18n CommonJS scripts stay .js). - Release workflow: zip -r exclusions for scripts/, docs/, spec/, .busted so dev artifacts don't ship to end users. - PR workflow: split build_web_app into build_web_app + test_web_app for parallelism. The test job installs luarocks + busted + lsqlite3complete and runs pnpm test:lua. test-koplugin.mjs now hard-fails (instead of soft-skipping) when CI=true and a tool is missing — a broken CI toolchain previously exited 0 silently. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
137 lines
4.3 KiB
JavaScript
Executable File
137 lines
4.3 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
/**
|
|
* Build a Readest-${version}-1.koplugin.zip locally for sideloading
|
|
* onto a real KOReader device. Mirrors the same exclusions the CI
|
|
* release workflow uses (.github/workflows/release.yml: "create
|
|
* KOReader plugin zip"):
|
|
* scripts/ — i18n + build helpers
|
|
* docs/ — design notes
|
|
* spec/ — busted test suite
|
|
* .busted — busted runner config
|
|
*
|
|
* Usage:
|
|
* node apps/readest.koplugin/scripts/build-koplugin.js [--version X.Y.Z]
|
|
* [--out PATH]
|
|
* [--keep-meta]
|
|
*
|
|
* --version Stamp _meta.lua with this version before zipping. If
|
|
* omitted, a "dev-<sha>" placeholder is used so the
|
|
* installed plugin is identifiable. _meta.lua is restored
|
|
* after zipping unless --keep-meta is passed.
|
|
*
|
|
* --out PATH Destination zip path. Default:
|
|
* ./Readest-${version}-1.koplugin.zip
|
|
*
|
|
* --keep-meta Don't restore _meta.lua after the build. Useful when
|
|
* chaining a release tag.
|
|
*
|
|
* Requirements: a working `zip` binary on PATH (macOS/Linux/WSL/git-bash).
|
|
*/
|
|
|
|
import { execFileSync, spawnSync } from 'node:child_process';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const PLUGIN_DIR = path.resolve(__dirname, '..');
|
|
const PLUGIN_NAME = path.basename(PLUGIN_DIR); // "readest.koplugin"
|
|
const APPS_DIR = path.resolve(PLUGIN_DIR, '..');
|
|
const META_FILE = path.join(PLUGIN_DIR, '_meta.lua');
|
|
|
|
function parseArgs(argv) {
|
|
const out = { keepMeta: false };
|
|
for (let i = 0; i < argv.length; i++) {
|
|
const a = argv[i];
|
|
if (a === '--version') out.version = argv[++i];
|
|
else if (a === '--out') out.out = argv[++i];
|
|
else if (a === '--keep-meta') out.keepMeta = true;
|
|
else if (a === '-h' || a === '--help') {
|
|
console.log(`Usage:
|
|
node build-koplugin.js [--version X.Y.Z] [--out PATH] [--keep-meta]
|
|
|
|
Default version: dev-<git-sha>
|
|
Default out: ./Readest-<version>-1.koplugin.zip`);
|
|
process.exit(0);
|
|
} else {
|
|
console.error(`Unknown argument: ${a}`);
|
|
process.exit(2);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function shortGitSha() {
|
|
try {
|
|
return execFileSync('git', ['rev-parse', '--short=8', 'HEAD'], {
|
|
cwd: PLUGIN_DIR,
|
|
encoding: 'utf8',
|
|
}).trim();
|
|
} catch {
|
|
return 'unknown';
|
|
}
|
|
}
|
|
|
|
function which(cmd) {
|
|
const r = spawnSync(process.platform === 'win32' ? 'where' : 'which', [cmd]);
|
|
return r.status === 0;
|
|
}
|
|
|
|
function stampMeta(version) {
|
|
const original = fs.readFileSync(META_FILE, 'utf8');
|
|
// Inject ` version = "X",` immediately before the closing `}`.
|
|
// Same regex shape as the CI step's perl one-liner; matches a `}`
|
|
// at the start of a line and prepends the version line.
|
|
const stamped = original.replace(/^}/m, ` version = "${version}",\n}`);
|
|
fs.writeFileSync(META_FILE, stamped);
|
|
return original;
|
|
}
|
|
|
|
function restoreMeta(original) {
|
|
fs.writeFileSync(META_FILE, original);
|
|
}
|
|
|
|
function main() {
|
|
const args = parseArgs(process.argv.slice(2));
|
|
const version = args.version || `dev-${shortGitSha()}`;
|
|
const out = path.resolve(args.out || `Readest-${version}-1.koplugin.zip`);
|
|
|
|
if (!which('zip')) {
|
|
console.error('error: `zip` not found on PATH. Install via your package manager.');
|
|
process.exit(1);
|
|
}
|
|
|
|
// Remove an existing zip so `zip -r` doesn't accidentally append.
|
|
if (fs.existsSync(out)) fs.unlinkSync(out);
|
|
|
|
const originalMeta = stampMeta(version);
|
|
let zipExit = 1;
|
|
try {
|
|
const exclusions = [
|
|
`${PLUGIN_NAME}/scripts/*`,
|
|
`${PLUGIN_NAME}/docs/*`,
|
|
`${PLUGIN_NAME}/spec/*`,
|
|
`${PLUGIN_NAME}/.busted`,
|
|
];
|
|
const zipArgs = ['-r', out, PLUGIN_NAME, '-x', ...exclusions];
|
|
console.log(`Building ${path.basename(out)} from ${PLUGIN_DIR}`);
|
|
console.log(` excluding: ${exclusions.join(', ')}`);
|
|
const r = spawnSync('zip', zipArgs, {
|
|
cwd: APPS_DIR,
|
|
stdio: 'inherit',
|
|
});
|
|
zipExit = r.status === null ? 1 : r.status;
|
|
} finally {
|
|
if (!args.keepMeta) restoreMeta(originalMeta);
|
|
}
|
|
|
|
if (zipExit !== 0) {
|
|
console.error(`zip exited with code ${zipExit}`);
|
|
process.exit(zipExit);
|
|
}
|
|
const stat = fs.statSync(out);
|
|
console.log(`✓ ${out} (${(stat.size / 1024).toFixed(1)} KiB)`);
|
|
}
|
|
|
|
main();
|