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 0599cfa3..40c51469 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 @@ -62,13 +62,53 @@ jobs: - name: Upload artifacts uses: actions/upload-artifact@v4 with: - name: paperback-build + name: paperback-windows path: | target/release/paperback.zip target/release/paperback.zip.minisig target/release/paperback_setup.exe target/release/paperback_setup.exe.minisig 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 @@ -98,10 +138,11 @@ jobs: ## Commits since last release ${{ steps.release_notes.outputs.commits }} files: | - target/release/paperback.zip - target/release/paperback.zip.minisig - target/release/paperback_setup.exe - target/release/paperback_setup.exe.minisig + windows-build/paperback.zip + windows-build/paperback.zip.minisig + windows-build/paperback_setup.exe + windows-build/paperback_setup.exe.minigig + 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 815b3531..03b3afd4 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" @@ -150,6 +173,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" @@ -189,6 +237,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" @@ -525,6 +583,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" @@ -611,6 +679,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" @@ -662,6 +741,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" @@ -721,6 +858,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" @@ -740,6 +956,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" @@ -774,6 +1053,12 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +[[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" @@ -1132,7 +1417,7 @@ dependencies = [ "bzip2", "cc", "tar", - "thiserror", + "thiserror 2.0.18", "ureq", ] @@ -1244,6 +1529,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" @@ -1368,7 +1662,7 @@ dependencies = [ "rc4", "sha1 0.10.6", "sha2", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -1443,6 +1737,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" @@ -1455,6 +1774,7 @@ dependencies = [ "embed-manifest", "encoding_rs", "flate2", + "gtk", "libchm", "live-region", "minisign-verify", @@ -1649,13 +1969,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.11+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]] @@ -1714,7 +2078,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror", + "thiserror 2.0.18", "tokio", "tracing", "web-time", @@ -1735,7 +2099,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror", + "thiserror 2.0.18", "tinyvec", "tracing", "web-time", @@ -1973,7 +2337,7 @@ checksum = "9c845311f0ff7951c5506121a9ad75aec44d083c31583b2ea5a30bcb0b0abba0" dependencies = [ "cfg-if", "glob", - "proc-macro-crate", + "proc-macro-crate 3.5.0", "proc-macro2", "quote", "regex", @@ -2211,6 +2575,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_spanned" version = "1.1.1" @@ -2413,6 +2786,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" @@ -2430,6 +2816,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" @@ -2453,13 +2845,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]] @@ -2585,6 +2997,18 @@ 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 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + [[package]] name = "toml" version = "1.1.2+spec-1.1.0" @@ -2593,11 +3017,20 @@ checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" dependencies = [ "indexmap", "serde_core", - "serde_spanned", - "toml_datetime", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", "toml_writer", - "winnow", + "winnow 1.0.2", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", ] [[package]] @@ -2609,6 +3042,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 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + [[package]] name = "toml_edit" version = "0.25.11+spec-1.1.0" @@ -2616,9 +3073,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" dependencies = [ "indexmap", - "toml_datetime", + "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", - "winnow", + "winnow 1.0.2", ] [[package]] @@ -2627,7 +3084,7 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow", + "winnow 1.0.2", ] [[package]] @@ -2850,6 +3307,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" @@ -3029,6 +3492,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" @@ -3038,6 +3517,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" @@ -3315,6 +3800,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.2" @@ -3355,7 +3849,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" dependencies = [ "anyhow", - "heck", + "heck 0.5.0", "wit-parser", ] @@ -3366,7 +3860,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 42469c84..aea6f52e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,9 @@ ureq = { version = "3.3.0", default-features = false, features = ["json", "rustl wxdragon = { version = "0.9.15", features = ["webview"] } zip = { version = "8.5.1", 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", "Win32_UI_Controls_RichEdit", "Win32_Graphics_Gdi"] } 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 204ed667..0dec4a0f 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 00000000..0966613a Binary files /dev/null and b/icons/hicolor/128x128/apps/paperback.png differ diff --git a/icons/hicolor/16x16/apps/paperback.png b/icons/hicolor/16x16/apps/paperback.png new file mode 100644 index 00000000..5bdb744f Binary files /dev/null and b/icons/hicolor/16x16/apps/paperback.png differ diff --git a/icons/hicolor/256x256/apps/paperback.png b/icons/hicolor/256x256/apps/paperback.png new file mode 100644 index 00000000..24f5f86a Binary files /dev/null and b/icons/hicolor/256x256/apps/paperback.png differ diff --git a/icons/hicolor/32x32/apps/paperback.png b/icons/hicolor/32x32/apps/paperback.png new file mode 100644 index 00000000..44aa3635 Binary files /dev/null and b/icons/hicolor/32x32/apps/paperback.png differ diff --git a/icons/hicolor/48x48/apps/paperback.png b/icons/hicolor/48x48/apps/paperback.png new file mode 100644 index 00000000..ce887ce5 Binary files /dev/null and b/icons/hicolor/48x48/apps/paperback.png differ diff --git a/icons/hicolor/64x64/apps/paperback.png b/icons/hicolor/64x64/apps/paperback.png new file mode 100644 index 00000000..409ea873 Binary files /dev/null and b/icons/hicolor/64x64/apps/paperback.png differ 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 12bcb917..6d6a7176 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-04-13 15:24-0600\n" +"POT-Creation-Date: 2026-04-27 18:45-0600\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -34,6 +34,33 @@ msgstr "" msgid "Warning" msgstr "" +msgid "expanded" +msgstr "" + +msgid "collapsed" +msgstr "" + +msgid "Elements" +msgstr "" + +msgid "OK" +msgstr "" + +msgid "Cancel" +msgstr "" + +msgid "Headings" +msgstr "" + +msgid "Links" +msgstr "" + +msgid "Untitled" +msgstr "" + +msgid "Table of Contents" +msgstr "" + msgid "Options" msgstr "" @@ -148,12 +175,6 @@ msgstr "" msgid "Readability" msgstr "" -msgid "OK" -msgstr "" - -msgid "Cancel" -msgstr "" - msgid "Background: Default" msgstr "" @@ -223,9 +244,6 @@ msgstr "" msgid "Close" msgstr "" -msgid "Table of Contents" -msgstr "" - msgid "Root" msgstr "" @@ -235,9 +253,6 @@ msgstr "" msgid "No Selection" msgstr "" -msgid "Untitled" -msgstr "" - msgid "1 hour" msgstr "" @@ -401,18 +416,9 @@ msgstr "" msgid "&Minutes:" msgstr "" -msgid "Elements" -msgstr "" - msgid "&View:" msgstr "" -msgid "Headings" -msgstr "" - -msgid "Links" -msgstr "" - msgid "An accessible, lightweight, fast ebook and document reader" 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 fa17ee29..7d15175a 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, ReadabilityFont}, 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; @@ -1144,6 +1152,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)); @@ -1163,6 +1179,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) @@ -1176,6 +1193,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| { @@ -1189,6 +1207,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; @@ -1204,6 +1223,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; @@ -1244,6 +1266,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); @@ -1256,6 +1279,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() }; @@ -1268,6 +1292,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); @@ -2290,6 +2315,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); @@ -2311,6 +2344,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, @@ -2318,6 +2352,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(); @@ -2354,6 +2389,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, @@ -2411,6 +2447,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; @@ -2430,6 +2467,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, @@ -2466,6 +2504,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(); @@ -2474,6 +2513,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, @@ -2508,6 +2548,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::