From 21e2204f42ea74990e724d48a3a1d55e5cef17a7 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Mon, 2 Feb 2026 16:22:00 -0500 Subject: [PATCH 1/2] Add Linux support with Nix packaging, Flatpak, desktop integration, and CI/CD --- .envrc | 5 + .github/workflows/build.yml | 49 +- .gitignore | 6 + Cargo.lock | 520 ++++++++++++++++- Cargo.toml | 3 + README.md | 35 ++ doc/readme.md | 2 +- flake.lock | 61 ++ flake.nix | 226 ++++++++ icons/hicolor/128x128/apps/paperback.png | Bin 0 -> 3582 bytes icons/hicolor/16x16/apps/paperback.png | Bin 0 -> 375 bytes icons/hicolor/256x256/apps/paperback.png | Bin 0 -> 4884 bytes icons/hicolor/32x32/apps/paperback.png | Bin 0 -> 544 bytes icons/hicolor/48x48/apps/paperback.png | Bin 0 -> 1000 bytes icons/hicolor/64x64/apps/paperback.png | Bin 0 -> 320 bytes io.github.trypsynth.Paperback.yaml | 201 +++++++ paperback.desktop | 13 + po/paperback.pot | 684 ++++++++++++----------- src/ui.rs | 1 + src/ui/dialogs.rs | 41 ++ src/ui/dialogs/accessible_tree.rs | 262 +++++++++ src/ui/dialogs/elements_gtk.rs | 176 ++++++ src/ui/dialogs/toc_gtk.rs | 66 +++ src/ui/document_manager.rs | 83 ++- src/ui/main_window.rs | 12 +- 25 files changed, 2086 insertions(+), 360 deletions(-) create mode 100644 .envrc create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 icons/hicolor/128x128/apps/paperback.png create mode 100644 icons/hicolor/16x16/apps/paperback.png create mode 100644 icons/hicolor/256x256/apps/paperback.png create mode 100644 icons/hicolor/32x32/apps/paperback.png create mode 100644 icons/hicolor/48x48/apps/paperback.png create mode 100644 icons/hicolor/64x64/apps/paperback.png create mode 100644 io.github.trypsynth.Paperback.yaml create mode 100644 paperback.desktop create mode 100644 src/ui/dialogs/accessible_tree.rs create mode 100644 src/ui/dialogs/elements_gtk.rs create mode 100644 src/ui/dialogs/toc_gtk.rs diff --git a/.envrc b/.envrc new file mode 100644 index 00000000..0591e442 --- /dev/null +++ b/.envrc @@ -0,0 +1,5 @@ +# Optional: Automatically load Nix development environment when entering this directory +# Requires direnv (https://direnv.net/) and nix-direnv for automatic shell activation +# If you don't use direnv, run 'nix develop' manually instead +use flake +export PAPERBACK_COMMIT_HASH=dev \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f4ffb523..f58dc064 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,7 +7,7 @@ on: permissions: contents: write jobs: - build: + build-windows: runs-on: windows-latest steps: - name: Checkout repository @@ -45,11 +45,51 @@ jobs: - name: Upload artifacts uses: actions/upload-artifact@v4 with: - name: paperback-build + name: paperback-windows path: | target/release/paperback.zip target/release/paperback_setup.exe retention-days: 30 + + build-linux-flatpak: + runs-on: ubuntu-latest + container: + image: ghcr.io/flathub-infra/flatpak-github-actions:gnome-49 + options: --privileged + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Generate cargo sources for Flatpak + run: | + pip install aiohttp tomlkit + curl -sSL https://raw.githubusercontent.com/flatpak/flatpak-builder-tools/master/cargo/flatpak-cargo-generator.py -o flatpak-cargo-generator.py + python3 flatpak-cargo-generator.py Cargo.lock -o cargo-sources.json + - uses: flatpak/flatpak-github-actions/flatpak-builder@v6 + with: + bundle: paperback.flatpak + manifest-path: io.github.trypsynth.Paperback.yaml + cache-key: flatpak-builder-${{ github.sha }} + + release: + needs: [build-windows, build-linux-flatpak] + runs-on: ubuntu-latest + if: github.event_name == 'push' + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Download Windows artifacts + uses: actions/download-artifact@v4 + with: + name: paperback-windows + path: windows-build + - name: Download Linux Flatpak + uses: actions/download-artifact@v4 + with: + name: paperback-x86_64.flatpak + path: linux-build - name: Get latest tag reachable from HEAD id: get_tag shell: bash @@ -79,8 +119,9 @@ jobs: ## Commits since last release ${{ steps.release_notes.outputs.commits }} files: | - target/release/paperback.zip - target/release/paperback_setup.exe + windows-build/paperback.zip + windows-build/paperback_setup.exe + linux-build/paperback.flatpak prerelease: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1c86b1de..2bea54be 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,9 @@ web/_site/ *.pdf *.rtf .DS_Store +result +.direnv/ +.flatpak-builder/ +*.flatpak +repo-flatpak/ +cargo-sources.json diff --git a/Cargo.lock b/Cargo.lock index 143f5e63..607bcfa3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -34,6 +34,29 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -141,6 +164,31 @@ dependencies = [ "libbz2-rs-sys", ] +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + [[package]] name = "cbc" version = "0.1.2" @@ -180,6 +228,16 @@ dependencies = [ "web-time", ] +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -481,6 +539,16 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + [[package]] name = "filetime" version = "0.2.27" @@ -567,6 +635,17 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-io" version = "0.3.32" @@ -618,6 +697,64 @@ dependencies = [ "slab", ] +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -677,6 +814,85 @@ dependencies = [ "wasip3", ] +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + [[package]] name = "glob" version = "0.3.3" @@ -696,6 +912,69 @@ dependencies = [ "web-sys", ] +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "h2" version = "0.4.13" @@ -730,6 +1009,12 @@ version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "heck" version = "0.5.0" @@ -1078,7 +1363,7 @@ dependencies = [ "bzip2", "cc", "tar", - "thiserror", + "thiserror 2.0.18", "ureq", ] @@ -1190,6 +1475,15 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.17" @@ -1308,7 +1602,7 @@ dependencies = [ "rc4", "sha1", "sha2", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -1383,6 +1677,31 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + [[package]] name = "paperback" version = "0.8.5" @@ -1395,6 +1714,7 @@ dependencies = [ "embed-manifest", "encoding_rs", "flate2", + "gtk", "libchm", "live-region", "office-crypto", @@ -1593,13 +1913,57 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + [[package]] name = "proc-macro-crate" version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit", + "toml_edit 0.25.5+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", ] [[package]] @@ -1658,7 +2022,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror", + "thiserror 2.0.18", "tokio", "tracing", "web-time", @@ -1679,7 +2043,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror", + "thiserror 2.0.18", "tinyvec", "tracing", "web-time", @@ -1917,7 +2281,7 @@ checksum = "9c845311f0ff7951c5506121a9ad75aec44d083c31583b2ea5a30bcb0b0abba0" dependencies = [ "cfg-if", "glob", - "proc-macro-crate", + "proc-macro-crate 3.5.0", "proc-macro2", "quote", "regex", @@ -2155,6 +2519,15 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2337,6 +2710,19 @@ dependencies = [ "libc", ] +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + [[package]] name = "tap" version = "1.0.1" @@ -2354,6 +2740,12 @@ dependencies = [ "xattr", ] +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + [[package]] name = "tempfile" version = "3.27.0" @@ -2377,13 +2769,33 @@ dependencies = [ "utf-8", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -2509,6 +2921,27 @@ dependencies = [ "serde", ] +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + [[package]] name = "toml_datetime" version = "1.0.1+spec-1.1.0" @@ -2518,6 +2951,30 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + [[package]] name = "toml_edit" version = "0.25.5+spec-1.1.0" @@ -2525,9 +2982,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ca1a40644a28bce036923f6a431df0b34236949d111cc07cb6dca830c9ef2e1" dependencies = [ "indexmap", - "toml_datetime", + "toml_datetime 1.0.1+spec-1.1.0", "toml_parser", - "winnow", + "winnow 1.0.0", ] [[package]] @@ -2536,7 +2993,7 @@ version = "1.0.10+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420" dependencies = [ - "winnow", + "winnow 1.0.0", ] [[package]] @@ -2753,6 +3210,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + [[package]] name = "version_check" version = "0.9.5" @@ -2936,6 +3399,22 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + [[package]] name = "winapi-util" version = "0.1.11" @@ -2945,6 +3424,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows" version = "0.62.2" @@ -3222,6 +3707,15 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + [[package]] name = "winnow" version = "1.0.0" @@ -3237,7 +3731,7 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b68db261ef59e9e52806f688020631e987592bd83619edccda9c47d42cde4f6c" dependencies = [ - "toml", + "toml 0.5.11", ] [[package]] @@ -3256,7 +3750,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" dependencies = [ "anyhow", - "heck", + "heck 0.5.0", "wit-parser", ] @@ -3267,7 +3761,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", - "heck", + "heck 0.5.0", "indexmap", "prettyplease", "syn 2.0.117", diff --git a/Cargo.toml b/Cargo.toml index b586a916..96887751 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,9 @@ ureq = { version = "3.3.0", default-features = false, features = ["json", "rustl wxdragon = { version = "0.9.14", features = ["webview"] } zip = { version = "8.4.0", default-features = false, features = ["deflate"] } +[target.'cfg(target_os = "linux")'.dependencies] +gtk = { version = "0.18", default-features = false } + [target.'cfg(windows)'.dependencies] windows = { version = "0.62.2", features = ["Win32_UI_Accessibility", "Win32_System_Com", "Win32_UI_WindowsAndMessaging", "Win32_Foundation", "Win32_System_Ole", "Win32_System_Variant"] } diff --git a/README.md b/README.md index 1793bd93..486ae73a 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,41 @@ The following tools aren't required to build a functioning Paperback on a basic * `gettext` tools (`xgettext`, `msgfmt`, `msgmerge`) on your `PATH` to generate the translation template and compile translations. * InnoSetup installed to create the installer. +### Linux + +Building on Linux requires wxWidgets 3.2+ with GTK3 backend. The wxDragon build system will handle compiling the wxWidgets bindings. + +### Linux (Nix) + +**Run directly:** +```bash +nix run github:trypsynth/paperback +``` + +**Install to profile:** +```bash +nix profile install github:trypsynth/paperback +``` + +**Build from source:** +```bash +# Build and run +nix run + +# Or build without running: +nix build +``` + +### Linux (flatpak) + +```bash +flatpak-builder --force-clean --repo=repo-flatpak build io.github.trypsynth.Paperback.yaml +flatpak build-bundle repo-flatpak paperback.flatpak io.github.trypsynth.Paperback + +# Install the Flatpak: +flatpak --user install paperback.flatpak +``` + ## Contributing Contributions are welcome! Whether through issues, pull requests, discussions, or other means, your interest is most certainly appreciated. Thanks for using Paperback! diff --git a/doc/readme.md b/doc/readme.md index 8a799d75..f3e8385d 100644 --- a/doc/readme.md +++ b/doc/readme.md @@ -6,7 +6,7 @@ Paperback is a lightweight, fast, and accessible ebook and document reader for e ## System Requirements -Paperback currently runs on Windows 10 and 11, with support for macOS and Linux in the pipeline. +Paperback currently runs on Windows 10/11 and Linux. Support for macOS is in the pipeline. ## Features diff --git a/flake.lock b/flake.lock new file mode 100644 index 00000000..f1943347 --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1774386573, + "narHash": "sha256-4hAV26quOxdC6iyG7kYaZcM3VOskcPUrdCQd/nx8obc=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "46db2e09e1d3f113a13c0d7b81e2f221c63b8ce9", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 00000000..bdf6672d --- /dev/null +++ b/flake.nix @@ -0,0 +1,226 @@ +{ + description = "Paperback - A lightweight, fast, and accessible ebook and document reader"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = + { + self, + nixpkgs, + flake-utils, + }: + flake-utils.lib.eachDefaultSystem ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + + pdfiumLib = "${pkgs.pdfium-binaries}/lib"; + + commonNativeBuildInputs = with pkgs; [ + cmake + ninja + pkg-config + gettext + pandoc + cargo + rustc + makeBinaryWrapper + wrapGAppsHook3 + python3 + llvmPackages.libclang + gcc + ]; + + commonBuildInputs = with pkgs; [ + openssl + gtk3 + webkitgtk_4_1 + pdfium-binaries + libxkbcommon + libxtst + wayland + wayland-scanner + wayland-protocols + ]; + + commonEnv = { + LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib"; + BINDGEN_EXTRA_CLANG_ARGS = "-isystem ${pkgs.stdenv.cc.libc.dev}/include"; + CHMLIB_TARBALL = pkgs.fetchurl { + url = "http://www.jedrea.com/chmlib/chmlib-0.40.tar.bz2"; + sha256 = "3449d64b0cf71578b2c7e3ddc048d4af3661f44a83941ea074a7813f3a59ffa3"; + }; + }; + + in + { + packages.default = pkgs.stdenv.mkDerivation ( + commonEnv + // { + pname = "paperback"; + version = (builtins.fromTOML (builtins.readFile ./Cargo.toml)).package.version; + + src = ./.; + + nativeBuildInputs = commonNativeBuildInputs; + buildInputs = commonBuildInputs; + + preConfigure = '' + # Extract wxWidgets source so build.rs skips downloading + mkdir -p wxWidgets-extracted + ${pkgs.unzip}/bin/unzip -qo ${pkgs.fetchurl { + url = "https://github.com/wxWidgets/wxWidgets/releases/download/v3.3.2/wxWidgets-3.3.2.zip"; + sha256 = "sha256-9qVt5tj7VTFyMPuk72T4GmRq1vjEOdJxDZh1BJOopWk="; + }} -d wxWidgets-extracted + export WXWIDGETS_DIR="$PWD/wxWidgets-extracted" + + # Copy vendor dir to writable location so we can patch dependencies + cp -rL --no-preserve=mode ${pkgs.rustPlatform.importCargoLock { + lockFile = ./Cargo.lock; + outputHashes = { + "pdfium-0.1.1" = "sha256-J+BXxorzHJmC5JotofsN8AQLDGHOb6EZbIdJOPHZ/CY="; + }; + }} cargo-vendor + + # Patch libchm build.rs to use local chmlib tarball + echo 'use std::{env, fs, io::Cursor, path::PathBuf}; + use bzip2::read::BzDecoder; + use cc::Build; + use tar::Archive; + + fn main() { + let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); + let chmlib_dir = out_dir.join("chmlib-0.40"); + let src_dir = chmlib_dir.join("src"); + if !chmlib_dir.exists() { + let tarball_path = env::var("CHMLIB_TARBALL").expect("CHMLIB_TARBALL must be set"); + let buf = fs::read(&tarball_path).expect("Failed to read chmlib tarball"); + let mut archive = Archive::new(BzDecoder::new(Cursor::new(buf))); + archive.unpack(&out_dir).expect("Failed to extract chmlib"); + } + let chm_lib_path = src_dir.join("chm_lib.c"); + let mut contents = fs::read_to_string(&chm_lib_path).expect("Failed to read chm_lib.c"); + contents = contents.replace( + "/* yielding an error is preferable to yielding incorrect behavior */\n#error \"Please define the sized types for your platform in chm_lib.c\"", + "typedef unsigned char UChar;\ntypedef int16_t Int16;\ntypedef uint16_t UInt16;\ntypedef int32_t Int32;\ntypedef uint32_t UInt32;\ntypedef int64_t Int64;\ntypedef uint64_t UInt64;" + ); + contents = contents.replace("#if __sun || __sgi\n#include ", "#ifdef CHMLIB_HAVE_STRINGS_H\n#include "); + fs::write(&chm_lib_path, contents).expect("Failed to write patched chm_lib.c"); + Build::new() + .file(src_dir.join("chm_lib.c")) + .file(src_dir.join("lzx.c")) + .include(&src_dir) + .warnings(false) + .define("CHMLIB_HAVE_STRINGS_H", None) + .compile("chm"); + println!("cargo:rustc-link-lib=static=chm"); + }' > cargo-vendor/libchm-0.1.0/build.rs + + # Create cargo config for vendored sources + mkdir -p .cargo + echo '[source.crates-io] + replace-with = "vendored-sources" + + [source."git+https://github.com/AllenDang/wxDragon"] + git = "https://github.com/AllenDang/wxDragon" + replace-with = "vendored-sources" + + [source."git+https://github.com/aryanchoudharypro/PDFium-rs?branch=feature/tagged-pdf-support"] + git = "https://github.com/aryanchoudharypro/PDFium-rs" + branch = "feature/tagged-pdf-support" + replace-with = "vendored-sources" + + [source.vendored-sources]' > .cargo/config.toml + echo "directory = \"$PWD/cargo-vendor\"" >> .cargo/config.toml + ''; + + dontUseCmakeConfigure = true; + + configurePhase = '' + runHook preConfigure + runHook postConfigure + ''; + + buildPhase = '' + runHook preBuild + cargo build --release --offline + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + + install -Dm755 target/release/paperback $out/bin/.paperback-unwrapped + makeWrapper $out/bin/.paperback-unwrapped $out/bin/paperback \ + --prefix LD_LIBRARY_PATH : ${pdfiumLib} + + if [ -d target/release/langs ]; then + mkdir -p $out/share + cp -r target/release/langs $out/share/locale + fi + + install -Dm644 paperback.desktop $out/share/applications/paperback.desktop + for size in 16 32 48 64 128 256; do + if [ -f icons/hicolor/''${size}x''${size}/apps/paperback.png ]; then + install -Dm644 icons/hicolor/''${size}x''${size}/apps/paperback.png \ + $out/share/icons/hicolor/''${size}x''${size}/apps/paperback.png + fi + done + + runHook postInstall + ''; + + meta = with pkgs.lib; { + description = "A lightweight, fast, and accessible ebook and document reader"; + homepage = "https://github.com/trypsynth/paperback"; + license = licenses.mit; + platforms = platforms.linux; + mainProgram = "paperback"; + }; + } + ); + + devShells.default = pkgs.mkShell ( + commonEnv + // { + nativeBuildInputs = commonNativeBuildInputs; + buildInputs = commonBuildInputs; + + packages = with pkgs; [ + nil + nixfmt + clang-tools + gdb + lldb + (pkgs.python3Packages.buildPythonApplication { + pname = "flatpak-cargo-generator"; + version = "unstable-2024-01-01"; + format = "other"; + + src = pkgs.fetchFromGitHub { + owner = "flatpak"; + repo = "flatpak-builder-tools"; + rev = "db39dc0f75a3b24cfb09906f3aba2c13b0c48afe"; + hash = "sha256-TnGkivHjVbOCqcowWgCw+v2MIgHz+2zU5AU2PO/prFo="; + }; + + propagatedBuildInputs = with pkgs.python3Packages; [ + aiohttp + tomlkit + ]; + + installPhase = '' + install -Dm755 cargo/flatpak-cargo-generator.py $out/bin/flatpak-cargo-generator + ''; + }) + ]; + + LD_LIBRARY_PATH = pdfiumLib; + } + ); + } + ); +} diff --git a/icons/hicolor/128x128/apps/paperback.png b/icons/hicolor/128x128/apps/paperback.png new file mode 100644 index 0000000000000000000000000000000000000000..0966613a34834b89cbad74acb990d5c69b7893df GIT binary patch literal 3582 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 literal 0 HcmV?d00001 diff --git a/icons/hicolor/16x16/apps/paperback.png b/icons/hicolor/16x16/apps/paperback.png new file mode 100644 index 0000000000000000000000000000000000000000..5bdb744f4571bd564be116ca4daa319199cc48e2 GIT binary patch literal 375 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 literal 0 HcmV?d00001 diff --git a/icons/hicolor/32x32/apps/paperback.png b/icons/hicolor/32x32/apps/paperback.png new file mode 100644 index 0000000000000000000000000000000000000000..44aa36354dadf594dc03eb0149fe8f6e42be55a7 GIT binary patch literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7e6l0AZa85pY67#JE_7#My5g&JNk zFq9fFFuY1&V6d9Oz#v{QXIG#NP-07fPlzi>Pv;56&XclLTV$)Z$W(1n=r{pFoj`OP z!j^A4F4qQP%eNnw?>Mg5asVuTT(R}Axw*NWon6V~kW_%R0bsf|NqZb&j<7egLO%eUogXue_ULgx1DZt z+B|o0v2t^>@bIwsw~^h<%Zz>F-wkZ0KF>_qHf&}$HfGEr|jK?9+n1175)f>_UJa77qAiM)AZqR58g zf}|A>9zAQ+e(?0!p(J*BbNi4+26OqaaE>DGMp2I9?gbs)PK`5muxwWeXjIugWebl_ zfXEUDF|H?1M0beJ6q!0DG$he!lLlAH)hnh`W=t{Jx+S$xW5U&oS1+3zTw&bY+i*Z4 zwsB48%9%Smb>vt#iA@tb5#z(UDQpvq$O*n6)(XajA|5OZ3`t_TqNF13s3=2F%bOv_3QoTZ>n8jU&GzsapK$e??5%bfB*jQ^}7ntkoFUDZO4E7{0S8K z`0a-(#QQ+9{lw2-zkn*sD=N#%DX`u0=3=M>PcU%!3>rM~_61=IyG4M;-N02#Ap z&6+uL=Kufy3w5WbgW{}57SKfO7xc2-vWykPo)n}h4&Hpd8;5T2`^kA!q4{kWr|s;kwU)4XQR9;Ui@4KHm? z<(vtjfuYJz@=JMrb!YJ2veMZVG3CWeX^8_9778|R>|Cbl(mZqL9#-aNRVT5IFAY*( z)=p^H%G))&Y2iUzlO9EzL+X=MwXE7270u4^1~>1UYR%Q*pWxw>lo;rnmf+FpJhkZf zBNYWd-(-*TC(a}#oH*Cm_59g0#z4C=b;dwPMdv_6W@pbn=`h|GeoRf5j22W>^39pg z&So)}#YJ=~Pv}*Rdq-ux1VVzN-?n)(i>W_kv6%a$?`*{$zeCFtTdY>DTPN4{V9hV9 l9FA8iLJiu1tV(kk8Qe4(Zry+N4wO$BJYD@<);T3K0RYAy)iVG9 literal 0 HcmV?d00001 diff --git a/icons/hicolor/64x64/apps/paperback.png b/icons/hicolor/64x64/apps/paperback.png new file mode 100644 index 0000000000000000000000000000000000000000..409ea87336e015860c7719189799acf2a42155a6 GIT binary patch literal 320 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0J3?w7mbKU|eg=CK)Uj~LMH3o);76yi2K%s^g z3=E|P3=FRl7#OT(FffQ0%-I!a1C$U7@Ck7R(hLnC0Hpu_|9{}X0p`|Bpl$}nk|4ie z28U-i(mVTyj>?+avwrx_ZI8cMI8VUmVQW7xkI?U?yw7mw^JefpvESD+i;n2aAt_;D*Mz z2LvN7Kl4yvYK~bKxxs}=@W+aVOE$s@2~)I#7!4WOPH@aDzCn!p5z2KUN%%13H(%)78&qol`;+ E0BEggegFUf literal 0 HcmV?d00001 diff --git a/io.github.trypsynth.Paperback.yaml b/io.github.trypsynth.Paperback.yaml new file mode 100644 index 00000000..0f7dcaff --- /dev/null +++ b/io.github.trypsynth.Paperback.yaml @@ -0,0 +1,201 @@ +app-id: io.github.trypsynth.Paperback +runtime: org.gnome.Platform +runtime-version: '50' +sdk: org.gnome.Sdk +sdk-extensions: + - org.freedesktop.Sdk.Extension.rust-stable + - org.freedesktop.Sdk.Extension.llvm21 +command: paperback + +finish-args: + # Filesystem access to open documents + - --filesystem=home + # X11 and Wayland display + - --socket=x11 + - --socket=wayland + # GPU acceleration + - --device=dri + # Access to network for update checks and external links + - --share=network + # IPC for accessibility + - --share=ipc + +cleanup: + - /include + - /lib/cmake + - /lib/pkgconfig + - /share/bakefile + - /share/man + - /bin/wx-config + - '*.la' + - '*.a' + +modules: + # wxWidgets 3.3.2 - GUI toolkit (required by wxDragon 0.9.14) + - name: wxwidgets + sources: + - type: archive + url: https://github.com/wxWidgets/wxWidgets/releases/download/v3.3.2/wxWidgets-3.3.2.tar.bz2 + sha256: 50a28cb668de47b0e006cd6ebed8cf4f76c1cac6116fb3c978c44478219103f2 + config-opts: + - --enable-shared + - --with-gtk=3 + - --without-opengl + - --enable-display + - --enable-propgrid + - --enable-stc + - --with-libjpeg + - --with-libpng + - --with-zlib + - --enable-webview + - --disable-ribbon + cleanup: + - /bin/wxrc* + + # pandoc - Documentation build tool (build-time only) + - name: pandoc + buildsystem: simple + build-commands: + - install -Dm755 pandoc-3.8.2.1/bin/pandoc /app/bin/pandoc + cleanup: + - /bin/pandoc + sources: + - type: archive + url: https://github.com/jgm/pandoc/releases/download/3.8.2.1/pandoc-3.8.2.1-linux-amd64.tar.gz + sha256: b362815e21d8ad3629c124aa92baf54558da086ad72374b4f6fdd97b9f3275b0 + strip-components: 0 + + # SDL2 - needed by wxWidgets sound backend (wxdragon-sys builds wxWidgets with SDL support) + - name: SDL2 + buildsystem: cmake-ninja + builddir: true + config-opts: + - -DSDL_STATIC=OFF + - -DSDL_TEST=OFF + sources: + - type: archive + url: https://github.com/libsdl-org/SDL/releases/download/release-2.32.6/SDL2-2.32.6.tar.gz + sha256: 6a7a40d6c2e00016791815e1a9f4042809210bdf10cc78d2c75b45c4f52f93ad + cleanup: + - /bin + - /include + - /share + + # pdfium - PDF rendering library (pre-built binaries) + - name: pdfium + buildsystem: simple + sources: + - type: archive + url: https://github.com/bblanchon/pdfium-binaries/releases/download/chromium/7749/pdfium-linux-x64.tgz + sha256: 735abb28af7deab720dc54c8df3673a0a2a57bae9a0b16288842695fbd90a4b9 + build-commands: + - install -Dm644 libpdfium.so -t /app/lib/ + + # paperback - Main application + - name: paperback + buildsystem: simple + build-options: + append-path: /usr/lib/sdk/rust-stable/bin:/usr/lib/sdk/llvm21/bin:/app/bin + env: + CARGO_HOME: /run/build/paperback/cargo + CHMLIB_TARBALL: /run/build/paperback/chmlib-0.40.tar.bz2 + WXWIDGETS_DIR: /run/build/paperback/wxWidgets-extracted + LIBCLANG_PATH: /usr/lib/sdk/llvm21/lib + PAPERBACK_COMMIT_HASH: flatpak + RUSTFLAGS: -L/app/lib -lSDL2 + build-commands: + # Extract wxWidgets source for wxdragon-sys + - unzip -qo wxWidgets-3.3.2.zip -d wxWidgets-extracted + # Patch libchm's build.rs to use local chmlib tarball instead of downloading + - | + cat > cargo/vendor/libchm-0.1.0/build.rs << 'PATCHEOF' + use std::{ + env, fs, + io::Cursor, + path::{Path, PathBuf}, + }; + + use bzip2::read::BzDecoder; + use cc::Build; + use tar::Archive; + + fn main() { + let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); + let chmlib_dir = out_dir.join("chmlib-0.40"); + let src_dir = chmlib_dir.join("src"); + if !chmlib_dir.exists() { + extract_chmlib(&out_dir); + } + apply_patches(&src_dir); + let mut build = Build::new(); + build.file(src_dir.join("chm_lib.c")).file(src_dir.join("lzx.c")).include(&src_dir).warnings(false); + if cfg!(target_os = "windows") { + build.define("WIN32", None); + build.define("_WINDOWS", None); + } else { + build.define("CHMLIB_HAVE_STRINGS_H", None); + } + build.compile("chm"); + println!("cargo:rustc-link-lib=static=chm"); + } + + fn extract_chmlib(out_dir: &Path) { + let tarball_path = env::var("CHMLIB_TARBALL").expect("CHMLIB_TARBALL env var must be set for offline build"); + let buf = fs::read(&tarball_path).expect("Failed to read chmlib tarball"); + let decompressor = BzDecoder::new(Cursor::new(buf)); + let mut archive = Archive::new(decompressor); + archive.unpack(out_dir).expect("Failed to extract chmlib"); + } + + fn apply_patches(src_dir: &Path) { + let chm_lib_path = src_dir.join("chm_lib.c"); + let mut contents = fs::read_to_string(&chm_lib_path).expect("Failed to read chm_lib.c"); + contents = contents.replace("/* yielding an error is preferable to yielding incorrect behavior */\n#error \"Please define the sized types for your platform in chm_lib.c\"", "typedef unsigned char UChar;\ntypedef int16_t Int16;\ntypedef uint16_t UInt16;\ntypedef int32_t Int32;\ntypedef uint32_t UInt32;\ntypedef int64_t Int64;\ntypedef uint64_t UInt64;"); + contents = contents + .replace("#if __sun || __sgi\n#include ", "#ifdef CHMLIB_HAVE_STRINGS_H\n#include "); + fs::write(&chm_lib_path, contents).expect("Failed to write patched chm_lib.c"); + } + PATCHEOF + # Update .cargo-checksum.json for libchm + - | + python3 -c " + import json + with open('cargo/vendor/libchm-0.1.0/.cargo-checksum.json', 'r') as f: + data = json.load(f) + data['files'] = {} + with open('cargo/vendor/libchm-0.1.0/.cargo-checksum.json', 'w') as f: + json.dump(data, f) + " + # wxdragon-sys 0.9.14 natively respects WXWIDGETS_DIR env var, no patching needed + # Build with cargo + - cargo build --release --offline + # Install binary + - install -Dm755 target/release/paperback /app/bin/paperback + # Install translations (remove conflicting files from wxWidgets first) + - | + if [ -d target/release/langs ]; then + mkdir -p /app/share/locale + for lang in target/release/langs/*; do + langname=$(basename "$lang") + rm -rf "/app/share/locale/$langname" + cp -r "$lang" "/app/share/locale/" + done + fi + # Install desktop file + - install -Dm644 paperback.desktop /app/share/applications/io.github.trypsynth.Paperback.desktop + # Update Icon field in desktop file to use app-id + - sed -i 's/Icon=paperback/Icon=io.github.trypsynth.Paperback/' /app/share/applications/io.github.trypsynth.Paperback.desktop + # Install icons + - for size in 16 32 48 64 128 256; do if [ -f icons/hicolor/${size}x${size}/apps/paperback.png ]; then install -Dm644 icons/hicolor/${size}x${size}/apps/paperback.png /app/share/icons/hicolor/${size}x${size}/apps/io.github.trypsynth.Paperback.png; fi; done + sources: + - type: dir + path: . + - cargo-sources.json + # chmlib tarball for offline build (libchm crate normally downloads this) + - type: file + url: http://www.jedrea.com/chmlib/chmlib-0.40.tar.bz2 + sha256: 3449d64b0cf71578b2c7e3ddc048d4af3661f44a83941ea074a7813f3a59ffa3 + # wxWidgets source for wxdragon-sys (normally downloads at build time) + - type: file + url: https://github.com/wxWidgets/wxWidgets/releases/download/v3.3.2/wxWidgets-3.3.2.zip + sha256: f6a56de6d8fb55317230fba4ef64f81a646ad6f8c439d2710d98750493a8a569 diff --git a/paperback.desktop b/paperback.desktop new file mode 100644 index 00000000..7ea0eb94 --- /dev/null +++ b/paperback.desktop @@ -0,0 +1,13 @@ +[Desktop Entry] +Version=1.5 +Type=Application +Name=Paperback +GenericName=Document Reader +Comment=Lightweight, fast, and accessible ebook and document reader +Exec=paperback %F +Icon=paperback +Terminal=false +Categories=Office;Viewer;GTK; +Keywords=ebook;epub;pdf;rtf;document;reader;chm;accessibility;screen-reader; +StartupNotify=true +MimeType=application/epub+zip;application/pdf;application/x-chm;application/vnd.openxmlformats-officedocument.wordprocessingml.document;application/vnd.oasis.opendocument.text;application/x-fictionbook+xml;text/html;text/markdown;text/plain;application/rtf;text/rtf;application/vnd.oasis.opendocument.presentation;application/vnd.openxmlformats-officedocument.presentationml.presentation; diff --git a/po/paperback.pot b/po/paperback.pot index 887d30de..a8897d04 100644 --- a/po/paperback.pot +++ b/po/paperback.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: paperback 0.8.5\n" "Report-Msgid-Bugs-To: https://github.com/trypsynth/paperback/issues\n" -"POT-Creation-Date: 2026-03-22 10:12-0600\n" +"POT-Creation-Date: 2026-03-26 12:04-0400\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,946 +17,962 @@ msgstr "" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" -msgid "Figure" +msgid "Previous Section\t[" msgstr "" -msgid "Image" +msgid "Go to previous section" msgstr "" -msgid "" -"This PDF contains images only, with no extractable text. You may need to run " -"it through OCR software to read its contents." +msgid "Next Section\t]" msgstr "" -msgid "Failed to create IPC server" +msgid "Go to next section" msgstr "" -msgid "Warning" +msgid "Go to &Page\tCtrl+P" msgstr "" -msgid "Options" +msgid "Previous Pa&ge\tShift+P" msgstr "" -msgid "&Restore previously opened documents on startup" +msgid "Next Pag&e\tP" msgstr "" -msgid "&Word wrap" +msgid "Previous Lin&k\tShift+K" msgstr "" -msgid "&Minimize to system tray" +msgid "Next Lin&k\tK" msgstr "" -msgid "&Start maximized" +msgid "Previous Ima&ge\tShift+G" msgstr "" -msgid "Show compact &go menu" +msgid "Next Ima&ge\tG" msgstr "" -msgid "&Wrap navigation" +msgid "Previous Figu&re\tShift+F" msgstr "" -msgid "Play &sounds on bookmarks and notes" +msgid "Next Figu&re\tF" msgstr "" -msgid "Check for &updates on startup" +msgid "Previous &Table\tShift+T" msgstr "" -msgid "Number of &recent documents to show:" +msgid "Next &Table\tT" msgstr "" -msgid "&Language:" +msgid "Previous Se¶tor\tShift+S" msgstr "" -msgid "Update Channel:" +msgid "Next Se¶tor\tS" msgstr "" -msgid "Stable" +msgid "Previous L&ist\tShift+L" msgstr "" -msgid "Dev" +msgid "Next L&ist\tL" msgstr "" -msgid "General" +msgid "Previous List &Item\tShift+I" msgstr "" -msgid "Reading" +msgid "Next List I&tem\tI" msgstr "" -msgid "OK" +msgid "Next Heading Level 1\t1" msgstr "" -msgid "Cancel" +msgid "Previous Heading Level &2\tShift+2" msgstr "" -msgid "Jump to Bookmark" +msgid "Next Heading Level 2\t2" msgstr "" -msgid "&Filter:" +msgid "Previous Heading Level &3\tShift+3" msgstr "" -msgid "All" +msgid "Next Heading Level 3\t3" msgstr "" -msgid "Bookmarks" +msgid "Previous Heading Level &4\tShift+4" msgstr "" -msgid "Notes" +msgid "Next Heading Level 4\t4" msgstr "" -msgid "&Edit Note" +msgid "Previous Heading Level &5\tShift+5" msgstr "" -msgid "&Delete" +msgid "Next Heading Level 5\t5" msgstr "" -msgid "&Jump" +msgid "Previous Heading Level &6\tShift+6" msgstr "" -msgid "&Cancel" +msgid "Next Heading Level 6\t6" msgstr "" -msgid "blank" +msgid "&Previous Heading\tShift+H" msgstr "" -msgid "Please select a bookmark to jump to." +msgid "Go to previous heading" msgstr "" -msgid "Error" +msgid "&Next Heading\tH" msgstr "" -msgid "Bookmark Note" +msgid "Go to next heading" msgstr "" -msgid "Edit bookmark note:" +msgid "Previous Heading Level &1\tShift+1" msgstr "" -msgid "View Note" +msgid "&Previous Bookmark\tShift+B" msgstr "" -msgid "Close" +msgid "Go to previous bookmark" msgstr "" -msgid "Table of Contents" +msgid "&Next Bookmark\tB" msgstr "" -msgid "Root" +msgid "Go to next bookmark" msgstr "" -msgid "Please select a section from the table of contents." +msgid "Previous &Note\tShift+N" msgstr "" -msgid "No Selection" +msgid "Go to previous note" msgstr "" -msgid "Untitled" +msgid "Next N&ote\tN" msgstr "" -msgid "Document Info" +msgid "Go to next note" msgstr "" -msgid "Path:" +msgid "Jump to &All...\tCtrl+B" msgstr "" -msgid "Title:" +msgid "Show all bookmarks and notes" msgstr "" -msgid "Author:" +msgid "Jump to &Bookmarks Only...\tCtrl+Alt+B" msgstr "" -msgid "Words:" +msgid "Show bookmarks only" msgstr "" -msgid "Lines:" +msgid "Jump to Notes &Only...\tCtrl+Alt+M" msgstr "" -msgid "Characters:" +msgid "Show notes only" msgstr "" -msgid "Characters (excluding spaces):" +msgid "&View Note Text\tRawCtrl+Shift+W" msgstr "" -msgid "Go to Line" +msgid "&View Note Text\tCtrl+Shift+W" msgstr "" -msgid "&Line number:" +msgid "View the note at current position" msgstr "" -msgid "Go to page" +msgid "&File" msgstr "" -#, c-format -msgid "Go to page (%d/%d):" +msgid "&Go" msgstr "" -msgid "Go to Percent" +msgid "&Tools" msgstr "" -msgid "&Percent" +msgid "&Help" msgstr "" -msgid "P&ercent:" +msgid "&Open...\tCtrl+O" msgstr "" -msgid "&Yes" +msgid "Open a document" msgstr "" -msgid "&No" +msgid "&Close\tCtrl+W" msgstr "" -#, c-format -msgid "Update to %s" +msgid "&Close\tCtrl+F4" msgstr "" -msgid "A new version of Paperback is available. Here's what's new:" +msgid "Close the current document" msgstr "" -msgid "All Documents" +msgid "Close &All\tCtrl+Shift+W" msgstr "" -msgid "&search" +msgid "Close &All\tCtrl+Shift+F4" msgstr "" -msgid "File Name" +msgid "Close all documents" msgstr "" -msgid "Status" +msgid "Reopen &Last Closed\tCtrl+Shift+T" msgstr "" -msgid "Path" +msgid "Reopen the last closed document" msgstr "" -msgid "&Open" +msgid "&Recent Documents" msgstr "" -msgid "&Remove" +msgid "Open a recent document" msgstr "" -msgid "&Clear All" +msgid "E&xit\tCtrl+Q" msgstr "" -msgid "" -"Are you sure you want to remove this document from the list? This will also " -"remove its reading position." +msgid "Exit the application" msgstr "" -msgid "" -"Are you sure you want to remove these {} documents from the list? This will " -"also remove their reading positions." +msgid "&Find...\tCtrl+F" msgstr "" -msgid "Confirm" +msgid "Find text in the document" msgstr "" -msgid "" -"Are you sure you want to remove all documents from the list? This will also " -"remove all reading positions and bookmarks." +msgid "Find &Next\tF3" msgstr "" -msgid "Open As" +msgid "Find next occurrence" msgstr "" -msgid "" -"No suitable parser was found for {}.\n" -"How would you like to open this file?" +msgid "Find &Previous\tShift+F3" msgstr "" -msgid "Open &as:" +msgid "Find previous occurrence" msgstr "" -msgid "Plain Text" +msgid "Go to &line...\tCtrl+G" msgstr "" -msgid "HTML" +msgid "Go to a specific line" msgstr "" -msgid "Markdown" +msgid "Go to &percent...\tCtrl+Shift+G" msgstr "" -msgid "Open" +msgid "Go to a percentage of the document" msgstr "" -msgid "Closed" +msgid "Go &Back\tAlt+Left" msgstr "" -msgid "Missing" +msgid "Go back in history" msgstr "" -msgid "Sleep Timer" +msgid "Go &Forward\tAlt+Right" msgstr "" -msgid "&Minutes:" +msgid "Go forward in history" msgstr "" -msgid "Elements" +msgid "&Sections" msgstr "" -msgid "&View:" +msgid "Navigate by sections" msgstr "" -msgid "Headings" +msgid "&Headings" msgstr "" -msgid "Links" +msgid "Navigate by headings" msgstr "" -msgid "An accessible, lightweight, fast ebook and document reader" +msgid "&Pages" msgstr "" -msgid "File not found: {}" +msgid "Navigate by pages" msgstr "" -msgid "Password is required." +msgid "&Bookmarks" msgstr "" -msgid "Navigated to internal link." +msgid "Navigate by bookmarks" msgstr "" -msgid "Table View" +msgid "&Links" msgstr "" -msgid "Ready" +msgid "Navigate by links" msgstr "" -msgid "&Password:" +msgid "&Images" msgstr "" -msgid "Document Password" +msgid "Navigate by images" msgstr "" -msgid "Failed to load document." +msgid "&Figures" msgstr "" -msgid "Create &bookmark" +msgid "Navigate by figures" msgstr "" -msgid "Create bookmark" +msgid "&Tables" msgstr "" -msgid "Bookmark with ¬e" +msgid "Navigate by tables" msgstr "" -msgid "Create bookmark with note" +msgid "&Separators" msgstr "" -msgid "&Find" +msgid "Navigate by separators" msgstr "" -msgid "Find text" +msgid "&Lists" msgstr "" -msgid "Find &next" +msgid "Navigate by lists" msgstr "" -msgid "Find next match" +msgid "&Import Document Data...\tCtrl+Shift+I" msgstr "" -msgid "Find &previous" +msgid "Import bookmarks and position" msgstr "" -msgid "Find previous match" +msgid "&Export Document Data...\tCtrl+Shift+E" msgstr "" -msgid "Go to &page" +msgid "Export bookmarks and position" msgstr "" -msgid "Go to &line" +msgid "Export to &Plain Text...\tCtrl+E" msgstr "" -msgid "Go to line" +msgid "Export document as plain text" msgstr "" -msgid "Go to &percent" +msgid "&Word Count\tRawCtrl+W" msgstr "" -msgid "Go to percent" +msgid "&Word Count\tCtrl+W" msgstr "" -msgid "Find" +msgid "Show word count" msgstr "" -msgid "Find &what:" +msgid "Document &Info\tCtrl+I" msgstr "" -msgid "&Match case" +msgid "Show document information" msgstr "" -msgid "Match &whole word" +msgid "&Table of Contents\tCtrl+T" msgstr "" -msgid "Use ®ular expressions" +msgid "Show table of contents" msgstr "" -msgid "Find &Previous" +msgid "&Elements List...\tF7" msgstr "" -msgid "Find &Next" +msgid "Show elements list" msgstr "" -msgid "Not found." +msgid "Open &Containing Folder\tCtrl+Shift+C" msgstr "" -msgid "No more results. Wrapping search." +msgid "Open folder containing the document" msgstr "" -msgid "Failed to open containing folder." +msgid "Open in &Web View\tCtrl+Shift+V" msgstr "" -msgid "" -"readme.html not found. Please ensure the application was built properly." +msgid "Open document in web view" msgstr "" -msgid "Failed to launch default browser." +msgid "I&mport/Export" msgstr "" -msgid "Failed to open donation page in browser." +msgid "Import and export options" msgstr "" -msgid "No release notes were provided." +msgid "Toggle &Bookmark\tCtrl+Shift+B" msgstr "" -msgid "Paperback Update" +msgid "Bookmark with &Note\tCtrl+Shift+N" msgstr "" -msgid "Downloading update..." +msgid "&Options\tCtrl+," msgstr "" -msgid "No updates available." +msgid "&Sleep Timer...\tCtrl+Shift+S" msgstr "" -msgid "No updates available. Latest version: {}" +msgid "&About Paperback\tCtrl+F1" msgstr "" -msgid "Info" +msgid "About this application" msgstr "" -#, c-format -msgid "Failed to check for updates. HTTP status: %d" +msgid "View Help in &Browser\tF1" msgstr "" -msgid "Error checking for updates." +msgid "View help in default browser" msgstr "" -msgid "Failed to get current exe path" +msgid "View Help in &Paperback\tShift+F1" msgstr "" -msgid "Failed to launch installer script" +msgid "View help in Paperback" msgstr "" -msgid "Failed to launch update script" +msgid "Check for &Updates\tCtrl+Shift+U" msgstr "" -msgid "Unknown update file format." +msgid "Check for updates" msgstr "" -msgid "Update failed" +msgid "&Donate\tCtrl+D" msgstr "" -msgid "Unsupported format selected." +msgid "Support Paperback development" msgstr "" -msgid "Paperback" +msgid "(No recent documents)" msgstr "" -msgid "Paperback - {}" +msgid "Show All...\tCtrl+R" msgstr "" -msgid "{} chars" +msgid "&Restore" msgstr "" -msgid "Open Document" +msgid "Restore Paperback" msgstr "" -msgid "No pages." +msgid "E&xit" msgstr "" -msgid "document" +msgid "Exit Paperback" msgstr "" -msgid "Plain text files (*.txt)|*.txt|All files (*.*)|*.*" +msgid "Failed to create IPC server" msgstr "" -msgid "Export document to plain text" +msgid "Warning" msgstr "" -msgid "Failed to export document." +msgid "Line" msgstr "" -msgid "Paperback files (*.paperback)|*.paperback" +msgid "Character" msgstr "" -msgid "Export notes and bookmarks" +msgid "Reading" msgstr "" -msgid "Notes and bookmarks exported successfully." +msgid "Sleep timer" msgstr "" -msgid "Export Successful" +msgid "Ready" msgstr "" -msgid "Import notes and bookmarks" +msgid "Failed to open containing folder." msgstr "" -msgid "Notes and bookmarks imported successfully." +msgid "Error" msgstr "" -msgid "Import Successful" +msgid "" +"readme.html not found. Please ensure the application was built properly." msgstr "" -msgid "The document contains {} words." +msgid "Failed to launch default browser." msgstr "" -msgid "Word count" +msgid "Failed to open donation page in browser." msgstr "" -msgid "No table of contents." +msgid "No release notes were provided." msgstr "" -msgid "Web View" +msgid "Paperback Update" msgstr "" -msgid "Could not determine content to display in Web View." +msgid "Downloading update..." msgstr "" -msgid "Sleep timer cancelled." +msgid "No updates available." msgstr "" -msgid "Sleep timer set for 1 minute." +msgid "No updates available. Latest version: {}" +msgstr "" + +msgid "Info" msgstr "" #, c-format -msgid "Sleep timer set for %d minutes." +msgid "Failed to check for updates. HTTP status: %d" msgstr "" -msgid "No recent documents." +msgid "Error checking for updates." msgstr "" -msgid "Previous Section\t[" +msgid "Failed to get current exe path" msgstr "" -msgid "Go to previous section" +msgid "Failed to launch installer script" msgstr "" -msgid "Next Section\t]" +msgid "Failed to launch update script" msgstr "" -msgid "Go to next section" +msgid "Unknown update file format." msgstr "" -msgid "Go to &Page\tCtrl+P" +msgid "Update failed" msgstr "" -msgid "Previous Pa&ge\tShift+P" +msgid "Unsupported format selected." msgstr "" -msgid "Next Pag&e\tP" +msgid "Options" msgstr "" -msgid "Previous Lin&k\tShift+K" +msgid "&Restore previously opened documents on startup" msgstr "" -msgid "Next Lin&k\tK" +msgid "&Word wrap" msgstr "" -msgid "Previous Ima&ge\tShift+G" +msgid "&Minimize to system tray" msgstr "" -msgid "Next Ima&ge\tG" +msgid "&Start maximized" msgstr "" -msgid "Previous Figu&re\tShift+F" +msgid "Show compact &go menu" msgstr "" -msgid "Next Figu&re\tF" +msgid "&Wrap navigation" msgstr "" -msgid "Previous &Table\tShift+T" +msgid "Play &sounds on bookmarks and notes" msgstr "" -msgid "Next &Table\tT" +msgid "Check for &updates on startup" msgstr "" -msgid "Previous Se¶tor\tShift+S" +msgid "Number of &recent documents to show:" msgstr "" -msgid "Next Se¶tor\tS" +msgid "&Language:" msgstr "" -msgid "Previous L&ist\tShift+L" +msgid "Update Channel:" msgstr "" -msgid "Next L&ist\tL" +msgid "Stable" msgstr "" -msgid "Previous List &Item\tShift+I" +msgid "Dev" msgstr "" -msgid "Next List I&tem\tI" +msgid "General" msgstr "" -msgid "Next Heading Level 1\t1" +msgid "OK" msgstr "" -msgid "Previous Heading Level &2\tShift+2" +msgid "Cancel" msgstr "" -msgid "Next Heading Level 2\t2" +msgid "Jump to Bookmark" msgstr "" -msgid "Previous Heading Level &3\tShift+3" +msgid "&Filter:" msgstr "" -msgid "Next Heading Level 3\t3" +msgid "All" msgstr "" -msgid "Previous Heading Level &4\tShift+4" +msgid "Bookmarks" msgstr "" -msgid "Next Heading Level 4\t4" +msgid "Notes" msgstr "" -msgid "Previous Heading Level &5\tShift+5" +msgid "&Edit Note" msgstr "" -msgid "Next Heading Level 5\t5" +msgid "&Delete" +msgstr "" + +msgid "&Jump" +msgstr "" + +msgid "&Cancel" +msgstr "" + +msgid "blank" +msgstr "" + +msgid "Please select a bookmark to jump to." +msgstr "" + +msgid "Bookmark Note" msgstr "" -msgid "Previous Heading Level &6\tShift+6" +msgid "Edit bookmark note:" msgstr "" -msgid "Next Heading Level 6\t6" +msgid "View Note" msgstr "" -msgid "&Previous Heading\tShift+H" +msgid "Close" msgstr "" -msgid "Go to previous heading" +msgid "Table of Contents" msgstr "" -msgid "&Next Heading\tH" +msgid "Root" msgstr "" -msgid "Go to next heading" +msgid "Please select a section from the table of contents." msgstr "" -msgid "Previous Heading Level &1\tShift+1" +msgid "No Selection" msgstr "" -msgid "&Previous Bookmark\tShift+B" +msgid "Untitled" msgstr "" -msgid "Go to previous bookmark" +msgid "Document Info" msgstr "" -msgid "&Next Bookmark\tB" +msgid "Path:" msgstr "" -msgid "Go to next bookmark" +msgid "Title:" msgstr "" -msgid "Previous &Note\tShift+N" +msgid "Author:" msgstr "" -msgid "Go to previous note" +msgid "Words:" msgstr "" -msgid "Next N&ote\tN" +msgid "Lines:" msgstr "" -msgid "Go to next note" +msgid "Characters:" msgstr "" -msgid "Jump to &All...\tCtrl+B" +msgid "Characters (excluding spaces):" msgstr "" -msgid "Show all bookmarks and notes" +msgid "Go to Line" msgstr "" -msgid "Jump to &Bookmarks Only...\tCtrl+Alt+B" +msgid "&Line number:" msgstr "" -msgid "Show bookmarks only" +msgid "Go to page" msgstr "" -msgid "Jump to Notes &Only...\tCtrl+Alt+M" +#, c-format +msgid "Go to page (%d/%d):" msgstr "" -msgid "Show notes only" +msgid "Go to Percent" msgstr "" -msgid "&View Note Text\tRawCtrl+Shift+W" +msgid "&Percent" msgstr "" -msgid "&View Note Text\tCtrl+Shift+W" +msgid "P&ercent:" msgstr "" -msgid "View the note at current position" +msgid "&Yes" msgstr "" -msgid "&File" +msgid "&No" msgstr "" -msgid "&Go" +#, c-format +msgid "Update to %s" msgstr "" -msgid "&Tools" +msgid "A new version of Paperback is available. Here's what's new:" msgstr "" -msgid "&Help" +msgid "All Documents" msgstr "" -msgid "&Open...\tCtrl+O" +msgid "&search" msgstr "" -msgid "Open a document" +msgid "File Name" msgstr "" -msgid "&Close\tCtrl+W" +msgid "Status" msgstr "" -msgid "&Close\tCtrl+F4" +msgid "Path" msgstr "" -msgid "Close the current document" +msgid "&Open" msgstr "" -msgid "Close &All\tCtrl+Shift+W" +msgid "&Remove" msgstr "" -msgid "Close &All\tCtrl+Shift+F4" +msgid "&Clear All" msgstr "" -msgid "Close all documents" +msgid "" +"Are you sure you want to remove this document from the list? This will also " +"remove its reading position." msgstr "" -msgid "Reopen &Last Closed\tCtrl+Shift+T" +msgid "" +"Are you sure you want to remove these {} documents from the list? This will " +"also remove their reading positions." msgstr "" -msgid "Reopen the last closed document" +msgid "Confirm" msgstr "" -msgid "&Recent Documents" +msgid "" +"Are you sure you want to remove all documents from the list? This will also " +"remove all reading positions and bookmarks." msgstr "" -msgid "Open a recent document" +msgid "Open As" msgstr "" -msgid "E&xit\tCtrl+Q" +msgid "" +"No suitable parser was found for {}.\n" +"How would you like to open this file?" msgstr "" -msgid "Exit the application" +msgid "Open &as:" msgstr "" -msgid "&Find...\tCtrl+F" +msgid "Plain Text" msgstr "" -msgid "Find text in the document" +msgid "HTML" msgstr "" -msgid "Find &Next\tF3" +msgid "Markdown" msgstr "" -msgid "Find next occurrence" +msgid "Open" msgstr "" -msgid "Find &Previous\tShift+F3" +msgid "Closed" msgstr "" -msgid "Find previous occurrence" +msgid "Missing" msgstr "" -msgid "Go to &line...\tCtrl+G" +msgid "Sleep Timer" msgstr "" -msgid "Go to a specific line" +msgid "&Minutes:" msgstr "" -msgid "Go to &percent...\tCtrl+Shift+G" +msgid "Elements" msgstr "" -msgid "Go to a percentage of the document" +msgid "&View:" msgstr "" -msgid "Go &Back\tAlt+Left" +msgid "Headings" msgstr "" -msgid "Go back in history" +msgid "Links" msgstr "" -msgid "Go &Forward\tAlt+Right" +msgid "An accessible, lightweight, fast ebook and document reader" msgstr "" -msgid "Go forward in history" +msgid "expanded" msgstr "" -msgid "&Sections" +msgid "collapsed" msgstr "" -msgid "Navigate by sections" +msgid "Find" msgstr "" -msgid "&Headings" +msgid "Find &what:" msgstr "" -msgid "Navigate by headings" +msgid "&Match case" msgstr "" -msgid "&Pages" +msgid "Match &whole word" msgstr "" -msgid "Navigate by pages" +msgid "Use ®ular expressions" msgstr "" -msgid "&Bookmarks" +msgid "Find &Previous" msgstr "" -msgid "Navigate by bookmarks" +msgid "Find &Next" msgstr "" -msgid "&Links" +msgid "Not found." msgstr "" -msgid "Navigate by links" +msgid "No more results. Wrapping search." msgstr "" -msgid "&Images" +msgid "Paperback" msgstr "" -msgid "Navigate by images" +msgid "Paperback - {}" msgstr "" -msgid "&Figures" +msgid "{} chars" msgstr "" -msgid "Navigate by figures" +msgid "Open Document" msgstr "" -msgid "&Tables" +msgid "No pages." msgstr "" -msgid "Navigate by tables" +msgid "document" msgstr "" -msgid "&Separators" +msgid "Plain text files (*.txt)|*.txt|All files (*.*)|*.*" msgstr "" -msgid "Navigate by separators" +msgid "Export document to plain text" msgstr "" -msgid "&Lists" +msgid "Failed to export document." msgstr "" -msgid "Navigate by lists" +msgid "Paperback files (*.paperback)|*.paperback" msgstr "" -msgid "&Import Document Data...\tCtrl+Shift+I" +msgid "Export notes and bookmarks" msgstr "" -msgid "Import bookmarks and position" +msgid "Notes and bookmarks exported successfully." msgstr "" -msgid "&Export Document Data...\tCtrl+Shift+E" +msgid "Export Successful" msgstr "" -msgid "Export bookmarks and position" +msgid "Import notes and bookmarks" msgstr "" -msgid "Export to &Plain Text...\tCtrl+E" +msgid "Notes and bookmarks imported successfully." msgstr "" -msgid "Export document as plain text" +msgid "Import Successful" msgstr "" -msgid "&Word Count\tRawCtrl+W" +msgid "The document contains {} words." msgstr "" -msgid "&Word Count\tCtrl+W" +msgid "Word count" msgstr "" -msgid "Show word count" +msgid "No table of contents." msgstr "" -msgid "Document &Info\tCtrl+I" +msgid "Web View" msgstr "" -msgid "Show document information" +msgid "Could not determine content to display in Web View." msgstr "" -msgid "&Table of Contents\tCtrl+T" +msgid "Sleep timer cancelled." msgstr "" -msgid "Show table of contents" +msgid "Sleep timer set for 1 minute." msgstr "" -msgid "&Elements List...\tF7" +#, c-format +msgid "Sleep timer set for %d minutes." msgstr "" -msgid "Show elements list" +msgid "No recent documents." msgstr "" -msgid "Open &Containing Folder\tCtrl+Shift+C" +msgid "File not found: {}" msgstr "" -msgid "Open folder containing the document" +msgid "Password is required." msgstr "" -msgid "Open in &Web View\tCtrl+Shift+V" +msgid "Navigated to internal link." msgstr "" -msgid "Open document in web view" +msgid "Table View" msgstr "" -msgid "I&mport/Export" +msgid "&Password:" msgstr "" -msgid "Import and export options" +msgid "Document Password" msgstr "" -msgid "Toggle &Bookmark\tCtrl+Shift+B" +msgid "Failed to load document." msgstr "" -msgid "Bookmark with &Note\tCtrl+Shift+N" +msgid "Create &bookmark" msgstr "" -msgid "&Options\tCtrl+," +msgid "Create bookmark" msgstr "" -msgid "&Sleep Timer...\tCtrl+Shift+S" +msgid "Bookmark with ¬e" msgstr "" -msgid "&About Paperback\tCtrl+F1" +msgid "Create bookmark with note" msgstr "" -msgid "About this application" +msgid "&Find" msgstr "" -msgid "View Help in &Browser\tF1" +msgid "Find text" msgstr "" -msgid "View help in default browser" +msgid "Find &next" msgstr "" -msgid "View Help in &Paperback\tShift+F1" +msgid "Find next match" msgstr "" -msgid "View help in Paperback" +msgid "Find &previous" msgstr "" -msgid "Check for &Updates\tCtrl+Shift+U" +msgid "Find previous match" msgstr "" -msgid "Check for updates" +msgid "Go to &page" msgstr "" -msgid "&Donate\tCtrl+D" +msgid "Go to &line" msgstr "" -msgid "Support Paperback development" +msgid "Go to line" msgstr "" -msgid "(No recent documents)" +msgid "Go to &percent" msgstr "" -msgid "Show All...\tCtrl+R" +msgid "Go to percent" msgstr "" msgid "No sections." @@ -1127,23 +1143,13 @@ msgstr "" msgid "No note at the current position." msgstr "" -msgid "Line" -msgstr "" - -msgid "Character" -msgstr "" - -msgid "Sleep timer" -msgstr "" - -msgid "&Restore" -msgstr "" - -msgid "Restore Paperback" +msgid "Figure" msgstr "" -msgid "E&xit" +msgid "Image" msgstr "" -msgid "Exit Paperback" +msgid "" +"This PDF contains images only, with no extractable text. You may need to run " +"it through OCR software to read its contents." msgstr "" diff --git a/src/ui.rs b/src/ui.rs index 65dd383b..81e2ef81 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -9,6 +9,7 @@ mod menu_ids; mod navigation; mod sounds; mod status; +#[cfg(not(target_os = "linux"))] mod tray; pub use app::PaperbackApp; diff --git a/src/ui/dialogs.rs b/src/ui/dialogs.rs index 03f91c74..82ac7c07 100644 --- a/src/ui/dialogs.rs +++ b/src/ui/dialogs.rs @@ -10,6 +10,13 @@ use std::{ use bitflags::bitflags; use wxdragon::{event::WebViewEvents, ffi, prelude::*, translations::translate as t, widgets::WebView}; +#[cfg(target_os = "linux")] +mod accessible_tree; +#[cfg(target_os = "linux")] +mod elements_gtk; +#[cfg(target_os = "linux")] +mod toc_gtk; + use crate::{ config::ConfigManager, document::{DocumentStats, TocItem}, @@ -29,6 +36,7 @@ const DOC_INFO_WIDTH: i32 = 600; const DOC_INFO_HEIGHT: i32 = 400; const KEY_DELETE: i32 = 127; const KEY_NUMPAD_DELETE: i32 = 330; +#[cfg(not(target_os = "linux"))] const KEY_SPACE: i32 = 32; const KEY_ESCAPE: i32 = 27; const KEY_RETURN: i32 = 13; @@ -844,6 +852,14 @@ pub fn show_view_note_dialog(parent: &dyn WxWidget, note_text: &str) { } pub fn show_toc_dialog(parent: &Frame, toc_items: &[TocItem], current_offset: i32) -> Option { + #[cfg(target_os = "linux")] + return toc_gtk::show_toc_dialog(parent, toc_items, current_offset); + #[cfg(not(target_os = "linux"))] + return show_toc_dialog_wx(parent, toc_items, current_offset); +} + +#[cfg(not(target_os = "linux"))] +fn show_toc_dialog_wx(parent: &Frame, toc_items: &[TocItem], current_offset: i32) -> Option { let dialog_title = t("Table of Contents"); let dialog = Dialog::builder(parent, &dialog_title).build(); let selected_offset = Rc::new(Cell::new(-1)); @@ -863,6 +879,7 @@ pub fn show_toc_dialog(parent: &Frame, toc_items: &[TocItem], current_offset: i3 } } +#[cfg(not(target_os = "linux"))] fn build_toc_tree(dialog: Dialog, toc_items: &[TocItem], current_offset: i32) -> (TreeCtrl, TreeItemId) { let tree = TreeCtrl::builder(&dialog) .with_style(TreeCtrlStyle::Default | TreeCtrlStyle::HideRoot) @@ -876,6 +893,7 @@ fn build_toc_tree(dialog: Dialog, toc_items: &[TocItem], current_offset: i32) -> (tree, root) } +#[cfg(not(target_os = "linux"))] fn bind_toc_selection(tree: TreeCtrl, selected_offset: Rc>) { let tree_for_sel = tree; tree.on_selection_changed(move |event| { @@ -889,6 +907,7 @@ fn bind_toc_selection(tree: TreeCtrl, selected_offset: Rc>) { }); } +#[cfg(not(target_os = "linux"))] fn bind_toc_activation(dialog: Dialog, tree: TreeCtrl, selected_offset: Rc>) { let dialog_for_activate = dialog; let tree_for_activate = tree; @@ -904,6 +923,7 @@ fn bind_toc_activation(dialog: Dialog, tree: TreeCtrl, selected_offset: Rc (Button, Button) { let ok_button = Button::builder(&dialog).with_label(&t("OK")).build(); let cancel_button = Button::builder(&dialog).with_id(wxdragon::id::ID_CANCEL).with_label(&t("Cancel")).build(); (ok_button, cancel_button) } +#[cfg(not(target_os = "linux"))] fn bind_toc_ok(dialog: Dialog, ok_button: Button, selected_offset: Rc>) { dialog.set_escape_id(wxdragon::id::ID_CANCEL); let dialog_for_ok = dialog; @@ -944,6 +966,7 @@ fn bind_toc_ok(dialog: Dialog, ok_button: Button, selected_offset: Rc> }); } +#[cfg(not(target_os = "linux"))] fn bind_toc_layout(dialog: Dialog, tree: TreeCtrl, ok_button: Button, cancel_button: Button) { let content_sizer = BoxSizer::builder(Orientation::Vertical).build(); content_sizer.add(&tree, 1, SizerFlag::Expand | SizerFlag::All, DIALOG_PADDING); @@ -956,6 +979,7 @@ fn bind_toc_layout(dialog: Dialog, tree: TreeCtrl, ok_button: Button, cancel_but dialog.centre(); } +#[cfg(not(target_os = "linux"))] fn populate_toc_tree(tree: TreeCtrl, parent: &TreeItemId, items: &[TocItem]) { for item in items { let display_text = if item.name.is_empty() { t("Untitled") } else { item.name.clone() }; @@ -968,6 +992,7 @@ fn populate_toc_tree(tree: TreeCtrl, parent: &TreeItemId, items: &[TocItem]) { } } +#[cfg(not(target_os = "linux"))] fn find_and_select_item(tree: TreeCtrl, parent: &TreeItemId, offset: i32) -> bool { if let Some((child, mut cookie)) = tree.get_first_child(parent) { let mut current_child = Some(child); @@ -1912,6 +1937,14 @@ pub fn show_web_view_dialog( } pub fn show_elements_dialog(parent: &Frame, session: &DocumentSession, current_pos: i64) -> Option { + #[cfg(target_os = "linux")] + return elements_gtk::show_elements_dialog(parent, session, current_pos); + #[cfg(not(target_os = "linux"))] + return show_elements_dialog_wx(parent, session, current_pos); +} + +#[cfg(not(target_os = "linux"))] +fn show_elements_dialog_wx(parent: &Frame, session: &DocumentSession, current_pos: i64) -> Option { let dialog = Dialog::builder(parent, &t("Elements")).build(); let ElementsDialogUi { content_sizer, view_choice, headings_tree, links_list } = build_elements_dialog_ui(dialog); let (selected_offset, link_offsets) = populate_elements_dialog(session, current_pos, headings_tree, links_list); @@ -1933,6 +1966,7 @@ pub fn show_elements_dialog(parent: &Frame, session: &DocumentSession, current_p } } +#[cfg(not(target_os = "linux"))] struct ElementsDialogUi { content_sizer: BoxSizer, view_choice: ComboBox, @@ -1940,6 +1974,7 @@ struct ElementsDialogUi { links_list: ListBox, } +#[cfg(not(target_os = "linux"))] fn build_elements_dialog_ui(dialog: Dialog) -> ElementsDialogUi { let content_sizer = BoxSizer::builder(Orientation::Vertical).build(); let choice_sizer = BoxSizer::builder(Orientation::Horizontal).build(); @@ -1976,6 +2011,7 @@ fn build_elements_dialog_ui(dialog: Dialog) -> ElementsDialogUi { ElementsDialogUi { content_sizer, view_choice, headings_tree, links_list } } +#[cfg(not(target_os = "linux"))] fn populate_elements_dialog( session: &DocumentSession, current_pos: i64, @@ -2033,6 +2069,7 @@ fn populate_elements_dialog( (selected_offset, Rc::new(link_offsets)) } +#[cfg(not(target_os = "linux"))] fn bind_elements_view_toggle(view_choice: ComboBox, headings_tree: TreeCtrl, links_list: ListBox, dialog: Dialog) { let headings_tree_for_choice = headings_tree; let links_list_for_choice = links_list; @@ -2052,6 +2089,7 @@ fn bind_elements_view_toggle(view_choice: ComboBox, headings_tree: TreeCtrl, lin }); } +#[cfg(not(target_os = "linux"))] fn bind_elements_activation( dialog: Dialog, headings_tree: TreeCtrl, @@ -2088,6 +2126,7 @@ fn bind_elements_activation( }); } +#[cfg(not(target_os = "linux"))] fn build_elements_buttons(dialog: Dialog) -> (Button, Button) { let ok_button = Button::builder(&dialog).with_id(wxdragon::id::ID_OK).with_label(&t("OK")).build(); let cancel_button = Button::builder(&dialog).with_id(wxdragon::id::ID_CANCEL).with_label(&t("Cancel")).build(); @@ -2096,6 +2135,7 @@ fn build_elements_buttons(dialog: Dialog) -> (Button, Button) { (ok_button, cancel_button) } +#[cfg(not(target_os = "linux"))] fn bind_elements_ok_action( dialog: Dialog, view_choice: ComboBox, @@ -2130,6 +2170,7 @@ fn bind_elements_ok_action( }); } +#[cfg(not(target_os = "linux"))] fn finalize_elements_layout(dialog: Dialog, content_sizer: BoxSizer, ok_button: Button, cancel_button: Button) { let button_sizer = BoxSizer::builder(Orientation::Horizontal).build(); button_sizer.add_stretch_spacer(1); diff --git a/src/ui/dialogs/accessible_tree.rs b/src/ui/dialogs/accessible_tree.rs new file mode 100644 index 00000000..3366a733 --- /dev/null +++ b/src/ui/dialogs/accessible_tree.rs @@ -0,0 +1,262 @@ +use std::{cell::RefCell, ffi::c_void, rc::Rc}; + +use gtk::{Dialog, Label, ListBox, ListBoxRow, ResponseType, Widget, Window, gdk::EventKey, glib::{Propagation, translate}, prelude::*}; +use wxdragon::{prelude::{Frame, WxWidget}, translations::translate as t}; + +const GDK_KEY_LEFT: u32 = 0xff51; +const GDK_KEY_RIGHT: u32 = 0xff53; +const GDK_KEY_TAB: u32 = 0xff09; +const GDK_KEY_ISO_LEFT_TAB: u32 = 0xfe20; +const ATK_POLITENESS_POLITE: i32 = 1; + +unsafe extern "C" { + safe fn gtk_widget_get_accessible(widget: *mut c_void) -> *mut c_void; + unsafe fn atk_object_set_name(obj: *mut c_void, name: *const std::ffi::c_char); + unsafe fn g_signal_emit_by_name(instance: *mut c_void, signal_name: *const std::ffi::c_char, ...); +} + +struct RowInfo { + depth: i32, + has_children: bool, + expanded: bool, +} + +/// A GtkListBox that simulates accessible tree expand/collapse behavior. +pub struct AccessibleTree { + pub list_box: ListBox, + rows: Rc>>, + offsets: Vec, +} + +impl AccessibleTree { + pub fn new() -> Self { + let list_box = ListBox::new(); + list_box.set_selection_mode(gtk::SelectionMode::Browse); + list_box.set_activate_on_single_click(false); + Self { + list_box, + rows: Rc::new(RefCell::new(Vec::new())), + offsets: Vec::new(), + } + } + + /// Add an item to the tree. Items must be added in depth-first order. + pub fn add_item(&mut self, name: &str, offset: i64, depth: i32, has_children: bool) { + let indent = " ".repeat(depth as usize); + let display_text = format!("{indent}{name}"); + + let label = Label::new(Some(&display_text)); + label.set_xalign(0.0); + let row = ListBoxRow::new(); + row.add(&label); + self.list_box.add(&row); + + self.rows.borrow_mut().push(RowInfo { depth, has_children, expanded: false }); + self.offsets.push(offset); + + let idx = self.offsets.len() - 1; + set_row_accessible_name(&self.list_box, &self.rows.borrow(), idx); + } + + /// Expand ancestors of the item at the given offset and return its row index. + pub fn expand_to_offset(&self, offset: i64) -> Option { + let target_idx = self.offsets.iter().position(|&o| o == offset)?; + let mut info = self.rows.borrow_mut(); + let target_depth = info[target_idx].depth; + let mut required_depth = target_depth - 1; + if required_depth >= 0 { + for j in (0..target_idx).rev() { + if info[j].depth == required_depth { + info[j].expanded = true; + set_row_accessible_name(&self.list_box, &info, j); + if required_depth == 0 { + break; + } + required_depth -= 1; + } + } + } + update_visibility(&self.list_box, &info); + Some(target_idx as i32) + } + + /// Show the tree with initial visibility applied, and focus the given row index. + pub fn show_and_focus(&self, focus_idx: i32) { + update_visibility(&self.list_box, &self.rows.borrow()); + if let Some(row) = self.list_box.row_at_index(focus_idx) { + self.list_box.select_row(Some(&row)); + row.grab_focus(); + } + } + + /// Connect selection tracking, activation, and key handling. + /// `shift_tab_target`: widget to focus on Shift-Tab. + pub fn connect_events(&self, dialog: &Dialog, on_select: Rc, shift_tab_target: impl IsA + 'static) { + let offsets_for_sel = self.offsets.clone(); + self.list_box.connect_row_selected(move |_, row| { + if let Some(row) = row { + let idx = row.index() as usize; + if let Some(&offset) = offsets_for_sel.get(idx) { + on_select(offset); + } + } + }); + + let dialog_clone = dialog.clone(); + self.list_box.connect_row_activated(move |_, _| { + dialog_clone.response(ResponseType::Ok); + }); + + let rows_for_key = Rc::clone(&self.rows); + let dialog_for_key = dialog.clone(); + let shift_target: Widget = shift_tab_target.upcast(); + self.list_box.connect_key_press_event(move |lb, event| { + handle_key(lb, event, &rows_for_key, &dialog_for_key, &shift_target) + }); + } +} + +fn handle_key(lb: &ListBox, event: &EventKey, rows: &Rc>>, dialog: &Dialog, shift_tab_target: &Widget) -> Propagation { + let keyval = *event.keyval(); + + if keyval == GDK_KEY_TAB || keyval == GDK_KEY_ISO_LEFT_TAB { + if keyval == GDK_KEY_TAB { + if let Some(button) = dialog.widget_for_response(ResponseType::Ok) { + button.grab_focus(); + } + } else { + shift_tab_target.grab_focus(); + } + return Propagation::Stop; + } + + if keyval != GDK_KEY_LEFT && keyval != GDK_KEY_RIGHT { + return Propagation::Proceed; + } + + let Some(selected) = lb.selected_row() else { return Propagation::Proceed }; + let idx = selected.index() as usize; + let mut info = rows.borrow_mut(); + if idx >= info.len() { + return Propagation::Proceed; + } + + if keyval == GDK_KEY_RIGHT { + if info[idx].has_children && !info[idx].expanded { + info[idx].expanded = true; + update_visibility(lb, &info); + notify_expand_change(lb, &info, idx); + return Propagation::Stop; + } + } else if info[idx].has_children && info[idx].expanded { + collapse_recursive(&mut info, idx); + update_visibility(lb, &info); + notify_expand_change(lb, &info, idx); + return Propagation::Stop; + } else if info[idx].depth > 0 { + let parent_depth = info[idx].depth - 1; + for j in (0..idx).rev() { + if info[j].depth == parent_depth { + drop(info); + if let Some(row) = lb.row_at_index(j as i32) { + lb.select_row(Some(&row)); + row.grab_focus(); + } + return Propagation::Stop; + } + } + } + Propagation::Proceed +} + +fn collapse_recursive(info: &mut [RowInfo], idx: usize) { + info[idx].expanded = false; + let depth = info[idx].depth; + for j in (idx + 1)..info.len() { + if info[j].depth <= depth { + break; + } + if info[j].has_children { + info[j].expanded = false; + } + } +} + +fn update_visibility(list_box: &ListBox, info: &[RowInfo]) { + for i in 0..info.len() { + if let Some(row) = list_box.row_at_index(i as i32) { + row.set_visible(is_visible(info, i)); + } + } +} + +fn is_visible(info: &[RowInfo], idx: usize) -> bool { + if info[idx].depth == 0 { + return true; + } + let mut required_depth = info[idx].depth - 1; + for j in (0..idx).rev() { + if info[j].depth == required_depth { + if !info[j].expanded { + return false; + } + if required_depth == 0 { + return true; + } + required_depth -= 1; + } + } + false +} + +fn set_row_accessible_name(list_box: &ListBox, info: &[RowInfo], idx: usize) { + let Some(row) = list_box.row_at_index(idx as i32) else { return }; + let label_text = row.child() + .and_then(|w| w.downcast::