Compare commits

...

298 Commits

Author SHA1 Message Date
Frans de Jonge f2691b833e MuPDF: add mobi filetype 21 hours ago
Frans de Jonge b46f03f092 [minor] Fix typo in XLSX
Dumb typo in #11955.
21 hours ago
Frans de Jonge 8977e5e12f
[minor] Remove whitespace from last line in zh_stroke_data (#11956)
The busted install failure in #11620 came before those checks.
1 day ago
weijiuqiao 8f1b476444
Chinese keyboard: accept kanji stroke order too (#11620)
This will make the stroke-based Chinese character keyboard compatible with Japanese kanji stroke order, with an increase of the data file by about 80kb.

This could be a temporary solution for Japanese users to type kanji before better methods are implemented.

The stroke order data are extracted from this repo: https://github.com/KanjiVG/kanjivg.
1 day ago
Frans de Jonge 8960f3d478
MuPDF: add txt, cfb, docx, xslx, pptx filetypes (#11955)
New in 1.24.0. See <https://mupdf.com/releases/history>.

Possible due to <https://github.com/koreader/koreader/pull/11940>.
1 day ago
NiLuJe 6a4d433aa9
Kobo: check for RGBA vs BGRA the proper way (#11953)
i.e., poke at the fb vinfo data to check the component offsets.

Fix #11952
1 day ago
SomeGuy c5600ffe19
SortWidget, DictQuickLookup: rename some functions (#11949)
addressing concerns found #11933
2 days ago
Benoit Pierre 99447414dc
mupdf: update to 1.24.2 (#11940)
Depends on https://github.com/koreader/koreader-base/pull/1805.

Closes:
- #5422 
- #9927
- #10879
2 days ago
ziz57 93407c8947
Readerlink: fix scheme detection for external links (#11942)
Following RFC 3986.
2 days ago
Benoit Pierre 4c6919ac2a bump base: update tesseract, leptonica and libk2pdfopt
- update leptonica to 1.84.1
- update tesseract to 5.3.4
- update libk2pdfopt to 2.55
2 days ago
Benoit Pierre 10e6f489d0 kopt: honor `TESSDATA_PREFIX` environment variable
Don't override it by default, but honor it if present.
2 days ago
hius07 14519bc076
Dispatcher: add File browser actions 'Set display mode', 'Sort By' (#11921) 2 days ago
hius07 b06272592d
Annotation list: flexible item heights (#11918) 2 days ago
zwim d2ff789543
[plugin] AutoWarmth: add more gestures for autowarmth activation (#11946)
Fixes #11274.
2 days ago
SomeGuy 1b37aa1bd6
Keyboard menu: remove swipe setting from NT (#11944) 2 days ago
hius07 84d28dc5d9
FontList: skip Kindle blacklisted fonts (#11932)
Closes #11927.
2 days ago
Benoit Pierre ea51435237
bump base (#11941)
Preliminary bump (https://github.com/koreader/koreader-base/pull/1800 will be included in #11930):

- https://github.com/koreader/koreader-base/pull/1790
- https://github.com/koreader/koreader-base/pull/1795
- https://github.com/koreader/koreader-base/pull/1796
- https://github.com/koreader/koreader-base/pull/1797
- https://github.com/koreader/koreader-base/pull/1798
- https://github.com/koreader/koreader-base/pull/1799
- https://github.com/koreader/koreader-base/pull/1801
- https://github.com/koreader/koreader-base/pull/1802
- https://github.com/koreader/koreader-base/pull/1803
- https://github.com/koreader/koreader-base/pull/1804
2 days ago
SomeGuy 512065fa14
ReaderLink: make AddCurrentLocationToStack always show notification when activated via keypress (#11925)
As seen here https://github.com/koreader/koreader/pull/11900#discussion_r1615474301.
2 days ago
SomeGuy c429ac8c3f
Menu button support for dictquicklookup, sortwidget, keyboard settings for NT (#11933)
Menu key support for some widgets. would have closed #11783 and #6463 (cumulative with @comphilip's fixes)

It fixes a problem where some virtual keys were not available for non-touch users. closes #11862

Also, adds keyboard settings to non-touch devices. closes #11934
2 days ago
Benoit Pierre 2f0e456a42 ci/macos: dump binaries runtime path & dependencies
Useful information for investigating issues with the bundle.
4 days ago
Benoit Pierre b7c1957e4b macos: fix bundle
The new build system should normally be free from "brew infections",
and already have the proper runtime path and library names.
4 days ago
Benoit Pierre 4750b4a4cd ci/macos: drop unnecessary build dependencies 4 days ago
Benoit Pierre 404c7c0dfe ci: update build directory trimming before caching 4 days ago
Benoit Pierre 5a465f413b tweaks for new build system
- forward unknown make targets to base
- add `cmake` and `staging` directories to ignored output artifacts
- drop `package.path` and `package.cpath` luarocks specific entries
4 days ago
Benoit Pierre 1c9c35dcb3 bump base 4 days ago
nairyo c7a59145a3
[fix] VocabBuilder: restore erroneously removed onShowVocabBuilder function (#11915)
Fixes #11913.
6 days ago
Piotrek Marciniak 9b1a21ef82
Export highlights: empty annotations as nil instead of empty string (#11912)
Fixes Readwise exports
6 days ago
SomeGuy 1aefd80ea6
ReaderBookmark: "bookmark current page" removed from NT kindles (#11907)
Discussed https://github.com/koreader/koreader/issues/11834#issuecomment-2132411929 onwards.
7 days ago
Frans de Jonge 94372c2adf
Remove ShowFontMenu (#11904)
Some clean up, as suggested in <https://github.com/koreader/koreader/pull/11864#discussion_r1615312993>.
7 days ago
SomeGuy eb63cf655f
CoverBrowser: update MosaicMenu FocusManager grid view to 2D (#11906)
This PR [and #11884] are for #11834.
7 days ago
Frans de Jonge ffc43030ec
ReaderLink: change (hasScreenKB or hasSymKey) add to notification stack shortcut to Press instead of Down (#11905)
See 316c6a0624 (r1615314584)
7 days ago
hius07 9223cde2bd
Status bar: fix book progress (#11885) 7 days ago
Frans de Jonge 8f2bd5420d
Introduce Device:useDPadAsActionKeys() (#11900)
1. Non-Kindle-specific `hasFiveWay` behavior is changed to `hasDPad and useDPadAsActionKeys`. For now they remain Kindle-specific in practice, unless one sets `useDPadAsActionKeys = yes` in a user patch.
2. With that disambiguation out of the way, `hasFiveWay` itself is further disambiguated into `hasScreenKB` and `hasSymKey`, as per the actual property being used, rather than something that tends to correlate with it. (It needn't be Kindle-specific per se, but non-Kindle devices have equivalent shortcuts with for example `Shift`.)
  Running the emulator with `DISABLE_TOUCH=1` will set `hasSymKey = yes`, which can be tested with right shift.

Closes #11887.
1 week ago
Martín Fernández 3fb2f18041
update tool to generate metadata translations (#11869)
* updated:

    - strings to translate
    - english metadata

* added:

    - appstream: metadata generator
    - appstream: translation of screenshot captions, if they're present.
    - appstream: link to gh release notes
1 week ago
Frans de Jonge a21db40745
[i18n] ReaderBookmark: don't bother translators with a space (#11902) 1 week ago
SomeGuy 00d0affd70
Remove very long press settings from NT (#11898) 1 week ago
SomeGuy d217f5c161
Menu widget: limit right as hold to hasFewKeys (#11890)
Addresses concerns in #11884.
1 week ago
hius07 076f77282b
FileManager copy/move: do not paste to itself (#11878) 1 week ago
hius07 c6e6d72cf3
Collections: add collection mark to books (#11868) 1 week ago
ziz57 7925455b68
Add ReaderLink::registerScheme for plugins handling non-http(s) links (#11889)
Currently, links with a scheme other than http or https are rejected. But plugins may want to handle them. This allows them to, by registering the scheme with self.ui.link:registerScheme("example") during the plugin's init.
1 week ago
Frans de Jonge b222900cb9
Enable Alt+Shift+G for screenshots on all devices with a keyboard (#11888) 1 week ago
hius07 556e5bd6b4
VirtualKeyboard: delete word on backspace swipe west (#11843)
Closes #11831.
1 week ago
SomeGuy 81575ae24f
Global long press on K4, 'ScreenKB' + 'Press' (this time for real) (#11884)
discussed #11834

then merged #11872, then reverted #11881
1 week ago
NiLuJe f0f37e3153
Kobo: Boost saturation for CFA refreshes (and add a dev setting to disable it) (#11883)
Being able to disable it can be interesting, because it *is* a somewhat destructive process.

* Requires a base bump:
https://github.com/koreader/koreader-base/pull/1793
https://github.com/koreader/koreader-base/pull/1794
1 week ago
Frans de Jonge 07b507370f
Revert "Global long press on K4, 'ScreenKB' + 'Press' (#11872)" (#11881)
This reverts commit 3f64ecfd28.
1 week ago
poire-z fd7e224c16 userpatch: add a few helpers that can be used in userpatches
Make the few tricks we discovered readily available,
which should make user patches simpler.
1 week ago
poire-z f00a88aef7 AltStatusBar: show page info similar as in footer
We now build the "page/total %" string ourselves,
with the same logic as used in the footer (including
when hidden flows or reference page numbers are used)
and give it to crengine to be displayed instead of
its own way of doing it.
1 week ago
poire-z 54a0cdd737 ReaderFooter: fix minor issues with pages left & chapter progress
When using Reference page numbers, "Pages left" was showing the
same info as "Current page", because Reference pages, being strings,
couldn't be used for arithmetics. But we can just count the number
of items left in the Reference pages array of strings.

Also fix edge case with "page progress" with hidden flow when
we are before the first chapter with a hidden flow before.
1 week ago
Frans de Jonge 20d8a5c313
FocusManager: enable more keyboard shortcuts by default (#11871) 2 weeks ago
SomeGuy 3f64ecfd28
Global long press on K4, 'ScreenKB' + 'Press' (#11872)
discussed #11834
2 weeks ago
Frans de Jonge 5b18c30336
Quickstart guide: set dir="rtl" when we're in an RTL language (#11867)
Reported by @Monirzadeh in <https://github.com/koreader/koreader/issues/11147#issuecomment-2122164344>.
2 weeks ago
SomeGuy e859109885
Status bar: major UI makeover (#11678) 2 weeks ago
poire-z 40814bf12b
bump crengine: minor fixes (ruby, a crash, non-linear flows) (#11863)
Includes:
- html5.css: really ensure ruby centering
- getRenderedWidths(): fix possible crash with 0-width images
- Page splitting: ignore empty non-linear flows
- LvDocView header: allow overriding "page/total %"
CreDocument: add setPageInfoOverride() to allow tweaking
top status bar display of page number/count/% (to be
implemented in a later commit).
2 weeks ago
SomeGuy 6c7e2a9c62
Support screenshots on Non-Touch with ScreenKB as modifier (#11802)
on devices (mostly kindles) with keyboards: Alt + Shift + G
on kindle 4: ScreenKB + Menu
2 weeks ago
SomeGuy 577c5d454f
Non-touch DPad improvements (#11749)
Closes #11295.
2 weeks ago
Martín Fernández 36d2e3cf74
Add README.md to Linux package (#11859) 2 weeks ago
yparitcher 59fb906921
NetworkListener: fix FM integration & properly gate behind hasWifiToggle (#11858) 2 weeks ago
NiLuJe 4d9c6523ad
Input: Some more followups to the input device auto-detection stuff (#11855)
Switch to a new `input.fdopen` API & wrapper so we can keep the fds opened by `fbink_input_scan` instead of closing them to re-open them right after that...

This should hopefully help on racy zForce devices that attempt to handle power management when opening/closing the device. We know this sometimes horribly fail to re-activate the IR grid (c.f., our manual activation on resume), but this apparently could also happen here (re: #11844) because of the quick succession of open->close->open.
2 weeks ago
Martín Fernández 1eb2095ead
Desktop: add all supported mimetypes. (#11847)
Change the comment to "ebook reader"
2 weeks ago
Martín Fernández 89a6ae28a6
appstream: update metadata (#11851)
proper requires, supports and recommends
all supported mimetypes
add branding colors
2 weeks ago
Martín Fernández 635d243152
debian: update metadata (#11854)
Slightly modified to avoid: E: koreader: description-starts-with-package-name
2 weeks ago
Nico Hirsch 3b97e2988a
Shorten home folder path for info messages (#11853)
The info messages for opening files always show the full path. By using `filemanagerutil.abbreviate`, the info messages now show the shortened home folder path (only if the setting "shorten home folder" is enabled).
2 weeks ago
Martín Fernández ba8891082c
fix after #11833 (#11849) 2 weeks ago
Tomáš Janoušek e94550a261
PocketBook: Open links in the on-device web browser (#11787)
Many PocketBook devices include a web browser, but when clicking a link in KOReader, there was no option to open the link in the browser, there was only an option to show a QR code (which can then be scanned by a smartphone).

This commit implements `canOpenLink`/`openLink` on PocketBook using the "browser.app", if available.

Tested on PB740 (InkPad 3).

Fixes: https://github.com/koreader/koreader/issues/11782
Related: https://github.com/koreader/koreader/issues/6597
2 weeks ago
Martín Fernández 7bded465eb
SDL: add flatpak flavor (#11833)
* ship metadata with generic linux binaries
* append release version & data at build time
2 weeks ago
Martín Fernández d801af6d41
bump metadata translations (#11842)
updating metadata for 50 languages
short_description.txt: 8 new | 18 updated | 24 not translated
full_description.txt: 10 new | 15 updated | 25 not translated
2 weeks ago
hius07 daf0fa4b4b
MultiInputDialog: fix keyboard height changed (#11832) 2 weeks ago
hius07 6b192c346a
Annotations: Bookmark list improvement (#11825) 2 weeks ago
hius07 46449eb06e
Page overlap: add dashed line marker (#11804)
Requested by our contributor in #11734 (comment).
2 weeks ago
nairyo 126c01e1b5
Replace tweak_buttons_func with an event instead (#11777)
Also see https://github.com/Ajatt-Tools/anki.koplugin/issues/22
2 weeks ago
hius07 db63db11b2
FileManager: safe initial path (#11774)
We do not like trailing slash in the path (except root).
Closes #11772.
2 weeks ago
NiLuJe 05168b22f5
Kindle: Don't forget about fiveways on legacy devices & fix a couple input-scan misdetections (#11827)
* Kindle: Don't forget to open INPU_DPAD devices for the fiveways. Somehow managed to skip my mind, they're often on a separate input device.

Regression since #11807

* Update FBInk to fix a few cases of input_scan misdetection (on misconfigured drivers (e.g., no DIRECT prop on supported kernels), or old kernels with no EVIOCGPROP support).

Fix #11824
3 weeks ago
Philip Chan 5d63907cae
fix #11810: only set FocusManager layout if hasDPod (#11811) 3 weeks ago
NiLuJe fd5260f2ce
Support auto-detection of input devices via fbink_input (#11807)
* Kobo: Drop a bunch of if ladder crap and switch to auto-detection of input devices via fbink_input
* Kindle: Drop an even larger bundle of crap to do the same ;p. (re: #11392)
* ExternalKeyboard: Switch to fbink_input to whitelist keyboards instead of the manual parsing of caps via its FindKeyboard class
* Input: Extended open/close wrappers to handle logging & tracking of dupe open/close calls.
3 weeks ago
Martín Fernández 79be8a10b1
appimage: keep metadata in sync with flathub (#11819)
Because it is actually pulled from here :p
3 weeks ago
Benoit Pierre a9a023c062
doc: fix android NDK / SDK instructions (#11818) 3 weeks ago
Benoit Pierre ea538900ba ci/macos: install ninja 3 weeks ago
Benoit Pierre b2d495fcdd ci: force color output 3 weeks ago
Benoit Pierre 0aaa8e7c5b ci/macos: drop unnecessary permissions stanza 3 weeks ago
Benoit Pierre f82cc31717 ci/macos: add caching to speedup workflow 3 weeks ago
Benoit Pierre a12b075e07 ci/macos: avoid concurrent workflow runs on the same ref 3 weeks ago
Benoit Pierre b28c58b902 ci/circle: tweak resource classes 3 weeks ago
Benoit Pierre 76bdb7e65c ci/circle: improve caching
Don't cache the whole base directory; instead independently cache:
- the build output directory
- the ccache directory

This allow completely avoiding the need for building base if a
cache (keyed on the relevant parts of the git tree for base) is
successfully restored, as well as reducing the amount of cached
data.

Additionally, ensure caches are isolated by branch name (but allow
falling back to a "master" cache for restore): we don't want PRs
using each others' caches.
3 weeks ago
mergen3107 8a316f928a
Kindle: remove separate L and R orientations (#11780)
Fixes #11743
3 weeks ago
Benoit Pierre 25d29aca4a
android: fix building with kodev (#11817)
If the Android NDK and/or SDK are not setup, `kodev` will try to invoke
the `android-ndk` and/or `android-sdk` make targets.  This can't be done
with `TARGET=android`, as `base/Makefile.defs` will error out trying to
call the (missing) compiler to get the target machine. Those rules need
to be available to all targets.
3 weeks ago
Benoit Pierre c9c8089188
bump base (#11814)
- https://github.com/koreader/koreader-base/pull/1782
- https://github.com/koreader/koreader-base/pull/1783
- https://github.com/koreader/koreader-base/pull/1784
3 weeks ago
Martín Fernández 2c6808ba78
Add generic Linux package (#9351)
Relates to https://github.com/koreader/koreader/issues/9268
Requires https://github.com/koreader/koreader-base/pull/1504

Usage: 
- `kodev release linux` for native package (same arch as host)
- `LINUX_ARCH=arm release linux` for armhf on a x86_64 host.
- `LINUX_ARCH=arm64 release linux` for arm64 on a x86_64 host.

It produces a `koreader-linux-$ARCH-$VERSION.tar.xz` archive, where $ARCH follows `uname -m` convention {x86_64, armv7l, aarch64}

To generate the debian package from the generic archive the following command is required

`./platform/linux/do_debian_package.sh path_to.tar.xz`
3 weeks ago
sdasda7777 eb7af994e1
Check if /usr/bin symlink is present to fix instance reuse issue on older Pocketbooks (#11764)
Fixes #11760.
3 weeks ago
Nico Hirsch 3e04184638
calibre: add wireless connectionstart/stop actions (#11806)
* Register start stop connection actions

* Match code order

* Title to lowercase
3 weeks ago
Philip Chan f0a3bcf05b
NT: add Hot swapping dictionaries button and edit button into FocusManager's layout (#11803)
fix #11783
3 weeks ago
Benoit Pierre 7ba42579eb bump base 3 weeks ago
Benoit Pierre 8c0362f9ed make: ignore thirdparty directory when installing 3 weeks ago
Benoit Pierre 5a146414db make: symlink fonts / plugins on install 3 weeks ago
Benoit Pierre 49e3251e7b make: support changing the build / install directory location 3 weeks ago
Benoit Pierre f749fc2fd9 make: move target specific rules to dedicated files 3 weeks ago
Benoit Pierre 417d56b440 make: small improvement
Avoid duplicate `make -C base` call when building from scratch, as well
as support avoiding the remaining call (using `make --assume-old=base`).
3 weeks ago
Benoit Pierre a4526633dd make: reduce the number of `$(shell …)` calls
Through use of `$(INSTALL_DIR)`.
3 weeks ago
Benoit Pierre 5efba26d80 make: ensure `base/Makefile.defs` can be included
Automatically run `fetchthirdparty`.
3 weeks ago
Benoit Pierre 3f8f87d294 make: minor cleanup 3 weeks ago
Benoit Pierre a4400b3ccb tools/mk7z: support debian unstable / ubuntu 22.04
The version of `7z` provided by `p7zip-full` is now the same as `7zip`:

- symlinks are dereferenced by default (no support for `-l`)
- `7z -ba h` appends a trailing `/` to directories
  (not present in the output of `7z -slt l`)
3 weeks ago
Benoit Pierre 2d4b12c99c tools/mk7z: tweak manifest handling code
To support mawk (default awk on debian unstable).
3 weeks ago
Benoit Pierre e921ed1bc7 tools/mk7z: fix subtle bug in manifest handling
A string like `1E2` is interpreted as `100` (`1×10²`):

```bash
▹ echo 0E036904 | awk '{ if ($1) print $1 }'
▹ echo 0E036904 | awk '{ if ($1!="") print $1 }'
0E036904
```
3 weeks ago
SomeGuy baab326332
[ReaderHighlight] Remove duplicated setting on NT devices (#11750)
Closes #11747.
3 weeks ago
Joshua Bullock 1398154546
Gesture: Add toggle for orientation lock (#11795) 3 weeks ago
Predrag Đokić eb6e5e3c20
Kindle: Fix missing Amazon UI screensaver after exiting KOreader (#11794) 3 weeks ago
hius07 12c3c190b0
Annotations: fixes 2 (#11788)
* readerbookmark: fix index for reverse sorting

* readerhighlight: fix pdf tap on highlight detection
3 weeks ago
Benoit Pierre 075edf9980 tests/readerhighlight: speedup
Reduce time to run those tests by 74% on my machine (~8.8s → ~2.3s).
4 weeks ago
Benoit Pierre 554e8daf99 tests/readerhighlight: avoid changing versioned files
Work on a copy of `test/sample.pdf`.
4 weeks ago
Benoit Pierre 089c19cb39 tests/readerbookmark: avoid changing versioned files
Work on a copy of `test/sample.pdf`.
4 weeks ago
Benoit Pierre 3e809b6c7b tests/readerlink: speedup
Reduce time to run those tests by 95% on my machine (~17s → 0.8s).
4 weeks ago
Benoit Pierre fdf19e98c3 tests/readerlink: factorize setup/teardown code 4 weeks ago
Benoit Pierre d0ca04c48d tests/readerlink: reorganize
Group tests by file type (EPUB or PDF).
4 weeks ago
Benoit Pierre b67c6147d5 tests: ensure successive testsuite runs work
Don't carry over some old settings. Additionally, avoid clobbering the standard reader settings.
4 weeks ago
peicuiping a7e34673e6
chore: remove repetitive words (#11785) 4 weeks ago
Valentin Dubois 526a1fb727
Initial support for new Kobo (Clara B/W + Colour, Libra Colour) (#11737)
* Support the Clara B&W, Clara Colour & Libra Colour
* Enable HW dithering on *all* the Kobo MTK devices
* Enforce 32bpp instead of 8bpp for Kobo devices with a color panel (the driver doesn't actually support 8bpp anyway)
* Enable standby support on MTK (whenever possible, i.e., not when plugged in, as that is horribly, horribly broken).
* Enforce the dedicated "color" waveform mode for image content in ScreenSaver, ImageViewer & Reader.
* Fix charging LED support on MTK
* Tweak the frontlight ramp on MTK + LM3630 so that it actually ramps smoothly
4 weeks ago
Benoit Pierre 821120828b
tests: fix EPUB test (#11776)
The `droid sans fallback` test incorrectly assumes `Droid Sans Mono`
will be the first font in the list of fallback fonts, but the list
is alphabetically sorted.

Cf. https://github.com/koreader/koreader/pull/10566
4 weeks ago
sdasda7777 8530282d38
Add Czech keyboard layout (#11769)
Fixes #11339.
4 weeks ago
hius07 725df17c45
Annotations: fixes (#11761) 4 weeks ago
hius07 c47d3b3177
Menu widget: cleanup (#11759) 4 weeks ago
zwim bf58723af1
[CoverImage] Honor rotation after opening of a book (#11752)
If rotation changes, create a new cache entry and use that if necessary.

Fixes #11713.
4 weeks ago
SomeGuy e51b71f463
[Gestures.plugin] Clarify some rotation gesture strings (#11753) 4 weeks ago
poire-z 001e90db1e ReaderSearch: delay adding to location stack until we change page
Search may highlight matches on the current page, and if there
are no others in the book, we would stay there and resume reading;
but we would get a spurious previous location (ie. in Book map)
that we would not expect keeping, and we may accidently go back
to previous location, losing our reading position.
Avoid this by only adding current page to location stack when
actually leaving the current page.
Because of complications with other reading modes, this is
currently only enabled for CRE document when in page mode.
4 weeks ago
poire-z 03d16270d9 Statistics: handle correctly page change when reading paused
As currently we only handle ReadingPaused/Resumed on suspend
and resume, no page change should happen while paused, so
there is no current issue to fix. But let's get ready for
other use cases (which may be brought in by user patches,
ie. pausing while the SkimWidget is displayed, and obviously
triggers page changes).
4 weeks ago
poire-z 7d94562602 ReaderToc: fix issues when on a page before first Toc Item
When on a page before the first ToC item (the first chapter),
that first chapter title may be shown in the footer instead
of a more correct blank title.
This bug might still have had other small effect elsewhere.
4 weeks ago
poire-z ca90b982b4 ReaderStyleTweak: reword CSS suggestions info text 4 weeks ago
poire-z 1c9a6509a2 HttpInspector: allow browsing global variables 4 weeks ago
Benoit Pierre 89b1280166
bump base (#11748)
- https://github.com/koreader/koreader-base/pull/1773
- https://github.com/koreader/koreader-base/pull/1775
- https://github.com/koreader/koreader-base/pull/1776
- https://github.com/koreader/koreader-base/pull/1777
- https://github.com/koreader/koreader-base/pull/1778
4 weeks ago
hius07 d82815952e
Annotations, part 1 (#11563)
New format to handle annotations (page bookmarks, highlights, notes) and store them in the book metadata files.
1 month ago
hius07 6b0d97bf22
Multiple collections (#11693) 1 month ago
Benoit Pierre 087ddd1510 ci: small macOS cleanups
Reduce differences with the configuration used on koreader-base.
1 month ago
Benoit Pierre 7392dd2ad4 ci: tweak macOS CI checkout phase
Use the same parameters as for the koreader-base job (faster).
1 month ago
Benoit Pierre 54ded2c578 ci: fix macOS build
Work around the fact that the lua@5.1 brew formula has been disabled.

Additionally, drop ragel (no necessary anymore).
1 month ago
Benoit Pierre ad6e3b34c4 bump luajit-launcher 1 month ago
Benoit Pierre 64213bf3d9 bump base 1 month ago
Benoit Pierre 6d5ad05e9f ci: bump docker images 1 month ago
hius07 8ff846ba6e
SortWidget: sort alphabetically (#11705) 1 month ago
Benoit Pierre e8544316a8
pocketbook: simplify device model detection (#11721)
To avoid avoid having to manually handle the multiple
ways a model name reported by inkview can be formatted.
1 month ago
vyaus f793c6a36c
Gesture: add jump to random page (#11727) 1 month ago
Frans de Jonge 34abb4e22b
Update translations for v2024.04 (#11726) 1 month ago
Galunid ca14420372
Add relevant nil guards to prevent reflow crashes (#11715)
closes #10854 #9272 #4481
1 month ago
hius07 f5be04a738
Keyboard: detect tap if swipes are disabled (#11699)
Discussed in #11668.
1 month ago
mergen3107 caea0e8fb2
Kindle Scribe: improve gyro detection, replace accel with acc keyword (#11696)
Fixes #11691.
1 month ago
ElimGarak1 bfc84795c8
Add PocketBook Era Color (PB700K3) (#11695)
Fixes #11684.
1 month ago
Max Ignatenko 87c85bf94d
NewsDownloader: use <content:encoded> from RSS item, if available (#11694)
This makes it work much nicer for feeds that provide the full content in the feed itself.
2 months ago
SomeGuy c70c9f0905
Hide hidden flows and custom toc from non-touch devices (#11690) 2 months ago
mergen3107 d3011571a3
Kindle Scribe: find accelerometer based on hardware (#11642)
Closes #11392 (temporarily).
2 months ago
Max Ignatenko b872191dc9
Stop touching night mode when control_nightmode == false (#11685)
This is a regression introduced in #10762
2 months ago
Benoit Pierre 715f5aa747 ci: cleanup CircleCI fetch phases 2 months ago
Benoit Pierre 8b8258b44a make: speedup `fetchthirdparty`
- shallow clone submodules configured as such
- on CI, shallow clone all submodules
- use 3 jobs
2 months ago
Benoit Pierre f3dcc70d8d l10n: mark git submodule for shallow updates 2 months ago
Benoit Pierre 4a343d3669 ci: bump CircleCI docker images 2 months ago
Benoit Pierre 21c6e37f5e ci: remove CircleCI unnecessary steps
The docker images should already contain all the necessary tools.
2 months ago
Benoit Pierre c345de5d71
ci: drop travis configuration (unused) (#11676) 2 months ago
Benoit Pierre ad6607cb4c
make: improve `appimageupdate` rule, take 2 (#11671)
The FUSE detection is false positive under Gitlab's pipeline,
so just use the hidden `--appimage-extract-and-run` option to
automatically and always "extract & run" the appimage tool.
2 months ago
Frans de Jonge f93cc6e916
[AppImage] AppImageTool 13 (#5275) (#11665)
https://github.com/AppImage/AppImageKit/releases/tag/13
2 months ago
Benoit Pierre 8b4b3025eb
make: improve `appimageupdate` rule (#11669)
Instead of trying to detect if running under Docker, use a simpler
wildcard check to detect if FUSE support is available. This as the
advantage of supporting Docker, rootless Docker, and Podman.
2 months ago
Benoit Pierre 0dbfac22b0
make: fix `appimageupdate` rule (#11662)
Update sdnio library soversion to account for new docker image based on Ubuntu 20.04.
2 months ago
Benoit Pierre 12552f1c71
doc: remove ragel from the build requirements (#11661)
Not necessary anymore.
2 months ago
Benoit Pierre c503e9e848
make: fix test rule (#11659)
Use the same code as in base (Cf. https://github.com/koreader/koreader-base/pull/1756).
2 months ago
poire-z d178273671
bump base, luajit-launcher, avoid assert failure in sub-processes (#11658)
Bump base:
- Update LuaJIT to 20240310
- harfbuzz: fix ccache 4.9.1 errors
- ci: drop unnecessary macOS build dependency
- CRE: avoid assert failure in sub-processes
Bump luajit-launcher:
- Update LuaJIT to 20240310
Frontend:
- CreDocument: prevent assert failure in sub-processes
2 months ago
Frans de Jonge 31c28378e7
[i18n] GetText: fix fuzzy detection for certain strings (#11648)
Reported in <https://github.com/koreader/koreader/pull/11647#issue-2233008767>.

Probably overlooked in #5807
2 months ago
SomeGuy fb86acaf1a
Sleep screen wording fixes (#11647) 2 months ago
Frans de Jonge ad266f46c2
GetText: fix crash if a value is used as unique value and as singular/plural (#11643)
Concretely 1 second, %1 seconds since #11549 added `1 second` all by itself.
2 months ago
SomeGuy 279f16aa23
Sleep screen menu reworked (#11549) 2 months ago
Bastian Wagner aaa56a814f
CreDocument: support added 'identifier' metadata (#11628)
Currently not exposed in Book information.
2 months ago
poire-z ac4d4756f8
bump crengine: various fixes, updated russian hyphenation (#11637)
Includes:
- Russian hyphenation: allow hyphens after Russian "не" prefix
- EPUB: add "identifiers" to doc props
- EPUB: avoid crash when '@font-face' in <head><style>
- FB2: fix block images sizing and centering
- lvrend: fix positioning of bottom border on empty block elements
Also includes:
- cre: add 'identifiers' to doc props
- build and ci: various tweaks
2 months ago
Tomas Janousek 1fbdc1f19d NetworkMgr: Limit the hasDefaultRoute check in isOnline to PocketBooks
It's not necessary on other platforms. To be more precise, it would be
necessary on Kobo as well, but there's a different workaround in place:
https://github.com/koreader/koreader/pull/6424/files#diff-be863601c59a2d6607af6b04b3be2392ec4494df6d25dae48250fae57b737f61R216-R224
2 months ago
Tomas Janousek 4fa6783dbb NetworkMgr: Use cheaper/simpler hasDefaultRoute in isOnline
Device:getDefaultRoute parses /proc/net/route and converts the hex
addresses to textual IP addresses, but in `isOnline` we don't care what
address the gateway actually has, we only care about whether we have a
default route into the Internet.

This provides a simpler alternative that does the equivalent of
"ip route get 203.0.113.1 || ip route get 2001:db8::1" (note that it
does support IPv6-only connectivity as opposed to
Device:getDefaultRoute) and returns true if we have a route.

Inspired by https://github.com/pavel-odintsov/get_default_outgoing_ip_linux
2 months ago
Tomas Janousek b3e125a7ce NetworkMgr: Fix PocketBook losing net access
Doing the `isOnline` check (`socket.dns.toip("dns.msftncsi.com")`)
without having internet connectivity (`!isConnected`) results in the
`isOnline` check never succeeding again even if connectivity is later
acquired. This is most likely caused by /etc/resolv.conf only being
parsed once - https://sourceware.org/bugzilla/show_bug.cgi?id=984, an
issue that was fixed in glibc 2.26 (PocketBook firmware U740.6.8.2461
has glibc 2.23).

This fix works around the problem by checking if we have a default route
first before even attempting to check `isOnline`. If we don't, then
`isOnline` is (almost) guaranteed to fail anyway.

We could alternatively check `isConnected` instead, but that only checks
wireless connectivity on many platforms, and we could have internet
access via USBNet instead. Checking for the default route via any
interface should work reliably for both wireless and USBNet
connectivity.

Another alternative fix is to add a fallback nameserver to
/etc/resolv.conf like we do for the Kobo platform [1]. Unfortunately,
this fix would not work in the following (rather common) scenario:

1. PocketBook boots, connects to WiFi
2. KOReader starts, /etc/resolv.conf looks all right, no fallback needed
3. PocketBook goes to sleep, disconnects from WiFi, clears resolv.conf
4. PocketBook wakes up, stays disconnected
5. KOReader user does a Wikipedia lookup, networking freezes

[1]: https://github.com/koreader/koreader/pull/6424/files#diff-be863601c59a2d6607af6b04b3be2392ec4494df6d25dae48250fae57b737f61R216-R224

Fixes: https://github.com/koreader/koreader/issues/10183
Related: https://github.com/koreader/koreader/issues/6421
2 months ago
hugleo cd7d3b5ef6
Bump luajit-launcher (#11636)
Add support for C64P
2 months ago
Denis Malinovsky 6cc970dbb4
Do `flashui` refresh for first display of History page (#11603)
This fixes ghosting for color Pocketbook devices

Fixes #11602.
2 months ago
hius07 c415aea67e
File manager: fix classic mode item font size (#11626)
Closes #11625.
2 months ago
Frans de Jonge 601925ab87
[minor] CSS tweaks: fix typo cellpading → celpadding (#11618) 2 months ago
Frans de Jonge ec54afcc0d
[minor] ReaderStyletweak: fix typo (#11616) 2 months ago
Frans de Jonge a7edf213a5
[minor] FileChooser: use en-dash instead of dash for UI (#11615) 2 months ago
SomeGuy f6588e95e8
ReaderHighlight: ~~strikeout~~ → Strikethrough (#11619) 2 months ago
Frans de Jonge b1a80921ad
Revert "ReaderPaging: enable Kindle page-turn animations (#11424)" (#11617)
This reverts commit ee7c3ab551.
2 months ago
Frans de Jonge b9261e0245
[i18n] Font UI fallbacks: insert replacement characters with template for Weblate compatibility (#11614)
Added in <https://github.com/koreader/koreader/pull/8722> and unproblematic for the past two years, but Weblate has now decided to complain about it to the extent of disabling translations:

> Error message
String contains control character: 'If some book titles, dictionary entries and such are not displayed well but shown as \ufffe\ufffe or ��, it may be necessary to download the required fonts for those languages. They can then be enabled as additional UI fallback fonts.\nFonts for many languages can be downloaded at:\n\nhttps://fonts.google.com/noto\n\nOnly fonts named "Noto Sans xyz" or "Noto Sans xyz UI" (regular, not bold nor italic, not Serif) will be available in this menu. However, bold fonts will be used if their corresponding regular fonts exist.'
2 months ago
Benoit Pierre dce92020c1
bump base: fix sdcv binaries on non-Android/macOS platforms (#11609)
Closes #11604.
2 months ago
hasezoey 42c93a7623
FileChooser collates: use "item.sort_percent" for sorting + extra `on-hold` handling (#11592)
re https://github.com/koreader/koreader/pull/11524#issuecomment-1984110990
re https://github.com/koreader/koreader/pull/11542#issuecomment-2018164346

re #11592
2 months ago
SomeGuy fbd3e822fe
ReaderView: increase underline thickness (#11586)
Closes #11581.
2 months ago
Denis Malinovsky 501cba6ebe
PocketBook: remove unused `color_saturation` parameter (#11597)
Bumps base for https://github.com/koreader/koreader-base/pull/1751

Includes several build system improvements as well
2 months ago
dkabot ee7c3ab551
ReaderPaging: enable Kindle page-turn animations (#11424)
Fixes #11423.
2 months ago
Frans de Jonge ca8e9352ba
[i18n] Add en-GB and Romanian translation to UI (#11577)
* Add en_GB translation to UI

Follow-up to https://github.com/koreader/koreader-translations/pull/167

* Uncomment Romanian
2 months ago
Denis Malinovsky f4a8514545
ReaderTypography: update menu features symbols for Russian (#11570) 2 months ago
poire-z 86cb5cbd4c
bump crengine: various fixes, improved Russian typography (#11578)
Includes:
- In-page footnotes: avoid with '-cr-hint: noteref-ignore'
- In-page footnotes: ensure they don't cross "flows"
- Tables: fix rendering when negative text-indent
- FB2 cover drawing: ensure _invertImages flag
- EPUB: fallback to look for a cover in the first fragment
- TextLang: Russian: add typography rules
Also update to libunibreak 6.1.
2 months ago
poire-z 67cd647d1a
ReaderToc: add option to show chapter lengths (#11546) 2 months ago
hius07 c8c4e0301a
covermenu: fix file dialog (#11576) 2 months ago
Frans de Jonge 9387fcd2d0
[fix] ReaderLink: fix copy link (#11569)
Fixes #11567.
3 months ago
hius07 bb98cbf612
ReaderStatus: update status modification time (#11568)
In the "End of the book" action.
Closes #11564 (No idea how we got a book without modification time but the time stamp must be updated in any case)
3 months ago
hugleo 424fa9b6fd
ReaderView: ensure that the pan zoom direction is not nil (#11557)
Fix gif and web files not opening: https://github.com/koreader/koreader/pull/11425#issuecomment-2002164851
3 months ago
Martín Fernández 5d2782ddfe
bump luajit-launcher (#11554)
Add Linfiny eNote / Avalue ENT-13T1 / QuirkLogic Papyr
3 months ago
Martín Fernández 3638adc841
appimage: get full path before changing to assets dir (#11552) 3 months ago
Jo Van Bulck 4d19ce24ed
Dictionary: Fix download message when connection fails (#11548) 3 months ago
hugleo 95ce974ef9
bump base/libk2pdfopt and luajit-launcher (#11540)
- libk2pdfopt: allow asymmetric margins for auto-crop.
- luajit-launcher: new devices for e-ink test
3 months ago
poire-z 9a84755e56
Wikipedia EPUBs: add settings for include/highres images (#11544)
Allows setting a default answer to the "Include image"
and "slightly higher quality images" questions when
saving a Wikipedia article as EPUB.
3 months ago
poire-z 191ef2b4c0
Book style tweak: add more suggestions in "CSS ≡" (#11533) 3 months ago
hugleo b026f84949
ReaderZooming: make auto-crop default be page content (#11438) 3 months ago
SomeGuy ac0483bfce
Screensaver: 0% only on first page, 100% only on final page (#11541)
Fixes #11516
3 months ago
Frans de Jonge 20813cab53
Wallabag: properly deal with mimetype actually being content-type (#11532)
A typo snuck in #11492, which should've read `not type(article.mimetype) == "string" or type(article.mimetype) == "string" and not article.mimetype:find("^text/html")`. But in most cases the behavior would've been identically broken because of the same underlying issue: Wallabag mimetype is actually HTTP content-type.

Fixes #11528.

Also introduces a new setting associated with the behavior in case people have different preferences.
3 months ago
hius07 5414858b74
Menu widget: fix item property name (#11534) 3 months ago
poire-z 0eaa2d8248 Wikipedia: tweak EPUB css to force epub.css look
Include in the EPUB stylesheet most of our epub.css
tweaks, so we get our expected styling even when
html5.css is used as the default stylesheet.
(Users liking the "web page" look can still get it
by enabling some of our existing style tweaks.)
3 months ago
poire-z 70e6f4ce09 util.prettifyCSS(): handle better :is() and similar 3 months ago
poire-z e3d7669588 Style tweaks: add tweak to avoid some presentational hints 3 months ago
poire-z 9467034f3f ReaderTypeset: tweak Style> menu logic and defaults
- Reword and document most menu items.
- Handle internally two default styles, one applying only
  to FB2/FB2 books, and the other to all other formats.
- Also don't reset the stylesheet to epub.css when
  toggling Embedded Styles to off.
3 months ago
poire-z 93b3e3504d bump crengine: better conformance to the HTML Standard rendering
Includes:
- Hyphenation: update French.pattern
- [CI] Add stylelint to help prevent typos in CSS
- In-page footnotes: better handle duplicated ids
- lvrend: handle in-page footnotes in table <caption>
- lvstsheet: fix compiler warnings
- LVString: ignore CJK chars in lStr_findWordBounds()
- lvtext: AddLine(): handle some CJK + space edge case
- EPUB: look for EPUB3 cover even when EPUB2 cover advertized
- List items: proper per-specs positionning and sizing
- epub.css: add/use "@media (-cr-max-cre-dom-version: 20180527)"
- fb2def.h: add more HTML element and attributes names
- CSS: generic support for handling presentational hints
- CSS: add support for private -cr-apply-func:
- ldomDocumentWriterFilter: remove attribute to CSS conversion
- lvrend: more proper rendering of block images
- lvrend: keep margin_left/right updated when "auto"
- CSS: add support for handling HTML's align= attribute
- lvrend: fix HR and images positionning when floats involved
- epub.css, html5.css: minor updates for easier stylesheet switch
- epub.css, html5.css: major updates for better conformance
- fb2.css: fix CI stylelint warnings
Also update to libunibreak 6.0.
3 months ago
hius07 6f896e9383
ReaderFooter: chapter progress bar (#11505) 3 months ago
hius07 bdb1c3135c
Fulltext search: default mode, fixes (#11507) 3 months ago
hius07 8df885438c
Bookmarks: edit highlighted text (#11484) 3 months ago
hius07 66afeebe85
CoverBrowser: show cached cover of deleted file in history (#11523) 3 months ago
poire-z c65d128032
Menu, DictQuickLookup, TextViewer: allow mousewheel scrolling (#11525)
We also need to catch it in TrapWidget so we can
interrupt Wikipedia articles images loading.
3 months ago
zwim c8f39c3841
Merge pull request #11498 from zwim/optimize_rounded
UI: optimize and anti-alias rounded rectangles
3 months ago
zwim 58b3367d18 anti alias rounded corners in UI 3 months ago
zwim 764f8ba647 bump base: Use Bresenham's algorithm and anti-aliasing for rounded corners 3 months ago
NiLuJe dd8560cef8
Input: Don't let wacom pens clobber the slot of the next contact (#11520)
If there's only one contact, we won't get an ABS_MT_SLOT, so we need to
make sure we fall back to the main finger slot once we've caught a tool
switch.

Also, move the dedicated pen slot further away, so it has zero chance of
being detected as a potential buddy contact to a finger contact.

Fix #11514
3 months ago
hius07 7edb64ecee
Upper menu: fix generating the menu (#11513)
revert all changes to the upper menu made in #11495
3 months ago
Frans de Jonge c4a09b3076
Remove bountysource from README (#11385)
Closes #11384.
3 months ago
Frans de Jonge d239623c46
Update translations for v2024.03 (#11508) 3 months ago
hius07 ec98c6334b
MenuSearch: open upper menu when needed only (#11495) 3 months ago
hasezoey ade1daca3a
FileChooser: "percent - unopened - finished last" consider status "complete" as 100% (#11472)
re #11369
3 months ago
Frans de Jonge 3e7ab199e7
Wallabag: match `text/html` mimetype as starting with rather than exactly (#11492)
Previously unnoticed or changed Wallabag behavior can provide a mimetype of for example `text/html; utf-8`, which wouldn't be an exact match to `text/html`.

Fixes #11481.
3 months ago
hius07 2fca0ee989
Reader: use existing module instances on start (#11485) 3 months ago
hius07 72a6fa1e64
FileManager: less lfs calls when selecting files (#11476) 3 months ago
weijiuqiao 5939c82bcf
VocabBuilder.koplugin: support batch operations in book filtering (#11475)
Added support
1. Select only this book
2. Select all books
3. Select all books on this page
4. Deselect all books on this page 

when filtering.

Ref. #11471
4 months ago
hius07 db2336440f
Alt status bar: custom metadata support (#11463) 4 months ago
Hzj_jie d4c78aaa4f
Kindle oasis has no ambient brightness sensor (#11456)
I did not notice an ambient brightness sensor, nor adaptive brightness feature. (Ref: https://www.geekwire.com/2016/kindle-oasis-review-amazons-premium-e-reader-use-premium-features/)

Was it previously wrongly configured?
4 months ago
hugleo 52fae11da7
ReaderView: ensure pan zoom mode on document open (#11425)
When we open the document for the first time, the pan
positions are not being applied. If I use the bottom-to-top
mode we should see the bottom first, but the top is being
shown instead.
4 months ago
hugleo ef0077df23
ReaderPaging: fix offset issue on next page in pan zoom (#11408) 4 months ago
hugleo 041117cbb6
ReaderZooming: revert Autocrop fix (#11325)
From f990937f9f.
4 months ago
hasezoey 76980098ec
FileChooser: add new sorting method (#11369)
"percent - unopened - finished last"
(ie. 90% > 50% > 0% > unopened > 100%)
4 months ago
hius07 5d4747c593
FileManager: less lfs calls (#11452)
Use item info provided by FileChooser.
Also fix showing PathChooser with invalid path,
and fix issue when opening non-supported files.
4 months ago
poire-z 76bf85738a Style tweaks: add inpage foootnote classnames and a tweak 4 months ago
poire-z d77b511849 bump crengine: text selection and footnotes fixes and tweaks
Includes:
- LvDocView: allow setting custom title/authors/series
- elementFromPoint(): fix possible crash when float at end of document
- Non-linear fragments: fix generic handling on erm_final
- DrawBorder: fix bottom border inset/outset drawing
- getSegmentRects(): allow segments to include images
- getRangeText(): allow gathering images
- Allow standalone image in link to trigger in-page footnotes
- ldomDocument::render(): avoid uneeded deserialization on each page turn
- lvtinydom: add ldomNode::getAllInnerAttributeValues()
- LVFootNote: avoid retrieving internal CompactArray object
- In-page footnotes: allows for multiple id= inside them

cre.cpp:
- rename overrideDocumentProp() to setAltDocumentProp()
- isLinkToFootnote(): handle image-only links
- text selection functions: add includeImages param
4 months ago
poire-z fb39fe93ed Fix docs CI failing after previous commit 4 months ago
poire-z 0506ffe289
HttpInspector: new plugin for developers to inspect KOReader (#11457)
Can be used to inspect the state of the objects in
a running KOReader.
It can also be used to execute actions (like the ones
available to associate to a gesture) with HTTP requests
from a remote computer/devices/gadgets.
The TCP server side is provided either with a new
ZeroMQ StreamMessageQueueServer (thanks bneo99),
or with a LuaSocket based SimpleTCPServer.
Minor UIManager tweak to avoid uneeded inputevent
when such a ZeroMQ module is running.
4 months ago
poire-z 8010808a1f
bump base: libjpeg-turbo, libpng, and other tweaks (#11462)
Includes:
- bump thirdparty/libjpeg-turbo 3.0.2
- PB: make sure libinkview-compat actually ends up w/ inkview as a DT_NEEDED
- Update to libpng 1.6.41
- cre.cpp: add overrideDocumentProp()
4 months ago
Frans de Jonge bdd475f55f
[i18n] ReaderSearch: add translation context to "All" button (#11352)
Cf. https://github.com/koreader/koreader/pull/11313/files#r1439406753
4 months ago
hius07 50fcc04725
CoverBrowser: do not call lfs on every item (#11449) 4 months ago
hius07 b8090c641c
Minimize DocSettings:open() calls (#11437) 4 months ago
hius07 c3bb2263b7
DocSettings: check legacy history location only once (#11439) 4 months ago
hius07 962477e7c6
TextViewer: make find result bold (#11427) 4 months ago
zwim f836f6a237
Clear UI leftovers before doing an OTA-install, add unmovable to InfoMessage and ConfirmBox (#11412) 4 months ago
hius07 38a14ff3a0
PathChooser: fix calling Folder shortcuts (#11367) 4 months ago
hius07 d94b819eb1
CoverBrowser: use bookinfo cache in Classic mode (#11404)
Getting bookinfo from the cache db is much faster than opening a book, let's use it in Classic mode too.

With fallback if no metadata available.
4 months ago
hius07 b832d43d98
CoverBrowser: fix classic mode update cache (#11398) 4 months ago
hius07 f246b3d19c
Text editor: refresh path on saving file (#11396)
File size was changed.
4 months ago
NiLuJe 75ffc3bb76 Terminal: Better yet, disable it properly on prereq failure 4 months ago
NiLuJe a906838123 Terminal: Clearer error message on prereq failures
Pluginloader uses pcall, so, throw with an actual error message

Re: #11394
4 months ago
NiLuJe c9da681874 Bump base
https://github.com/koreader/koreader-base/pull/1732
4 months ago
NiLuJe a8a944cbaf ScreenSaverWidget: Simplify the setDirty call
No need for a closure, there's no dimen involved.
4 months ago
NiLuJe 39744d7642 TouchMenu: TouchMenuItem's UnderlineContainer should probably have its own dimen object
The coordinates could wonky otherwise, as TouchMenuItem is an
InputContainer.
Mostly harmless in practice as that UC is its main widget.
4 months ago
NiLuJe 4785df48a9 PRSTux: Disable OTAs
We haven't been offering any for years, and I've just killed zsync
support there anyway.
4 months ago
hius07 925aa728c9
Fulltext search: action to show last findall results (#11388) 4 months ago
NiLuJe 82bf8bf379 ScreenSaver: Properly exclude macOS resource forks again
The previous check was inlined in the dir walk, so it always saw a
relative path.
Here, it gets an absolute one instead, so act accordingly ;).

Fix #11390
Regression since #11056
5 months ago
NiLuJe e5cb24a891 ConfigDialog: Simplify the previous fix
Geom's intersectWith & friends have nil guards in place since #7664,
so the insane workaround is completely superfluous.
5 months ago
NiLuJe 19ed35e59c ConfigDialog: Don't enforce 0x0 dimensions on initial display
FrameContainer now behaves like other widgets, and no longer re-computes
dimensions in paintTo *if you provide a dimen*.
Since we do here, for.... reasons I'm not entirely sure still make any
sense, make sure we actually compute valid dimensions,
instead of an empty rect from Geom:new ;).

Fix #11389
5 months ago
NiLuJe 7f33d94c4c Bump base
https://github.com/koreader/koreader-base/pull/1728
https://github.com/koreader/koreader-base/pull/1730
5 months ago
NiLuJe 90ae4acca6 Chore: Review FrameContainer constructors for shared dimen objects
Nothing else seems problematic, this is mostly just cosmetic sanitization
around Geom objects.
5 months ago
NiLuJe 65e22ceafc Widgets: Fix a bunch of regressions after the FrameContainer change in #11364
FrameContainer now preserves its self.dimen, which means it cannot be
shared with another widget or container that might also modify it during
painting ;).

Fix #11370
Fix https://github.com/koreader/koreader/pull/11364#issuecomment-1894454657
Fix #11379
5 months ago
NiLuJe 5e4d182608 Fold the debug logging flip inside the "Report a bug" entry
Also, implement a Notification source that will *always* be shown,
regardless of user preferences, and use it here.
5 months ago
NiLuJe 72250daba8 UI: Add a verbose debug logging checkbox next to "Report a bug"
@hius07 mentioned something to that effect a while back, makes sense.

Unlike the set of checkmarks in the dev settings, this one flips both
debug + verbose at once, *and* asks for a restart for framebuffer's
sake.

Also update the "Report a bug" spiel to request verbose debug logs.
5 months ago
NiLuJe 55171212c3 Misc: Forgot to update that comment given the latest VirtualKey tweaks
;).
5 months ago
poire-z 43d36b2ea9 TextBoxWidget: allow showing bits of text in bold
Allow for embedding "tags" (invalid Unicode codepoints)
in the text string to trigger some text formatting:
for now only bolding some parts of text is possible.

Use it with fulltext search "all results" to highlight the
matched word (instead of the previously used brackets).
5 months ago
poire-z 487e5f667a ReaderThumbnail: prevent standby during thumbnails generation
PageBrowser could have strange issues on devices with
aggressive autostandby (ie. failure to read data
from subprocess).
5 months ago
hius07 e0ed04a1a1
CoverBrowser: fix cover cache check #2 (#11380) 5 months ago
hius07 43831236ce
CoverBrowser: doc_settings cache improvement (#11360) 5 months ago
cookiebit e3b4cbe71a
Virtual keyboard: add Scandinavian layouts (Norwegian, Swedish & Danish) (#11365) 5 months ago
Andrei Ignatev 4c503b0f11
Frontlight widget: add left button as close key on hasFewKeys devices (#11376)
Fixes issue #8023
Related to issue #4029

The fix takes exactly the same approach as other PRs like #6195 to add some usability to devices with few hardware keys. The front-light widget can now be closed using the left key on the d-pad.
5 months ago
NiLuJe 3b129e2ada Chore: Fix a few more widgets that were breaking dimen refs in
paintTo...
5 months ago
NiLuJe d33bb0452c VirtualKeyboard: Simplify our top-level dimen handling
Make sure it tracks the right widget, and stays accurate post-paint,
by fixing FrameContainer's paintTo method *not* to break the dimen ref...
5 months ago
NiLuJe 259b4ac950 VirtualKeyboard: Try to include the padding in the key's hitbox
Followup to 40d27ac3e5
Re: #11357
5 months ago
gbyl a84ab265b8
reMarkable: More robust Wi-Fi scripts (#11287)
Fixing a bunch of issues in the process.

---------

Co-authored-by: gbyl <gbyl@users.noreply.github.com>
5 months ago
NiLuJe 40d27ac3e5 InputDialog: Don't close the VirtualKeyboard when you tap inside a key's
*border*

That's going to be the sneaky bug to beat for 2024, lol ;).

Fix #11357
5 months ago
NiLuJe 5bd78ab3b4 ReaderHighlight: Don't try to close a non-existent widget in onClose 5 months ago
NiLuJe c529c1cce3 Kindle: Log the args from WakeupFromSuspend & ReadyToSuspend 5 months ago
NiLuJe d205c72119 ReaderStatus: Fix some more lifecycle issues in onEndOfBook
Delay anything that attempts to destroy the current document instance if
we're inside an event handler.

Re: https://github.com/koreader/koreader/issues/11147#issuecomment-1891005869
5 months ago
NiLuJe 0f5547a128 Bump base
https://github.com/koreader/koreader-base/pull/1723
https://github.com/koreader/koreader-base/pull/1725
https://github.com/koreader/koreader-base/pull/1726
https://github.com/koreader/koreader-base/pull/1727
5 months ago
NiLuJe 588bf38c84 Kindle: Log the suspend/wakeup source
We currently don't do anything with it, but this might help someone come
up with fancier smartcover handling, like we do on Kobo...

Simplify the fake events w/args checks:
We can just hitcheck the table directly, no need for another hash

Also catch ExitedSS on Kindle.
And, again, dn't do anything with it ;p.
5 months ago
hugleo 98b598ddcb
Bump luajit-launcher (#11358)
Fix Android build error
5 months ago
hius07 234a68d80b
CoverBrowser: fix cover cache check (#11356) 5 months ago
poire-z e9b2a07352
bump crengine: fix CSS cascade, support :is(), other fixes (#11353)
Includes:
- Fix getToc() not cached on initial loading
- CSS: add support for forgotten "border-style: hidden"
- CSS: fix parsing of 'div :something' and 'div [attr]'
- CSS: fix checking E[foo~="value"]
- CSS: order rules as written when building a selector
- CSS: fix useragent vs. author stylesheet CSS cascade
- CSS: pass useragent_sheet flag all along parse() code
- CSS: add private selector syntax to match against text
- CSS: add support for :is(), :where() and :not() pseudoclasses
- EPUB: don't ignore any <spine> item
- bump CACHE_FILE_FORMAT_VERSION
5 months ago
NiLuJe b2b87bd651 Bump platform/android/luajit-launcher
https://github.com/koreader/android-luajit-launcher/pull/436
https://github.com/koreader/android-luajit-launcher/pull/458
https://github.com/koreader/android-luajit-launcher/pull/460
https://github.com/koreader/android-luajit-launcher/pull/462
5 months ago
NiLuJe 5cbd65acc1 Bump base
https://github.com/koreader/koreader-base/pull/1722
5 months ago
hius07 c4e9e6bc51
filemanagermenu: fix classic mode items per page (#11346) 5 months ago
hius07 0ceb88a9a3
Fulltext search: all entries in entire document (#11313) 5 months ago
poire-z 5f5162d95c
bump base: add cre.findAllText(), many 3rd party bumps (#11347) 5 months ago
poire-z a025faae4e Text selection: show top left icon when very-long-press delay reached 5 months ago
poire-z 455e904120 Text selection: add option to disable corner scroll
People using 2-steps text selection (via the Select button) may
want to not be annoyed by this feature.
Also tweak a bit its behaviour, requiring now the text selection
to come from outside a corder into a corner to activate it, which
should allow starting text selection from a corner without
triggering a scroll yet.
5 months ago
NiLuJe dcfcc81dcf Android: Don't forget to call Generic.exit on Device:exit ;).
Fix #11345
5 months ago
NiLuJe 0f194d88b9 OTAManager: Slightly less cryptic error on connectivity issues
(On either end).

Re: #11258
5 months ago
NiLuJe de3e420c1e DocSettings: Abort early if doc_path is nil or empty in findSidecarFile
Regression from #11020 compared to the previous getSidecarFile behavior
5 months ago
NiLuJe 8968c41070 i18n: Some more unicode escapes => raw bytes shenanigans
Fix #11332
5 months ago
NiLuJe 871ebb0573 Screensaver: nil guard lastfile in cover mode
`hasSidecarFile` assumes you're feeding it a string.
Plus, that check was super funky anyway.

Fix #11336
5 months ago
sonix-github 87915de32b
Fix missing diacritics on first row of Slovak keyboard layout (#11343) 5 months ago
NiLuJe c97d20cd24
Chore: Make sure we always pass a rect to `fb:refreshFull` (#11307)
* UIManager: Init a full Geom on region-less refreshes in _refresh
* Never call refreshFull with no arguments
  I got rid of the low-level nil guards, because UIManager itself guarantees that it can never happen
* Bump base (https://github.com/koreader/koreader-base/pull/1718) (fix #11303)
* Kindle: Re-enable HW dithering on the Scribe
  Now that the underlying issue is fixed in base ;).
5 months ago
NiLuJe 18c17829b7
Input: Simplify input slot storage alloc (#11296)
* Input: Harden setCurrentMtSlotChecked
  The current implementation was assuming that the only case where we
  might be missing slot storage was for the *first* contact point,
  given that ABS_MT_SLOT is (if all goes well) guaranteed to be present
  and come first for every subsequent additional contact points.
  While this works just fine in practice, we can simplify and generalize
  the check by just checking if we've actually recorded the requested
  slot, even if it's not the first contact point.
  The hit check is possibly ever so slightly faster than the length
  computation, to boot.
* Input: Handle snow_protocol devices with newer hardware revisions that do *NOT* need the snow quirks.
  If a sane input frame is detected, the snow quirks will be disabled at runtime, ensuring sane behavior.
  Given the extremely non-standard behavior of the snow quirks, this is fairly easy to detect,
  as a snow device will *never* emit EV_ABS:ABS_MT_TRACKING_ID:-1, so if we catch one, it's not snow ;).
  (We've had reports of this on a Clara HD, FWIW)
5 months ago
zwim 2c33fc6576
Reader: Do less work on same-orientation non-gyro rotations (#11297)
Namely, don't recompute layouts, as they do not change.
(The gyro codepaths were already doing something similar.)

* Keep ConfigDialog, FileManagerMenu & ReaderMenu open on rotation.
  (In practice, only ConfigDialog is affected, as *Menu doesn't handle the rotation event.)
* Plugged an instance leak in the aforementioned Menu classes.
* Unify behavior & code with the gyro codepaths.
5 months ago
hius07 ea9ef6781c
CoverBrowser: adjustable mosaic grid (#11232) 5 months ago

@ -15,7 +15,7 @@ if [ -z "${CIRCLE_PULL_REQUEST}" ] && [ "${CIRCLE_BRANCH}" = 'master' ]; then
if [ "${CIRCLE_NODE_INDEX}" = 0 ]; then
travis_retry make coverage
pushd koreader-*/koreader && {
pushd install/koreader && {
# see https://github.com/codecov/example-lua
bash <(curl -s https://codecov.io/bash)
} && popd || exit

@ -4,4 +4,11 @@ CI_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=/dev/null
source "${CI_DIR}/common.sh"
make all
# Build.
cmd=(make all)
if [[ -d base/build ]]; then
cmd+=(--assume-old=base)
fi
"${cmd[@]}"
# vim: sw=4

@ -60,16 +60,3 @@ retry_cmd() {
set -e
return ${result}
}
# export CI_BUILD_DIR=${TRAVIS_BUILD_DIR}
# use eval to get fully expanded path
eval CI_BUILD_DIR="${CIRCLE_WORKING_DIRECTORY}"
export CI_BUILD_DIR
test -e "${HOME}/bin" || mkdir "${HOME}/bin"
export PATH=${PWD}/bin:${HOME}/bin:${PATH}
export PATH=${PATH}:${CI_BUILD_DIR}/install/bin
if [ -f "${CI_BUILD_DIR}/install/bin/luarocks" ]; then
# add local rocks to $PATH
eval "$(luarocks path --bin)"
fi

@ -1,22 +0,0 @@
#!/usr/bin/env bash
CI_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=/dev/null
source "${CI_DIR}/common.sh"
rm -rf "${HOME}/.luarocks"
mkdir "${HOME}/.luarocks"
cp "${CI_BUILD_DIR}/install/etc/luarocks/config.lua" "${HOME}/.luarocks/config.lua"
echo "wrap_bin_scripts = false" >>"${HOME}/.luarocks/config.lua"
travis_retry luarocks --local install luafilesystem
# for verbose_print module
travis_retry luarocks --local install ansicolors
travis_retry luarocks --local install busted 2.0.0-1
#- mv -f $HOME/.luarocks/bin/busted_bootstrap $HOME/.luarocks/bin/busted
travis_retry luarocks --local install luacheck 0.25.0-1
travis_retry luarocks --local install lanes # for parallel luacheck
# used only on master branch but added to cache for better speed
travis_retry luarocks --local install ldoc
travis_retry luarocks --local install luacov

@ -1,69 +0,0 @@
#!/usr/bin/env bash
CI_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=/dev/null
source "${CI_DIR}/common.sh"
# print some useful info
echo "BUILD_DIR: ${CI_BUILD_DIR}"
echo "pwd: $(pwd)"
ls
# toss submodules if there are any changes
# if [ "$(git status --ignore-submodules=dirty --porcelain)" ]; then
# "--ignore-submodules=dirty", removed temporarily, as it did not notice as
# expected that base was updated and kept using old cached base
if [ "$(git status --ignore-submodules=dirty --porcelain)" ]; then
# what changed?
git status
# purge and reinit submodules
git submodule deinit -f .
git submodule update --init
else
echo -e "${ANSI_GREEN}Using cached submodules."
fi
# install our own updated luarocks
echo "luarocks installation path: ${CI_BUILD_DIR}"
if [ ! -f "${CI_BUILD_DIR}/install/bin/luarocks" ]; then
git clone https://github.com/torch/luajit-rocks.git
pushd luajit-rocks && {
git checkout 6529891
cmake . -DWITH_LUAJIT21=ON -DCMAKE_INSTALL_PREFIX="${CI_BUILD_DIR}/install"
make install
} && popd || exit
else
echo -e "${ANSI_GREEN}Using cached luarocks."
fi
if [ ! -d "${HOME}/.luarocks" ] || [ ! -f "${HOME}/.luarocks/$(md5sum <"${CI_DIR}/helper_luarocks.sh")" ]; then
echo -e "${ANSI_GREEN}Grabbing new .luarocks."
sudo apt-get update
# install openssl devel for luasec
sudo apt-get -y install libssl-dev
"${CI_DIR}/helper_luarocks.sh"
touch "${HOME}/.luarocks/$(md5sum <"${CI_DIR}/helper_luarocks.sh")"
else
echo -e "${ANSI_GREEN}Using cached .luarocks."
fi
#install our own updated shellcheck
SHELLCHECK_VERSION="v0.8.0"
SHELLCHECK_URL="https://github.com/koalaman/shellcheck/releases/download/${SHELLCHECK_VERSION?}/shellcheck-${SHELLCHECK_VERSION?}.linux.x86_64.tar.xz"
if ! command -v shellcheck; then
curl -sSL "${SHELLCHECK_URL}" | tar --exclude 'SHA256SUMS' --strip-components=1 -C "${HOME}/bin" -xJf -
chmod +x "${HOME}/bin/shellcheck"
shellcheck --version
else
echo -e "${ANSI_GREEN}Using cached shellcheck."
fi
# install shfmt
SHFMT_URL="https://github.com/mvdan/sh/releases/download/v3.2.0/shfmt_v3.2.0_linux_amd64"
if [ "$(shfmt --version)" != "v3.2.0" ]; then
curl -sSL "${SHFMT_URL}" -o "${HOME}/bin/shfmt"
chmod +x "${HOME}/bin/shfmt"
else
echo -e "${ANSI_GREEN}Using cached shfmt."
fi

@ -4,9 +4,11 @@ CI_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=/dev/null
source "${CI_DIR}/common.sh"
pushd koreader-emulator-x86_64-linux-gnu/koreader && {
pushd install/koreader && {
# the circleci command spits out newlines; we want spaces instead
BUSTED_SPEC_FILE="$(circleci tests glob "spec/front/unit/*_spec.lua" | circleci tests split --split-by=timings --timings-type=filename | tr '\n' ' ')"
} && popd || exit
make testfront BUSTED_SPEC_FILE="${BUSTED_SPEC_FILE}"
# vim: sw=4

@ -1,112 +1,165 @@
version: 2
version: "2.1"
workflows:
version: 2
build:
jobs:
- build
- docs:
context: koreader-vars
filters:
branches:
only: master
requires:
- build
# Parameters. {{{
jobs:
build:
parameters:
# Bump this to reset all caches.
cache_epoch:
type: integer
default: 0
# }}}
# Executors. {{{
executors:
base:
docker:
- image: koreader/kobase:0.3.0
- image: koreader/kobase:0.3.2-20.04
auth:
username: $DOCKER_USERNAME
password: $DOCKER_PASSWORD
environment:
EMULATE_READER: 1
# }}}
# Jobs. {{{
jobs:
# Build. {{{
build:
executor: base
resource_class: medium
environment:
BASH_ENV: "~/.bashrc"
CCACHE_MAXSIZE: "256M"
CLICOLOR_FORCE: "1"
EMULATE_READER: "1"
MAKEFLAGS: "OUTPUT_DIR=build INSTALL_DIR=install"
parallelism: 2
steps:
# Checkout / fetch. {{{
- checkout
- run:
name: Fetch
command: .ci/fetch.sh
# }}}
# Check.
- run:
name: Check
command: .ci/check.sh
# Restore / setup caches. {{{
- run:
name: Generate cache key
command: make -C base TARGET= cache-key
- restore_cache:
name: Restore build directory
keys:
# binary dependencies require {{ arch }} because there are different CPUs in use on the servers
- deps-{{ arch }}-{{ checksum ".ci/install.sh" }}-{{ checksum ".ci/helper_luarocks.sh" }}
# need to init some stuff first or git will complain when sticking in base cache
- run: git submodule init base && git submodule update base && pushd base && git submodule init && git submodule update && popd
# we can't use command output directly for cache check so we write it to git-rev-base
- run: pushd base && git_rev_base=$(git describe HEAD) && popd && echo $git_rev_base && echo $git_rev_base >git-rev-base
- &CACHE_KEY_BUILD_DIR '<< pipeline.parameters.cache_epoch >>-{{ .Environment.CIRCLE_JOB }}-build-{{ arch }}-{{ checksum "base/cache-key" }}'
- restore_cache:
name: Restore build cache
keys:
- build-{{ arch }}-{{ checksum "git-rev-base" }}
- run: echo 'export PATH=${HOME}/bin:${PATH}' >> $BASH_ENV
# installs and caches testing tools
- run:
name: install
command: .ci/install.sh
- save_cache:
key: deps-{{ arch }}-{{ checksum ".ci/install.sh" }}-{{ checksum ".ci/helper_luarocks.sh" }}
paths:
- "/home/ko/bin"
- "/home/ko/.luarocks"
# compiled luarocks binaries
- "install"
# installs everything and caches base
- &CACHE_KEY_BUILD_CACHE '<< pipeline.parameters.cache_epoch >>-{{ .Environment.CIRCLE_JOB }}-ccache-{{ arch }}-{{ checksum "base/cache-key" }}'
- '<< pipeline.parameters.cache_epoch >>-{{ .Environment.CIRCLE_JOB }}-ccache-{{ arch }}-'
- run:
name: fetch
command: .ci/fetch.sh
name: Setup build cache
command: |
set -x
which ccache
ccache --version
ccache --zero-stats
ccache --show-config
# }}}
# Build.
- run:
name: check
command: .ci/check.sh
- run:
name: build
name: Build
command: .ci/build.sh
# we want to save cache prior to testing so we don't have to clean it up
# Clean / save caches. {{{
# We want to save cache prior to testing so we don't have to clean it up.
- run:
name: Clean caches
when: always
command: |
set -x
# Trim the build directory.
rm -rf base/build/{cmake,staging,thirdparty}
ccache --cleanup >/dev/null
ccache --show-stats
- save_cache:
key: build-{{ arch }}-{{ checksum "git-rev-base" }}
name: Save build cache
key: *CACHE_KEY_BUILD_CACHE
paths:
- "/home/ko/.ccache"
- "base"
# our lovely unit tests
- /home/ko/.ccache
- save_cache:
name: Save build directory
key: *CACHE_KEY_BUILD_DIR
paths:
- base/build
# }}}
# Tests / coverage. {{{
# Our lovely unit tests.
- run:
name: test
name: Test
command: .ci/test.sh
# docs, coverage, and test timing (can we use two outputs at once?); master branch only
# Docs, coverage, and test timing (can we use two outputs at once?); master branch only.
- run:
name: coverage
name: Coverage
command: .ci/after_success.sh
# by storing the test results CircleCI automatically distributes tests based on execution time
# By storing the test results CircleCI automatically distributes tests based on execution time.
- store_test_results:
path: koreader-emulator-x86_64-linux-gnu/koreader
# CircleCI doesn't make the test results available as artifacts (October 2017)
path: &TESTS_XML install/koreader/junit-test-results.xml
# CircleCI doesn't make the test results available as artifacts (October 2017).
- store_artifacts:
path: koreader-emulator-x86_64-linux-gnu/koreader/junit-test-results.xml
path: *TESTS_XML
# }}}
# }}}
# Docs. {{{
docs:
docker:
- image: koreader/kobase:0.3.0
auth:
username: $DOCKER_USERNAME
password: $DOCKER_PASSWORD
environment:
EMULATE_READER: 1
executor: base
resource_class: small
environment:
BASH_ENV: "~/.bashrc"
parallelism: 1
steps:
- checkout
- restore_cache:
keys:
# binary dependencies require {{ arch }} because there are different CPUs in use on the servers
- deps-{{ arch }}-{{ checksum ".ci/install.sh" }}-{{ checksum ".ci/helper_luarocks.sh" }}
# need to init some stuff first or git will complain when sticking in base cache
- run: git submodule init base && git submodule update base && pushd base && git submodule init && git submodule update && popd
# we can't use command output directly for cache check so we write it to git-rev-base
- run: pushd base && git_rev_base=$(git describe HEAD) && popd && echo $git_rev_base && echo $git_rev_base >git-rev-base
- run:
name: init-submodules
command: git submodule init && git submodule sync && git submodule update
name: fetch
command: .ci/fetch.sh
# docs, coverage, and test timing (can we use two outputs at once?); master branch only
- run:
name: docs-and-translation
command: .ci/after_success_docs_translation.sh
# }}}
# }}}
# Workflows. {{{
workflows:
version: 2
build:
jobs:
- build
- docs:
context: koreader-vars
filters:
branches:
only: master
requires:
- build
# }}}
# vim: foldmethod=marker foldlevel=0

@ -1,12 +1,18 @@
name: build
name: macos
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on: [push, pull_request]
permissions:
contents: read
defaults:
run:
shell: bash
jobs:
macos_build:
macos:
# macos-11, macos-12 & macos-13 are broken at this time being.
# https://github.com/koreader/koreader/issues/8686,
@ -17,31 +23,159 @@ jobs:
# 10.15 is no longer supported so we are running 13 just to make sure the build does not break.
runs-on: macos-13
env:
# Bump number to reset all caches.
CACHE_EPOCH: '0'
CLICOLOR_FORCE: '1'
MACOSX_DEPLOYMENT_TARGET: '10.15'
MAKEFLAGS: 'OUTPUT_DIR=build INSTALL_DIR=install TARGET=macos'
steps:
- name: XCode version
run: xcode-select -p
- name: Check out Git repository
# Checkout / fetch. {{{
- name: Checkout
uses: actions/checkout@v4
with:
clean: false
fetch-depth: 0
filter: tree:0
show-progress: false
- name: Fetch
run: make fetchthirdparty
# }}}
# Restore / setup caches. {{{
- name: Generate cache key
run: make -C base TARGET= cache-key
- name: Restore build directory
id: build-restore
uses: actions/cache/restore@v4
with:
path: base/build
key: ${{ env.CACHE_EPOCH }}-${{ runner.os }}-build-${{ hashFiles('base/cache-key') }}
- name: Homebrew install dependencies
# Compared to the README, adds ccache for faster compilation times
# Compared to the emulator, adds p7zip.
- name: Restore build cache
id: ccache-restore
if: steps.build-restore.outputs.cache-hit != 'true'
uses: actions/cache/restore@v4
with:
path: /Users/runner/Library/Caches/ccache
key: ${{ env.CACHE_EPOCH }}-${{ runner.os }}-ccache-${{ hashFiles('base/cache-key') }}
restore-keys: ${{ env.CACHE_EPOCH }}-${{ runner.os }}-ccache-
- name: Install ccache
if: steps.build-restore.outputs.cache-hit != 'true'
run: |
wget --progress=dot:mega https://github.com/ccache/ccache/releases/download/v4.9.1/ccache-4.9.1-darwin.tar.gz
tar xf ccache-4.9.1-darwin.tar.gz
printf '%s\n' "$PWD/ccache-4.9.1-darwin" >>"${GITHUB_PATH}"
- name: Setup build cache
if: steps.build-restore.outputs.cache-hit != 'true'
run: |
set -x
which ccache
ccache --version
ccache --zero-stats
ccache --max-size=256M
ccache --show-config
# }}}
# Install dependencies. {{{
- name: Setup Python
if: steps.build-restore.outputs.cache-hit != 'true'
uses: actions/setup-python@v5
with:
# Note: Python 3.12 removal of `distutils` breaks GLib's build.
python-version: '3.11'
- name: Install homebrew dependencies
# Compared to the README, adds p7zip.
run: |
packages=(
nasm binutils coreutils libtool autoconf automake cmake make
sdl2 lua@5.1 luarocks gettext pkg-config wget
gnu-getopt grep p7zip ninja
)
# Lua 5.1 is disabled, so we need to work around that:
# - fetch all packages
brew fetch "${packages[@]}"
# - disable auto-updates
export HOMEBREW_NO_AUTO_UPDATE=1
# - install lua@5.1 from cache
brew install "$(brew --cache lua@5.1)"
# - and install the rest
brew install "${packages[@]}"
- name: Update PATH
run: >
brew install -q nasm ragel binutils coreutils libtool autoconf automake cmake makedepend
sdl2 lua@5.1 luarocks gettext pkg-config wget gnu-getopt grep bison
ccache p7zip
printf '%s\n'
"$(brew --prefix)/opt/gettext/bin"
"$(brew --prefix)/opt/gnu-getopt/bin"
"$(brew --prefix)/opt/grep/libexec/gnubin"
"$(brew --prefix)/opt/make/libexec/gnubin"
| tee "${GITHUB_PATH}"
# }}}
# Build. {{{
- name: Build
if: steps.build-restore.outputs.cache-hit != 'true'
run: make base
- name: Dump binaries runtime path & dependencies
run: make bindeps
# }}}
# Clean / save caches. {{{
- name: Building in progress…
- name: Clean caches
if: steps.build-restore.outputs.cache-hit != 'true' && always()
run: |
export MACOSX_DEPLOYMENT_TARGET=10.15;
export PATH="$(brew --prefix)/opt/gettext/bin:$(brew --prefix)/opt/gnu-getopt/bin:$(brew --prefix)/opt/bison/bin:$(brew --prefix)/opt/grep/libexec/gnubin:${PATH}";
./kodev release macos
set -x
# Trim the build directory.
rm -rf base/build/{cmake,staging,thirdparty}
ccache --cleanup >/dev/null
ccache --show-stats --verbose
- name: Uploading artifacts
- name: Save build cache
uses: actions/cache/save@v4
if: steps.build-restore.outputs.cache-hit != 'true' && steps.ccache-restore.outputs.cache-hit != 'true'
with:
path: /Users/runner/Library/Caches/ccache
key: ${{ steps.ccache-restore.outputs.cache-primary-key }}
- name: Save build directory
uses: actions/cache/save@v4
if: steps.build-restore.outputs.cache-hit != 'true'
with:
path: base/build
key: ${{ steps.build-restore.outputs.cache-primary-key }}
# }}}
# Generate / upload artifact. {{{
- name: Generate artifact
run: make update --assume-old=base
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: koreader-macos
path: '*.7z'
# }}}
# vim: foldmethod=marker foldlevel=0

1
.gitmodules vendored

@ -18,3 +18,4 @@
[submodule "l10n"]
path = l10n
url = https://github.com/koreader/koreader-translations.git
shallow = true

@ -1,48 +0,0 @@
language: c
# sudo: false
sudo: true
dist: trusty
compiler:
- gcc
env:
global:
- "PATH=${HOME}/bin:${PATH}"
matrix:
- EMULATE_READER=1
cache:
apt: true
directories:
- "${HOME}/bin"
# compiled luarocks binaries
- "${TRAVIS_BUILD_DIR}/install"
# base build
- "${TRAVIS_BUILD_DIR}/base"
- "${HOME}/.ccache"
- "${HOME}/.luarocks"
before_cache:
# don't quote like you normally would or it won't expand
- rm -frv ${TRAVIS_BUILD_DIR}/base/build/*/cache/*
# don't cache unit tests
- rm -frv ${TRAVIS_BUILD_DIR}/base/build/*/spec
addons:
apt:
sources:
- ubuntu-toolchain-r-test
packages:
- g++-4.8
- libsdl1.2-dev
# luasec dependencies
- libssl1.0.0
- nasm
# OpenSSL likes this (package contains makedepend)
- xutils-dev
before_install: .ci/before_install.sh
install: .ci/install.sh
script: .ci/script.sh
after_success: .ci/after_success.sh

@ -1,20 +1,21 @@
PHONY = all android-ndk android-sdk base clean coverage doc fetchthirdparty po pot static-check test testfront
# koreader-base directory
KOR_BASE?=base
# the repository might not have been checked out yet, so make this
# able to fail:
-include $(KOR_BASE)/Makefile.defs
include $(KOR_BASE)/Makefile.defs
RELEASE_DATE := $(shell git show -s --format=format:"%cd" --date=short HEAD)
# We want VERSION to carry the version of the KOReader main repo, not that of koreader-base
VERSION:=$(shell git describe HEAD)
VERSION := $(shell git describe HEAD)
# Only append date if we're not on a whole version, like v2018.11
ifneq (,$(findstring -,$(VERSION)))
VERSION:=$(VERSION)_$(shell git describe HEAD | xargs git show -s --format=format:"%cd" --date=short)
VERSION := $(VERSION)_$(RELEASE_DATE)
endif
# releases do not contain tests and misc data
IS_RELEASE := $(if $(or $(EMULATE_READER),$(WIN32)),,1)
IS_RELEASE := $(if $(or $(IS_RELEASE),$(APPIMAGE),$(DEBIAN),$(MACOS)),1,)
IS_RELEASE := $(if $(or $(IS_RELEASE),$(APPIMAGE),$(LINUX),$(MACOS)),1,)
ifeq ($(ANDROID_ARCH), arm64)
ANDROID_ABI?=arm64-v8a
@ -31,7 +32,18 @@ endif
ANDROID_VERSION?=$(shell git rev-list --count HEAD)
ANDROID_NAME?=$(VERSION)
MACHINE=$(shell $(CC) -dumpmachine 2>/dev/null)
LINUX_ARCH?=native
ifeq ($(LINUX_ARCH), native)
LINUX_ARCH_NAME:=$(shell uname -m)
else ifeq ($(LINUX_ARCH), arm64)
LINUX_ARCH_NAME:=aarch64
else ifeq ($(LINUX_ARCH), arm)
LINUX_ARCH_NAME:=armv7l
endif
LINUX_ARCH_NAME?=$(LINUX_ARCH)
MACHINE=$(TARGET_MACHINE)
ifdef KODEBUG
MACHINE:=$(MACHINE)-debug
KODEDUG_SUFFIX:=-debug
@ -43,72 +55,51 @@ else
DIST:=emulator
endif
INSTALL_DIR=koreader-$(DIST)-$(MACHINE)
INSTALL_DIR ?= koreader-$(DIST)-$(MACHINE)
# platform directories
PLATFORM_DIR=platform
COMMON_DIR=$(PLATFORM_DIR)/common
ANDROID_DIR=$(PLATFORM_DIR)/android
ANDROID_LAUNCHER_DIR:=$(ANDROID_DIR)/luajit-launcher
ANDROID_ASSETS:=$(ANDROID_LAUNCHER_DIR)/assets/module
ANDROID_LIBS_ROOT:=$(ANDROID_LAUNCHER_DIR)/libs
ANDROID_LIBS_ABI:=$(ANDROID_LIBS_ROOT)/$(ANDROID_ABI)
APPIMAGE_DIR=$(PLATFORM_DIR)/appimage
CERVANTES_DIR=$(PLATFORM_DIR)/cervantes
DEBIAN_DIR=$(PLATFORM_DIR)/debian
KINDLE_DIR=$(PLATFORM_DIR)/kindle
KOBO_DIR=$(PLATFORM_DIR)/kobo
MACOS_DIR=$(PLATFORM_DIR)/mac
POCKETBOOK_DIR=$(PLATFORM_DIR)/pocketbook
REMARKABLE_DIR=$(PLATFORM_DIR)/remarkable
SONY_PRSTUX_DIR=$(PLATFORM_DIR)/sony-prstux
UBUNTUTOUCH_DIR=$(PLATFORM_DIR)/ubuntu-touch
UBUNTUTOUCH_SDL_DIR:=$(UBUNTUTOUCH_DIR)/ubuntu-touch-sdl
WIN32_DIR=$(PLATFORM_DIR)/win32
# appimage setup
APPIMAGETOOL=appimagetool-x86_64.AppImage
APPIMAGETOOL_URL=https://github.com/AppImage/AppImageKit/releases/download/12/appimagetool-x86_64.AppImage
# set to 1 if in Docker
DOCKER:=$(shell grep -q docker /proc/1/cgroup 2>/dev/null && echo 1)
# files to link from main directory
INSTALL_FILES=reader.lua setupkoenv.lua frontend resources defaults.lua datastorage.lua \
l10n tools README.md COPYING
all: $(if $(ANDROID),,$(KOR_BASE)/$(OUTPUT_DIR)/luajit)
$(MAKE) -C $(KOR_BASE)
ifeq ($(abspath $(OUTPUT_DIR)),$(OUTPUT_DIR))
ABSOLUTE_OUTPUT_DIR = $(OUTPUT_DIR)
else
ABSOLUTE_OUTPUT_DIR = $(KOR_BASE)/$(OUTPUT_DIR)
endif
OUTPUT_DIR_ARTIFACTS = $(ABSOLUTE_OUTPUT_DIR)/!(cache|cmake|history|staging|thirdparty)
all: base
install -d $(INSTALL_DIR)/koreader
rm -f $(INSTALL_DIR)/koreader/git-rev; echo "$(VERSION)" > $(INSTALL_DIR)/koreader/git-rev
ifdef ANDROID
rm -f android-fdroid-version; echo -e "$(ANDROID_NAME)\n$(ANDROID_VERSION)" > koreader-android-fdroid-latest
endif
ifeq ($(IS_RELEASE),1)
$(RCP) -fL $(KOR_BASE)/$(OUTPUT_DIR)/. $(INSTALL_DIR)/koreader/.
bash -O extglob -c '$(RCP) -fL $(OUTPUT_DIR_ARTIFACTS) $(INSTALL_DIR)/koreader/'
else
cp -f $(KOR_BASE)/ev_replay.py $(INSTALL_DIR)/koreader/
@echo "[*] create symlink instead of copying files in development mode"
cd $(INSTALL_DIR)/koreader && \
bash -O extglob -c "ln -sf ../../$(KOR_BASE)/$(OUTPUT_DIR)/!(cache|history) ."
bash -O extglob -c '$(SYMLINK) $(OUTPUT_DIR_ARTIFACTS) $(INSTALL_DIR)/koreader/'
ifneq (,$(EMULATE_READER))
@echo "[*] install front spec only for the emulator"
cd $(INSTALL_DIR)/koreader/spec && test -e front || \
ln -sf ../../../../spec ./front
cd $(INSTALL_DIR)/koreader/spec/front/unit && test -e data || \
ln -sf ../../test ./data
$(SYMLINK) $(abspath spec) $(INSTALL_DIR)/koreader/spec/front
$(SYMLINK) $(abspath test) $(INSTALL_DIR)/koreader/spec/front/unit/data
endif
endif
for f in $(INSTALL_FILES); do \
ln -sf ../../$$f $(INSTALL_DIR)/koreader/; \
done
$(SYMLINK) $(abspath $(INSTALL_FILES)) $(INSTALL_DIR)/koreader/
ifdef ANDROID
cd $(INSTALL_DIR)/koreader && \
ln -sf ../../$(ANDROID_DIR)/*.lua .
$(SYMLINK) $(abspath $(ANDROID_DIR)/*.lua) $(INSTALL_DIR)/koreader/
endif
@echo "[*] Install update once marker"
@echo "# This file indicates that update once patches have not been applied yet." > $(INSTALL_DIR)/koreader/update_once.marker
ifdef WIN32
@echo "[*] Install runtime libraries for win32..."
cd $(INSTALL_DIR)/koreader && cp ../../$(WIN32_DIR)/*.dll .
$(SYMLINK) $(abspath $(WIN32_DIR)/*.dll) $(INSTALL_DIR)/koreader/
endif
ifdef SHIP_SHARED_STL
@echo "[*] Install C++ runtime..."
@ -117,36 +108,28 @@ ifdef SHIP_SHARED_STL
$(STRIP) --strip-unneeded $(INSTALL_DIR)/koreader/libs/$(notdir $(SHARED_STL_LIB))
endif
@echo "[*] Install plugins"
@# TODO: link istead of cp?
$(RCP) plugins/. $(INSTALL_DIR)/koreader/plugins/.
@# purge deleted plugins
for d in $$(ls $(INSTALL_DIR)/koreader/plugins); do \
test -d plugins/$$d || rm -rf $(INSTALL_DIR)/koreader/plugins/$$d ; done
$(SYMLINK) $(abspath plugins) $(INSTALL_DIR)/koreader/
@echo "[*] Install resources"
$(RCP) -pL resources/fonts/. $(INSTALL_DIR)/koreader/fonts/.
$(SYMLINK) $(abspath resources/fonts/*) $(INSTALL_DIR)/koreader/fonts/
install -d $(INSTALL_DIR)/koreader/{screenshots,data/{dict,tessdata},fonts/host,ota}
ifeq ($(IS_RELEASE),1)
@echo "[*] Clean up, remove unused files for releases"
rm -rf $(INSTALL_DIR)/koreader/data/{cr3.ini,cr3skin-format.txt,desktop,devices,manual}
endif
$(KOR_BASE)/$(OUTPUT_DIR)/luajit:
base:
$(MAKE) -C $(KOR_BASE)
$(INSTALL_DIR)/koreader/.busted: .busted
ln -sf ../../.busted $(INSTALL_DIR)/koreader
$(SYMLINK) $(abspath .busted) $@
$(INSTALL_DIR)/koreader/.luacov:
test -e $(INSTALL_DIR)/koreader/.luacov || \
ln -sf ../../.luacov $(INSTALL_DIR)/koreader
$(SYMLINK) $(abspath .luacov) $@
testfront: $(INSTALL_DIR)/koreader/.busted
# sdr files may have unexpected impact on unit testing
-rm -rf spec/unit/data/*.sdr
cd $(INSTALL_DIR)/koreader && ./luajit $(shell which busted) \
--sort-files \
--output=gtest \
--exclude-tags=notest $(BUSTED_OVERRIDES) $(BUSTED_SPEC_FILE)
cd $(INSTALL_DIR)/koreader && $(BUSTED_LUAJIT) $(BUSTED_OVERRIDES) $(BUSTED_SPEC_FILE)
test: $(INSTALL_DIR)/koreader/.busted
$(MAKE) -C $(KOR_BASE) test
@ -163,10 +146,21 @@ coverage: $(INSTALL_DIR)/koreader/.luacov
+$$(($$(grep -nm1 -e "^Summary$$" luacov.report.out|cut -d: -f1)-1)) \
luacov.report.out
fetchthirdparty:
$(KOR_BASE)/Makefile.defs fetchthirdparty:
git submodule init
git submodule sync
git submodule update
ifneq (,$(CI))
git submodule update --depth 1 --jobs 3
else
# Force shallow clones of submodules configured as such.
git submodule update --jobs 3 --depth 1 $(shell \
git config --file=.gitmodules --name-only --get-regexp '^submodule\.[^.]+\.shallow$$' true \
| sed 's/\.shallow$$/.path/' \
| xargs -n1 git config --file=.gitmodules \
)
# Update the rest.
git submodule update --jobs 3
endif
$(MAKE) -C $(KOR_BASE) fetchthirdparty
VERBOSE ?= @
@ -184,413 +178,10 @@ dist-clean: clean
$(MAKE) -C $(KOR_BASE) dist-clean
$(MAKE) -C doc clean
KINDLE_PACKAGE:=koreader-$(DIST)$(KODEDUG_SUFFIX)-$(VERSION).zip
KINDLE_PACKAGE_OTA:=koreader-$(DIST)$(KODEDUG_SUFFIX)-$(VERSION).targz
ZIP_EXCLUDE=-x "*.swp" -x "*.swo" -x "*.orig" -x "*.un~"
# Don't bundle launchpad on touch devices..
ifeq ($(TARGET), kindle-legacy)
KINDLE_LEGACY_LAUNCHER:=launchpad
# Include target specific rules.
ifneq (,$(wildcard make/$(TARGET).mk))
include make/$(TARGET).mk
endif
kindleupdate: all
# ensure that the binaries were built for ARM
file $(INSTALL_DIR)/koreader/luajit | grep ARM || exit 1
# remove old package if any
rm -f $(KINDLE_PACKAGE)
# Kindle launching scripts
ln -sf ../$(KINDLE_DIR)/extensions $(INSTALL_DIR)/
ln -sf ../$(KINDLE_DIR)/launchpad $(INSTALL_DIR)/
ln -sf ../../$(KINDLE_DIR)/koreader.sh $(INSTALL_DIR)/koreader
ln -sf ../../$(KINDLE_DIR)/libkohelper.sh $(INSTALL_DIR)/koreader
ln -sf ../../../../../$(KINDLE_DIR)/libkohelper.sh $(INSTALL_DIR)/extensions/koreader/bin
ln -sf ../../$(COMMON_DIR)/spinning_zsync $(INSTALL_DIR)/koreader
ln -sf ../../$(KINDLE_DIR)/wmctrl $(INSTALL_DIR)/koreader
# create new package
cd $(INSTALL_DIR) && pwd && \
zip -9 -r \
../$(KINDLE_PACKAGE) \
extensions koreader $(KINDLE_LEGACY_LAUNCHER) \
-x "koreader/resources/fonts/*" "koreader/ota/*" \
"koreader/resources/icons/src/*" "koreader/spec/*" \
$(ZIP_EXCLUDE)
# generate kindleupdate package index file
zipinfo -1 $(KINDLE_PACKAGE) > \
$(INSTALL_DIR)/koreader/ota/package.index
echo "koreader/ota/package.index" >> $(INSTALL_DIR)/koreader/ota/package.index
# update index file in zip package
cd $(INSTALL_DIR) && zip -u ../$(KINDLE_PACKAGE) \
koreader/ota/package.index
# make gzip kindleupdate for zsync OTA update
# note that the targz file extension is intended to keep ISP from caching
# the file, see koreader#1644.
cd $(INSTALL_DIR) && \
tar --hard-dereference -I"gzip --rsyncable" -cah --no-recursion -f ../$(KINDLE_PACKAGE_OTA) \
-T koreader/ota/package.index
KOBO_PACKAGE:=koreader-kobo$(KODEDUG_SUFFIX)-$(VERSION).zip
KOBO_PACKAGE_OTA:=koreader-kobo$(KODEDUG_SUFFIX)-$(VERSION).targz
koboupdate: all
# ensure that the binaries were built for ARM
file $(INSTALL_DIR)/koreader/luajit | grep ARM || exit 1
# remove old package if any
rm -f $(KOBO_PACKAGE)
# Kobo launching scripts
cp $(KOBO_DIR)/koreader.png $(INSTALL_DIR)/koreader.png
cp $(KOBO_DIR)/*.sh $(INSTALL_DIR)/koreader
cp $(COMMON_DIR)/spinning_zsync $(INSTALL_DIR)/koreader
# create new package
cd $(INSTALL_DIR) && \
zip -9 -r \
../$(KOBO_PACKAGE) \
koreader -x "koreader/resources/fonts/*" \
"koreader/resources/icons/src/*" "koreader/spec/*" \
$(ZIP_EXCLUDE)
# generate koboupdate package index file
zipinfo -1 $(KOBO_PACKAGE) > \
$(INSTALL_DIR)/koreader/ota/package.index
echo "koreader/ota/package.index" >> $(INSTALL_DIR)/koreader/ota/package.index
# update index file in zip package
cd $(INSTALL_DIR) && zip -u ../$(KOBO_PACKAGE) \
koreader/ota/package.index koreader.png README_kobo.txt
# make gzip koboupdate for zsync OTA update
cd $(INSTALL_DIR) && \
tar --hard-dereference -I"gzip --rsyncable" -cah --no-recursion -f ../$(KOBO_PACKAGE_OTA) \
-T koreader/ota/package.index
PB_PACKAGE:=koreader-pocketbook$(KODEDUG_SUFFIX)-$(VERSION).zip
PB_PACKAGE_OTA:=koreader-pocketbook$(KODEDUG_SUFFIX)-$(VERSION).targz
pbupdate: all
# ensure that the binaries were built for ARM
file $(INSTALL_DIR)/koreader/luajit | grep ARM || exit 1
# remove old package if any
rm -f $(PB_PACKAGE)
# Pocketbook launching scripts
mkdir -p $(INSTALL_DIR)/applications
mkdir -p $(INSTALL_DIR)/system/bin
cp $(POCKETBOOK_DIR)/koreader.app $(INSTALL_DIR)/applications
cp $(POCKETBOOK_DIR)/system_koreader.app $(INSTALL_DIR)/system/bin/koreader.app
cp $(COMMON_DIR)/spinning_zsync $(INSTALL_DIR)/koreader
cp -rfL $(INSTALL_DIR)/koreader $(INSTALL_DIR)/applications
find $(INSTALL_DIR)/applications/koreader \
-type f \( -name "*.gif" -o -name "*.html" -o -name "*.md" -o -name "*.txt" \) \
-exec rm -vf {} \;
# create new package
cd $(INSTALL_DIR) && \
zip -9 -r \
../$(PB_PACKAGE) \
applications -x "applications/koreader/resources/fonts/*" \
"applications/koreader/resources/icons/src/*" "applications/koreader/spec/*" \
$(ZIP_EXCLUDE)
# generate koboupdate package index file
zipinfo -1 $(PB_PACKAGE) > \
$(INSTALL_DIR)/applications/koreader/ota/package.index
echo "applications/koreader/ota/package.index" >> \
$(INSTALL_DIR)/applications/koreader/ota/package.index
# hack file path when running tar in parent directory of koreader
sed -i -e 's/^/..\//' \
$(INSTALL_DIR)/applications/koreader/ota/package.index
# update index file in zip package
cd $(INSTALL_DIR) && zip -ru ../$(PB_PACKAGE) \
applications/koreader/ota/package.index system
# make gzip pbupdate for zsync OTA update
cd $(INSTALL_DIR)/applications && \
tar --hard-dereference -I"gzip --rsyncable" -cah --no-recursion -f ../../$(PB_PACKAGE_OTA) \
-T koreader/ota/package.index
utupdate: all
# ensure that the binaries were built for ARM
file $(INSTALL_DIR)/koreader/luajit | grep ARM || exit 1
# remove old package if any
rm -f koreader-ubuntu-touch-$(MACHINE)-$(VERSION).click
ln -sf ../../$(UBUNTUTOUCH_DIR)/koreader.sh $(INSTALL_DIR)/koreader
ln -sf ../../$(UBUNTUTOUCH_DIR)/manifest.json $(INSTALL_DIR)/koreader
ln -sf ../../$(UBUNTUTOUCH_DIR)/koreader.apparmor $(INSTALL_DIR)/koreader
ln -sf ../../$(UBUNTUTOUCH_DIR)/koreader.apparmor.openstore $(INSTALL_DIR)/koreader
ln -sf ../../$(UBUNTUTOUCH_DIR)/koreader.desktop $(INSTALL_DIR)/koreader
ln -sf ../../$(UBUNTUTOUCH_DIR)/koreader.png $(INSTALL_DIR)/koreader
ln -sf ../../../$(UBUNTUTOUCH_DIR)/libSDL2.so $(INSTALL_DIR)/koreader/libs
# create new package
cd $(INSTALL_DIR) && pwd && \
zip -9 -r \
../koreader-$(DIST)-$(MACHINE)-$(VERSION).zip \
koreader -x "koreader/resources/fonts/*" "koreader/ota/*" \
"koreader/resources/icons/src/*" "koreader/spec/*" \
$(ZIP_EXCLUDE)
# generate ubuntu touch click package
rm -rf $(INSTALL_DIR)/tmp && mkdir -p $(INSTALL_DIR)/tmp
cd $(INSTALL_DIR)/tmp && \
unzip ../../koreader-$(DIST)-$(MACHINE)-$(VERSION).zip && \
click build koreader && \
mv *.click ../../koreader-$(DIST)-$(MACHINE)-$(VERSION).click
appimageupdate: all
# remove old package if any
rm -f koreader-appimage-$(MACHINE)-$(VERSION).appimage
ln -sf ../../$(APPIMAGE_DIR)/AppRun $(INSTALL_DIR)/koreader
ln -sf ../../$(APPIMAGE_DIR)/koreader.appdata.xml $(INSTALL_DIR)/koreader
ln -sf ../../$(APPIMAGE_DIR)/koreader.desktop $(INSTALL_DIR)/koreader
ln -sf ../../resources/koreader.png $(INSTALL_DIR)/koreader
# TODO at best this is DebUbuntu specific
ln -sf /usr/lib/x86_64-linux-gnu/libSDL2-2.0.so.0 $(INSTALL_DIR)/koreader/libs/libSDL2.so
# required for our stock Ubuntu SDL even though we don't use sound
# the readlink is a half-hearted attempt at being generic; the echo libsndio.so.6.1 is specific to the nightly builds
ln -sf /usr/lib/x86_64-linux-gnu/$(shell readlink /usr/lib/x86_64-linux-gnu/libsndio.so || echo libsndio.so.6.1) $(INSTALL_DIR)/koreader/libs/
# also copy libbsd.so.0, cf. https://github.com/koreader/koreader/issues/4627
ln -sf /lib/x86_64-linux-gnu/libbsd.so.0 $(INSTALL_DIR)/koreader/libs/
ifeq ("$(wildcard $(APPIMAGETOOL))","")
# download appimagetool
wget "$(APPIMAGETOOL_URL)"
chmod a+x "$(APPIMAGETOOL)"
endif
ifeq ($(DOCKER), 1)
# remove previously extracted appimagetool, if any
rm -rf squashfs-root
./$(APPIMAGETOOL) --appimage-extract
endif
cd $(INSTALL_DIR) && pwd && \
rm -rf tmp && mkdir -p tmp && \
cp -Lr koreader tmp && \
rm -rf tmp/koreader/ota && \
rm -rf tmp/koreader/resources/icons/src && \
rm -rf tmp/koreader/spec
# generate AppImage
cd $(INSTALL_DIR)/tmp && \
ARCH=x86_64 ../../$(if $(DOCKER),squashfs-root/AppRun,$(APPIMAGETOOL)) koreader && \
mv *.AppImage ../../koreader-$(DIST)-$(MACHINE)-$(VERSION).AppImage
androidupdate: all
# Note: do not remove the module directory so there's no need
# for `mk7z.sh` to always recreate `assets.7z` from scratch.
rm -rfv $(ANDROID_LIBS_ROOT)
mkdir -p $(ANDROID_ASSETS) $(ANDROID_LIBS_ABI)
# APK version
echo $(VERSION) > $(ANDROID_ASSETS)/version.txt
# shared libraries are stored as raw assets
cp -pR $(INSTALL_DIR)/koreader/libs $(ANDROID_LAUNCHER_DIR)/assets
# in runtime luajit-launcher's libluajit.so will be loaded
rm -vf $(ANDROID_LAUNCHER_DIR)/assets/libs/libluajit.so
# binaries are stored as shared libraries to prevent W^X exception on Android 10+
# https://developer.android.com/about/versions/10/behavior-changes-10#execute-permission
cp -pR $(INSTALL_DIR)/koreader/sdcv $(ANDROID_LIBS_ABI)/libsdcv.so
echo "sdcv libsdcv.so" > $(ANDROID_ASSETS)/map.txt
# assets are compressed manually and stored inside the APK.
cd $(INSTALL_DIR)/koreader && \
./tools/mk7z.sh \
../../$(ANDROID_ASSETS)/koreader.7z \
"$$(git show -s --format='%ci')" \
-m0=lzma2 -mx=9 \
-- . \
'-x!cache' \
'-x!clipboard' \
'-x!data/dict' \
'-x!data/tessdata' \
'-x!history' \
'-x!l10n/templates' \
'-x!libs' \
'-x!ota' \
'-x!resources/fonts*' \
'-x!resources/icons/src*' \
'-x!rocks/bin' \
'-x!rocks/lib/luarocks' \
'-x!screenshots' \
'-x!sdcv' \
'-x!spec' \
'-x!tools' \
'-xr!.*' \
'-xr!COPYING' \
'-xr!NOTES.txt' \
'-xr!NOTICE' \
'-xr!README.md' \
;
# make the android APK
# Note: filter out the `--debug=…` make flag
# so the old crummy version provided by the
# NDK does not blow a gasket.
MAKEFLAGS='$(filter-out --debug=%,$(MAKEFLAGS))' \
$(MAKE) -C $(ANDROID_LAUNCHER_DIR) $(if $(KODEBUG), debug, release) \
ANDROID_APPNAME=KOReader \
ANDROID_VERSION=$(ANDROID_VERSION) \
ANDROID_NAME=$(ANDROID_NAME) \
ANDROID_FLAVOR=$(ANDROID_FLAVOR)
cp $(ANDROID_LAUNCHER_DIR)/bin/NativeActivity.apk \
koreader-android-$(ANDROID_ARCH)$(KODEDUG_SUFFIX)-$(VERSION).apk
debianupdate: all
mkdir -pv \
$(INSTALL_DIR)/debian/usr/bin \
$(INSTALL_DIR)/debian/usr/lib \
$(INSTALL_DIR)/debian/usr/share/pixmaps \
$(INSTALL_DIR)/debian/usr/share/applications \
$(INSTALL_DIR)/debian/usr/share/doc/koreader \
$(INSTALL_DIR)/debian/usr/share/man/man1
cp -pv resources/koreader.png $(INSTALL_DIR)/debian/usr/share/pixmaps
cp -pv $(DEBIAN_DIR)/koreader.desktop $(INSTALL_DIR)/debian/usr/share/applications
cp -pv $(DEBIAN_DIR)/copyright COPYING $(INSTALL_DIR)/debian/usr/share/doc/koreader
cp -pv $(DEBIAN_DIR)/koreader.sh $(INSTALL_DIR)/debian/usr/bin/koreader
cp -Lr $(INSTALL_DIR)/koreader $(INSTALL_DIR)/debian/usr/lib
gzip -cn9 $(DEBIAN_DIR)/changelog > $(INSTALL_DIR)/debian/usr/share/doc/koreader/changelog.Debian.gz
gzip -cn9 $(DEBIAN_DIR)/koreader.1 > $(INSTALL_DIR)/debian/usr/share/man/man1/koreader.1.gz
chmod 644 \
$(INSTALL_DIR)/debian/usr/share/doc/koreader/changelog.Debian.gz \
$(INSTALL_DIR)/debian/usr/share/doc/koreader/copyright \
$(INSTALL_DIR)/debian/usr/share/man/man1/koreader.1.gz
rm -rf \
$(INSTALL_DIR)/debian/usr/lib/koreader/{ota,cache,clipboard,screenshots,spec,tools,resources/fonts,resources/icons/src}
macosupdate: all
mkdir -p \
$(INSTALL_DIR)/bundle/Contents/MacOS \
$(INSTALL_DIR)/bundle/Contents/Resources
cp -pv $(MACOS_DIR)/koreader.icns $(INSTALL_DIR)/bundle/Contents/Resources/icon.icns
cp -LR $(INSTALL_DIR)/koreader $(INSTALL_DIR)/bundle/Contents
cp -pRv $(MACOS_DIR)/menu.xml $(INSTALL_DIR)/bundle/Contents/MainMenu.xib
ibtool --compile "$(INSTALL_DIR)/bundle/Contents/Resources/Base.lproj/MainMenu.nib" "$(INSTALL_DIR)/bundle/Contents/MainMenu.xib"
rm -rfv "$(INSTALL_DIR)/bundle/Contents/MainMenu.xib"
REMARKABLE_PACKAGE:=koreader-remarkable$(KODEDUG_SUFFIX)-$(VERSION).zip
REMARKABLE_PACKAGE_OTA:=koreader-remarkable$(KODEDUG_SUFFIX)-$(VERSION).targz
remarkableupdate: all
# ensure that the binaries were built for ARM
file $(INSTALL_DIR)/koreader/luajit | grep ARM || exit 1
# remove old package if any
rm -f $(REMARKABLE_PACKAGE)
# Remarkable scripts
cp $(REMARKABLE_DIR)/* $(INSTALL_DIR)/koreader
cp $(COMMON_DIR)/spinning_zsync $(INSTALL_DIR)/koreader
# create new package
cd $(INSTALL_DIR) && \
zip -9 -r \
../$(REMARKABLE_PACKAGE) \
koreader -x "koreader/resources/fonts/*" \
"koreader/resources/icons/src/*" "koreader/spec/*" \
$(ZIP_EXCLUDE)
# generate update package index file
zipinfo -1 $(REMARKABLE_PACKAGE) > \
$(INSTALL_DIR)/koreader/ota/package.index
echo "koreader/ota/package.index" >> $(INSTALL_DIR)/koreader/ota/package.index
# update index file in zip package
cd $(INSTALL_DIR) && zip -u ../$(REMARKABLE_PACKAGE) \
koreader/ota/package.index
# make gzip remarkable update for zsync OTA update
cd $(INSTALL_DIR) && \
tar -I"gzip --rsyncable" -cah --no-recursion -f ../$(REMARKABLE_PACKAGE_OTA) \
-T koreader/ota/package.index
SONY_PRSTUX_PACKAGE:=koreader-sony-prstux$(KODEDUG_SUFFIX)-$(VERSION).zip
SONY_PRSTUX_PACKAGE_OTA:=koreader-sony-prstux$(KODEDUG_SUFFIX)-$(VERSION).targz
sony-prstuxupdate: all
# ensure that the binaries were built for ARM
file $(INSTALL_DIR)/koreader/luajit | grep ARM || exit 1
# remove old package if any
rm -f $(SONY_PRSTUX_PACKAGE)
# Sony PRSTUX launching scripts
cp $(SONY_PRSTUX_DIR)/*.sh $(INSTALL_DIR)/koreader
# create new package
cd $(INSTALL_DIR) && \
zip -9 -r \
../$(SONY_PRSTUX_PACKAGE) \
koreader -x "koreader/resources/fonts/*" \
"koreader/resources/icons/src/*" "koreader/spec/*" \
$(ZIP_EXCLUDE)
# generate update package index file
zipinfo -1 $(SONY_PRSTUX_PACKAGE) > \
$(INSTALL_DIR)/koreader/ota/package.index
echo "koreader/ota/package.index" >> $(INSTALL_DIR)/koreader/ota/package.index
# update index file in zip package
cd $(INSTALL_DIR) && zip -u ../$(SONY_PRSTUX_PACKAGE) \
koreader/ota/package.index
# make gzip sonyprstux update for zsync OTA update
cd $(INSTALL_DIR) && \
tar --hard-dereference -I"gzip --rsyncable" -cah --no-recursion -f ../$(SONY_PRSTUX_PACKAGE_OTA) \
-T koreader/ota/package.index
CERVANTES_PACKAGE:=koreader-cervantes$(KODEDUG_SUFFIX)-$(VERSION).zip
CERVANTES_PACKAGE_OTA:=koreader-cervantes$(KODEDUG_SUFFIX)-$(VERSION).targz
cervantesupdate: all
# ensure that the binaries were built for ARM
file $(INSTALL_DIR)/koreader/luajit | grep ARM || exit 1
# remove old package if any
rm -f $(CERVANTES_PACKAGE)
# Cervantes launching scripts
cp $(COMMON_DIR)/spinning_zsync $(INSTALL_DIR)/koreader/spinning_zsync.sh
cp $(CERVANTES_DIR)/*.sh $(INSTALL_DIR)/koreader
cp $(CERVANTES_DIR)/spinning_zsync $(INSTALL_DIR)/koreader
# create new package
cd $(INSTALL_DIR) && \
zip -9 -r \
../$(CERVANTES_PACKAGE) \
koreader -x "koreader/resources/fonts/*" \
"koreader/resources/icons/src/*" "koreader/spec/*" \
$(ZIP_EXCLUDE)
# generate update package index file
zipinfo -1 $(CERVANTES_PACKAGE) > \
$(INSTALL_DIR)/koreader/ota/package.index
echo "koreader/ota/package.index" >> $(INSTALL_DIR)/koreader/ota/package.index
# update index file in zip package
cd $(INSTALL_DIR) && zip -u ../$(CERVANTES_PACKAGE) \
koreader/ota/package.index
# make gzip cervantes update for zsync OTA update
cd $(INSTALL_DIR) && \
tar --hard-dereference -I"gzip --rsyncable" -cah --no-recursion -f ../$(CERVANTES_PACKAGE_OTA) \
-T koreader/ota/package.index
update:
ifeq ($(TARGET), android)
make androidupdate
else ifeq ($(TARGET), appimage)
make appimageupdate
else ifeq ($(TARGET), cervantes)
make cervantesupdate
else ifeq ($(TARGET), kindle)
make kindleupdate
else ifeq ($(TARGET), kindle-legacy)
make kindleupdate
else ifeq ($(TARGET), kindlepw2)
make kindleupdate
else ifeq ($(TARGET), kobo)
make koboupdate
else ifeq ($(TARGET), pocketbook)
make pbupdate
else ifeq ($(TARGET), sony-prstux)
make sony-prstuxupdate
else ifeq ($(TARGET), remarkable)
make remarkableupdate
else ifeq ($(TARGET), ubuntu-touch)
make utupdate
else ifeq ($(TARGET), debian)
make debianupdate
$(CURDIR)/platform/debian/do_debian_package.sh $(INSTALL_DIR)
else ifeq ($(TARGET), debian-armel)
make debianupdate
$(CURDIR)/platform/debian/do_debian_package.sh $(INSTALL_DIR) armel
else ifeq ($(TARGET), debian-armhf)
make debianupdate
$(CURDIR)/platform/debian/do_debian_package.sh $(INSTALL_DIR) armhf
else ifeq ($(TARGET), debian-arm64)
make debianupdate
$(CURDIR)/platform/debian/do_debian_package.sh $(INSTALL_DIR) arm64
else ifeq ($(TARGET), macos)
make macosupdate
$(CURDIR)/platform/mac/do_mac_bundle.sh $(INSTALL_DIR)
endif
androiddev: androidupdate
$(MAKE) -C $(ANDROID_LAUNCHER_DIR) dev
android-ndk:
$(MAKE) -C $(KOR_BASE)/toolchain $(ANDROID_NDK_HOME)
@ -598,7 +189,6 @@ android-ndk:
android-sdk:
$(MAKE) -C $(KOR_BASE)/toolchain $(ANDROID_HOME)
# for gettext
DOMAIN=koreader
TEMPLATE_DIR=l10n/templates
@ -629,4 +219,10 @@ static-check:
doc:
make -C doc
.PHONY: all clean doc test update
.NOTPARALLEL:
.PHONY: $(PHONY)
LEFTOVERS = $(filter-out $(PHONY) $(INSTALL_DIR)/%,$(MAKECMDGOALS))
.PHONY: $(LEFTOVERS)
$(LEFTOVERS):
$(MAKE) -C $(KOR_BASE) $@

@ -71,7 +71,7 @@ KOReader is developed and supported by volunteers all around the world. There ar
- document lesser-known features on the [wiki][link-wiki]
- help others with your knowledge on the [forum][link-forum]
Right now we only support [liberapay](https://liberapay.com/KOReader) donations, but you can also create a [bounty][link-bountysource] to motivate others to work on a specific bug or feature request.
Right now we only support [liberapay](https://liberapay.com/KOReader) donations.
## Contributors

@ -1 +1 @@
Subproject commit d83148b27e25b9332678221f6737d66be7e73318
Subproject commit a6e112a5ee06e6939203accbd0736134af8939cd

@ -17,7 +17,7 @@ function DataStorage:getDataDir()
local package_name = app_id:match("^(.-)_")
-- confined ubuntu app has write access to this dir
data_dir = string.format("%s/%s", os.getenv("XDG_DATA_HOME"), package_name)
elseif os.getenv("APPIMAGE") or os.getenv("KO_MULTIUSER") then
elseif os.getenv("APPIMAGE") or os.getenv("FLATPAK") or os.getenv("KO_MULTIUSER") then
if os.getenv("XDG_CONFIG_HOME") then
data_dir = string.format("%s/%s", os.getenv("XDG_CONFIG_HOME"), "koreader")
if lfs.attributes(os.getenv("XDG_CONFIG_HOME"), "mode") ~= "directory" then
@ -77,6 +77,7 @@ local function initDataDir()
"data/dict",
"data/tessdata",
-- "docsettings", -- created when needed
-- "hashdocsettings", -- created when needed
-- "history", -- legacy/obsolete sidecar files
"ota",
-- "patches", -- must be created manually by the interested user

@ -13,7 +13,7 @@ You can skip most of the following instructions if desired, and use our premade
To get and compile the source you must have `patch`, `wget`, `unzip`, `git`,
`cmake` and `luarocks` installed, as well as a version of `autoconf`
greater than 2.64. You also need `nasm`, `ragel`, and of course a compiler like `gcc`
greater than 2.64. You also need `nasm`, and of course a compiler like `gcc`
or `clang`.
### Debian/Ubuntu and derivates
@ -22,7 +22,7 @@ Install the prerequisites using APT:
```
sudo apt-get install build-essential git patch wget unzip \
gettext autoconf automake cmake libtool libtool-bin nasm ragel luarocks lua5.1 libsdl2-dev \
gettext autoconf automake cmake libtool libtool-bin nasm luarocks lua5.1 libsdl2-dev \
libssl-dev libffi-dev libc6-dev-i386 xutils-dev linux-libc-dev:i386 zlib1g:i386
```
@ -31,7 +31,7 @@ libssl-dev libffi-dev libc6-dev-i386 xutils-dev linux-libc-dev:i386 zlib1g:i386
Install the prerequisites using DNF:
```
sudo dnf install libstdc++-static SDL SDL-devel patch wget unzip git cmake luarocks autoconf nasm ragel gcc
sudo dnf install libstdc++-static SDL SDL-devel patch wget unzip git cmake luarocks autoconf nasm gcc
```
### macOS
@ -39,7 +39,7 @@ sudo dnf install libstdc++-static SDL SDL-devel patch wget unzip git cmake luaro
Install the prerequisites using [Homebrew](https://brew.sh/):
```
brew install nasm ragel binutils coreutils libtool autoconf automake cmake makedepend \
brew install nasm binutils coreutils libtool autoconf automake cmake makedepend \
sdl2 lua@5.1 luarocks gettext pkg-config wget gnu-getopt grep bison
```

@ -17,10 +17,10 @@ Each target has its own architecture and you'll need to setup a proper cross-com
A compatible version of the Android NDK and SDK will be downloaded automatically by `./kodev release android` if no NDK or SDK is provided in environment variables. For that purpose you can use:
```
NDK=/ndk/location SDK=/sdk/location ./kodev release android
ANDROID_NDK_HOME=/ndk/location ANDROID_HOME=/sdk/location ./kodev release android
```
If you want to use your own installed tools please make sure that you have the **NDKr15c** and the SDK for Android 9 (**API level 28**) already installed.
If you want to use your own installed tools please make sure that you have the **NDKr23c** and the SDK for Android 9 (**API level 28**) already installed.
#### for embedded linux devices

@ -1,7 +1,6 @@
local BD = require("ui/bidi")
local Blitbuffer = require("ffi/blitbuffer")
local ButtonDialog = require("ui/widget/buttondialog")
local CenterContainer = require("ui/widget/container/centercontainer")
local CheckButton = require("ui/widget/checkbutton")
local ConfirmBox = require("ui/widget/confirmbox")
local Device = require("device")
@ -24,6 +23,7 @@ local InputDialog = require("ui/widget/inputdialog")
local LanguageSupport = require("languagesupport")
local Menu = require("ui/widget/menu")
local MultiConfirmBox = require("ui/widget/multiconfirmbox")
local NetworkListener = require("ui/network/networklistener")
local PluginLoader = require("pluginloader")
local ReadCollection = require("readcollection")
local ReaderDeviceStatus = require("apps/reader/modules/readerdevicestatus")
@ -171,32 +171,35 @@ function FileManager:setupLayout()
return true
end
function file_chooser:onFileSelect(file) -- luacheck: ignore
function file_chooser:onFileSelect(item) -- luacheck: ignore
local file = item.path
if file_manager.select_mode then
if file_manager.selected_files[file] then
file_manager.selected_files[file] = nil
item.dim = nil
else
file_manager.selected_files[file] = true
item.dim = true
end
self:refreshPath()
self:updateItems()
else
file_manager:openFile(file)
end
return true
end
function file_chooser:onFileHold(file)
function file_chooser:onFileHold(item)
if file_manager.select_mode then
file_manager:tapPlus()
else
self:showFileDialog(file)
self:showFileDialog(item)
end
end
function file_chooser:showFileDialog(file) -- luacheck: ignore
local is_file = isFile(file)
local is_folder = lfs.attributes(file, "mode") == "directory"
local is_not_parent_folder = BaseUtil.basename(file) ~= ".."
function file_chooser:showFileDialog(item) -- luacheck: ignore
local file = item.path
local is_file = item.is_file
local is_not_parent_folder = not item.is_go_up
local function close_dialog_callback()
UIManager:close(self.file_dialog)
@ -234,7 +237,8 @@ function FileManager:setupLayout()
file_manager:onToggleSelectMode(true) -- no full screen refresh
if is_file then
file_manager.selected_files[file] = true
self:refreshPath()
item.dim = true
self:updateItems()
end
end,
},
@ -269,13 +273,27 @@ function FileManager:setupLayout()
}
if is_file then
self.book_props = nil -- in 'self' to provide access to it in CoverBrowser
local has_provider = DocumentRegistry:hasProvider(file)
if has_provider or DocSettings:hasSidecarFile(file) then
table.insert(buttons, filemanagerutil.genStatusButtonsRow(file, close_dialog_refresh_callback))
local has_sidecar = DocSettings:hasSidecarFile(file)
if has_provider or has_sidecar then
self.book_props = file_manager.coverbrowser and file_manager.coverbrowser:getBookInfo(file)
local doc_settings_or_file
if has_sidecar then
doc_settings_or_file = DocSettings:open(file)
if not self.book_props then
local props = doc_settings_or_file:readSetting("doc_props")
self.book_props = FileManagerBookInfo.extendProps(props, file)
self.book_props.has_cover = true -- to enable "Book cover" button, we do not know if cover exists
end
else
doc_settings_or_file = file
end
table.insert(buttons, filemanagerutil.genStatusButtonsRow(doc_settings_or_file, close_dialog_refresh_callback))
table.insert(buttons, {}) -- separator
table.insert(buttons, {
filemanagerutil.genResetSettingsButton(file, close_dialog_refresh_callback),
filemanagerutil.genAddRemoveFavoritesButton(file, close_dialog_callback),
filemanagerutil.genResetSettingsButton(doc_settings_or_file, close_dialog_refresh_callback),
file_manager.collections:genAddToCollectionButton(file, close_dialog_callback, refresh_callback),
})
end
table.insert(buttons, {
@ -286,12 +304,12 @@ function FileManager:setupLayout()
file_manager:showOpenWithDialog(file)
end,
},
filemanagerutil.genBookInformationButton(file, close_dialog_callback),
filemanagerutil.genBookInformationButton(file, self.book_props, close_dialog_callback),
})
if has_provider then
table.insert(buttons, {
filemanagerutil.genBookCoverButton(file, close_dialog_callback),
filemanagerutil.genBookDescriptionButton(file, close_dialog_callback),
filemanagerutil.genBookCoverButton(file, self.book_props, close_dialog_callback),
filemanagerutil.genBookDescriptionButton(file, self.book_props, close_dialog_callback),
})
end
if Device:canExecuteScript(file) then
@ -310,9 +328,7 @@ function FileManager:setupLayout()
},
})
end
end
if is_folder then
else -- folder
local folder = BaseUtil.realpath(file)
table.insert(buttons, {
{
@ -364,6 +380,9 @@ function FileManager:registerKeyEvents()
self.key_events.Home = { { "Home" } }
-- Override the menu.lua way of handling the back key
self.file_chooser.key_events.Back = { { Device.input.group.Back } }
if Device:hasScreenKB() then
self.key_events.KeyToggleWifi = { { "ScreenKB", "Home" }, event = "ToggleWifi" }
end
if not Device:hasFewKeys() then
-- Also remove the handler assigned to the "Back" key by menu.lua
self.file_chooser.key_events.Close = nil
@ -408,6 +427,7 @@ function FileManager:init()
self:registerModule("wikipedia", ReaderWikipedia:new{ ui = self })
self:registerModule("devicestatus", ReaderDeviceStatus:new{ ui = self })
self:registerModule("devicelistener", DeviceListener:new{ ui = self })
self:registerModule("networklistener", NetworkListener:new{ ui = self })
-- koreader plugins
for _, plugin_module in ipairs(PluginLoader:loadPlugins()) do
@ -423,11 +443,6 @@ function FileManager:init()
end
end
if Device:hasWifiToggle() then
local NetworkListener = require("ui/network/networklistener")
table.insert(self, NetworkListener:new{ ui = self })
end
self:initGesListener()
self:handleEvent(Event:new("SetDimensions", self.dimen))
@ -500,11 +515,17 @@ function FileManager:tapPlus()
local title, buttons
if self.select_mode then
local function toggle_select_mode_callback()
self:onToggleSelectMode()
end
local select_count = util.tableSize(self.selected_files)
local actions_enabled = select_count > 0
title = actions_enabled and T(N_("1 file selected", "%1 files selected", select_count), select_count)
or _("No files selected")
buttons = {
{
self.collections:genAddToCollectionButton(self.selected_files, close_dialog_callback, toggle_select_mode_callback),
},
{
{
text = _("Show selected files list"),
@ -528,8 +549,7 @@ function FileManager:tapPlus()
text = _("Select all files in folder"),
callback = function()
UIManager:close(self.file_dialog)
self.file_chooser:selectAllFilesInFolder()
self:onRefresh()
self.file_chooser:selectAllFilesInFolder(true)
end,
},
{
@ -550,7 +570,7 @@ function FileManager:tapPlus()
for file in pairs (self.selected_files) do
self.selected_files[file] = nil
end
self:onRefresh()
self.file_chooser:selectAllFilesInFolder(false) -- undim
end,
},
{
@ -711,7 +731,7 @@ function FileManager:reinit(path, focused_file)
UIManager:flushSettings()
self.dimen = Screen:getSize()
-- backup the root path and path items
self.root_path = path or self.file_chooser.path
self.root_path = BaseUtil.realpath(path or self.file_chooser.path)
local path_items_backup = {}
for k, v in pairs(self.file_chooser.path_items) do
path_items_backup[k] = v
@ -837,6 +857,10 @@ function FileManager:pasteFileFromClipboard(file)
local dest_path = BaseUtil.realpath(file or self.file_chooser.path)
dest_path = isFile(dest_path) and dest_path:match("(.*/)") or dest_path
local dest_file = BaseUtil.joinPath(dest_path, orig_name)
if orig_file == dest_file or orig_file == dest_path then -- do not paste to itself
self.clipboard = nil
return
end
local is_file = isFile(orig_file)
local function doPaste()
@ -928,19 +952,23 @@ function FileManager:pasteSelectedFiles(overwrite)
for orig_file in pairs(self.selected_files) do
local orig_name = BaseUtil.basename(orig_file)
local dest_file = BaseUtil.joinPath(dest_path, orig_name)
local ok
local dest_mode = lfs.attributes(dest_file, "mode")
if not dest_mode or (dest_mode == "file" and overwrite) then
if self.cutfile then
ok = self:moveFile(orig_file, dest_path)
else
ok = self:copyRecursive(orig_file, dest_path)
end
end
if ok then
DocSettings.updateLocation(orig_file, dest_file, not self.cutfile)
ok_files[orig_file] = true
if BaseUtil.realpath(orig_file) == dest_file then -- do not paste to itself
self.selected_files[orig_file] = nil
else
local ok
local dest_mode = lfs.attributes(dest_file, "mode")
if not dest_mode or (dest_mode == "file" and overwrite) then
if self.cutfile then
ok = self:moveFile(orig_file, dest_path)
else
ok = self:copyRecursive(orig_file, dest_path)
end
end
if ok then
DocSettings.updateLocation(orig_file, dest_file, not self.cutfile)
ok_files[orig_file] = true
self.selected_files[orig_file] = nil
end
end
end
local skipped_nb = util.tableSize(self.selected_files)
@ -1188,7 +1216,7 @@ function FileManager:showFiles(path, focused_file)
FileManager.instance:onClose()
end
path = path or G_reader_settings:readSetting("lastdir") or filemanagerutil.getDefaultDir()
path = BaseUtil.realpath(path or G_reader_settings:readSetting("lastdir") or filemanagerutil.getDefaultDir())
G_reader_settings:saveSetting("lastdir", path)
self:setRotationMode()
local file_manager = FileManager:new{
@ -1314,25 +1342,22 @@ function FileManager:showSelectedFilesList()
end
table.sort(selected_files, sorting)
local menu_container = CenterContainer:new{
dimen = Screen:getSize(),
}
local menu = Menu:new{
local menu
menu = Menu:new{
title = T(_("Selected files (%1)"), #selected_files),
item_table = selected_files,
is_borderless = true,
is_popout = false,
truncate_left = true,
show_parent = menu_container,
onMenuSelect = function(_, item)
UIManager:close(menu_container)
UIManager:close(menu)
self.file_chooser:changeToPath(util.splitFilePathName(item.filepath), item.filepath)
end,
close_callback = function()
UIManager:close(menu_container)
UIManager:close(menu)
end,
}
table.insert(menu_container, menu)
menu:switchItemTable(T(_("Selected files (%1)"), #selected_files), selected_files)
UIManager:show(menu_container)
UIManager:show(menu)
end
function FileManager:showOpenWithDialog(file)
@ -1412,13 +1437,14 @@ function FileManager:showOpenWithDialog(file)
end
end
local t = {}
for extension, provider_key in BaseUtil.orderedPairs(associated_providers) do
for extension, provider_key in pairs(associated_providers) do
local provider = DocumentRegistry:getProviderFromKey(provider_key)
if provider then
local space = string.rep(" ", max_len - #extension)
table.insert(t, T("%1%2: %3", extension, space, provider.provider_name))
end
end
table.sort(t)
UIManager:show(InfoMessage:new{
text = table.concat(t, "\n"),
monospace_font = true,
@ -1477,9 +1503,8 @@ function FileManager:showOpenWithDialog(file)
end
function FileManager:openFile(file, provider, doc_caller_callback, aux_caller_callback)
if not provider then -- check associated
local provider_key = DocumentRegistry:getAssociatedProviderKey(file)
provider = provider_key and DocumentRegistry:getProviderFromKey(provider_key)
if provider == nil then
provider = DocumentRegistry:getProvider(file, true) -- include auxiliary
end
if provider and provider.order then -- auxiliary
if aux_caller_callback then
@ -1499,4 +1524,67 @@ function FileManager:openFile(file, provider, doc_caller_callback, aux_caller_ca
end
end
-- Dispatcher helpers
function FileManager.getDisplayModeActions()
local action_names, action_texts = { "classic" }, { _("Classic (filename only)") }
local ui = FileManager.instance or require("apps/reader/readerui").instance
if ui.coverbrowser then
for _, v in ipairs(ui.coverbrowser.modes) do
local action_text, action_name = unpack(v)
if action_name then -- skip Classic
table.insert(action_names, action_name)
table.insert(action_texts, action_text)
end
end
end
return action_names, action_texts
end
function FileManager:onSetDisplayMode(mode)
if self.coverbrowser then
mode = mode ~= "classic" and mode or nil
self.coverbrowser:setDisplayMode(mode)
end
return true
end
function FileManager.getSortByActions()
local collates = {}
for k, v in pairs(FileChooser.collates) do
table.insert(collates, {
name = k,
text = v.text,
menu_order = v.menu_order,
})
end
table.sort(collates, function(a, b) return a.menu_order < b.menu_order end)
local action_names, action_texts = {}, {}
for _, v in ipairs(collates) do
table.insert(action_names, v.name)
table.insert(action_texts, v.text)
end
return action_names, action_texts
end
function FileManager:onSetSortBy(mode)
G_reader_settings:saveSetting("collate", mode)
self.file_chooser:clearSortingCache()
self.file_chooser:refreshPath()
return true
end
function FileManager:onSetReverseSorting(toggle)
G_reader_settings:saveSetting("reverse_collate", toggle or nil)
self.file_chooser:refreshPath()
return true
end
function FileManager:onSetMixedSorting(toggle)
G_reader_settings:saveSetting("collate_mixed", toggle or nil)
self.file_chooser:refreshPath()
return true
end
return FileManager

@ -1,16 +1,26 @@
local BD = require("ui/bidi")
local ButtonDialog = require("ui/widget/buttondialog")
local ConfirmBox = require("ui/widget/confirmbox")
local Device = require("device")
local DocumentRegistry = require("document/documentregistry")
local DocSettings = require("docsettings")
local FileManagerBookInfo = require("apps/filemanager/filemanagerbookinfo")
local InfoMessage = require("ui/widget/infomessage")
local InputDialog = require("ui/widget/inputdialog")
local Menu = require("ui/widget/menu")
local ReadCollection = require("readcollection")
local SortWidget = require("ui/widget/sortwidget")
local UIManager = require("ui/uimanager")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local Screen = require("device").screen
local filemanagerutil = require("apps/filemanager/filemanagerutil")
local _ = require("gettext")
local T = require("ffi/util").template
local util = require("util")
local FileManagerCollection = WidgetContainer:extend{
title = _("Favorites"),
title = _("Collections"),
default_collection_title = _("Favorites"),
checkmark = "\u{2713}",
}
function FileManagerCollection:init()
@ -18,38 +28,90 @@ function FileManagerCollection:init()
end
function FileManagerCollection:addToMainMenu(menu_items)
menu_items.favorites = {
text = self.default_collection_title,
callback = function()
self:onShowColl()
end,
}
menu_items.collections = {
text = self.title,
callback = function()
self:onShowColl()
self:onShowCollList()
end,
}
end
function FileManagerCollection:updateItemTable()
-- collection
function FileManagerCollection:getCollectionTitle(collection_name)
return collection_name == ReadCollection.default_collection_name
and self.default_collection_title -- favorites
or collection_name
end
function FileManagerCollection:onShowColl(collection_name)
collection_name = collection_name or ReadCollection.default_collection_name
self.coll_menu = Menu:new{
ui = self.ui,
covers_fullscreen = true, -- hint for UIManager:_repaint()
is_borderless = true,
is_popout = false,
-- item and book cover thumbnail dimensions in Mosaic and Detailed list display modes
-- must be equal in File manager, History and Collection windows to avoid image scaling
title_bar_fm_style = true,
title_bar_left_icon = "appbar.menu",
onLeftButtonTap = function() self:showCollDialog() end,
onMenuChoice = self.onMenuChoice,
onMenuHold = self.onMenuHold,
onSetRotationMode = self.MenuSetRotationModeHandler,
_manager = self,
collection_name = collection_name,
}
self.coll_menu.close_callback = function()
if self.files_updated then
if self.ui.file_chooser then
self.ui.file_chooser:refreshPath()
end
self.files_updated = nil
end
UIManager:close(self.coll_menu)
self.coll_menu = nil
end
self:updateItemTable()
UIManager:show(self.coll_menu)
return true
end
function FileManagerCollection:updateItemTable(show_last_item)
local item_table = {}
for _, item in pairs(ReadCollection.coll[self.coll_menu.collection_name]) do
table.insert(item_table, item)
end
table.sort(item_table, function(v1, v2) return v1.order < v2.order end)
self.coll_menu:switchItemTable(self.title, item_table, -1)
if #item_table > 1 then
table.sort(item_table, function(v1, v2) return v1.order < v2.order end)
end
local title = self:getCollectionTitle(self.coll_menu.collection_name)
title = T("%1 (%2)", title, #item_table)
local item_number = show_last_item and #item_table or -1
self.coll_menu:switchItemTable(title, item_table, item_number)
end
function FileManagerCollection:onMenuChoice(item)
local file = item.file
if self.ui.document then
if self.ui.document.file ~= file then
self.ui:switchDocument(file)
if self.ui.document.file ~= item.file then
self.ui:switchDocument(item.file)
end
else
local ReaderUI = require("apps/reader/readerui")
ReaderUI:showReader(file)
self.ui:openFile(item.file)
end
end
function FileManagerCollection:onMenuHold(item)
local file = item.file
self.collfile_dialog = nil
self.book_props = self.ui.coverbrowser and self.ui.coverbrowser:getBookInfo(file)
local function close_dialog_callback()
UIManager:close(self.collfile_dialog)
end
@ -65,13 +127,31 @@ function FileManagerCollection:onMenuHold(item)
local is_currently_opened = file == (self.ui.document and self.ui.document.file)
local buttons = {}
local doc_settings_or_file = is_currently_opened and self.ui.doc_settings or file
local doc_settings_or_file
if is_currently_opened then
doc_settings_or_file = self.ui.doc_settings
if not self.book_props then
self.book_props = self.ui.doc_props
self.book_props.has_cover = true
end
else
if DocSettings:hasSidecarFile(file) then
doc_settings_or_file = DocSettings:open(file)
if not self.book_props then
local props = doc_settings_or_file:readSetting("doc_props")
self.book_props = FileManagerBookInfo.extendProps(props, file)
self.book_props.has_cover = true
end
else
doc_settings_or_file = file
end
end
table.insert(buttons, filemanagerutil.genStatusButtonsRow(doc_settings_or_file, close_dialog_update_callback))
table.insert(buttons, {}) -- separator
table.insert(buttons, {
filemanagerutil.genResetSettingsButton(file, close_dialog_update_callback, is_currently_opened),
filemanagerutil.genResetSettingsButton(doc_settings_or_file, close_dialog_update_callback, is_currently_opened),
{
text = _("Remove from favorites"),
text = _("Remove from collection"),
callback = function()
UIManager:close(self.collfile_dialog)
ReadCollection:removeItem(file, self.collection_name)
@ -81,11 +161,11 @@ function FileManagerCollection:onMenuHold(item)
})
table.insert(buttons, {
filemanagerutil.genShowFolderButton(file, close_dialog_menu_callback),
filemanagerutil.genBookInformationButton(file, close_dialog_callback),
filemanagerutil.genBookInformationButton(file, self.book_props, close_dialog_callback),
})
table.insert(buttons, {
filemanagerutil.genBookCoverButton(file, close_dialog_callback),
filemanagerutil.genBookDescriptionButton(file, close_dialog_callback),
filemanagerutil.genBookCoverButton(file, self.book_props, close_dialog_callback),
filemanagerutil.genBookDescriptionButton(file, self.book_props, close_dialog_callback),
})
if Device:canExecuteScript(file) then
@ -95,7 +175,7 @@ function FileManagerCollection:onMenuHold(item)
end
self.collfile_dialog = ButtonDialog:new{
title = item.text,
title = BD.filename(item.text),
title_align = "center",
buttons = buttons,
}
@ -103,79 +183,37 @@ function FileManagerCollection:onMenuHold(item)
return true
end
function FileManagerCollection:MenuSetRotationModeHandler(rotation)
if rotation ~= nil and rotation ~= Screen:getRotationMode() then
UIManager:close(self._manager.coll_menu)
if self._manager.ui.view and self._manager.ui.view.onSetRotationMode then
self._manager.ui.view:onSetRotationMode(rotation)
elseif self._manager.ui.onSetRotationMode then
self._manager.ui:onSetRotationMode(rotation)
else
Screen:setRotationMode(rotation)
end
self._manager:onShowColl()
end
return true
end
function FileManagerCollection:onShowColl(collection_name)
collection_name = collection_name or ReadCollection.default_collection_name
self.coll_menu = Menu:new{
ui = self.ui,
covers_fullscreen = true, -- hint for UIManager:_repaint()
is_borderless = true,
is_popout = false,
-- item and book cover thumbnail dimensions in Mosaic and Detailed list display modes
-- must be equal in File manager, History and Collection windows to avoid image scaling
title_bar_fm_style = true,
title_bar_left_icon = "appbar.menu",
onLeftButtonTap = function() self:showCollDialog() end,
onMenuChoice = self.onMenuChoice,
onMenuHold = self.onMenuHold,
onSetRotationMode = self.MenuSetRotationModeHandler,
_manager = self,
collection_name = collection_name,
}
self.coll_menu.close_callback = function()
if self.files_updated then
if self.ui.file_chooser then
self.ui.file_chooser:refreshPath()
end
self.files_updated = nil
end
UIManager:close(self.coll_menu)
self.coll_menu = nil
end
self:updateItemTable()
UIManager:show(self.coll_menu)
return true
end
function FileManagerCollection:showCollDialog()
local coll_dialog
local buttons = {
{{
text = _("Sort favorites"),
text = _("Collections"),
callback = function()
UIManager:close(coll_dialog)
self.coll_menu.close_callback()
self:onShowCollList()
end,
}},
{}, -- separator
{{
text = _("Arrange books in collection"),
callback = function()
UIManager:close(coll_dialog)
self:sortCollection()
end,
}},
{{
text = _("Add a book to favorites"),
text = _("Add a book to collection"),
callback = function()
UIManager:close(coll_dialog)
local PathChooser = require("ui/widget/pathchooser")
local path_chooser = PathChooser:new{
path = G_reader_settings:readSetting("home_dir"),
select_directory = false,
file_filter = function(file)
return DocumentRegistry:hasProvider(file)
end,
onConfirm = function(file)
if not ReadCollection:hasFile(file) then
if not ReadCollection:isFileInCollection(file, self.coll_menu.collection_name) then
ReadCollection:addItem(file, self.coll_menu.collection_name)
self:updateItemTable()
self:updateItemTable(true) -- show added item
end
end,
}
@ -184,19 +222,20 @@ function FileManagerCollection:showCollDialog()
}},
}
if self.ui.document then
local has_file = ReadCollection:hasFile(self.ui.document.file)
local file = self.ui.document.file
local is_in_collection = ReadCollection:isFileInCollection(file, self.coll_menu.collection_name)
table.insert(buttons, {{
text_func = function()
return has_file and _("Remove current book from favorites") or _("Add current book to favorites")
return is_in_collection and _("Remove current book from collection") or _("Add current book to collection")
end,
callback = function()
UIManager:close(coll_dialog)
if has_file then
ReadCollection:removeItem(self.ui.document.file)
if is_in_collection then
ReadCollection:removeItem(file, self.coll_menu.collection_name)
else
ReadCollection:addItem(self.ui.document.file, self.coll_menu.collection_name)
ReadCollection:addItem(file, self.coll_menu.collection_name)
end
self:updateItemTable()
self:updateItemTable(not is_in_collection)
end,
}})
end
@ -207,12 +246,10 @@ function FileManagerCollection:showCollDialog()
end
function FileManagerCollection:sortCollection()
local item_table = ReadCollection:getOrderedCollection(self.coll_menu.collection_name)
local SortWidget = require("ui/widget/sortwidget")
local sort_widget
sort_widget = SortWidget:new{
title = _("Sort favorites"),
item_table = item_table,
title = _("Arrange books in collection"),
item_table = ReadCollection:getOrderedCollection(self.coll_menu.collection_name),
callback = function()
ReadCollection:updateCollectionOrder(self.coll_menu.collection_name, sort_widget.item_table)
self:updateItemTable()
@ -221,10 +258,341 @@ function FileManagerCollection:sortCollection()
UIManager:show(sort_widget)
end
function FileManagerCollection:MenuSetRotationModeHandler(rotation)
if rotation ~= nil and rotation ~= Screen:getRotationMode() then
UIManager:close(self._manager.coll_menu)
if self._manager.ui.view and self._manager.ui.view.onSetRotationMode then
self._manager.ui.view:onSetRotationMode(rotation)
elseif self._manager.ui.onSetRotationMode then
self._manager.ui:onSetRotationMode(rotation)
else
Screen:setRotationMode(rotation)
end
self._manager:onShowColl()
end
return true
end
function FileManagerCollection:onBookMetadataChanged()
if self.coll_menu then
self.coll_menu:updateItems()
end
end
-- collection list
function FileManagerCollection:onShowCollList(file_or_files, caller_callback, no_dialog)
self.selected_colections = nil
if file_or_files then -- select mode
if type(file_or_files) == "string" then -- checkmark collections containing the file
self.selected_colections = ReadCollection:getCollectionsWithFile(file_or_files)
else -- do not checkmark any
self.selected_colections = {}
end
end
self.coll_list = Menu:new{
subtitle = "",
covers_fullscreen = true,
is_borderless = true,
is_popout = false,
title_bar_fm_style = true,
title_bar_left_icon = file_or_files and "check" or "appbar.menu",
onLeftButtonTap = function() self:showCollListDialog(caller_callback, no_dialog) end,
onMenuChoice = self.onCollListChoice,
onMenuHold = self.onCollListHold,
onSetRotationMode = self.MenuSetRotationModeHandler,
_manager = self,
}
self.coll_list.close_callback = function(force_close)
if force_close or self.selected_colections == nil then
UIManager:close(self.coll_list)
self.coll_list = nil
end
end
self:updateCollListItemTable(true) -- init
UIManager:show(self.coll_list)
return true
end
function FileManagerCollection:updateCollListItemTable(do_init, item_number)
local item_table
if do_init then
item_table = {}
for name, coll in pairs(ReadCollection.coll) do
local mandatory
if self.selected_colections then
mandatory = self.selected_colections[name] and self.checkmark or " "
self.coll_list.items_mandatory_font_size = self.coll_list.font_size
else
mandatory = util.tableSize(coll)
end
table.insert(item_table, {
text = self:getCollectionTitle(name),
mandatory = mandatory,
name = name,
order = ReadCollection.coll_order[name],
})
end
if #item_table > 1 then
table.sort(item_table, function(v1, v2) return v1.order < v2.order end)
end
else
item_table = self.coll_list.item_table
end
local title = T(_("Collections (%1)"), #item_table)
local subtitle
if self.selected_colections then
local selected_nb = util.tableSize(self.selected_colections)
subtitle = self.selected_colections and T(_("Selected collections: %1"), selected_nb)
if do_init and selected_nb > 0 then -- show first collection containing the long-pressed book
for i, item in ipairs(item_table) do
if self.selected_colections[item.name] then
item_number = i
break
end
end
end
end
self.coll_list:switchItemTable(title, item_table, item_number or -1, nil, subtitle)
end
function FileManagerCollection:onCollListChoice(item)
if self._manager.selected_colections then
if item.mandatory == self._manager.checkmark then
self.item_table[item.idx].mandatory = " "
self._manager.selected_colections[item.name] = nil
else
self.item_table[item.idx].mandatory = self._manager.checkmark
self._manager.selected_colections[item.name] = true
end
self._manager:updateCollListItemTable()
else
self._manager:onShowColl(item.name)
end
end
function FileManagerCollection:onCollListHold(item)
if item.name == ReadCollection.default_collection_name -- Favorites non-editable
or self._manager.selected_colections then -- select mode
return
end
local button_dialog
local buttons = {
{
{
text = _("Remove collection"),
callback = function()
UIManager:close(button_dialog)
self._manager:removeCollection(item)
end
},
{
text = _("Rename collection"),
callback = function()
UIManager:close(button_dialog)
self._manager:renameCollection(item)
end
},
},
}
button_dialog = ButtonDialog:new{
title = item.text,
title_align = "center",
buttons = buttons,
}
UIManager:show(button_dialog)
return true
end
function FileManagerCollection:showCollListDialog(caller_callback, no_dialog)
if no_dialog then
caller_callback()
self.coll_list.close_callback(true)
return
end
local button_dialog, buttons
local new_collection_button = {
{
text = _("New collection"),
callback = function()
UIManager:close(button_dialog)
self:addCollection()
end,
},
}
if self.selected_colections then -- select mode
buttons = {
new_collection_button,
{}, -- separator
{
{
text = _("Deselect all"),
callback = function()
UIManager:close(button_dialog)
for name in pairs(self.selected_colections) do
self.selected_colections[name] = nil
end
self:updateCollListItemTable(true)
end,
},
{
text = _("Select all"),
callback = function()
UIManager:close(button_dialog)
for name in pairs(ReadCollection.coll) do
self.selected_colections[name] = true
end
self:updateCollListItemTable(true)
end,
},
},
{
{
text = _("Apply selection"),
callback = function()
UIManager:close(button_dialog)
caller_callback()
self.coll_list.close_callback(true)
end,
},
},
}
else
buttons = {
new_collection_button,
{
{
text = _("Arrange collections"),
callback = function()
UIManager:close(button_dialog)
self:sortCollections()
end,
},
},
}
end
button_dialog = ButtonDialog:new{
buttons = buttons,
}
UIManager:show(button_dialog)
end
function FileManagerCollection:editCollectionName(editCallback, old_name)
local input_dialog
input_dialog = InputDialog:new{
title = _("Enter collection name"),
input = old_name,
input_hint = old_name,
buttons = {{
{
text = _("Cancel"),
id = "close",
callback = function()
UIManager:close(input_dialog)
end,
},
{
text = _("Save"),
callback = function()
local new_name = input_dialog:getInputText()
if new_name == "" or new_name == old_name then return end
if ReadCollection.coll[new_name] then
UIManager:show(InfoMessage:new{
text = T(_("Collection already exists: %1"), new_name),
})
else
UIManager:close(input_dialog)
editCallback(new_name)
end
end,
},
}},
}
UIManager:show(input_dialog)
input_dialog:onShowKeyboard()
end
function FileManagerCollection:addCollection()
local editCallback = function(name)
ReadCollection:addCollection(name)
local mandatory
if self.selected_colections then
self.selected_colections[name] = true
mandatory = self.checkmark
else
mandatory = 0
end
table.insert(self.coll_list.item_table, {
text = name,
mandatory = mandatory,
name = name,
order = ReadCollection.coll_order[name],
})
self:updateCollListItemTable(false, #self.coll_list.item_table) -- show added item
end
self:editCollectionName(editCallback)
end
function FileManagerCollection:renameCollection(item)
local editCallback = function(name)
ReadCollection:renameCollection(item.name, name)
self.coll_list.item_table[item.idx].text = name
self.coll_list.item_table[item.idx].name = name
self:updateCollListItemTable()
end
self:editCollectionName(editCallback, item.name)
end
function FileManagerCollection:removeCollection(item)
UIManager:show(ConfirmBox:new{
text = _("Remove collection?") .. "\n\n" .. item.text,
ok_text = _("Remove"),
ok_callback = function()
ReadCollection:removeCollection(item.name)
table.remove(self.coll_list.item_table, item.idx)
self:updateCollListItemTable()
end,
})
end
function FileManagerCollection:sortCollections()
local sort_widget
sort_widget = SortWidget:new{
title = _("Arrange collections"),
item_table = util.tableDeepCopy(self.coll_list.item_table),
callback = function()
ReadCollection:updateCollectionListOrder(sort_widget.item_table)
self:updateCollListItemTable(true) -- init
end,
}
UIManager:show(sort_widget)
end
-- external
function FileManagerCollection:genAddToCollectionButton(file_or_files, caller_pre_callback, caller_post_callback, button_disabled)
return {
text = _("Add to collection"),
enabled = not button_disabled,
callback = function()
if caller_pre_callback then
caller_pre_callback()
end
local caller_callback = function()
if type(file_or_files) == "string" then
ReadCollection:addRemoveItemMultiple(file_or_files, self.selected_colections)
else -- selected files
ReadCollection:addItemsMultiple(file_or_files, self.selected_colections)
end
if caller_post_callback then
caller_post_callback()
end
end
self:onShowCollList(file_or_files, caller_callback)
end,
}
end
return FileManagerCollection

@ -1,6 +1,5 @@
local ButtonDialog = require("ui/widget/buttondialog")
local CheckButton = require("ui/widget/checkbutton")
local CenterContainer = require("ui/widget/container/centercontainer")
local ConfirmBox = require("ui/widget/confirmbox")
local DocSettings = require("docsettings")
local DocumentRegistry = require("document/documentregistry")
@ -16,7 +15,6 @@ local lfs = require("libs/libkoreader-lfs")
local util = require("util")
local _ = require("gettext")
local N_ = _.ngettext
local Screen = require("device").screen
local T = require("ffi/util").template
local FileSearcher = WidgetContainer:extend{
@ -25,9 +23,6 @@ local FileSearcher = WidgetContainer:extend{
include_metadata = false,
}
function FileSearcher:init()
end
function FileSearcher:onShowFileSearch(search_string)
local search_dialog
local check_button_case, check_button_subfolders, check_button_metadata
@ -225,28 +220,26 @@ function FileSearcher:showSearchResultsMessage(no_results)
end
function FileSearcher:showSearchResults(results)
local menu_container = CenterContainer:new{
dimen = Screen:getSize(),
}
self.search_menu = Menu:new{
title = T(_("Search results (%1)"), #results),
subtitle = T(_("Query: %1"), self.search_string),
item_table = results,
ui = self.ui,
covers_fullscreen = true, -- hint for UIManager:_repaint()
is_borderless = true,
is_popout = false,
show_parent = menu_container,
title_bar_fm_style = true,
onMenuSelect = self.onMenuSelect,
onMenuHold = self.onMenuHold,
handle_hold_on_hold_release = true,
}
table.insert(menu_container, self.search_menu)
self.search_menu.close_callback = function()
UIManager:close(menu_container)
UIManager:close(self.search_menu)
if self.ui.file_chooser then
self.ui.file_chooser:refreshPath()
end
end
self.search_menu:switchItemTable(T(_("Search results (%1)"), #results), results)
UIManager:show(menu_container)
UIManager:show(self.search_menu)
if self.no_metadata_count ~= 0 then
self:showSearchResultsMessage()
end
@ -254,7 +247,7 @@ end
function FileSearcher:onMenuSelect(item)
local file = item.path
local dialog
local bookinfo, dialog
local function close_dialog_callback()
UIManager:close(dialog)
end
@ -266,12 +259,13 @@ function FileSearcher:onMenuSelect(item)
if item.is_file then
local is_currently_opened = self.ui.document and self.ui.document.file == file
if DocumentRegistry:hasProvider(file) or DocSettings:hasSidecarFile(file) then
bookinfo = self.ui.coverbrowser and self.ui.coverbrowser:getBookInfo(file)
local doc_settings_or_file = is_currently_opened and self.ui.doc_settings or file
table.insert(buttons, filemanagerutil.genStatusButtonsRow(doc_settings_or_file, close_dialog_callback))
table.insert(buttons, {}) -- separator
table.insert(buttons, {
filemanagerutil.genResetSettingsButton(file, close_dialog_callback, is_currently_opened),
filemanagerutil.genAddRemoveFavoritesButton(file, close_dialog_callback),
self.ui.collections:genAddToCollectionButton(file, close_dialog_callback),
})
end
table.insert(buttons, {
@ -293,7 +287,7 @@ function FileSearcher:onMenuSelect(item)
FileManager:showDeleteFileDialog(file, post_delete_callback)
end,
},
filemanagerutil.genBookInformationButton(file, close_dialog_callback),
filemanagerutil.genBookInformationButton(file, bookinfo, close_dialog_callback),
})
end
table.insert(buttons, {
@ -308,8 +302,17 @@ function FileSearcher:onMenuSelect(item)
end,
},
})
local title = file
if bookinfo then
if bookinfo.title then
title = title .. "\n\n" .. T(_("Title: %1"), bookinfo.title)
end
if bookinfo.authors then
title = title .. "\n" .. T(_("Authors: %1"), bookinfo.authors:gsub("[\n\t]", "|"))
end
end
dialog = ButtonDialog:new{
title = file,
title = title .. "\n",
buttons = buttons,
}
UIManager:show(dialog)

@ -2,8 +2,11 @@ local BD = require("ui/bidi")
local ButtonDialog = require("ui/widget/buttondialog")
local CheckButton = require("ui/widget/checkbutton")
local ConfirmBox = require("ui/widget/confirmbox")
local DocSettings = require("docsettings")
local FileManagerBookInfo = require("apps/filemanager/filemanagerbookinfo")
local InputDialog = require("ui/widget/inputdialog")
local Menu = require("ui/widget/menu")
local ReadCollection = require("readcollection")
local UIManager = require("ui/uimanager")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local Screen = require("device").screen
@ -62,32 +65,33 @@ function FileManagerHistory:fetchStatuses(count)
end
function FileManagerHistory:updateItemTable()
-- try to stay on current page
local select_number = nil
if self.hist_menu.page and self.hist_menu.perpage and self.hist_menu.page > 0 then
select_number = (self.hist_menu.page - 1) * self.hist_menu.perpage + 1
end
self.count = { all = #require("readhistory").hist,
reading = 0, abandoned = 0, complete = 0, deleted = 0, new = 0, }
local item_table = {}
for _, v in ipairs(require("readhistory").hist) do
if self:isItemMatch(v) then
if self.is_frozen and v.status == "complete" then
v.mandatory_dim = true
local item = util.tableDeepCopy(v)
if item.select_enabled and ReadCollection:isFileInCollections(item.file) then
item.mandatory = "" .. item.mandatory
end
if self.is_frozen and item.status == "complete" then
item.mandatory_dim = true
end
table.insert(item_table, v)
table.insert(item_table, item)
end
if self.statuses_fetched then
self.count[v.status] = self.count[v.status] + 1
end
end
local subtitle
local subtitle = ""
if self.search_string then
subtitle = T(_("Search results (%1)"), #item_table)
elseif self.selected_colections then
subtitle = T(_("Filtered by collections (%1)"), #item_table)
elseif self.filter ~= "all" then
subtitle = T(_("Status: %1 (%2)"), filter_text[self.filter]:lower(), #item_table)
end
self.hist_menu:switchItemTable(nil, item_table, select_number, nil, subtitle or "")
self.hist_menu:switchItemTable(nil, item_table, -1, nil, subtitle)
end
function FileManagerHistory:isItemMatch(item)
@ -106,6 +110,13 @@ function FileManagerHistory:isItemMatch(item)
end
end
end
if self.selected_colections then
for name in pairs(self.selected_colections) do
if not ReadCollection:isFileInCollection(item.file, name) then
return false
end
end
end
return self.filter == "all" or item.status == self.filter
end
@ -119,13 +130,15 @@ function FileManagerHistory:onMenuChoice(item)
self.ui:switchDocument(item.file)
end
else
local ReaderUI = require("apps/reader/readerui")
ReaderUI:showReader(item.file)
self.ui:openFile(item.file)
end
end
function FileManagerHistory:onMenuHold(item)
local file = item.file
self.histfile_dialog = nil
self.book_props = self.ui.coverbrowser and self.ui.coverbrowser:getBookInfo(file)
local function close_dialog_callback()
UIManager:close(self.histfile_dialog)
end
@ -133,9 +146,9 @@ function FileManagerHistory:onMenuHold(item)
UIManager:close(self.histfile_dialog)
self._manager.hist_menu.close_callback()
end
local function status_button_callback()
local function close_dialog_update_callback()
UIManager:close(self.histfile_dialog)
if self._manager.filter ~= "all" then
if self._manager.filter ~= "all" or self._manager.is_frozen then
self._manager:fetchStatuses(false)
else
self._manager.statuses_fetched = false
@ -143,17 +156,38 @@ function FileManagerHistory:onMenuHold(item)
self._manager:updateItemTable()
self._manager.files_updated = true -- sidecar folder may be created/deleted
end
local is_currently_opened = item.file == (self.ui.document and self.ui.document.file)
local function update_callback()
self._manager:updateItemTable()
end
local is_currently_opened = file == (self.ui.document and self.ui.document.file)
local buttons = {}
local doc_settings_or_file
if is_currently_opened then
doc_settings_or_file = self.ui.doc_settings
if not self.book_props then
self.book_props = self.ui.doc_props
self.book_props.has_cover = true
end
else
if DocSettings:hasSidecarFile(file) then
doc_settings_or_file = DocSettings:open(file)
if not self.book_props then
local props = doc_settings_or_file:readSetting("doc_props")
self.book_props = FileManagerBookInfo.extendProps(props, file)
self.book_props.has_cover = true
end
else
doc_settings_or_file = file
end
end
if not item.dim then
local doc_settings_or_file = is_currently_opened and self.ui.doc_settings or item.file
table.insert(buttons, filemanagerutil.genStatusButtonsRow(doc_settings_or_file, status_button_callback))
table.insert(buttons, filemanagerutil.genStatusButtonsRow(doc_settings_or_file, close_dialog_update_callback))
table.insert(buttons, {}) -- separator
end
table.insert(buttons, {
filemanagerutil.genResetSettingsButton(item.file, status_button_callback, is_currently_opened),
filemanagerutil.genAddRemoveFavoritesButton(item.file, close_dialog_callback, item.dim),
filemanagerutil.genResetSettingsButton(doc_settings_or_file, close_dialog_update_callback, is_currently_opened),
self._manager.ui.collections:genAddToCollectionButton(file, close_dialog_callback, update_callback, item.dim),
})
table.insert(buttons, {
{
@ -166,7 +200,7 @@ function FileManagerHistory:onMenuHold(item)
self._manager.files_updated = true
end
local FileManager = require("apps/filemanager/filemanager")
FileManager:showDeleteFileDialog(item.file, post_delete_callback)
FileManager:showDeleteFileDialog(file, post_delete_callback)
end,
},
{
@ -179,12 +213,12 @@ function FileManagerHistory:onMenuHold(item)
},
})
table.insert(buttons, {
filemanagerutil.genShowFolderButton(item.file, close_dialog_menu_callback, item.dim),
filemanagerutil.genBookInformationButton(item.file, close_dialog_callback, item.dim),
filemanagerutil.genShowFolderButton(file, close_dialog_menu_callback, item.dim),
filemanagerutil.genBookInformationButton(file, self.book_props, close_dialog_callback, item.dim),
})
table.insert(buttons, {
filemanagerutil.genBookCoverButton(item.file, close_dialog_callback, item.dim),
filemanagerutil.genBookDescriptionButton(item.file, close_dialog_callback, item.dim),
filemanagerutil.genBookCoverButton(file, self.book_props, close_dialog_callback, item.dim),
filemanagerutil.genBookDescriptionButton(file, self.book_props, close_dialog_callback, item.dim),
})
self.histfile_dialog = ButtonDialog:new{
@ -236,6 +270,7 @@ function FileManagerHistory:onShowHist(search_info)
self.case_sensitive = search_info.case_sensitive
else
self.search_string = nil
self.selected_colections = nil
end
self.filter = G_reader_settings:readSetting("history_filter", "all")
self.is_frozen = G_reader_settings:isTrue("history_freeze_finished_books")
@ -255,7 +290,7 @@ function FileManagerHistory:onShowHist(search_info)
self.hist_menu = nil
G_reader_settings:saveSetting("history_filter", self.filter)
end
UIManager:show(self.hist_menu)
UIManager:show(self.hist_menu, "flashui")
return true
end
@ -274,6 +309,7 @@ function FileManagerHistory:showHistDialog()
self.filter = filter
if filter == "all" then -- reset all filters
self.search_string = nil
self.selected_colections = nil
end
self:updateItemTable()
end,
@ -289,6 +325,19 @@ function FileManagerHistory:showHistDialog()
genFilterButton("abandoned"),
genFilterButton("complete"),
})
table.insert(buttons, {
{
text = _("Filter by collections"),
callback = function()
UIManager:close(hist_dialog)
local caller_callback = function()
self.selected_colections = self.ui.collections.selected_colections
self:updateItemTable()
end
self.ui.collections:onShowCollList({}, caller_callback, true) -- do not select any, no dialog to apply
end,
},
})
table.insert(buttons, {
{
text = _("Search in filename and book metadata"),

@ -4,9 +4,11 @@ local ConfirmBox = require("ui/widget/confirmbox")
local Device = require("device")
local FFIUtil = require("ffi/util")
local InputContainer = require("ui/widget/container/inputcontainer")
local KeyValuePage = require("ui/widget/keyvaluepage")
local PluginLoader = require("pluginloader")
local SetDefaults = require("apps/filemanager/filemanagersetdefaults")
local Size = require("ui/size")
local SpinWidget = require("ui/widget/spinwidget")
local UIManager = require("ui/uimanager")
local Screen = Device.screen
local filemanagerutil = require("apps/filemanager/filemanagerutil")
@ -59,11 +61,15 @@ end
function FileManagerMenu:registerKeyEvents()
if Device:hasKeys() then
self.key_events.ShowMenu = { { "Menu" } }
if Device:hasScreenKB() then
self.key_events.OpenLastDoc = { { "ScreenKB", "Back" } }
end
end
end
FileManagerMenu.onPhysicalKeyboardConnected = FileManagerMenu.registerKeyEvents
-- NOTE: FileManager emits a SetDimensions on init, it's our only caller
function FileManagerMenu:initGesListener()
if not Device:isTouchDevice() then return end
@ -168,49 +174,51 @@ function FileManagerMenu:setUpdateItemTable()
text = _("Classic mode settings"),
sub_item_table = {
{
text = _("Items per page"),
text_func = function()
return T(_("Items per page: %1"),
G_reader_settings:readSetting("items_per_page") or FileChooser.items_per_page_default)
end,
help_text = _([[This sets the number of items per page in:
- File browser, history and favorites in 'classic' display mode
- Search results and folder shortcuts
- File and folder selection
- Calibre and OPDS browsers/search results]]),
callback = function()
local SpinWidget = require("ui/widget/spinwidget")
local Menu = require("ui/widget/menu")
local default_perpage = Menu.items_per_page_default
local curr_perpage = G_reader_settings:readSetting("items_per_page") or default_perpage
local items = SpinWidget:new{
value = curr_perpage,
value_min = 6,
value_max = 24,
default_value = default_perpage,
callback = function(touchmenu_instance)
local default_value = FileChooser.items_per_page_default
local current_value = G_reader_settings:readSetting("items_per_page") or default_value
local widget = SpinWidget:new{
title_text = _("Items per page"),
value = current_value,
value_min = 6,
value_max = 30,
default_value = default_value,
keep_shown_on_apply = true,
callback = function(spin)
G_reader_settings:saveSetting("items_per_page", spin.value)
self.ui:onRefresh()
end
FileChooser:refreshPath()
touchmenu_instance:updateItems()
end,
}
UIManager:show(items)
UIManager:show(widget)
end,
},
{
text = _("Item font size"),
callback = function()
local SpinWidget = require("ui/widget/spinwidget")
local Menu = require("ui/widget/menu")
local curr_perpage = G_reader_settings:readSetting("items_per_page") or Menu.items_per_page_default
local default_font_size = Menu.getItemFontSize(curr_perpage)
local curr_font_size = G_reader_settings:readSetting("items_font_size") or default_font_size
local items_font = SpinWidget:new{
value = curr_font_size,
text_func = function()
return T(_("Item font size: %1"), FileChooser.font_size)
end,
callback = function(touchmenu_instance)
local current_value = FileChooser.font_size
local default_value = FileChooser.getItemFontSize(G_reader_settings:readSetting("items_per_page")
or FileChooser.items_per_page_default)
local widget = SpinWidget:new{
title_text = _("Item font size"),
value = current_value,
value_min = 10,
value_max = 72,
default_value = default_font_size,
default_value = default_value,
keep_shown_on_apply = true,
title_text = _("Item font size"),
callback = function(spin)
if spin.value == default_font_size then
if spin.value == default_value then
-- We can't know if the user has set a size or hit "Use default", but
-- assume that if it is the default font size, he will prefer to have
-- our default font size if he later updates per-page
@ -218,10 +226,11 @@ function FileManagerMenu:setUpdateItemTable()
else
G_reader_settings:saveSetting("items_font_size", spin.value)
end
self.ui:onRefresh()
end
FileChooser:refreshPath()
touchmenu_instance:updateItems()
end,
}
UIManager:show(items_font)
UIManager:show(widget)
end,
},
{
@ -373,26 +382,28 @@ To:
separator = true,
},
{
text = _("Info lists items per page"),
text_func = function()
local default_value = KeyValuePage.getDefaultItemsPerPage()
local current_value = G_reader_settings:readSetting("keyvalues_per_page") or default_value
return T(_("Info lists items per page: %1"), current_value)
end,
help_text = _([[This sets the number of items per page in:
- Book information
- Dictionary and Wikipedia lookup history
- Reading statistics details
- A few other plugins]]),
keep_menu_open = true,
callback = function()
local SpinWidget = require("ui/widget/spinwidget")
local KeyValuePage = require("ui/widget/keyvaluepage")
local default_perpage = KeyValuePage:getDefaultKeyValuesPerPage()
local curr_perpage = G_reader_settings:readSetting("keyvalues_per_page") or default_perpage
local items = SpinWidget:new{
value = curr_perpage,
callback = function(touchmenu_instance)
local default_value = KeyValuePage.getDefaultItemsPerPage()
local current_value = G_reader_settings:readSetting("keyvalues_per_page") or default_value
local widget = SpinWidget:new{
value = current_value,
value_min = 10,
value_max = 24,
default_value = default_perpage,
value_max = 30,
default_value = default_value,
title_text = _("Info lists items per page"),
callback = function(spin)
if spin.value == default_perpage then
if spin.value == default_value then
-- We can't know if the user has set a value or hit "Use default", but
-- assume that if it is the default, he will prefer to stay with our
-- default if he later changes screen DPI
@ -400,9 +411,10 @@ To:
else
G_reader_settings:saveSetting("keyvalues_per_page", spin.value)
end
touchmenu_instance:updateItems()
end
}
UIManager:show(items)
UIManager:show(widget)
end,
},
},
@ -445,7 +457,7 @@ To:
if Device:supportsScreensaver() then
self.menu_items.screensaver = {
text = _("Screensaver"),
text = _("Sleep screen"),
sub_item_table = require("ui/elements/screensaver_menu"),
}
end
@ -617,6 +629,30 @@ To:
end,
})
end
if Device:isKobo() and Device:hasColorScreen() then
table.insert(self.menu_items.developer_options.sub_item_table, {
-- We default to a flag (G2) that slightly boosts saturation,
-- but it *is* a destructive process, so we want to allow disabling it.
-- @translators CFA is a technical term for the technology behind eInk's color panels. It stands for Color Film/Filter Array, leave the abbreviation alone ;).
text = _("Disable CFA post-processing"),
checked_func = function()
return G_reader_settings:isTrue("no_cfa_post_processing")
end,
callback = function()
G_reader_settings:flipNilOrFalse("no_cfa_post_processing")
UIManager:askForRestart()
end,
})
end
table.insert(self.menu_items.developer_options.sub_item_table, {
text = _("Anti-alias rounded corners"),
checked_func = function()
return G_reader_settings:nilOrTrue("anti_alias_ui")
end,
callback = function()
G_reader_settings:flipNilOrTrue("anti_alias_ui")
end,
})
--- @note: Currently, only Kobo implements this quirk
if Device:hasEinkScreen() and Device:isKobo() then
table.insert(self.menu_items.developer_options.sub_item_table, {
@ -962,10 +998,11 @@ function FileManagerMenu:onShowMenu(tab_index)
end
function FileManagerMenu:onCloseFileManagerMenu()
if not self.menu_container then return end
if not self.menu_container then return true end
local last_tab_index = self.menu_container[1].last_index
G_reader_settings:saveSetting("filemanagermenu_tab_index", last_tab_index)
UIManager:close(self.menu_container)
self.menu_container = nil
return true
end
@ -1003,11 +1040,14 @@ function FileManagerMenu:onSwipeShowMenu(ges)
end
function FileManagerMenu:onSetDimensions(dimen)
self:onCloseFileManagerMenu()
-- update listening according to new screen dimen
if Device:isTouchDevice() then
self:initGesListener()
-- This widget doesn't support in-place layout updates, so, close & reopen
if self.menu_container then
self:onCloseFileManagerMenu()
self:onShowMenu()
end
-- update gesture zones according to new screen dimen
self:initGesListener()
end
function FileManagerMenu:onMenuSearch()

@ -73,11 +73,18 @@ end
-- Purge doc settings except kept
function filemanagerutil.resetDocumentSettings(file)
local settings_to_keep = {
annotations = true,
annotations_paging = true,
annotations_rolling = true,
bookmarks = true,
bookmarks_paging = true,
bookmarks_rolling = true,
bookmarks_sorted_20220106 = true,
bookmarks_version = true,
cre_dom_version = true,
highlight = true,
highlight_paging = true,
highlight_rolling = true,
highlights_imported = true,
last_page = true,
last_xpointer = true,
@ -108,9 +115,14 @@ function filemanagerutil.getStatus(file)
end
-- Set a document status ("reading", "complete", or "abandoned")
function filemanagerutil.setStatus(file, status)
function filemanagerutil.setStatus(doc_settings_or_file, status)
-- In case the book doesn't have a sidecar file, this'll create it
local doc_settings = DocSettings:open(file)
local doc_settings
if type(doc_settings_or_file) == "table" then
doc_settings = doc_settings_or_file
else
doc_settings = DocSettings:open(doc_settings_or_file)
end
local summary = doc_settings:readSetting("summary", {})
summary.status = status
summary.modified = os.date("%Y-%m-%d", os.time())
@ -130,24 +142,24 @@ end
-- Generate all book status file dialog buttons in a row
function filemanagerutil.genStatusButtonsRow(doc_settings_or_file, caller_callback)
local summary, status
if type(doc_settings_or_file) == "table" then -- currently opened file
summary = doc_settings_or_file:readSetting("summary")
local file, summary, status
if type(doc_settings_or_file) == "table" then
file = doc_settings_or_file:readSetting("doc_path")
summary = doc_settings_or_file:readSetting("summary", {})
status = summary.status
else
status = filemanagerutil.getStatus(doc_settings_or_file)
file = doc_settings_or_file
summary = {}
status = filemanagerutil.getStatus(file)
end
local function genStatusButton(to_status)
return {
text = filemanagerutil.statusToString(to_status) .. (status == to_status and "" or ""),
id = to_status, -- used by covermenu
enabled = status ~= to_status,
callback = function()
if summary then -- currently opened file
summary.status = to_status
else
filemanagerutil.setStatus(doc_settings_or_file, to_status)
end
summary.status = to_status
filemanagerutil.setStatus(doc_settings_or_file, to_status)
UIManager:broadcastEvent(Event:new("DocSettingsItemsChanged", file, { summary = summary })) -- for CoverBrowser
caller_callback()
end,
}
@ -160,16 +172,22 @@ function filemanagerutil.genStatusButtonsRow(doc_settings_or_file, caller_callba
end
-- Generate "Reset" file dialog button
function filemanagerutil.genResetSettingsButton(file, caller_callback, button_disabled)
file = ffiutil.realpath(file) or file
local has_sidecar_file = DocSettings:hasSidecarFile(file)
function filemanagerutil.genResetSettingsButton(doc_settings_or_file, caller_callback, button_disabled)
local doc_settings, file, has_sidecar_file
if type(doc_settings_or_file) == "table" then
doc_settings = doc_settings_or_file
file = doc_settings_or_file:readSetting("doc_path")
has_sidecar_file = true
else
file = ffiutil.realpath(doc_settings_or_file) or doc_settings_or_file
has_sidecar_file = DocSettings:hasSidecarFile(file)
end
local custom_cover_file = DocSettings:findCustomCoverFile(file)
local has_custom_cover_file = custom_cover_file and true or false
local custom_metadata_file = DocSettings:findCustomMetadataFile(file)
local has_custom_metadata_file = custom_metadata_file and true or false
return {
text = _("Reset"),
id = "reset", -- used by covermenu
enabled = not button_disabled and (has_sidecar_file or has_custom_metadata_file or has_custom_cover_file),
callback = function()
local CheckButton = require("ui/widget/checkbutton")
@ -186,11 +204,12 @@ function filemanagerutil.genResetSettingsButton(file, caller_callback, button_di
custom_cover_file = check_button_cover.checked and custom_cover_file,
custom_metadata_file = check_button_metadata.checked and custom_metadata_file,
}
DocSettings:open(file):purge(nil, data_to_purge)
(doc_settings or DocSettings:open(file)):purge(nil, data_to_purge)
if data_to_purge.custom_cover_file or data_to_purge.custom_metadata_file then
UIManager:broadcastEvent(Event:new("InvalidateMetadataCache", file))
end
if data_to_purge.doc_settings then
UIManager:broadcastEvent(Event:new("DocSettingsItemsChanged", file)) -- for CoverBrowser
require("readhistory"):fileSettingsPurged(file)
end
caller_callback()
@ -222,25 +241,6 @@ function filemanagerutil.genResetSettingsButton(file, caller_callback, button_di
}
end
function filemanagerutil.genAddRemoveFavoritesButton(file, caller_callback, button_disabled)
local ReadCollection = require("readcollection")
local has_file = ReadCollection:hasFile(file)
return {
text_func = function()
return has_file and _("Remove from favorites") or _("Add to favorites")
end,
enabled = not button_disabled,
callback = function()
caller_callback()
if has_file then
ReadCollection:removeItem(file)
else
ReadCollection:addItem(file)
end
end,
}
end
function filemanagerutil.genShowFolderButton(file, caller_callback, button_disabled)
return {
text = _("Show folder"),
@ -260,38 +260,41 @@ function filemanagerutil.genShowFolderButton(file, caller_callback, button_disab
}
end
function filemanagerutil.genBookInformationButton(file, caller_callback, button_disabled)
function filemanagerutil.genBookInformationButton(file, book_props, caller_callback, button_disabled)
return {
text = _("Book information"),
id = "book_information", -- used by covermenu
enabled = not button_disabled,
callback = function()
caller_callback()
require("apps/filemanager/filemanagerbookinfo"):show(file)
local FileManagerBookInfo = require("apps/filemanager/filemanagerbookinfo")
FileManagerBookInfo:show(file, book_props and FileManagerBookInfo.extendProps(book_props))
end,
}
end
function filemanagerutil.genBookCoverButton(file, caller_callback, button_disabled)
function filemanagerutil.genBookCoverButton(file, book_props, caller_callback, button_disabled)
local has_cover = book_props and book_props.has_cover
return {
text = _("Book cover"),
id = "book_cover", -- used by covermenu
enabled = not button_disabled,
enabled = (not button_disabled and (not book_props or has_cover)) and true or false,
callback = function()
caller_callback()
require("apps/filemanager/filemanagerbookinfo"):onShowBookCover(file)
local FileManagerBookInfo = require("apps/filemanager/filemanagerbookinfo")
FileManagerBookInfo:onShowBookCover(file)
end,
}
end
function filemanagerutil.genBookDescriptionButton(file, caller_callback, button_disabled)
function filemanagerutil.genBookDescriptionButton(file, book_props, caller_callback, button_disabled)
local description = book_props and book_props.description
return {
text = _("Book description"),
id = "book_description", -- used by covermenu
enabled = not button_disabled,
-- enabled for deleted books if description is kept in CoverBrowser bookinfo cache
enabled = (not (button_disabled or book_props) or description) and true or false,
callback = function()
caller_callback()
require("apps/filemanager/filemanagerbookinfo"):onShowBookDescription(nil, file)
local FileManagerBookInfo = require("apps/filemanager/filemanagerbookinfo")
FileManagerBookInfo:onShowBookDescription(description, file)
end,
}
end

@ -0,0 +1,423 @@
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local logger = require("logger")
local _ = require("gettext")
local T = require("ffi/util").template
local ReaderAnnotation = WidgetContainer:extend{
annotations = nil, -- array sorted by annotation position order, ascending
}
-- build, read, save
function ReaderAnnotation:buildAnnotation(bm, highlights, init)
-- bm: associated single bookmark ; highlights: tables with all highlights
local note = bm.text
if note == "" then
note = nil
end
local chapter = bm.chapter
local hl, pageno = self:getHighlightByDatetime(highlights, bm.datetime)
if init then
if note and self.ui.bookmark:isBookmarkAutoText(bm) then
note = nil
end
if chapter == nil then
chapter = self.ui.toc:getTocTitleByPage(bm.page)
end
pageno = self.ui.paging and bm.page or self.document:getPageFromXPointer(bm.page)
end
if self.ui.paging and bm.pos0 and not bm.pos0.page then
-- old single-page reflow highlights do not have page in position
bm.pos0.page = bm.page
bm.pos1.page = bm.page
end
if not hl then -- page bookmark or orphaned bookmark
hl = {}
if bm.highlighted then -- orphaned bookmark
hl.drawer = self.view.highlight.saved_drawer
hl.color = self.view.highlight.saved_color
if self.ui.paging then
if bm.pos0.page == bm.pos1.page then
hl.pboxes = self.document:getPageBoxesFromPositions(bm.page, bm.pos0, bm.pos1)
else -- multi-page highlight, restore the first box only
hl.pboxes = self.document:getPageBoxesFromPositions(bm.page, bm.pos0, bm.pos0)
end
end
end
end
return { -- annotation
datetime = bm.datetime, -- creation time, not changeable
drawer = hl.drawer, -- highlight drawer
color = hl.color, -- highlight color
text = bm.notes, -- highlighted text, editable
text_edited = hl.edited, -- true if highlighted text has been edited
note = note, -- user's note, editable
chapter = chapter, -- book chapter title
pageno = pageno, -- book page number
page = bm.page, -- highlight location, xPointer or number (pdf)
pos0 = bm.pos0, -- highlight start position, xPointer (== page) or table (pdf)
pos1 = bm.pos1, -- highlight end position, xPointer or table (pdf)
pboxes = hl.pboxes, -- pdf pboxes, used only and changeable by addMarkupAnnotation
ext = hl.ext, -- pdf multi-page highlight
}
end
function ReaderAnnotation:getHighlightByDatetime(highlights, datetime)
for pageno, page_highlights in pairs(highlights) do
for _, highlight in ipairs(page_highlights) do
if highlight.datetime == datetime then
return highlight, pageno
end
end
end
end
function ReaderAnnotation:getAnnotationsFromBookmarksHighlights(bookmarks, highlights, init)
local annotations = {}
for i = #bookmarks, 1, -1 do
table.insert(annotations, self:buildAnnotation(bookmarks[i], highlights, init))
end
if init then
self:sortItems(annotations)
end
return annotations
end
function ReaderAnnotation:onReadSettings(config)
local annotations = config:readSetting("annotations")
if annotations then
-- KOHighlights may set this key when it has merged annotations from different sources:
-- we want to make sure they are updated and sorted
local needs_update = config:isTrue("annotations_externally_modified")
local needs_sort -- if incompatible annotations were built of old highlights/bookmarks
-- Annotation formats in crengine and mupdf are incompatible.
local has_annotations = #annotations > 0
local annotations_type = has_annotations and type(annotations[1].page)
if self.ui.rolling and annotations_type ~= "string" then -- incompatible format loaded, or empty
if has_annotations then -- backup incompatible format if not empty
config:saveSetting("annotations_paging", annotations)
end
-- load compatible format
annotations = config:readSetting("annotations_rolling") or {}
config:delSetting("annotations_rolling")
needs_sort = true
elseif self.ui.paging and annotations_type ~= "number" then
if has_annotations then
config:saveSetting("annotations_rolling", annotations)
end
annotations = config:readSetting("annotations_paging") or {}
config:delSetting("annotations_paging")
needs_sort = true
end
self.annotations = annotations
if needs_update or needs_sort then
if self.ui.rolling then
self.ui:registerPostInitCallback(function()
self:updatedAnnotations(needs_update, needs_sort)
end)
else
self:updatedAnnotations(needs_update, needs_sort)
end
config:delSetting("annotations_externally_modified")
end
else -- first run
if self.ui.rolling then
self.ui:registerPostInitCallback(function()
self:migrateToAnnotations(config)
end)
else
self:migrateToAnnotations(config)
end
end
end
function ReaderAnnotation:migrateToAnnotations(config)
local bookmarks = config:readSetting("bookmarks") or {}
local highlights = config:readSetting("highlight") or {}
if config:hasNot("highlights_imported") then
-- before 2014, saved highlights were not added to bookmarks when they were created.
for page, hls in pairs(highlights) do
for _, hl in ipairs(hls) do
local hl_page = self.ui.paging and page or hl.pos0
-- highlights saved by some old versions don't have pos0 field
-- we just ignore those highlights
if hl_page then
local item = {
datetime = hl.datetime,
highlighted = true,
notes = hl.text,
page = hl_page,
pos0 = hl.pos0,
pos1 = hl.pos1,
}
if self.ui.paging then
item.pos0.page = page
item.pos1.page = page
end
table.insert(bookmarks, item)
end
end
end
end
-- Bookmarks/highlights formats in crengine and mupdf are incompatible.
local has_bookmarks = #bookmarks > 0
local bookmarks_type = has_bookmarks and type(bookmarks[1].page)
if self.ui.rolling then
if bookmarks_type == "string" then -- compatible format loaded, check for incompatible old backup
if config:has("bookmarks_paging") then -- save incompatible old backup
local bookmarks_paging = config:readSetting("bookmarks_paging")
local highlights_paging = config:readSetting("highlight_paging")
local annotations = self:getAnnotationsFromBookmarksHighlights(bookmarks_paging, highlights_paging)
config:saveSetting("annotations_paging", annotations)
config:delSetting("bookmarks_paging")
config:delSetting("highlight_paging")
end
else -- incompatible format loaded, or empty
if has_bookmarks then -- save incompatible format if not empty
local annotations = self:getAnnotationsFromBookmarksHighlights(bookmarks, highlights)
config:saveSetting("annotations_paging", annotations)
end
-- load compatible format
bookmarks = config:readSetting("bookmarks_rolling") or {}
highlights = config:readSetting("highlight_rolling") or {}
config:delSetting("bookmarks_rolling")
config:delSetting("highlight_rolling")
end
else -- self.ui.paging
if bookmarks_type == "number" then
if config:has("bookmarks_rolling") then
local bookmarks_rolling = config:readSetting("bookmarks_rolling")
local highlights_rolling = config:readSetting("highlight_rolling")
local annotations = self:getAnnotationsFromBookmarksHighlights(bookmarks_rolling, highlights_rolling)
config:saveSetting("annotations_rolling", annotations)
config:delSetting("bookmarks_rolling")
config:delSetting("highlight_rolling")
end
else
if has_bookmarks then
local annotations = self:getAnnotationsFromBookmarksHighlights(bookmarks, highlights)
config:saveSetting("annotations_rolling", annotations)
end
bookmarks = config:readSetting("bookmarks_paging") or {}
highlights = config:readSetting("highlight_paging") or {}
config:delSetting("bookmarks_paging")
config:delSetting("highlight_paging")
end
end
self.annotations = self:getAnnotationsFromBookmarksHighlights(bookmarks, highlights, true)
end
function ReaderAnnotation:onDocumentRerendered()
self.needs_update = true
end
function ReaderAnnotation:onCloseDocument()
self:updatePageNumbers()
end
function ReaderAnnotation:onSaveSettings()
self:updatePageNumbers()
self.ui.doc_settings:saveSetting("annotations", self.annotations)
end
-- items handling
function ReaderAnnotation:updatePageNumbers()
if self.needs_update and self.ui.rolling then -- triggered by ReaderRolling on document layout change
for _, item in ipairs(self.annotations) do
item.pageno = self.document:getPageFromXPointer(item.page)
end
end
self.needs_update = nil
end
function ReaderAnnotation:sortItems(items)
if #items > 1 then
local sort_func = self.ui.rolling and function(a, b) return self:isItemInPositionOrderRolling(a, b) end
or function(a, b) return self:isItemInPositionOrderPaging(a, b) end
table.sort(items, sort_func)
end
end
function ReaderAnnotation:updatedAnnotations(needs_update, needs_sort)
if needs_update then
self.needs_update = true
self:updatePageNumbers()
needs_sort = true
end
if needs_sort then
self:sortItems(self.annotations)
end
end
function ReaderAnnotation:updateItemByXPointer(item)
-- called by ReaderRolling:checkXPointersAndProposeDOMVersionUpgrade()
local chapter = self.ui.toc:getTocTitleByPage(item.page)
if chapter == "" then
chapter = nil
end
if not item.drawer then -- page bookmark
item.text = chapter and T(_("in %1"), chapter) or nil
end
item.chapter = chapter
item.pageno = self.document:getPageFromXPointer(item.page)
end
function ReaderAnnotation:isItemInPositionOrderRolling(a, b)
local a_page = self.document:getPageFromXPointer(a.page)
local b_page = self.document:getPageFromXPointer(b.page)
if a_page == b_page then -- both items in the same page
if a.drawer and b.drawer then -- both items are highlights, compare positions
local compare_xp = self.document:compareXPointers(a.page, b.page)
if compare_xp then
if compare_xp == 0 then -- both highlights with the same start, compare ends
compare_xp = self.document:compareXPointers(a.pos1, b.pos1)
if compare_xp then
return compare_xp > 0
end
logger.warn("Invalid xpointer in highlight:", a.pos1, b.pos1)
return true
end
return compare_xp > 0
end
-- if compare_xp is nil, some xpointer is invalid and "a" will be sorted first to page 1
logger.warn("Invalid xpointer in highlight:", a.page, b.page)
return true
end
return not a.drawer -- have page bookmarks before highlights
end
return a_page < b_page
end
function ReaderAnnotation:isItemInPositionOrderPaging(a, b)
if a.page == b.page then -- both items in the same page
if a.drawer and b.drawer then -- both items are highlights, compare positions
local is_reflow = self.document.configurable.text_wrap -- save reflow mode
self.document.configurable.text_wrap = 0 -- native positions
-- sort start and end positions of each highlight
local a_start, a_end, b_start, b_end, result
if self.document:comparePositions(a.pos0, a.pos1) > 0 then
a_start, a_end = a.pos0, a.pos1
else
a_start, a_end = a.pos1, a.pos0
end
if self.document:comparePositions(b.pos0, b.pos1) > 0 then
b_start, b_end = b.pos0, b.pos1
else
b_start, b_end = b.pos1, b.pos0
end
-- compare start positions
local compare_pos = self.document:comparePositions(a_start, b_start)
if compare_pos == 0 then -- both highlights with the same start, compare ends
result = self.document:comparePositions(a_end, b_end) > 0
else
result = compare_pos > 0
end
self.document.configurable.text_wrap = is_reflow -- restore reflow mode
return result
end
return not a.drawer -- have page bookmarks before highlights
end
return a.page < b.page
end
function ReaderAnnotation:getItemIndex(item, no_binary)
local doesMatch
if item.datetime then
doesMatch = function(a, b)
return a.datetime == b.datetime
end
else
if self.ui.rolling then
doesMatch = function(a, b)
if a.text ~= b.text or a.pos0 ~= b.pos0 or a.pos1 ~= b.pos1 then
return false
end
return true
end
else
doesMatch = function(a, b)
if a.text ~= b.text or a.pos0.page ~= b.pos0.page
or a.pos0.x ~= b.pos0.x or a.pos1.x ~= b.pos1.x
or a.pos0.y ~= b.pos0.y or a.pos1.y ~= b.pos1.y then
return false
end
return true
end
end
end
if not no_binary then
local isInOrder = self.ui.rolling and self.isItemInPositionOrderRolling or self.isItemInPositionOrderPaging
local _start, _end, _middle = 1, #self.annotations
while _start <= _end do
_middle = bit.rshift(_start + _end, 1)
local v = self.annotations[_middle]
if doesMatch(item, v) then
return _middle
elseif isInOrder(self, item, v) then
_end = _middle - 1
else
_start = _middle + 1
end
end
end
for i, v in ipairs(self.annotations) do
if doesMatch(item, v) then
return i
end
end
end
function ReaderAnnotation:getInsertionIndex(item)
local isInOrder = self.ui.rolling and self.isItemInPositionOrderRolling or self.isItemInPositionOrderPaging
local _start, _end, _middle, direction = 1, #self.annotations, 1, 0
while _start <= _end do
_middle = bit.rshift(_start + _end, 1)
if isInOrder(self, item, self.annotations[_middle]) then
_end, direction = _middle - 1, 0
else
_start, direction = _middle + 1, 1
end
end
return _middle + direction
end
function ReaderAnnotation:addItem(item)
item.datetime = os.date("%Y-%m-%d %H:%M:%S")
item.pageno = self.ui.paging and item.page or self.document:getPageFromXPointer(item.page)
local index = self:getInsertionIndex(item)
table.insert(self.annotations, index, item)
return index
end
-- info
function ReaderAnnotation:hasAnnotations()
return #self.annotations > 0
end
function ReaderAnnotation:getNumberOfAnnotations()
return #self.annotations
end
function ReaderAnnotation:getNumberOfHighlightsAndNotes() -- for Statistics plugin
local highlights = 0
local notes = 0
for _, item in ipairs(self.annotations) do
if item.drawer then
if item.note then
notes = notes + 1
else
highlights = highlights + 1
end
end
end
return highlights, notes
end
return ReaderAnnotation

File diff suppressed because it is too large Load Diff

@ -158,11 +158,11 @@ function ReaderConfig:onSwipeShowConfigMenu(ges)
end
end
-- For some reason, things are fine and dandy without any of this for rotations, but we need it for actual resizes...
function ReaderConfig:onSetDimensions(dimen)
-- since we cannot redraw config_dialog with new size, we close
-- the old one on screen size change
if self.config_dialog then
self.config_dialog:closeDialog()
-- init basically calls update & initGesListener and nothing else, which is exactly what we want.
self.config_dialog:init()
end
end

@ -41,6 +41,11 @@ function ReaderCoptListener:onReadSettings(config)
self.document._document:setIntProperty("window.status.battery.percent", self.battery_percent)
self.document._document:setIntProperty("window.status.pos.percent", self.reading_percent)
-- We will build the top status bar page info string ourselves,
-- if we have to display any chunk of it
self.page_info_override = self.page_number == 1 or self.page_count == 1 or self.reading_percent == 1
self.document:setPageInfoOverride("") -- an empty string lets crengine display its own page info
self:onTimeFormatChanged()
-- Enable or disable crengine header status line (note that for crengine, 0=header enabled, 1=header disabled)
@ -64,6 +69,101 @@ function ReaderCoptListener:onReadSettings(config)
self:rescheduleHeaderRefreshIfNeeded() -- schedule (or not) first refresh
end
function ReaderCoptListener:onReaderReady()
-- custom metadata support for alt status bar and cre synthetic cover
for prop_key in pairs(self.document.prop_to_cre_prop) do
local orig_prop_value = self.ui.doc_settings:readSetting(prop_key)
local custom_prop_key = prop_key == "title" and "display_title" or prop_key
local custom_prop_value = self.ui.doc_props[custom_prop_key]
if custom_prop_value ~= orig_prop_value then
self.document:setAltDocumentProp(prop_key, custom_prop_value)
end
end
end
function ReaderCoptListener:updatePageInfoOverride(pageno)
if not (self.document.configurable.status_line == 0 and self.view.view_mode == "page" and self.page_info_override) then
return
end
-- There are a few cases where we may not be updated on change, at least:
-- - when toggling ReaderPageMap's "Use reference page numbers"
-- - when changing footer's nb of digits after decimal point
-- but we will update on next page turn. Let's not bother.
local page_pre = ""
local page_number = pageno
local page_sep = " / "
local page_count = self.ui.document:getPageCount()
local page_post = ""
local percentage = page_number / page_count
local percentage_pre = ""
local percentage_post = ""
-- Let's use the same setting for nb of digits after decimal point as configured for the footer
local percentage_digits = self.ui.view.footer.settings.progress_pct_format
local percentage_fmt = "%." .. percentage_digits .. "f%%"
-- We want the same output as with ReaderFooter's page_progress() and percentage()
-- but here each item (page number, page counte, percentage) is individually toggable,
-- so try to get something that make sense when not all are enabled
if self.ui.pagemap and self.ui.pagemap:wantsPageLabels() then
-- These become strings here
page_number = self.ui.pagemap:getCurrentPageLabel(true)
page_count = self.ui.pagemap:getLastPageLabel(true)
elseif self.ui.document:hasHiddenFlows() then
local flow = self.ui.document:getPageFlow(pageno)
page_number = tostring(self.ui.document:getPageNumberInFlow(pageno))
page_count = tostring(self.ui.document:getTotalPagesInFlow(flow))
percentage = page_number / page_count
if flow == 0 then
page_sep = " // "
else
page_pre = "["
page_post = "]"..tostring(flow)
percentage_pre = "["
percentage_post = "]"
end
end
local page_info = ""
if self.page_number or self.page_count then
page_info = page_info .. page_pre
if self.page_number then
page_info = page_info .. page_number
if self.page_count then
page_info = page_info .. page_sep
end
end
if self.page_count then
page_info = page_info .. page_count
end
page_info = page_info .. page_post
if self.reading_percent then
page_info = page_info .. " " -- (double space as done by crengine's own drawing)
end
end
if self.reading_percent then
page_info = page_info .. percentage_pre .. percentage_fmt:format(percentage*100) .. percentage_post
end
self.document:setPageInfoOverride(page_info)
end
function ReaderCoptListener:onPageUpdate(pageno)
self:updatePageInfoOverride(pageno)
end
function ReaderCoptListener:onPosUpdate(pos, pageno)
self:updatePageInfoOverride(pageno)
end
function ReaderCoptListener:onBookMetadataChanged(prop_updated)
-- custom metadata support for alt status bar and cre synthetic cover
local prop_key = prop_updated and prop_updated.metadata_key_updated
if prop_key and self.document.prop_to_cre_prop[prop_key] then
self.document:setAltDocumentProp(prop_key, prop_updated.doc_props[prop_key])
self:updateHeader()
end
end
function ReaderCoptListener:onConfigChange(option_name, option_value)
-- font_size and line_spacing are historically and sadly shared by both mupdf and cre reader modules,
-- but fortunately they can be distinguished by their different ranges
@ -170,6 +270,8 @@ ReaderCoptListener.onSuspend = ReaderCoptListener.unscheduleHeaderRefresh
function ReaderCoptListener:setAndSave(setting, property, value)
self.document._document:setIntProperty(property, value)
G_reader_settings:saveSetting(setting, value)
self.page_info_override = self.page_number == 1 or self.page_count == 1 or self.reading_percent == 1
self.document:setPageInfoOverride("")
-- Have crengine redraw it (even if hidden by the menu at this time)
self.ui.rolling:updateBatteryState()
self:updateHeader()
@ -190,7 +292,7 @@ function ReaderCoptListener:getAltStatusBarMenu()
separator = true,
sub_item_table = {
{
text = _("About alternate status bar"),
text = _("About alt status bar"),
keep_menu_open = true,
callback = function()
UIManager:show(InfoMessage:new{
@ -273,15 +375,18 @@ function ReaderCoptListener:getAltStatusBarMenu()
},
{
text_func = function()
local status = _("off")
local status = _("Battery status")
if self.battery == 1 then
if self.battery_percent == 1 then
status = _("percentage")
status = _("Battery status: percentage")
else
status = _("icon")
status = _("Battery status: icon")
end
end
return T(_("Battery status: %1"), status)
return status
end,
checked_func = function()
return self.battery == 1
end,
sub_item_table = {
{

@ -21,8 +21,6 @@ function ReaderCropping:onPageCrop(mode)
-- backup original zoom mode as cropping use "page" zoom mode
self.orig_zoom_mode = self.view.zoom_mode
if mode == "auto" then
--- @fixme: This is weird. "auto" crop happens to be the default, yet the default zoom mode/genus is "page", not "content".
--- This effectively yields different results whether auto is enabled by default, or toggled at runtime...
if self.document.configurable.text_wrap ~= 1 then
self:setCropZoomMode(true)
end

@ -424,7 +424,7 @@ function ReaderDictionary:addToMainMenu(menu_items)
end
end
function ReaderDictionary:onLookupWord(word, is_sane, boxes, highlight, link, tweak_buttons_func)
function ReaderDictionary:onLookupWord(word, is_sane, boxes, highlight, link)
logger.dbg("dict lookup word:", word, boxes)
-- escape quotes and other funny characters in word
word = self:cleanSelection(word, is_sane)
@ -440,7 +440,7 @@ function ReaderDictionary:onLookupWord(word, is_sane, boxes, highlight, link, tw
-- Wrapped through Trapper, as we may be using Trapper:dismissablePopen() in it
Trapper:wrap(function()
self:stardictLookup(word, self.enabled_dict_names, not disable_fuzzy_search, boxes, link, tweak_buttons_func)
self:stardictLookup(word, self.enabled_dict_names, not disable_fuzzy_search, boxes, link)
end)
return true
end
@ -932,7 +932,7 @@ function ReaderDictionary:startSdcv(word, dict_names, fuzzy_search)
return results
end
function ReaderDictionary:stardictLookup(word, dict_names, fuzzy_search, boxes, link, tweak_buttons_func)
function ReaderDictionary:stardictLookup(word, dict_names, fuzzy_search, boxes, link)
if word == "" then
return
end
@ -992,16 +992,15 @@ function ReaderDictionary:stardictLookup(word, dict_names, fuzzy_search, boxes,
return
end
self:showDict(word, tidyMarkup(results), boxes, link, tweak_buttons_func)
self:showDict(word, tidyMarkup(results), boxes, link)
end
function ReaderDictionary:showDict(word, results, boxes, link, tweak_buttons_func)
function ReaderDictionary:showDict(word, results, boxes, link)
if results and results[1] then
logger.dbg("showing quick lookup window", #DictQuickLookup.window_list+1, ":", word, results)
self.dict_window = DictQuickLookup:new{
ui = self.ui,
highlight = self.highlight,
tweak_buttons_func = tweak_buttons_func,
dialog = self.dialog,
-- original lookup word
word = word,
@ -1114,15 +1113,24 @@ function ReaderDictionary:downloadDictionary(dict, download_location, continue)
--logger.dbg(headers)
file_size = headers and headers["content-length"]
UIManager:show(ConfirmBox:new{
text = T(_("Dictionary filesize is %1 (%2 bytes). Continue with download?"), util.getFriendlySize(file_size), util.getFormattedSize(file_size)),
ok_text = _("Download"),
ok_callback = function()
-- call ourselves with continue = true
self:downloadDictionary(dict, download_location, true)
end,
})
return
if file_size then
UIManager:show(ConfirmBox:new{
text = T(_("Dictionary filesize is %1 (%2 bytes). Continue with download?"), util.getFriendlySize(file_size), util.getFormattedSize(file_size)),
ok_text = _("Download"),
ok_callback = function()
-- call ourselves with continue = true
self:downloadDictionary(dict, download_location, true)
end,
})
return
else
logger.dbg("ReaderDictionary: Request failed; response headers:", headers)
UIManager:show(InfoMessage:new{
text = _("Failed to fetch dictionary. Are you online?"),
--timeout = 3,
})
return false
end
else
UIManager:nextTick(function()
UIManager:show(InfoMessage:new{

@ -26,6 +26,12 @@ function ReaderFlipping:init()
width = icon_size,
height = icon_size,
}
self.long_hold_widget = IconWidget:new{
icon = "appbar.pokeball",
width = icon_size,
height = icon_size,
alpha = true,
}
icon_size = Screen:scaleBySize(36)
self.select_mode_widget = IconWidget:new{
icon = "texture-box",
@ -93,6 +99,8 @@ function ReaderFlipping:paintTo(bb, x, y)
elseif self.ui.highlight.select_mode then
-- highlight select mode
widget = self.select_mode_widget
elseif self.ui.highlight.long_hold_reached then
widget = self.long_hold_widget
elseif self.ui.rolling and self.ui.rolling.rendering_state then
-- epub rerendering
widget = self:getRollingRenderingStateIconWidget()

@ -1,5 +1,4 @@
local BD = require("ui/bidi")
local CenterContainer = require("ui/widget/container/centercontainer")
local ConfirmBox = require("ui/widget/confirmbox")
local Device = require("device")
local Event = require("ui/event")
@ -8,7 +7,6 @@ local FontList = require("fontlist")
local InfoMessage = require("ui/widget/infomessage")
local Input = Device.input
local InputContainer = require("ui/widget/container/inputcontainer")
local Menu = require("ui/widget/menu")
local MultiConfirmBox = require("ui/widget/multiconfirmbox")
local Notification = require("ui/widget/notification")
local Screen = require("device").screen
@ -142,20 +140,19 @@ function ReaderFont:onGesture() end
function ReaderFont:registerKeyEvents()
if Device:hasKeyboard() then
-- add shortcut for keyboard
self.key_events = {
ShowFontMenu = { { "F" } },
IncreaseSize = {
if not (Device:hasScreenKB() or Device:hasSymKey()) then
-- add shortcut for keyboard
self.key_events.IncreaseSize = {
{ "Shift", Input.group.PgFwd },
event = "ChangeSize",
args = 0.5
},
DecreaseSize = {
}
self.key_events.DecreaseSize = {
{ "Shift", Input.group.PgBack },
event = "ChangeSize",
args = -0.5
},
}
}
end
end
end
@ -171,10 +168,8 @@ function ReaderFont:onReadSettings(config)
or self.ui.document.default_font
self.ui.document:setFontFace(self.font_face)
self.header_font_face = config:readSetting("header_font_face")
or G_reader_settings:readSetting("header_font")
or self.ui.document.header_font
self.ui.document:setHeaderFont(self.header_font_face)
local header_font = G_reader_settings:readSetting("header_font") or self.ui.document.header_font
self.ui.document:setHeaderFont(header_font)
self.ui.document:setFontSize(Screen:scaleBySize(self.configurable.font_size))
self.ui.document:setFontBaseWeight(self.configurable.font_base_weight)
@ -198,34 +193,6 @@ function ReaderFont:onReadSettings(config)
end)
end
function ReaderFont:onShowFontMenu()
-- build menu widget
local main_menu = Menu:new{
title = self.font_menu_title,
item_table = self.face_table,
width = Screen:getWidth() - 100,
height = math.floor(Screen:getHeight() * 0.5),
single_line = true,
items_per_page = 8,
items_font_size = Menu.getItemFontSize(8),
}
-- build container
local menu_container = CenterContainer:new{
dimen = Screen:getSize(),
main_menu,
}
main_menu.close_callback = function()
UIManager:close(menu_container)
end
-- show menu
main_menu.show_parent = menu_container
UIManager:show(menu_container)
return true
end
--[[
UpdatePos event is used to tell ReaderRolling to update pos.
--]]
@ -311,7 +278,6 @@ end
function ReaderFont:onSaveSettings()
self.ui.doc_settings:saveSetting("font_face", self.font_face)
self.ui.doc_settings:saveSetting("header_font_face", self.header_font_face)
self.ui.doc_settings:saveSetting("font_family_fonts", self.font_family_fonts)
end
@ -331,7 +297,7 @@ function ReaderFont:makeDefault(face, is_monospace, touchmenu_instance)
-- to be set as a fallback font, and allow it to be set as the
-- default monospace font.
UIManager:show(MultiConfirmBox:new{
text = T(_("Would you like %1 to be used as the default font (★), or the monospace font (\u{1F13C})?"), face),
text = T(_("Would you like %1 to be used as the default font (★), or the monospace font (🄼)?"), face), -- [M] is U+1F13C
choice1_text = _("Default"),
choice1_callback = function()
G_reader_settings:saveSetting("cre_font", face)
@ -436,8 +402,8 @@ local FONT_FAMILIES = {
-- On 2nd page
{ "cursive", _("Cursive") },
{ "fantasy", _("Fantasy") },
{ "emoji", _("Emoji \u{1F60A}") },
{ "fangsong", _("Fang Song \u{4EFF}\u{5B8B}") },
{ "emoji", _("Emoji") .. " 😊" }, -- U+1F60A
{ "fangsong", _("Fang Song") .. " 仿宋" }, -- U+4EFF U+5B8B
{ "math", _("Math") },
}

File diff suppressed because it is too large Load Diff

@ -181,4 +181,28 @@ function ReaderGoto:onGoToEnd()
return true
end
function ReaderGoto:onGoToRandomPage()
local page_count = self.document:getPageCount()
if page_count == 1 then return true end
local current_page = self.ui:getCurrentPage()
if self.pages_pool == nil then
self.pages_pool = {}
end
if #self.pages_pool == 0 or (#self.pages_pool == 1 and self.pages_pool[1] == current_page) then
for i = 1, page_count do
self.pages_pool[i] = i
end
end
while true do
local random_page_idx = math.random(1, #self.pages_pool)
local random_page = self.pages_pool[random_page_idx]
if random_page ~= current_page then
table.remove(self.pages_pool, random_page_idx)
self.ui.link:addCurrentLocationToStack()
self.ui:handleEvent(Event:new("GotoPage", random_page))
return true
end
end
end
return ReaderGoto

@ -1,4 +1,5 @@
local ConfirmBox = require("ui/widget/confirmbox")
local Device = require("device")
local Event = require("ui/event")
local InfoMessage = require("ui/widget/infomessage")
local InputDialog = require("ui/widget/inputdialog")
@ -119,6 +120,10 @@ function ReaderHandMade:onToggleHandmadeFlows()
end
function ReaderHandMade:addToMainMenu(menu_items)
-- As it's currently impossible to create custom hidden flows on non-touch, and really impractical to create a custom toc, it's better hide these features completely for now.
if not Device:isTouchDevice() then
return
end
menu_items.handmade_toc = {
text = _("Custom table of contents") .. " " .. self.custom_toc_symbol,
checked_func = function() return self.toc_enabled end,

File diff suppressed because it is too large Load Diff

@ -3,7 +3,7 @@ ReaderLink is an abstraction for document-specific link interfaces.
]]
local BD = require("ui/bidi")
local ButtonDialogTitle = require("ui/widget/buttondialogtitle")
local ButtonDialog = require("ui/widget/buttondialog")
local ConfirmBox = require("ui/widget/confirmbox")
local Device = require("device")
local DocumentRegistry = require("document/documentregistry")
@ -67,6 +67,7 @@ local ReaderLink = InputContainer:extend{
location_stack = nil, -- table, per-instance
forward_location_stack = nil, -- table, per-instance
_external_link_buttons = nil,
supported_external_schemes = {"http", "https"},
}
function ReaderLink:init()
@ -118,7 +119,7 @@ function ReaderLink:init()
end)
if G_reader_settings:isTrue("opening_page_location_stack") then
-- Add location at book opening to stack
self.ui:registerPostReadyCallback(function()
self.ui:registerPostReaderReadyCallback(function()
self:addCurrentLocationToStack()
end)
end
@ -142,6 +143,7 @@ function ReaderLink:init()
return {
text = _("Copy"),
callback = function()
Device.input.setClipboardText(link_url)
UIManager:close(this.external_link_dialog)
end,
}
@ -224,10 +226,25 @@ function ReaderLink:init()
end
end
function ReaderLink:registerScheme(scheme)
table.insert(self.supported_external_schemes, scheme)
end
function ReaderLink:onGesture() end
function ReaderLink:registerKeyEvents()
if Device:hasKeys() then
if Device:hasScreenKB() or Device:hasSymKey() then
self.key_events.GotoSelectedPageLink = { { "Press" }, event = "GotoSelectedPageLink" }
if Device:hasKeyboard() then
self.key_events.AddCurrentLocationToStackNonTouch = { { "Shift", "Press" } }
self.key_events.SelectNextPageLink = { { "Shift", "LPgFwd" }, event = "SelectNextPageLink" }
self.key_events.SelectPrevPageLink = { { "Shift", "LPgBack" }, event = "SelectPrevPageLink" }
else
self.key_events.AddCurrentLocationToStackNonTouch = { { "ScreenKB", "Press" } }
self.key_events.SelectNextPageLink = { { "ScreenKB", "LPgFwd" }, event = "SelectNextPageLink" }
self.key_events.SelectPrevPageLink = { { "ScreenKB", "LPgBack" }, event = "SelectPrevPageLink" }
end
elseif Device:hasKeys() then
self.key_events = {
SelectNextPageLink = {
{ "Tab" },
@ -235,7 +252,6 @@ function ReaderLink:registerKeyEvents()
},
SelectPrevPageLink = {
{ "Shift", "Tab" },
{ "Sym", "Tab" }, -- Shift or Sym + Tab
event = "SelectPrevPageLink",
},
GotoSelectedPageLink = {
@ -702,6 +718,13 @@ function ReaderLink:onAddCurrentLocationToStack(show_notification)
if show_notification then
Notification:notify(_("Current location added to history."))
end
return true
end
function ReaderLink:onAddCurrentLocationToStackNonTouch()
self:addCurrentLocationToStack()
Notification:notify(_("Current location added to history."), Notification.SOURCE_ALWAYS_SHOW)
return true
end
-- Remember current location so we can go back to it
@ -711,6 +734,10 @@ function ReaderLink:addCurrentLocationToStack(loc)
table.insert(self.location_stack, location)
end
function ReaderLink:popFromLocationStack()
return table.remove(self.location_stack)
end
function ReaderLink:onClearLocationStack(show_notification)
self.location_stack = {}
self:onClearForwardLocationStack()
@ -807,8 +834,9 @@ function ReaderLink:onGotoLink(link, neglect_current_location, allow_footnote_po
end
logger.dbg("ReaderLink:onGotoLink: External link:", link_url)
local is_http_link = link_url:find("^https?://") ~= nil
if is_http_link and self:onGoToExternalLink(link_url) then
local scheme = link_url:match("^(%w[%w+%-.]*):")
local is_supported_external_link = scheme and util.arrayContains(self.supported_external_schemes, scheme:lower())
if is_supported_external_link and self:onGoToExternalLink(link_url) then
return true
end
@ -857,7 +885,7 @@ end
function ReaderLink:onGoToExternalLink(link_url)
local buttons, title = self:getButtonsForExternalLinkDialog(link_url)
self.external_link_dialog = ButtonDialogTitle:new{
self.external_link_dialog = ButtonDialog:new{
title = title,
buttons = buttons,
}
@ -1058,7 +1086,9 @@ function ReaderLink:onGoToPageLink(ges, internal_links_only, max_distance)
for _, link in ipairs(links) do
-- link.uri may be an empty string with some invalid links: ignore them
if link.section or (link.uri and link.uri ~= "") then
if link.segments then
-- Note: we may get segments empty in some conditions (in which
-- case we'll fallback to the 'else' branch and using x/y)
if link.segments and #link.segments > 0 then
-- With segments, each is a horizontal segment, with start_x < end_x,
-- and we should compute the distance from gesture position to
-- each segment.

@ -95,7 +95,7 @@ function ReaderMenu:getPreviousFile()
return require("readhistory"):getPreviousFile(self.ui.document.file)
end
function ReaderMenu:onReaderReady()
function ReaderMenu:initGesListener()
if not Device:isTouchDevice() then return end
local DTAP_ZONE_MENU = G_defaults:readSetting("DTAP_ZONE_MENU")
@ -179,6 +179,8 @@ function ReaderMenu:onReaderReady()
})
end
ReaderMenu.onReaderReady = ReaderMenu.initGesListener
function ReaderMenu:setUpdateItemTable()
for _, widget in pairs(self.registered_widgets) do
local ok, err = pcall(widget.addToMainMenu, widget, self.menu_items)
@ -252,7 +254,7 @@ function ReaderMenu:setUpdateItemTable()
if Device:supportsScreensaver() then
local ss_book_settings = {
text = _("Exclude this book's content and cover from screensaver"),
text = _("Do not show this book cover on sleep screen"),
enabled_func = function()
if self.ui and self.ui.document then
local screensaverType = G_reader_settings:readSetting("screensaver_type")
@ -283,7 +285,7 @@ function ReaderMenu:setUpdateItemTable()
end
table.insert(screensaver_sub_item_table, ss_book_settings)
self.menu_items.screensaver = {
text = _("Screensaver"),
text = _("Sleep screen"),
sub_item_table = screensaver_sub_item_table,
}
end
@ -456,16 +458,24 @@ function ReaderMenu:onShowMenu(tab_index)
end
function ReaderMenu:onCloseReaderMenu()
if self.menu_container then
self.last_tab_index = self.menu_container[1].last_index
self:onSaveSettings()
UIManager:close(self.menu_container)
end
if not self.menu_container then return true end
self.last_tab_index = self.menu_container[1].last_index
self:onSaveSettings()
UIManager:close(self.menu_container)
self.menu_container = nil
return true
end
function ReaderMenu:onSetDimensions(dimen)
self:onCloseReaderMenu()
-- This widget doesn't support in-place layout updates, so, close & reopen
if self.menu_container then
self:onCloseReaderMenu()
self:onShowMenu()
end
-- update gesture zones according to new screen dimen
-- (On CRe, this will get called a second time by ReaderReady once the document is reloaded).
self:initGesListener()
end
function ReaderMenu:onCloseDocument()

@ -280,8 +280,11 @@ function ReaderPageMap:getCurrentPageLabel(clean_label)
-- For consistency, getPageMapCurrentPageLabel() returns the last page
-- label shown in the view if there are more than one (or the previous
-- one if there is none).
local label = self.ui.document:getPageMapCurrentPageLabel()
return clean_label and self:cleanPageLabel(label) or label
local label, idx, count = self.ui.document:getPageMapCurrentPageLabel()
if clean_label then
label = self:cleanPageLabel(label)
end
return label, idx, count
end
function ReaderPageMap:getFirstPageLabel(clean_label)

@ -51,7 +51,28 @@ end
function ReaderPaging:onGesture() end
function ReaderPaging:registerKeyEvents()
if Device:hasKeys() then
if Device:hasDPad() and Device:useDPadAsActionKeys() then
self.key_events.GotoNextPos = {
{ { "RPgFwd", "LPgFwd" } },
event = "GotoPosRel",
args = 1,
}
self.key_events.GotoPrevPos = {
{ { "RPgBack", "LPgBack" } },
event = "GotoPosRel",
args = -1,
}
self.key_events.GotoNextChapter = {
{ "Right" },
event = "GotoNextChapter",
args = 1,
}
self.key_events.GotoPrevChapter = {
{ "Left" },
event = "GotoPrevChapter",
args = -1,
}
elseif Device:hasKeys() then
self.key_events.GotoNextPage = {
{ { "RPgFwd", "LPgFwd", not Device:hasFewKeys() and "Right" } },
event = "GotoViewRel",
@ -673,10 +694,10 @@ function ReaderPaging:onInitScrollPageStates(orig_mode)
if self.view.page_scroll and self.view.state.page then
self.orig_page = self.current_page
self.view.page_states = {}
local blank_area = Geom:new{}
local blank_area = Geom:new()
blank_area:setSizeTo(self.view.visible_area)
while blank_area.h > 0 do
local offset = Geom:new{}
local offset = Geom:new()
-- caculate position in current page
if self.current_page == self.orig_page then
local page_area = self.view:getPageArea(
@ -823,7 +844,7 @@ function ReaderPaging:genPageStatesFromTop(top_page_state, blank_area, offset)
current_page = self.ui.document:getNextPage(current_page)
if current_page == 0 then break end -- end of document reached
self:_gotoPage(current_page, "scrolling")
state = self:getNextPageState(blank_area, Geom:new{})
state = self:getNextPageState(blank_area, Geom:new())
table.insert(page_states, state)
end
end
@ -848,7 +869,7 @@ function ReaderPaging:genPageStatesFromBottom(bottom_page_state, blank_area, off
current_page = self.ui.document:getPrevPage(current_page)
if current_page == 0 then break end -- start of document reached
self:_gotoPage(current_page, "scrolling")
state = self:getPrevPageState(blank_area, Geom:new{})
state = self:getPrevPageState(blank_area, Geom:new())
table.insert(page_states, 1, state)
end
end
@ -869,7 +890,7 @@ function ReaderPaging:onScrollPanRel(diff)
if diff == 0 then return true end
logger.dbg("pan relative height:", diff)
local offset = Geom:new{x = 0, y = diff}
local blank_area = Geom:new{}
local blank_area = Geom:new()
blank_area:setSizeTo(self.view.visible_area)
local new_page_states
if diff > 0 then
@ -913,7 +934,7 @@ function ReaderPaging:onScrollPageRel(page_diff)
return true
end
local blank_area = Geom:new{}
local blank_area = Geom:new()
blank_area:setSizeTo(self.view.visible_area)
local overlap = self.overlap
local offset = Geom:new{
@ -923,7 +944,7 @@ function ReaderPaging:onScrollPageRel(page_diff)
self.view.page_states = self:genPageStatesFromTop(last_page_state, blank_area, offset)
elseif page_diff < 0 then
-- page up, first page should be moved to bottom
local blank_area = Geom:new{}
local blank_area = Geom:new()
blank_area:setSizeTo(self.view.visible_area)
local overlap = self.overlap
local first_page_state = table.remove(self.view.page_states, 1)
@ -1029,8 +1050,15 @@ function ReaderPaging:onGotoPageRel(diff)
goto_end(y)
goto_end(x)
elseif new_page > 0 then
-- Be sure that the new and old view areas are reset so that no value is carried over to next page.
-- Without this, we would have panned_y = new_va.y - old_va.y > 0, and panned_y will be added to the next page's y direction.
-- This occurs when the current page has a y > 0 position (for example, a cropped page) and can fit the whole page height,
-- while the next page needs scrolling in the height.
self:_gotoPage(new_page)
new_va = self.visible_area:copy()
old_va = self.visible_area
goto_end(y, -y_diff)
goto_end(x, -x_diff)
else
goto_end(x)
end

@ -100,7 +100,7 @@ function ReaderRolling:init()
self.valid_cache_rendering_hash = self.ui.document:getDocumentRenderingHash(false)
end
end)
table.insert(self.ui.postReaderCallback, function()
table.insert(self.ui.postReaderReadyCallback, function()
self:updatePos()
-- Disable crengine internal history, with required redraw
self.ui.document:enableInternalHistory(false)
@ -116,7 +116,30 @@ end
function ReaderRolling:onGesture() end
function ReaderRolling:registerKeyEvents()
if Device:hasKeys() then
if Device:hasScreenKB() or Device:hasSymKey() then
self.key_events.GotoNextView = {
{ { "RPgFwd", "LPgFwd" } },
event = "GotoViewRel",
args = 1,
}
self.key_events.GotoPrevView = {
{ { "RPgBack", "LPgBack" } },
event = "GotoViewRel",
args = -1,
}
if Device:hasKeyboard() then
self.key_events.MoveUp = {
{ "Shift", "RPgBack" },
event = "Panning",
args = {0, -1},
}
self.key_events.MoveDown = {
{ "Shift", "RPgFwd" },
event = "Panning",
args = {0, 1},
}
end
elseif Device:hasKeys() then
self.key_events.GotoNextView = {
{ { "RPgFwd", "LPgFwd", "Right" } },
event = "GotoViewRel",
@ -128,7 +151,18 @@ function ReaderRolling:registerKeyEvents()
args = -1,
}
end
if Device:hasDPad() then
if Device:hasDPad() and Device:useDPadAsActionKeys() then
self.key_events.GotoNextChapter = {
{ "Right" },
event = "GotoNextChapter",
args = 1,
}
self.key_events.GotoPrevChapter = {
{ "Left" },
event = "GotoPrevChapter",
args = -1,
}
elseif Device:hasDPad() then
self.key_events.MoveUp = {
{ "Up" },
event = "Panning",
@ -140,6 +174,18 @@ function ReaderRolling:registerKeyEvents()
args = {0, 1},
}
end
if Device:hasScreenKB() then
self.key_events.MoveUp = {
{ "ScreenKB", "RPgBack" },
event = "Panning",
args = {0, -1},
}
self.key_events.MoveDown = {
{ "ScreenKB", "RPgFwd" },
event = "Panning",
args = {0, 1},
}
end
if Device:hasKeyboard() then
self.key_events.GotoFirst = {
{ "1" },
@ -234,7 +280,7 @@ function ReaderRolling:onReadSettings(config)
-- And check if we can migrate to a newest DOM version after
-- the book is loaded (unless the user told us not to).
if config:nilOrFalse("cre_keep_old_dom_version") then
self.ui:registerPostReadyCallback(function()
self.ui:registerPostReaderReadyCallback(function()
self:checkXPointersAndProposeDOMVersionUpgrade()
end)
end
@ -313,8 +359,6 @@ function ReaderRolling:onReadSettings(config)
end)
end
-- in scroll mode percent_finished must be save before close document
-- we cannot do it in onSaveSettings() because getLastPercent() uses self.ui.document
function ReaderRolling:onCloseDocument()
self:tearDownRerenderingAutomation()
-- Unschedule anything that might still somehow be...
@ -328,6 +372,7 @@ function ReaderRolling:onCloseDocument()
UIManager:unschedule(self.onUpdatePos)
self.current_header_height = nil -- show unload progress bar at top
-- we cannot do it in onSaveSettings() because getLastPercent() uses self.ui.document
self.ui.doc_settings:saveSetting("percent_finished", self:getLastPercent())
local cache_file_path = self.ui.document:getCacheFilePath() -- nil if no cache file
@ -375,15 +420,8 @@ function ReaderRolling:onCheckDomStyleCoherence()
end
function ReaderRolling:onSaveSettings()
-- remove last_percent config since its deprecated
self.ui.doc_settings:delSetting("last_percent")
self.ui.doc_settings:delSetting("last_percent") -- deprecated
self.ui.doc_settings:saveSetting("last_xpointer", self.xpointer)
-- in scrolling mode, the document may already be closed,
-- so we have to check the condition to avoid crash function self:getLastPercent()
-- that uses self.ui.document
if self.ui.document then
self.ui.doc_settings:saveSetting("percent_finished", self:getLastPercent())
end
self.ui.doc_settings:saveSetting("hide_nonlinear_flows", self.hide_nonlinear_flows)
self.ui.doc_settings:saveSetting("partial_rerendering", self.partial_rerendering)
end
@ -1037,9 +1075,9 @@ function ReaderRolling:onUpdatePos(force)
if self.batched_update_count > 0 then
return
end
if self.ui.postReaderCallback ~= nil then -- ReaderUI:init() not yet done
if self.ui.postReaderReadyCallback ~= nil then -- ReaderUI:init() not yet done
-- Don't schedule any updatePos as long as ReaderUI:init() is
-- not finished (one will be called in the ui.postReaderCallback
-- not finished (one will be called in the ui.postReaderReadyCallback
-- we have set above) to avoid multiple refreshes.
return true
end
@ -1137,7 +1175,7 @@ function ReaderRolling:onRedrawCurrentView()
end
function ReaderRolling:onSetDimensions(dimen)
if self.ui.postReaderCallback ~= nil then
if self.ui.postReaderReadyCallback ~= nil then
-- ReaderUI:init() not yet done: just set document dimensions
self.ui.document:setViewDimen(Screen:getSize())
-- (what's done in the following else is done elsewhere by
@ -1453,25 +1491,12 @@ function ReaderRolling:checkXPointersAndProposeDOMVersionUpgrade()
local applyFuncToXPointersSlots = function(func)
-- Last position
func(self, "xpointer", "last position in book")
-- Bookmarks
if self.ui.bookmark and self.ui.bookmark.bookmarks and #self.ui.bookmark.bookmarks > 0 then
-- Annotations
if self.ui.annotation and self.ui.annotation.annotations and #self.ui.annotation.annotations > 0 then
local slots = { "page", "pos0", "pos1" }
for _, bookmark in ipairs(self.ui.bookmark.bookmarks) do
for _, item in ipairs(self.ui.annotation.annotations) do
for _, slot in ipairs(slots) do
func(bookmark, slot, bookmark.notes or "bookmark")
end
end
end
-- Highlights
if self.view.highlight and self.view.highlight.saved then
local slots = { "pos0", "pos1" }
for page, items in pairs(self.view.highlight.saved) do
if items and #items > 0 then
for _, highlight in ipairs(items) do
for _, slot in ipairs(slots) do
func(highlight, slot, highlight.text or "highlight")
end
end
func(item, slot, item.text or "annotation")
end
end
end
@ -1517,6 +1542,9 @@ function ReaderRolling:checkXPointersAndProposeDOMVersionUpgrade()
local new_xp = normalized_xpointers[xp]
if new_xp then
obj[slot] = new_xp
if slot == "page" then
self.ui.annotation:updateItemByXPointer(obj)
end
else
-- Let lost/not-found XPointer be. There is a small chance that
-- it will be found (it it was made before the boxing code moved

@ -4,12 +4,16 @@ local CheckButton = require("ui/widget/checkbutton")
local Device = require("device")
local InfoMessage = require("ui/widget/infomessage")
local InputDialog = require("ui/widget/inputdialog")
local Menu = require("ui/widget/menu")
local Notification = require("ui/widget/notification")
local SpinWidget = require("ui/widget/spinwidget")
local TextBoxWidget = require("ui/widget/textboxwidget")
local UIManager = require("ui/uimanager")
local Utf8Proc = require("ffi/utf8proc")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local logger = require("logger")
local _ = require("gettext")
local C_ = _.pgettext
local Screen = Device.screen
local T = require("ffi/util").template
@ -27,7 +31,11 @@ local ReaderSearch = WidgetContainer:extend{
-- The speed of the search depends on the regexs. Complex ones might need some time, easy ones
-- go with the speed of light.
-- Setting max_hits higher, does not mean to require more memory. More hits means smaller single hits.
max_hits = 2048, -- maximum hits for search; timinges tested on a Tolino
max_hits = 2048, -- maximum hits for findText search; timinges tested on a Tolino
findall_max_hits = 5000, -- maximum hits for findAllText search
-- number of words before and after the search string in All search results
findall_nb_context_words = G_reader_settings:readSetting("fulltext_search_nb_context_words") or 3,
findall_results_per_page = G_reader_settings:readSetting("fulltext_search_results_per_page") or 10,
-- internal: whether we expect results on previous pages
-- (can be different from self.direction, if, from a page in the
@ -72,26 +80,108 @@ SRELL_ERROR_CODES[111] = _("Expression too complex, some hits will not be shown.
SRELL_ERROR_CODES[666] = _("Expression may lead to an extremely long search time.")
function ReaderSearch:addToMainMenu(menu_items)
menu_items.fulltext_search_settings = {
text = _("Fulltext search settings"),
sub_item_table = {
{
text = _("Show all results on text selection"),
help_text = _("When invoked after text selection, show a list with all results instead of highlighting matches in book pages."),
checked_func = function()
return G_reader_settings:isTrue("fulltext_search_find_all")
end,
callback = function()
G_reader_settings:flipNilOrFalse("fulltext_search_find_all")
end,
},
{
text_func = function()
return T(_("Words in context: %1"), self.findall_nb_context_words)
end,
keep_menu_open = true,
callback = function(touchmenu_instance)
local widget = SpinWidget:new{
title_text = _("Words in context"),
value = self.findall_nb_context_words,
value_min = 1,
value_max = 20,
default_value = 3,
callback = function(spin)
self.last_search_hash = nil
self.findall_nb_context_words = spin.value
G_reader_settings:saveSetting("fulltext_search_nb_context_words", spin.value)
touchmenu_instance:updateItems()
end,
}
UIManager:show(widget)
end,
},
{
text_func = function()
return T(_("Results per page: %1"), self.findall_results_per_page)
end,
keep_menu_open = true,
callback = function(touchmenu_instance)
local widget = SpinWidget:new{
title_text = _("Results per page"),
value = self.findall_results_per_page,
value_min = 6,
value_max = 24,
default_value = 10,
callback = function(spin)
self.findall_results_per_page = spin.value
G_reader_settings:saveSetting("fulltext_search_results_per_page", spin.value)
touchmenu_instance:updateItems()
end,
}
UIManager:show(widget)
end,
},
},
}
menu_items.fulltext_search = {
text = _("Fulltext search"),
callback = function()
self:onShowFulltextSearchInput()
end,
}
menu_items.fulltext_search_findall_results = {
text = _("Last fulltext search results"),
callback = function()
self:onShowFindAllResults()
end,
}
end
function ReaderSearch:searchText(text) -- from highlight dialog
if G_reader_settings:isTrue("fulltext_search_find_all") then
self.ui.highlight:clear()
self:searchCallback(nil, text)
else
self:searchCallback(0, text) -- forward
end
end
-- if reverse ~= 0 search backwards
function ReaderSearch:searchCallback(reverse)
local search_text = self.input_dialog:getInputText()
if search_text == "" then return end
-- search_text comes from our keyboard, and may contain multiple diacritics ordered
-- in any order: we'd rather have them normalized, and expect the book content to
-- be proper and normalized text.
self.last_search_text = search_text -- if shown again, show it as it has been inputted
search_text = Utf8Proc.normalize_NFC(search_text)
self.use_regex = self.check_button_regex.checked
self.case_insensitive = not self.check_button_case.checked
local regex_error = self.use_regex and self.ui.document:checkRegex(search_text)
-- if reverse == 1 search backwards
function ReaderSearch:searchCallback(reverse, text)
local search_text = text or self.input_dialog:getInputText()
if search_text == nil or search_text == "" then return end
self.ui.doc_settings:saveSetting("fulltext_search_last_search_text", search_text)
self.last_search_text = search_text
local regex_error
if text then -- from highlight dialog
self.use_regex = false
self.case_insensitive = true
else -- from input dialog
-- search_text comes from our keyboard, and may contain multiple diacritics ordered
-- in any order: we'd rather have them normalized, and expect the book content to
-- be proper and normalized text.
search_text = Utf8Proc.normalize_NFC(search_text)
self.use_regex = self.check_button_regex.checked
self.case_insensitive = not self.check_button_case.checked
regex_error = self.use_regex and self.ui.document:checkRegex(search_text)
end
if self.use_regex and regex_error ~= 0 then
logger.dbg("ReaderSearch: regex error", regex_error, SRELL_ERROR_CODES[regex_error])
local error_message
@ -103,7 +193,15 @@ function ReaderSearch:searchCallback(reverse)
UIManager:show(InfoMessage:new{ text = error_message })
else
UIManager:close(self.input_dialog)
self:onShowSearchDialog(search_text, reverse, self.use_regex, self.case_insensitive)
if reverse then
self.last_search_hash = nil
self:onShowSearchDialog(search_text, reverse, self.use_regex, self.case_insensitive)
else
local Trapper = require("ui/trapper")
Trapper:wrap(function()
self:findAllText(search_text)
end)
end
end
end
@ -116,7 +214,7 @@ function ReaderSearch:onShowFulltextSearchInput()
self.input_dialog = InputDialog:new{
title = _("Enter text to search for"),
width = math.floor(math.min(Screen:getWidth(), Screen:getHeight()) * 0.9),
input = self.last_search_text,
input = self.last_search_text or self.ui.doc_settings:readSetting("fulltext_search_last_search_text"),
buttons = {
{
{
@ -126,6 +224,13 @@ function ReaderSearch:onShowFulltextSearchInput()
UIManager:close(self.input_dialog)
end,
},
{
-- @translators Find all results in entire document, button displayed on the search bar, should be short.
text = C_("Search text", "All"),
callback = function()
self:searchCallback()
end,
},
{
text = backward_text,
callback = function()
@ -183,7 +288,10 @@ function ReaderSearch:onShowSearchDialog(text, direction, regex, case_insensitiv
local no_results = true -- for notification
local res = search_func(self, search_term, param, regex, case_insensitive)
if res then
if self.ui.document.info.has_pages then
if self.ui.paging then
if not current_page then -- initial search
current_page = self.ui.paging.current_page
end
no_results = false
self.ui.link:onGotoLink({page = res.page - 1}, neglect_current_location)
self.view.highlight.temp[res.page] = res
@ -252,8 +360,24 @@ function ReaderSearch:onShowSearchDialog(text, direction, regex, case_insensitiv
self.ui.link:onGotoLink({xpointer=valid_link}, neglect_current_location)
end
end
-- Don't add result pages to location ("Go back") stack
neglect_current_location = true
if not neglect_current_location then
-- Initial search: onGotoLink() has added the current page to the location stack,
-- and we don't want this to be done when showing further pages with results.
-- But if this initial search is showing results on the current page, we don't want
-- the original page added: we will do it when we jump to a different page.
-- For now, only do this with CreDocument. With PDF, whether in single page mode or
-- in scroll mode, the view can scroll a bit when showing results, and we want to
-- allow "go back" to restore the original viewport.
if self.ui.rolling and self.view.view_mode == "page" then
if current_page == (self.ui.rolling and self.ui.document:getCurrentPage() or self.ui.paging.current_page) then
self.ui.link:popFromLocationStack()
neglect_current_location = false
else
-- We won't add further result pages to the location stack ("Go back").
neglect_current_location = true
end
end
end
end
if no_results then
local notification_text
@ -315,7 +439,6 @@ function ReaderSearch:onShowSearchDialog(text, direction, regex, case_insensitiv
icon_height = Screen:scaleBySize(DGENERIC_ICON_SIZE * 0.8),
callback = function()
self.search_dialog:onClose()
self.last_search_text = text
self:onShowFulltextSearchInput()
end,
},
@ -370,6 +493,13 @@ function ReaderSearch:search(pattern, origin, regex, case_insensitive)
Device:setIgnoreInput(true)
local retval, words_found = self.ui.document:findText(pattern, origin, direction, case_insensitive, page, regex, self.max_hits)
Device:setIgnoreInput(false)
self:showErrorNotification(words_found, regex, self.max_hits)
return retval
end
function ReaderSearch:showErrorNotification(words_found, regex, max_hits)
regex = regex or self.use_regex
max_hits = max_hits or self.findall_max_hits
local regex_retval = regex and self.ui.document:getAndClearRegexSearchError()
if regex and regex_retval ~= 0 then
local error_message
@ -382,13 +512,12 @@ function ReaderSearch:search(pattern, origin, regex, case_insensitive)
text = error_message,
timeout = false,
})
elseif words_found and words_found > self.max_hits then
elseif words_found and words_found >= max_hits then
UIManager:show(Notification:new{
text =_("Too many hits"),
timeout = 4,
})
end
return retval
end
function ReaderSearch:searchFromStart(pattern, _, regex, case_insensitive)
@ -416,4 +545,102 @@ function ReaderSearch:searchNext(pattern, direction, regex, case_insensitive)
return self:search(pattern, 1, regex, case_insensitive)
end
function ReaderSearch:findAllText(search_text)
local last_search_hash = (self.last_search_text or "") .. tostring(self.case_insensitive) .. tostring(self.use_regex)
local not_cached = self.last_search_hash ~= last_search_hash
if not_cached then
local Trapper = require("ui/trapper")
local info = InfoMessage:new{ text = _("Searching… (tap to cancel)") }
UIManager:show(info)
UIManager:forceRePaint()
local completed, res = Trapper:dismissableRunInSubprocess(function()
return self.ui.document:findAllText(search_text,
self.case_insensitive, self.findall_nb_context_words, self.findall_max_hits, self.use_regex)
end, info)
if not completed then return end
UIManager:close(info)
self.last_search_hash = last_search_hash
self.findall_results = res
self.findall_results_item_index = nil
end
if self.findall_results then
self:onShowFindAllResults(not_cached)
else
UIManager:show(InfoMessage:new{ text = _("No results in the document") })
end
end
function ReaderSearch:onShowFindAllResults(not_cached)
if not self.last_search_hash or (not not_cached and self.findall_results == nil) then
-- no cached results, show input dialog
self:onShowFulltextSearchInput()
return
end
if self.ui.rolling and not_cached then -- for ui.paging: items are built in KoptInterface:findAllText()
for _, item in ipairs(self.findall_results) do
-- PDF/Kopt shows full words when only some part matches; let's do the same with CRE
local word = item.matched_text or ""
if item.matched_word_prefix then
word = item.matched_word_prefix .. word
end
if item.matched_word_suffix then
word = word .. item.matched_word_suffix
end
-- Make this word bolder, using Poor Text Formatting provided by TextBoxWidget
-- (we know this text ends up in a TextBoxWidget).
local text = TextBoxWidget.PTF_BOLD_START .. word .. TextBoxWidget.PTF_BOLD_END
-- append context before and after the word
if item.prev_text then
if not item.prev_text:find("%s$") then
text = " " .. text
end
text = item.prev_text .. text
end
if item.next_text then
if not item.next_text:find("^[%s%p]") then
text = text .. " "
end
text = text .. item.next_text
end
text = TextBoxWidget.PTF_HEADER .. text -- enable handling of our bold tags
item.text = text
item.mandatory = self.ui.bookmark:getBookmarkPageString(item.start)
end
end
local menu
menu = Menu:new{
title = T(_("Search results (%1)"), #self.findall_results),
subtitle = T(_("Query: %1"), self.last_search_text),
items_per_page = self.findall_results_per_page,
covers_fullscreen = true,
is_borderless = true,
is_popout = false,
title_bar_fm_style = true,
onMenuChoice = function(_, item)
if self.ui.rolling then
self.ui.link:addCurrentLocationToStack()
self.ui.rolling:onGotoXPointer(item.start, item.start) -- show target line marker
self.ui.document:getTextFromXPointers(item.start, item["end"], true) -- highlight
else
local page = item.mandatory
local boxes = {}
for i, box in ipairs(item.boxes) do
boxes[i] = self.ui.document:nativeToPageRectTransform(page, box)
end
self.ui.link:onGotoLink({ page = page - 1 })
self.view.highlight.temp[page] = boxes
end
end,
close_callback = function()
self.findall_results_item_index = menu.page * menu.perpage -- save page number to reopen
UIManager:close(menu)
end,
}
menu:switchItemTable(nil, self.findall_results, self.findall_results_item_index)
UIManager:show(menu)
self:showErrorNotification(#self.findall_results)
end
return ReaderSearch

@ -38,7 +38,10 @@ function ReaderStatus:onEndOfBook()
local QuickStart = require("ui/quickstart")
local last_file = G_reader_settings:readSetting("lastfile")
if last_file == QuickStart.quickstart_filename then
self:openFileBrowser()
-- Like onOpenNextDocumentInFolder, delay this so as not to break instance lifecycle
UIManager:nextTick(function()
self:openFileBrowser()
end)
return
end
@ -59,15 +62,15 @@ function ReaderStatus:onEndOfBook()
return self.summary.status == "complete" and _("Mark as reading") or _("Mark as finished")
end,
callback = function()
self:onMarkBook()
UIManager:close(button_dialog)
self:onMarkBook()
end,
},
{
text = _("Book status"),
callback = function()
self:onShowBookStatus()
UIManager:close(button_dialog)
self:onShowBookStatus()
end,
},
@ -76,16 +79,16 @@ function ReaderStatus:onEndOfBook()
{
text = _("Go to beginning"),
callback = function()
self.ui:handleEvent(Event:new("GoToBeginning"))
UIManager:close(button_dialog)
self.ui:handleEvent(Event:new("GoToBeginning"))
end,
},
{
text = _("Open next file"),
enabled = next_file_enabled,
callback = function()
self:onOpenNextDocumentInFolder()
UIManager:close(button_dialog)
self:onOpenNextDocumentInFolder()
end,
},
},
@ -93,15 +96,18 @@ function ReaderStatus:onEndOfBook()
{
text = _("Delete file"),
callback = function()
self:deleteFile()
UIManager:close(button_dialog)
self:deleteFile()
end,
},
{
text = _("File browser"),
callback = function()
self:openFileBrowser()
UIManager:close(button_dialog)
-- Ditto
UIManager:nextTick(function()
self:openFileBrowser()
end)
end,
},
},
@ -123,11 +129,7 @@ function ReaderStatus:onEndOfBook()
UIManager:show(info)
UIManager:forceRePaint()
UIManager:close(info)
-- Delay until the next tick, as this will destroy the Document instance,
-- but we may not be the final Event caught by said Document...
UIManager:nextTick(function()
self:onOpenNextDocumentInFolder()
end)
self:onOpenNextDocumentInFolder()
else
UIManager:show(InfoMessage:new{
text = _("Could not open next file. Sort by last read date does not support this feature."),
@ -172,7 +174,11 @@ function ReaderStatus:onOpenNextDocumentInFolder()
local FileChooser = require("ui/widget/filechooser")
local next_file = FileChooser:getNextFile(self.document.file)
if next_file then
self.ui:switchDocument(next_file)
-- Delay until the next tick, as this will destroy the Document instance,
-- but we may not be the final Event caught by said Document...
UIManager:nextTick(function()
self.ui:switchDocument(next_file)
end)
else
UIManager:show(InfoMessage:new{
text = _("This is the last file in the current folder. No next file to open."),
@ -213,6 +219,7 @@ end
-- Otherwise we change status from reading/abandoned to complete or from complete to reading.
function ReaderStatus:onMarkBook(mark_read)
self.summary.status = (not mark_read and self.summary.status == "complete") and "reading" or "complete"
self.summary.modified = os.date("%Y-%m-%d", os.time())
-- If History is called over Reader, it will read the file to get the book status, so save and flush
self.settings:saveSetting("summary", self.summary)
self.settings:flush()

@ -1,5 +1,6 @@
local BD = require("ui/bidi")
local Blitbuffer = require("ffi/blitbuffer")
local ButtonDialog = require("ui/widget/buttondialog")
local ButtonTable = require("ui/widget/buttontable")
local CenterContainer = require("ui/widget/container/centercontainer")
local CssTweaks = require("ui/data/css_tweaks")
@ -783,13 +784,123 @@ local BOOK_TWEAK_INPUT_HINT = T([[
%2]], _("You can add CSS snippets which will be applied only to this book."), BOOK_TWEAK_SAMPLE_CSS)
local CSS_SUGGESTIONS = {
{ "-cr-hint: footnote-inpage;", _("When set on a block element containing the target id of a href, this block element will be shown as an in-page footnote.")},
{ "-cr-hint: non-linear-combining;", _("Can be set on some specific DocFragments (ie. DocFragment[id*=16]) to ignore them in the linear pages flow.")},
{ "-cr-hint: toc-level1;", _("When set on an element, its text can be used to build the alternative table of contents.")},
{ "display: run-in !important;", _("When set on a block element, this element content will be inlined with the next block element.")},
{ "font-size: 1rem !important;", _("1rem will enforce your main font size.")},
{ "hyphens: none !important;", _("Disables hyphenation inside the targeted elements.")},
{ "text-indent: 1.2em !important;", _("1.2em is our default text indentation.")},
{ _("Long-press for info ⓘ"), _([[
This menu provides a non-exhaustive CSS syntax and properties list. It also shows some KOReader-specific, non-standard CSS features that can be useful with e-books.
Most of these bits are already used by our categorized 'Style tweaks' (found in the top menu). Long-press on any style-tweak option to see its code and its expected results. Should these not be enough to achieve your desired look, you may need to adjust them slightly: tap once on the CSS code-box to copy the code to the clipboard, paste it here and edit it.
Long-press on any item in this popup to get more information on what it does and what it can help solving.
Tap on the item to insert it: you can then edit it and combine it with others.]]), true },
{ _("Matching elements"), {
{ "p.className", _([[
p.className matches a <p> with class='className'.
*.className matches any element with class='className'.
p:not([class]) matches a <p> without any class= attribute.]])},
{ "aside > p", _([[
aside > p matches a <p> children of an <aside> element.
aside p (without any intermediate symbol) matches a <p> descendant of an <aside> element.]])},
{ "p + img", _([[
p + img matches a <img> if its immediate previous sibling is a <p>.
p ~ img matches a <img> if any of its previous siblings is a <p>.]])},
{ "p[name='what']", _([[
[name="what"] matches if the element has the attribute 'name' and its value is exactly 'what'.
[name] matches if the attribute 'name' is present.
[name~="what"] matches if the value of the attribute 'name' contains 'what' as a word (among other words separated by spaces).]])},
{ "p[name*='what' i]", _([[
[name*="what" i] matches any element having the attribute 'name' with a value that contains 'what', case insensitive.
[name^="what"] matches if the attribute value starts with 'what'.
[name$="what"] matches if the attribute value ends with 'what'.]])},
{ "p[_='what']", _([[
Similar in syntax to attribute matching, but matches the inner text of an element.
p[_="what"] matches any <p> whose text is exactly 'what'.
p[_] matches any non-empty <p>.
p:not([_]) matches any empty <p>.
p[_~="what"] matches any <p> that contains the word 'what'.]])},
{ "p[_*='what' i]", _([[
Similar in syntax to attribute matching, but matches the inner text of an element.
p[_*="what" i] matches any <p> that contains 'what', case insensitive.
p[_^="what"] matches any <p> whose text starts with 'what'.
(This can be used to match "Act" or "Scene", or character names in plays, and make them stand out.)
p[_$="what"] matches any <p> whose text ends with 'what'.]])},
{ "p:first-child", _([[
p:first-child matches a <p> that is the first child of its parent.
p:last-child matches a <p> that is the last child of its parent.
p:nth-child(odd) matches any other <p> in a series of sibling <p>.]])},
{ "Tip: use View HTML ⓘ", _([[
On a book page, select some text spanning around (before and after) the element you are interested in, and use 'View HTML'.
In the HTML viewer, long press on tags or text to get a list of selectors matching the element: tap on one of them to copy it to the clipboard.
You can then paste it here with long-press in the text box.]]), true},
}},
{ _("Common classic properties"), {
{ "font-size: 1rem !important;", _("1rem will enforce your main font size.")},
{ "font-weight: normal !important;", _("Remove bold. Use 'bold' to get bold.")},
{ "hyphens: none !important;", _("Disables hyphenation inside the targeted elements.")},
{ "text-indent: 1.2em !important;", _("1.2em is our default text indentation.")},
{ "break-before: always !important;", _("Start a new page with this element. Use 'avoid' to avoid a new page.")},
{ "color: black !important;", _("Force text to be black.")},
{ "background: transparent !important;", _("Remove any background color.")},
{ "max-width: 50vw !important;", _("Limit an element width to 50% of your screen width (use 'max-height: 50vh' for 50% of the screen height). Can be useful with <img> to limit their size.")},
}},
{ _("Private CSS properties"), {
{ "-cr-hint: footnote-inpage;", _("When set on a block element containing the target id of a href, this block element will be shown as an in-page footnote.")},
{ "-cr-hint: non-linear;", _("Can be set on some specific DocFragments (e.g. DocFragment[id$=_16]) to ignore them in the linear pages flow.")},
{ "-cr-hint: non-linear-combining;", _("Can be set on contiguous footnote blocks to ignore them in the linear pages flow.")},
{ "-cr-hint: toc-level1;", _("When set on an element, its text can be used to build the alternative table of contents. toc-level2 to toc-level6 can be used for nested chapters.")},
{ "-cr-hint: toc-ignore;", _("When set on an element, it will be ignored when building the alternative table of contents.")},
{ "-cr-hint: footnote;", _("Can be set on target of links (<div id='..'>) to have their link trigger as footnote popup, in case KOReader wrongly detect this target is not a footnote.")},
{ "-cr-hint: noteref;", _("Can be set on links (<a href='#..'>) to have them trigger as footnote popups, in case KOReader wrongly detect the links is not to a footnote.")},
{ "-cr-hint: noteref-ignore;", _([[
Can be set on links (<a href='#..'>) to have them NOT trigger footnote popups and in-page footnotes.
If some DocFragment presents an index of names with cross references, resulting in in-page footnotes taking half of these pages, you can avoid this with:
DocFragment[id$=_16] a { -cr-hint: noteref-ignore }]])},
}},
{ _("Useful 'content:' values"), {
{ _("Caution ⚠"), _([[
Be careful with these: stick them to a proper discriminating selector, like:
span.specificClassName
p[_*="keyword" i]
If used as-is, they will act on ALL elements!]]), true},
{ "::before {content: ' '}", _("Insert a visible space before an element.")},
{ "::before {content: '\\A0 '}", _("Insert a visible non-breakable space before an element, so it sticks to what's before.")},
{ "::before {content: '\\2060'}", _("U+2060 WORD JOINER may act as a glue (like an invisible non-breakable space) before an element, so it sticks to what's before.")},
{ "::before {content: '\\200B'}", _("U+200B ZERO WIDTH SPACE may allow a linebreak before an element, in case the absence of any space prevents that.")},
{ "::before {content: attr(title)}", _("Insert the value of the attribute 'title' at start of an element content.")},
{ "::before {content: '▶ '}", _("Prepend a visible marker.")},
{ "::before {content: '● '}", _("Prepend a visible marker.")},
{ "::before {content: '█ '}", _("Prepend a visible marker.")},
}},
}
function ReaderStyleTweak:editBookTweak(touchmenu_instance)
@ -869,19 +980,89 @@ function ReaderStyleTweak:editBookTweak(touchmenu_instance)
local suggestions_popup_widget
local buttons = {}
for _, suggestion in ipairs(CSS_SUGGESTIONS) do
local title = suggestion[1]
local is_submenu, submenu_items, description
if type(suggestion[2]) == "table" then
is_submenu = true
submenu_items = suggestion[2]
else
description = suggestion[2]
end
local is_info_only = suggestion[3]
local text
if is_submenu then -- add the same arrow we use for top menu submenus
text = require("ui/widget/menu").getMenuText({text=title, sub_item_table=true})
elseif is_info_only then
text = title
else
text = BD.ltr(title) -- CSS code, keep it LTR
end
table.insert(buttons, {{
text = suggestion[1],
text = text,
id = title,
align = "left",
callback = function()
UIManager:close(suggestions_popup_widget)
editor._input_widget:addChars(suggestion[1])
if is_info_only then
-- No CSS bit to insert, show description also on tap
UIManager:show(InfoMessage:new{ text = description })
return
end
if not is_submenu then -- insert as-is on tap
UIManager:close(suggestions_popup_widget)
editor:addTextToInput(title)
else
local sub_suggestions_popup_widget
local sub_buttons = {}
for _, sub_suggestion in ipairs(submenu_items) do
-- (No 2nd level submenu needed for now)
local sub_title = sub_suggestion[1]
local sub_description = sub_suggestion[2]
local sub_is_info_only = sub_suggestion[3]
local sub_text = sub_is_info_only and sub_title or BD.ltr(sub_title)
table.insert(sub_buttons, {{
text = sub_text,
align = "left",
callback = function()
if sub_is_info_only then
UIManager:show(InfoMessage:new{ text = sub_description })
return
end
UIManager:close(sub_suggestions_popup_widget)
UIManager:close(suggestions_popup_widget)
editor:addTextToInput(sub_title)
end,
hold_callback = sub_description and function()
UIManager:show(InfoMessage:new{ text = sub_description })
end,
}})
end
local anchor_func = function()
local d = suggestions_popup_widget:getButtonById(title).dimen:copy()
if BD.mirroredUILayout() then
d.x = d.x - d.w + Size.padding.default
else
d.x = d.x + d.w - Size.padding.default
end
-- As we don't know if we will pop up or down, anchor it on the middle of the item
d.y = d.y + math.floor(d.h / 2)
d.h = 1
return d, true
end
sub_suggestions_popup_widget = ButtonDialog:new{
modal = true, -- needed when keyboard is shown
width = math.floor(Screen:getWidth() * 0.9), -- max width, will get smaller
shrink_unneeded_width = true,
buttons = sub_buttons,
anchor = anchor_func,
}
UIManager:show(sub_suggestions_popup_widget)
end
end,
hold_callback = suggestion[2] and function()
UIManager:show(InfoMessage:new{ text = suggestion[2] })
hold_callback = description and function()
UIManager:show(InfoMessage:new{ text = description })
end or nil
}})
end
local ButtonDialog = require("ui/widget/buttondialog")
suggestions_popup_widget = ButtonDialog:new{
modal = true, -- needed when keyboard is shown
width = math.floor(Screen:getWidth() * 0.9), -- max width, will get smaller

@ -186,11 +186,11 @@ function ReaderThumbnail:removeFromCache(hash_subs, remove_only_non_matching)
return nb_removed, size_removed
end
function ReaderThumbnail:resetCachedPagesForBookmarks(...)
function ReaderThumbnail:resetCachedPagesForBookmarks(annotations)
-- Multiple bookmarks may be provided
local start_page, end_page
for i = 1, select("#", ...) do
local bm = select(i, ...)
for i = 1, #annotations do
local bm = annotations[i]
if self.ui.rolling then
-- Look at all properties that may be xpointers
for _, k in ipairs({"page", "pos0", "pos1"}) do
@ -278,6 +278,10 @@ function ReaderThumbnail:getPageThumbnail(page, width, height, batch_id, when_ge
end
function ReaderThumbnail:ensureTileGeneration()
if not self._standby_prevented then
self._standby_prevented = true
UIManager:preventStandby()
end
local has_pids_still_to_collect = self:collectPids()
local still_in_progress = false
@ -318,6 +322,11 @@ function ReaderThumbnail:ensureTileGeneration()
end
if self.req_in_progress or has_pids_still_to_collect or next(self.thumbnails_requests) then
self._ensureTileGeneration_action()
else
if self._standby_prevented then
self._standby_prevented = false
UIManager:allowStandby()
end
end
end
@ -513,6 +522,10 @@ function ReaderThumbnail:onCloseDocument()
self.tile_cache:clear()
self.tile_cache = nil
end
if self._standby_prevented then
self._standby_prevented = false
UIManager:allowStandby()
end
end
function ReaderThumbnail:onColorRenderingUpdate()
@ -524,9 +537,6 @@ end
ReaderThumbnail.onDocumentRerendered = ReaderThumbnail.resetCache
ReaderThumbnail.onDocumentPartiallyRerendered = ReaderThumbnail.resetCache
-- Emitted When adding/removing/updating bookmarks and highlights
ReaderThumbnail.onBookmarkAdded = ReaderThumbnail.resetCachedPagesForBookmarks
ReaderThumbnail.onBookmarkRemoved = ReaderThumbnail.resetCachedPagesForBookmarks
ReaderThumbnail.onBookmarkUpdated = ReaderThumbnail.resetCachedPagesForBookmarks
ReaderThumbnail.onBookmarkEdited = ReaderThumbnail.resetCachedPagesForBookmarks
ReaderThumbnail.onAnnotationsModified = ReaderThumbnail.resetCachedPagesForBookmarks
return ReaderThumbnail

@ -60,7 +60,9 @@ end
function ReaderToc:onGesture() end
function ReaderToc:registerKeyEvents()
if Device:hasKeyboard() then
if Device:hasScreenKB() then
self.key_events.ShowToc = { { "ScreenKB", "Up" } }
elseif Device:hasKeyboard() then
self.key_events.ShowToc = { { "T" } }
end
end
@ -104,6 +106,7 @@ end
function ReaderToc:resetToc()
self.toc = nil
self.toc_menu_items_built = false
self.toc_depth = nil
self.ticks = nil
self.ticks_flattened = nil
@ -314,6 +317,37 @@ function ReaderToc:validateAndFixToc()
self.toc_depth = max_depth
end
function ReaderToc:completeTocWithChapterLengths()
local toc = self.toc
local first = 1
local last = #toc
if last == 0 then
return
end
local prev_item_by_level = {}
for i = first, last do
local item = toc[i]
local page = item.page
local depth = item.depth
for j=#prev_item_by_level, depth, -1 do
local prev_item = prev_item_by_level[j]
if prev_item then
prev_item.chapter_length = page - prev_item.page
end
prev_item_by_level[j] = nil
end
prev_item_by_level[depth] = item
end
-- Set the length of the last ones
local page = self.ui.document:getPageCount()
for j=#prev_item_by_level, 0, -1 do
local prev_item = prev_item_by_level[j]
if prev_item then
prev_item.chapter_length = page - prev_item.page
end
end
end
function ReaderToc:getTocIndexByPage(pn_or_xp, skip_ignored_ticks)
self:fillToc()
if #self.toc == 0 then return end
@ -321,7 +355,7 @@ function ReaderToc:getTocIndexByPage(pn_or_xp, skip_ignored_ticks)
if type(pn_or_xp) == "string" then
return self:getAccurateTocIndexByXPointer(pn_or_xp, skip_ignored_ticks)
end
local prev_index = 1
local prev_index = 0
for _k,_v in ipairs(self.toc) do
if not skip_ignored_ticks or not self.toc_ticks_ignored_levels[_v.depth] then
if _v.page == pageno then
@ -336,7 +370,7 @@ function ReaderToc:getTocIndexByPage(pn_or_xp, skip_ignored_ticks)
prev_index = _k
end
end
return prev_index
return prev_index > 0 and prev_index or nil
end
function ReaderToc:getAccurateTocIndexByXPointer(xptr, skip_ignored_ticks)
@ -661,11 +695,16 @@ function ReaderToc:onShowToc()
local items_per_page = G_reader_settings:readSetting("toc_items_per_page") or self.toc_items_per_page_default
local items_font_size = G_reader_settings:readSetting("toc_items_font_size") or Menu.getItemFontSize(items_per_page)
local items_show_chapter_length = G_reader_settings:isTrue("toc_items_show_chapter_length")
local items_with_dots = G_reader_settings:nilOrTrue("toc_items_with_dots")
self:fillToc()
-- build menu items
if #self.toc > 0 and not self.toc[1].text then
if #self.toc > 0 and not self.toc_menu_items_built then
self.toc_menu_items_built = true
if items_show_chapter_length then
self:completeTocWithChapterLengths()
end
-- Have the width of 4 spaces be the unit of indentation
local tmp = TextWidget:new{
text = " ",
@ -679,6 +718,11 @@ function ReaderToc:onShowToc()
v.index = k
v.indent = toc_indent * (v.depth-1)
v.text = self:cleanUpTocTitle(v.title, true)
if items_show_chapter_length then
v.post_text = T("(%1)", v.chapter_length)
else
v.post_text = nil
end
v.bidi_wrap_func = BD.auto
v.mandatory = v.page
if has_hidden_flows then
@ -1147,6 +1191,17 @@ Enabling this option will restrict display to the chapter titles of progress bar
UIManager:show(items_font)
end,
}
menu_items.toc_items_show_chapter_length = {
text = _("Show chapter length"),
keep_menu_open = true,
checked_func = function()
return not G_reader_settings:nilOrFalse("toc_items_show_chapter_length")
end,
callback = function()
G_reader_settings:flipNilOrFalse("toc_items_show_chapter_length")
self.toc_menu_items_built = false
end
}
menu_items.toc_items_with_dots = {
text = _("With dots"),
keep_menu_open = true,

@ -17,7 +17,6 @@ local ReaderTypeset = WidgetContainer:extend{
-- @translators This is style in the sense meant by CSS (cascading style sheets), relating to the layout and presentation of the document. See <https://en.wikipedia.org/wiki/CSS> for more information.
css_menu_title = C_("CSS", "Style"),
css = nil,
internal_css = true,
unscaled_margins = nil,
}
@ -27,8 +26,16 @@ end
function ReaderTypeset:onReadSettings(config)
self.css = config:readSetting("css")
or G_reader_settings:readSetting("copt_css")
or self.ui.document.default_css
if not self.css then
if self.ui.document.is_fb2 then
self.css = G_reader_settings:readSetting("copt_fb2_css")
else
self.css = G_reader_settings:readSetting("copt_css")
end
end
if not self.css then
self.css = self.ui.document.default_css
end
local tweaks_css = self.ui.styletweak:getCssText()
self.ui.document:setStyleSheet(self.css, tweaks_css)
@ -93,7 +100,6 @@ function ReaderTypeset:onToggleEmbeddedStyleSheet(toggle)
else
self.configurable.embedded_css = 0
text = _("Disabled embedded styles.")
self:setStyleSheet(self.ui.document.default_css)
end
self.ui.document:setEmbeddedStyleSheet(self.configurable.embedded_css)
self.ui:handleEvent(Event:new("UpdatePos"))
@ -165,16 +171,17 @@ local OBSOLETED_CSS = {
}
function ReaderTypeset:genStyleSheetMenu()
local getStyleMenuItem = function(text, css_file, separator)
local getStyleMenuItem = function(text, css_file, description, fb2_compatible, separator)
return {
text_func = function()
return text .. (css_file == G_reader_settings:readSetting("copt_css") and "" or "")
local css_opt = self.ui.document.is_fb2 and "copt_fb2_css" or "copt_css"
return text .. (css_file == G_reader_settings:readSetting(css_opt) and "" or "")
end,
callback = function()
self:setStyleSheet(css_file or self.ui.document.default_css)
end,
hold_callback = function(touchmenu_instance)
self:makeDefaultStyleSheet(css_file, text, touchmenu_instance)
self:makeDefaultStyleSheet(css_file, text, description, touchmenu_instance)
end,
checked_func = function()
if not css_file then -- "Auto"
@ -182,6 +189,16 @@ function ReaderTypeset:genStyleSheetMenu()
end
return css_file == self.css
end,
enabled_func = function()
if fb2_compatible == true and not self.ui.document.is_fb2 then
return false
end
if fb2_compatible == false and self.ui.document.is_fb2 then
return false
end
-- if fb2_compatible==nil, we don't know (user css file)
return true
end,
separator = separator,
}
end
@ -189,8 +206,18 @@ function ReaderTypeset:genStyleSheetMenu()
local style_table = {}
local obsoleted_table = {}
table.insert(style_table, getStyleMenuItem(_("None"), ""))
table.insert(style_table, getStyleMenuItem(_("Auto"), nil, true))
table.insert(style_table, getStyleMenuItem(
_("None"),
"",
_("This sets an empty User-Agent stylesheet, and expects the document stylesheet to style everything (which publishers probably don't).\nThis is mostly only interesting for testing.")
))
table.insert(style_table, getStyleMenuItem(
_("Auto"),
nil,
_("This selects the default and preferred stylesheet for the document type."),
nil,
true -- separator
))
local css_files = {}
for f in lfs.dir("./data") do
@ -200,15 +227,39 @@ function ReaderTypeset:genStyleSheetMenu()
end
-- Add the 3 main styles
if css_files["epub.css"] then
table.insert(style_table, getStyleMenuItem(_("HTML / EPUB (epub.css)"), css_files["epub.css"]))
table.insert(style_table, getStyleMenuItem(
_("Traditional book look (epub.css)"),
css_files["epub.css"],
_([[
This is our book look-alike stylesheet: it extends the HTML standard stylesheet with styles aimed at making HTML content look more like a paper book (with justified text and indentation on paragraphs) than like a web page.
It is perfect for unstyled books, and might make styled books more readable.
It may cause some small issues on some books (miscentered titles, headings or separators, or unexpected text indentation), as publishers don't expect to have our added styles at play and need to reset them; try switching to html5.css when you notice such issues.]]),
false -- not fb2_compatible
))
css_files["epub.css"] = nil
end
if css_files["html5.css"] then
table.insert(style_table, getStyleMenuItem(_("HTML5 (html5.css)"), css_files["html5.css"]))
table.insert(style_table, getStyleMenuItem(
_("HTML Standard rendering (html5.css)"),
css_files["html5.css"],
_([[
This stylesheet conforms to the HTML Standard rendering suggestions (with a few limitations), similar to what most web browsers use.
As most publishers nowadays make and test their book with tools based on web browser engines, it is the stylesheet to use to see a book as these publishers intended.
On unstyled books though, it may give them the look of a web page (left aligned paragraphs without indentation and with spacing between them); try switching to epub.css when that happens.]]),
false -- not fb2_compatible
))
css_files["html5.css"] = nil
end
if css_files["fb2.css"] then
table.insert(style_table, getStyleMenuItem(_("FictionBook (fb2.css)"), css_files["fb2.css"], true))
table.insert(style_table, getStyleMenuItem(
_("FictionBook (fb2.css)"),
css_files["fb2.css"],
_([[
This stylesheet is to be used only with FB2 and FB3 documents, which are not classic HTML, and need some specific styling.
(FictionBook 2 & 3 are open XML-based e-book formats which originated and gained popularity in Russia.)]]),
true, -- fb2_compatible
true -- separator
))
css_files["fb2.css"] = nil
end
-- Add the obsoleted ones to the Obsolete sub menu
@ -216,7 +267,7 @@ function ReaderTypeset:genStyleSheetMenu()
for __, css in ipairs(OBSOLETED_CSS) do
obsoleted_css[css_files[css]] = css
if css_files[css] then
table.insert(obsoleted_table, getStyleMenuItem(css, css_files[css]))
table.insert(obsoleted_table, getStyleMenuItem(css, css_files[css], _("This stylesheet is obsolete: don't use it. It is kept solely to be able to open documents last read years ago and to migrate their highlights.")))
css_files[css] = nil
end
end
@ -227,7 +278,7 @@ function ReaderTypeset:genStyleSheetMenu()
end
table.sort(user_files)
for __, css in ipairs(user_files) do
table.insert(style_table, getStyleMenuItem(css, css_files[css]))
table.insert(style_table, getStyleMenuItem(css, css_files[css], _("This is a user added stylesheet.")))
end
style_table[#style_table].separator = true
@ -266,6 +317,7 @@ function ReaderTypeset:setStyleSheet(new_css)
end
end
-- Not used
function ReaderTypeset:setEmbededStyleSheetOnly()
if self.css ~= nil then
-- clear applied css
@ -355,11 +407,20 @@ function ReaderTypeset:addToMainMenu(menu_items)
}
end
function ReaderTypeset:makeDefaultStyleSheet(css, text, touchmenu_instance)
function ReaderTypeset:makeDefaultStyleSheet(css, name, description, touchmenu_instance)
local text = self.ui.document.is_fb2 and T(_("Set default style for FB2 documents to %1?"), BD.filename(name))
or T(_("Set default style to %1?"), BD.filename(name))
if description then
text = text .. "\n\n" .. description
end
UIManager:show(ConfirmBox:new{
text = T(_("Set default style to %1?"), BD.filename(text)),
text = text,
ok_callback = function()
G_reader_settings:saveSetting("copt_css", css)
if self.ui.document.is_fb2 then
G_reader_settings:saveSetting("copt_fb2_css", css)
else
G_reader_settings:saveSetting("copt_css", css)
end
if touchmenu_instance then touchmenu_instance:updateItems() end
end,
})

@ -84,9 +84,9 @@ local LANGUAGES = {
{ "pt-BR", {}, "HB ", _("Portuguese (BR)"), "Portuguese_BR.pattern" },
{ "rm", {"roh"}, "H ", _("Romansh"), "Romansh.pattern" },
{ "ro", {"ron"}, "H ", _("Romanian"), "Romanian.pattern" },
{ "ru", {"rus"}, "Hb ", _("Russian"), "Russian.pattern" },
{ "ru-GB", {}, "Hb ", _("Russian + English (UK)"), "Russian_EnGB.pattern" },
{ "ru-US", {}, "Hb ", _("Russian + English (US)"), "Russian_EnUS.pattern" },
{ "ru", {"rus"}, "HB ", _("Russian"), "Russian.pattern" },
{ "ru-GB", {}, "HB ", _("Russian + English (UK)"), "Russian_EnGB.pattern" },
{ "ru-US", {}, "HB ", _("Russian + English (US)"), "Russian_EnUS.pattern" },
{ "sr", {"srp"}, "HB ", _("Serbian"), "Serbian.pattern" },
{ "sk", {"slk"}, "HB ", _("Slovak"), "Slovak.pattern" },
{ "sl", {"slv"}, "H ", _("Slovenian"), "Slovenian.pattern" },

@ -95,7 +95,6 @@ function ReaderView:init()
temp_drawer = "invert",
temp = {},
saved_drawer = "lighten",
saved = {},
indicator = nil, -- geom: non-touch highlight position indicator: {x = 50, y=50}
}
self.page_states = {}
@ -210,22 +209,26 @@ function ReaderView:paintTo(bb, x, y)
end
end
-- dim last read area
-- mark last read area of overlapped pages
if not self.dim_area:isEmpty() and self:isOverlapAllowed() then
if self.page_overlap_style == "dim" then
bb:dimRect(
self.dim_area.x, self.dim_area.y,
self.dim_area.w, self.dim_area.h
)
elseif self.page_overlap_style == "arrow" then
local center_offset = bit.rshift(self.arrow.height, 1)
-- Paint at the proper y origin depending on wheter we paged forward (dim_area.y == 0) or backward
self.arrow:paintTo(bb, 0, self.dim_area.y == 0 and self.dim_area.h - center_offset or self.dim_area.y - center_offset)
elseif self.page_overlap_style == "line" then
bb:paintRect(0, self.dim_area.y == 0 and self.dim_area.h or self.dim_area.y,
self.dim_area.w, Size.line.medium, Blitbuffer.COLOR_BLACK)
bb:dimRect(self.dim_area.x, self.dim_area.y, self.dim_area.w, self.dim_area.h)
else
-- Paint at the proper y origin depending on whether we paged forward (dim_area.y == 0) or backward
local paint_y = self.dim_area.y == 0 and self.dim_area.h or self.dim_area.y
if self.page_overlap_style == "arrow" then
local center_offset = bit.rshift(self.arrow.height, 1)
self.arrow:paintTo(bb, 0, paint_y - center_offset)
elseif self.page_overlap_style == "line" then
bb:paintRect(0, paint_y, self.dim_area.w, Size.line.medium, Blitbuffer.COLOR_DARK_GRAY)
elseif self.page_overlap_style == "dashed_line" then
for i = 0, self.dim_area.w - 20, 20 do
bb:paintRect(i, paint_y, 14, Size.line.medium, Blitbuffer.COLOR_DARK_GRAY)
end
end
end
end
-- draw saved highlight
if self.highlight_visible then
self:drawSavedHighlight(bb, x, y)
@ -261,6 +264,10 @@ function ReaderView:paintTo(bb, x, y)
if self.ui.paging then
if self.document.hw_dithering then
self.dialog.dithered = true
-- Assume we're going to be showing colorful stuff on kaleido panels...
if Device:hasKaleidoWfm() then
UIManager:setDirty(nil, "color")
end
end
else
-- Whereas for CRe,
@ -276,6 +283,10 @@ function ReaderView:paintTo(bb, x, y)
if self.state.drawn == false and G_reader_settings:nilOrTrue("refresh_on_pages_with_images") then
UIManager:setDirty(nil, "full")
end
-- On Kaleido panels, we'll want to use GCC16 on the actual image, always...
if Device:hasKaleidoWfm() and img_coverage >= 0.075 then
UIManager:setDirty(nil, "color")
end
end
self.state.drawn = true
end
@ -418,7 +429,7 @@ function ReaderView:getScrollPagePosition(pos)
end
function ReaderView:getScrollPageRect(page, rect_p)
local rect_s = Geom:new{}
local rect_s = Geom:new()
for _, state in ipairs(self.page_states) do
local trans_p = Geom:new(rect_p):copy()
trans_p:transformByScale(state.zoom, state.zoom)
@ -463,7 +474,7 @@ function ReaderView:getSinglePagePosition(pos)
end
function ReaderView:getSinglePageRect(rect_p)
local rect_s = Geom:new{}
local rect_s = Geom:new()
local trans_p = Geom:new(rect_p):copy()
trans_p:transformByScale(self.state.zoom, self.state.zoom)
if self.visible_area:intersectWith(trans_p) then
@ -522,6 +533,7 @@ function ReaderView:drawTempHighlight(bb, x, y)
end
function ReaderView:drawSavedHighlight(bb, x, y)
if #self.ui.annotation.annotations == 0 then return end
if self.ui.paging then
self:drawPageSavedHighlight(bb, x, y)
else
@ -529,45 +541,18 @@ function ReaderView:drawSavedHighlight(bb, x, y)
end
end
-- Returns the list of highlights in page.
-- The list includes full single-page highlights and parts of multi-page highlights.
function ReaderView:getPageSavedHighlights(page)
local highlights = {}
local is_reflow = self.document.configurable.text_wrap
self.document.configurable.text_wrap = 0
for page_num, page_highlights in pairs(self.highlight.saved) do
for i, highlight in ipairs(page_highlights) do
-- old single-page reflow highlights do not have page in position
local pos0_page = highlight.pos0.page or page_num
local pos1_page = highlight.pos1.page or page_num
if pos0_page <= page and page <= pos1_page then
if pos0_page == pos1_page then -- single-page highlight
table.insert(highlights, highlight)
else -- multi-page highlight
local item = self.ui.highlight:getSavedExtendedHighlightPage(highlight, page, i)
table.insert(highlights, item)
end
end
end
end
self.document.configurable.text_wrap = is_reflow
return highlights
end
function ReaderView:drawPageSavedHighlight(bb, x, y)
local pages = self:getCurrentPageList()
for _, page in ipairs(pages) do
local items = self:getPageSavedHighlights(page)
local items = self.ui.highlight:getPageSavedHighlights(page)
for _, item in ipairs(items) do
local boxes = self.document:getPageBoxesFromPositions(page, item.pos0, item.pos1)
if boxes then
local drawer = item.drawer or self.highlight.saved_drawer
local draw_note_mark = self.highlight.note_mark and
self.ui.bookmark:getBookmarkNote({datetime = item.datetime})
local draw_note_mark = item.note and self.highlight.note_mark
for _, box in ipairs(boxes) do
local rect = self:pageToScreenTransform(page, box)
if rect then
self:drawHighlightRect(bb, x, y, rect, drawer, draw_note_mark)
self:drawHighlightRect(bb, x, y, rect, item.drawer, draw_note_mark)
if draw_note_mark and self.highlight.note_mark == "sidemark" then
draw_note_mark = false -- side mark in the first line only
end
@ -583,48 +568,38 @@ function ReaderView:drawXPointerSavedHighlight(bb, x, y)
-- showing menu...). We might want to cache these boxes per page (and
-- clear that cache when page layout change or highlights are added
-- or removed).
local cur_view_top, cur_view_bottom
for _, items in pairs(self.highlight.saved) do
if items then
for j = 1, #items do
local item = items[j]
local pos0, pos1 = item.pos0, item.pos1
-- document:getScreenBoxesFromPositions() is expensive, so we
-- first check this item is on current page
if not cur_view_top then
-- Even in page mode, it's safer to use pos and ui.dimen.h
-- than pages' xpointers pos, even if ui.dimen.h is a bit
-- larger than pages' heights
cur_view_top = self.document:getCurrentPos()
if self.view_mode == "page" and self.document:getVisiblePageCount() > 1 then
cur_view_bottom = cur_view_top + 2 * self.ui.dimen.h
else
cur_view_bottom = cur_view_top + self.ui.dimen.h
end
end
local spos0 = self.document:getPosFromXPointer(pos0)
local spos1 = self.document:getPosFromXPointer(pos1)
local start_pos = math.min(spos0, spos1)
local end_pos = math.max(spos0, spos1)
if start_pos <= cur_view_bottom and end_pos >= cur_view_top then
local boxes = self.document:getScreenBoxesFromPositions(pos0, pos1, true) -- get_segments=true
if boxes then
local drawer = item.drawer or self.highlight.saved_drawer
local draw_note_mark = self.highlight.note_mark and
self.ui.bookmark:getBookmarkNote({datetime = item.datetime})
for _, box in ipairs(boxes) do
if box.h ~= 0 then
self:drawHighlightRect(bb, x, y, box, drawer, draw_note_mark)
if draw_note_mark and self.highlight.note_mark == "sidemark" then
draw_note_mark = false -- side mark in the first line only
end
-- Even in page mode, it's safer to use pos and ui.dimen.h
-- than pages' xpointers pos, even if ui.dimen.h is a bit
-- larger than pages' heights
local cur_view_top = self.document:getCurrentPos()
local cur_view_bottom
if self.view_mode == "page" and self.document:getVisiblePageCount() > 1 then
cur_view_bottom = cur_view_top + 2 * self.ui.dimen.h
else
cur_view_bottom = cur_view_top + self.ui.dimen.h
end
for _, item in ipairs(self.ui.annotation.annotations) do
if item.drawer then
-- document:getScreenBoxesFromPositions() is expensive, so we
-- first check if this item is on current page
local start_pos = self.document:getPosFromXPointer(item.pos0)
local end_pos = self.document:getPosFromXPointer(item.pos1)
if start_pos <= cur_view_bottom and end_pos >= cur_view_top then
local boxes = self.document:getScreenBoxesFromPositions(item.pos0, item.pos1, true) -- get_segments=true
if boxes then
local draw_note_mark = item.note and self.highlight.note_mark
for _, box in ipairs(boxes) do
if box.h ~= 0 then
self:drawHighlightRect(bb, x, y, box, item.drawer, draw_note_mark)
if draw_note_mark and self.highlight.note_mark == "sidemark" then
draw_note_mark = false -- side mark in the first line only
end
end -- end for each box
end -- end if boxes
end
end
end
end -- end for each highlight
end
end
end -- end for all saved highlight
end
end
function ReaderView:drawHighlightRect(bb, _x, _y, rect, drawer, draw_note_mark)
@ -632,7 +607,7 @@ function ReaderView:drawHighlightRect(bb, _x, _y, rect, drawer, draw_note_mark)
if drawer == "lighten" then
bb:lightenRect(x, y, w, h, self.highlight.lighten_factor)
elseif drawer == "underscore" then
bb:paintRect(x, y + h - 1, w, Size.line.medium, Blitbuffer.COLOR_GRAY)
bb:paintRect(x, y + h - 1, w, Size.line.thick, Blitbuffer.COLOR_GRAY_4)
elseif drawer == "strikeout" then
local line_y = y + math.floor(h / 2) + 1
if self.ui.paging then
@ -695,7 +670,8 @@ function ReaderView:recalculate()
-- start from right of page_area
self.visible_area.x = self.page_area.x + self.page_area.w - self.visible_area.w
end
if self.ui.zooming.zoom_bottom_to_top then
-- Check if we are in zoom_bottom_to_top
if self.document.configurable.zoom_direction and self.document.configurable.zoom_direction >= 2 and self.document.configurable.zoom_direction <= 5 then
-- starts from bottom of page_area
self.visible_area.y = self.page_area.y + self.page_area.h - self.visible_area.h
else
@ -815,18 +791,38 @@ end
function ReaderView:onSetRotationMode(rotation)
if rotation ~= nil then
if rotation == Screen:getRotationMode() then
return true
local old_rotation = Screen:getRotationMode()
if rotation == old_rotation then
return
end
-- NOTE: We cannot rely on getScreenMode, as it actually checks the screen dimensions, instead of the rotation mode.
-- (i.e., it returns how the screen *looks* like, not how it's oriented relative to its native layout).
-- This would horribly break if you started in Portrait (both rotation and visually),
-- then resized your window to a Landscape layout *without* changing the rotation.
-- If you then attempted to switch to a Landscape *rotation*, it would mistakenly think the layout hadn't changed!
-- So, instead, as we're concerned with *rotation* layouts, just compare the two.
-- We use LinuxFB-style constants, so, Portraits are even, Landscapes are odds, making this trivial.
local matching_orientation = bit.band(rotation, 1) == bit.band(old_rotation, 1)
if rotation ~= old_rotation and matching_orientation then
-- No layout change, just rotate & repaint with a flash
Screen:setRotationMode(rotation)
UIManager:setDirty(self.dialog, "full")
Notification:notify(T(_("Rotation mode set to: %1"), optionsutil:getOptionText("SetRotationMode", rotation)))
return
end
Screen:setRotationMode(rotation)
end
UIManager:setDirty(self.dialog, "full")
UIManager:setDirty(nil, "full") -- SetDimensions will only request a partial, we want a flash
local new_screen_size = Screen:getSize()
self.ui:handleEvent(Event:new("SetDimensions", new_screen_size))
self.ui:onScreenResize(new_screen_size)
self.ui:handleEvent(Event:new("InitScrollPageStates"))
Notification:notify(T(_("Rotation mode set to: %1"), optionsutil:getOptionText("SetRotationMode", rotation)))
return true
return
end
function ReaderView:onSetDimensions(dimensions)
@ -893,40 +889,6 @@ function ReaderView:onReadSettings(config)
self:resetLayout()
local page_scroll = config:readSetting("kopt_page_scroll") or self.document.configurable.page_scroll
self.page_scroll = page_scroll == 1 and true or false
self.highlight.saved = config:readSetting("highlight", {})
-- Highlight formats in crengine and mupdf are incompatible.
-- Backup highlights when the document is opened with incompatible engine.
local page, page_highlights
while true do -- remove empty tables for pages without highlights and get the first page with highlights
page, page_highlights = next(self.highlight.saved)
if not page or #page_highlights > 0 then
break -- we're done (there is none, or there is some usable)
else
self.highlight.saved[page] = nil -- clean it up while we're at it, and find another one
end
end
if page_highlights then
local highlight_type = type(page_highlights[1].pos0)
if self.ui.rolling and highlight_type == "table" then
config:saveSetting("highlight_paging", self.highlight.saved)
self.highlight.saved = config:readSetting("highlight_rolling", {})
config:saveSetting("highlight", self.highlight.saved)
config:delSetting("highlight_rolling")
elseif self.ui.paging and highlight_type == "string" then
config:saveSetting("highlight_rolling", self.highlight.saved)
self.highlight.saved = config:readSetting("highlight_paging", {})
config:saveSetting("highlight", self.highlight.saved)
config:delSetting("highlight_paging")
end
else
if self.ui.rolling and config:has("highlight_rolling") then
self.highlight.saved = config:readSetting("highlight_rolling")
config:delSetting("highlight_rolling")
elseif self.ui.paging and config:has("highlight_paging") then
self.highlight.saved = config:readSetting("highlight_paging")
config:delSetting("highlight_paging")
end
end
self.inverse_reading_order = config:isTrue("inverse_reading_order") or G_reader_settings:isTrue("inverse_reading_order")
self.page_overlap_enable = config:isTrue("show_overlap_enable") or G_reader_settings:isTrue("page_overlap_enable") or G_defaults:readSetting("DSHOWOVERLAP")
self.page_overlap_style = config:readSetting("page_overlap_style") or G_reader_settings:readSetting("page_overlap_style") or "dim"
@ -1086,7 +1048,6 @@ function ReaderView:onSaveSettings()
if G_reader_settings:nilOrFalse("lock_rotation") then
self.document.configurable.rotation_mode = Screen:getRotationMode() -- will be saved by ReaderConfig
end
self.ui.doc_settings:saveSetting("highlight", self.highlight.saved)
self.ui.doc_settings:saveSetting("inverse_reading_order", self.inverse_reading_order)
self.ui.doc_settings:saveSetting("show_overlap_enable", self.page_overlap_enable)
self.ui.doc_settings:saveSetting("page_overlap_style", self.page_overlap_style)

@ -113,6 +113,17 @@ function ReaderWikipedia:addToMainMenu(menu_items)
})
end,
}
local function genChoiceMenuEntry(title, setting, value, default)
return {
text = title,
checked_func = function()
return G_reader_settings:readSetting(setting, default) == value
end,
callback = function()
G_reader_settings:saveSetting(setting, value)
end,
}
end
menu_items.wikipedia_settings = {
text = _("Wikipedia settings"),
sub_item_table = {
@ -170,6 +181,7 @@ function ReaderWikipedia:addToMainMenu(menu_items)
UIManager:show(wikilang_input)
wikilang_input:onShowKeyboard()
end,
separator = true,
},
{ -- setting used by dictquicklookup
text = _("Set Wikipedia 'Save as EPUB' folder"),
@ -199,6 +211,41 @@ You can choose an existing folder, or use a default folder named "Wikipedia" in
callback = function()
G_reader_settings:flipNilOrFalse("wikipedia_save_in_book_dir")
end,
},
{ -- setting used in wikipedia.lua
text_func = function()
local include_images = _("ask")
if G_reader_settings:readSetting("wikipedia_epub_include_images") == true then
include_images = _("always")
elseif G_reader_settings:readSetting("wikipedia_epub_include_images") == false then
include_images = _("never")
end
return T(_("Include images in EPUB: %1"), include_images)
end,
sub_item_table = {
genChoiceMenuEntry(_("Ask"), "wikipedia_epub_include_images", nil),
genChoiceMenuEntry(_("Include images"), "wikipedia_epub_include_images", true),
genChoiceMenuEntry(_("Don't include images"), "wikipedia_epub_include_images", false),
},
},
{ -- setting used in wikipedia.lua
text_func = function()
local images_quality = _("ask")
if G_reader_settings:readSetting("wikipedia_epub_highres_images") == true then
images_quality = _("higher")
elseif G_reader_settings:readSetting("wikipedia_epub_highres_images") == false then
images_quality = _("standard")
end
return T(_("Images quality in EPUB: %1"), images_quality)
end,
enabled_func = function()
return G_reader_settings:readSetting("wikipedia_epub_include_images") ~= false
end,
sub_item_table = {
genChoiceMenuEntry(_("Ask"), "wikipedia_epub_highres_images", nil),
genChoiceMenuEntry(_("Standard quality"), "wikipedia_epub_highres_images", false),
genChoiceMenuEntry(_("Higher quality"), "wikipedia_epub_highres_images", true),
},
separator = true,
},
{

@ -230,11 +230,10 @@ function ReaderZooming:onReadSettings(config)
-- Otherwise, build it from the split genus & type settings
local zoom_mode_genus = config:readSetting("kopt_zoom_mode_genus")
or G_reader_settings:readSetting("kopt_zoom_mode_genus")
or 3 -- autocrop is default then pagewidth will be the default as well
local zoom_mode_type = config:readSetting("kopt_zoom_mode_type")
or G_reader_settings:readSetting("kopt_zoom_mode_type")
if zoom_mode_genus or zoom_mode_type then
zoom_mode = self:combo_to_mode(zoom_mode_genus, zoom_mode_type)
end
zoom_mode = self:combo_to_mode(zoom_mode_genus, zoom_mode_type)
-- Validate it
zoom_mode = self.zoom_mode_label[zoom_mode] and zoom_mode or self.DEFAULT_ZOOM_MODE
@ -527,7 +526,7 @@ function ReaderZooming:getZoom(pageno)
local ubbox_dimen = self.ui.document:getUsedBBoxDimensions(pageno, 1)
-- if bbox is larger than the native page dimension render the full page
-- See discussion in koreader/koreader#970.
if (ubbox_dimen.w <= page_size.w and ubbox_dimen.h <= page_size.h) or (self.ui.document.configurable.trim_page == 1) then
if ubbox_dimen.w <= page_size.w and ubbox_dimen.h <= page_size.h then
page_size = ubbox_dimen
self.view:onBBoxUpdate(ubbox_dimen)
else

@ -21,9 +21,11 @@ local InfoMessage = require("ui/widget/infomessage")
local InputContainer = require("ui/widget/container/inputcontainer")
local InputDialog = require("ui/widget/inputdialog")
local LanguageSupport = require("languagesupport")
local NetworkListener = require("ui/network/networklistener")
local Notification = require("ui/widget/notification")
local PluginLoader = require("pluginloader")
local ReaderActivityIndicator = require("apps/reader/modules/readeractivityindicator")
local ReaderAnnotation = require("apps/reader/modules/readerannotation")
local ReaderBack = require("apps/reader/modules/readerback")
local ReaderBookmark = require("apps/reader/modules/readerbookmark")
local ReaderConfig = require("apps/reader/modules/readerconfig")
@ -60,6 +62,7 @@ local Screenshoter = require("ui/widget/screenshoter")
local SettingsMigration = require("ui/data/settings_migration")
local UIManager = require("ui/uimanager")
local ffiUtil = require("ffi/util")
local filemanagerutil = require("apps/filemanager/filemanagerutil")
local lfs = require("libs/libkoreader-lfs")
local logger = require("logger")
local time = require("ui/time")
@ -83,7 +86,7 @@ local ReaderUI = InputContainer:extend{
password = nil,
postInitCallback = nil,
postReaderCallback = nil,
postReaderReadyCallback = nil,
}
function ReaderUI:registerModule(name, ui_module, always_active)
@ -102,8 +105,8 @@ function ReaderUI:registerPostInitCallback(callback)
table.insert(self.postInitCallback, callback)
end
function ReaderUI:registerPostReadyCallback(callback)
table.insert(self.postReaderCallback, callback)
function ReaderUI:registerPostReaderReadyCallback(callback)
table.insert(self.postReaderReadyCallback, callback)
end
function ReaderUI:init()
@ -116,7 +119,7 @@ function ReaderUI:init()
Device:setIgnoreInput(true) -- Avoid ANRs on Android with unprocessed events.
self.postInitCallback = {}
self.postReaderCallback = {}
self.postReaderReadyCallback = {}
-- if we are not the top level dialog ourselves, it must be given in the table
if not self.dialog then
self.dialog = self
@ -182,6 +185,12 @@ function ReaderUI:init()
view = self.view,
ui = self
})
self:registerModule("annotation", ReaderAnnotation:new{
dialog = self.dialog,
view = self.view,
ui = self,
document = self.document,
})
-- reader goto controller
-- "goto" being a dirty keyword in Lua?
self:registerModule("gotopage", ReaderGoto:new{
@ -427,6 +436,12 @@ function ReaderUI:init()
view = self.view,
ui = self,
})
self:registerModule("networklistener", NetworkListener:new {
document = self.document,
view = self.view,
ui = self,
})
-- koreader plugins
for _, plugin_module in ipairs(PluginLoader:loadPlugins()) do
local ok, plugin_or_err = PluginLoader:createPluginInstance(
@ -444,15 +459,6 @@ function ReaderUI:init()
end
end
if Device:hasWifiToggle() then
local NetworkListener = require("ui/network/networklistener")
self:registerModule("networklistener", NetworkListener:new {
document = self.document,
view = self.view,
ui = self,
})
end
-- Allow others to change settings based on external factors
-- Must be called after plugins are loaded & before setting are read.
self:handleEvent(Event:new("DocSettingsLoad", self.doc_settings, self.document))
@ -470,30 +476,31 @@ function ReaderUI:init()
-- And have an extended and customized copy in memory for quick access.
self.doc_props = FileManagerBookInfo.extendProps(props, self.document.file)
-- Set "reading" status if there is no status.
local md5 = self.doc_settings:readSetting("partial_md5_checksum")
if md5 == nil then
md5 = util.partialMD5(self.document.file)
self.doc_settings:saveSetting("partial_md5_checksum", md5)
end
local summary = self.doc_settings:readSetting("summary", {})
if summary.status == nil then
summary.status = "reading"
summary.modified = os.date("%Y-%m-%d", os.time())
end
local md5 = self.doc_settings:readSetting("partial_md5_checksum")
if md5 == nil then
md5 = util.partialMD5(self.document.file)
self.doc_settings:saveSetting("partial_md5_checksum", md5)
if summary.status ~= "complete" or not G_reader_settings:isTrue("history_freeze_finished_books") then
require("readhistory"):addItem(self.document.file) -- (will update "lastfile")
end
require("readhistory"):addItem(self.document.file) -- (will update "lastfile")
-- After initialisation notify that document is loaded and rendered
-- CREngine only reports correct page count after rendering is done
-- Need the same event for PDF document
self:handleEvent(Event:new("ReaderReady", self.doc_settings))
for _,v in ipairs(self.postReaderCallback) do
for _,v in ipairs(self.postReaderReadyCallback) do
v()
end
self.postReaderCallback = nil
self.postReaderReadyCallback = nil
Device:setIgnoreInput(false) -- Allow processing of events (on Android).
Input:inhibitInputUntil(0.2)
@ -516,6 +523,18 @@ function ReaderUI:registerKeyEvents()
if Device:hasKeys() then
self.key_events.Home = { { "Home" } }
self.key_events.Reload = { { "F5" } }
if Device:hasDPad() and Device:useDPadAsActionKeys() then
self.key_events.KeyContentSelection = { { { "Up", "Down" } }, event = "StartHighlightIndicator" }
end
if Device:hasScreenKB() or Device:hasSymKey() then
if Device:hasKeyboard() then
self.key_events.KeyToggleWifi = { { "Shift", "Home" }, event = "ToggleWifi" }
self.key_events.OpenLastDoc = { { "Shift", "Back" } }
else -- Currently exclusively targets Kindle 4.
self.key_events.KeyToggleWifi = { { "ScreenKB", "Home" }, event = "ToggleWifi" }
self.key_events.OpenLastDoc = { { "ScreenKB", "Back" } }
end
end
end
end
@ -550,7 +569,6 @@ function ReaderUI:showFileManager(file)
local last_dir, last_file
if file then
last_dir = util.splitFilePathName(file)
last_dir = last_dir:match("(.*)/")
last_file = file
else
last_dir, last_file = self:getLastDirFile(true)
@ -583,16 +601,17 @@ end
function ReaderUI:showReader(file, provider, seamless)
logger.dbg("show reader ui")
file = ffiUtil.realpath(file)
if lfs.attributes(file, "mode") ~= "file" then
UIManager:show(InfoMessage:new{
text = T(_("File '%1' does not exist."), BD.filepath(file))
text = T(_("File '%1' does not exist."), BD.filepath(filemanagerutil.abbreviate(file)))
})
return
end
if not DocumentRegistry:hasProvider(file) and provider == nil then
UIManager:show(InfoMessage:new{
text = T(_("File '%1' is not supported."), BD.filepath(file))
text = T(_("File '%1' is not supported."), BD.filepath(filemanagerutil.abbreviate(file)))
})
self:showFileManager(file)
return
@ -608,7 +627,7 @@ end
function ReaderUI:showReaderCoroutine(file, provider, seamless)
UIManager:show(InfoMessage:new{
text = T(_("Opening file '%1'."), BD.filepath(file)),
text = T(_("Opening file '%1'."), BD.filepath(filemanagerutil.abbreviate(file))),
timeout = 0.0,
invisible = seamless,
})

@ -86,6 +86,13 @@ function Dbg:v(...)
end
end
--- Conditional logging with a stable ref.
function Dbg.log(...)
if Dbg.is_on then
return LvDEBUG(...)
end
end
--- Simple traceback.
function Dbg:traceback()
return LvDEBUG(debug.traceback())

@ -460,6 +460,8 @@ function Device:test()
end
function Device:exit()
Generic.exit(self)
android.LOGI(string.format("Stopping %s main activity", android.prop.name))
android.lib.ANativeActivity_finish(android.app.activity)
end

@ -204,6 +204,19 @@ if Device:hasGSensor() then
Notification:notify(new_text)
return true
end
function DeviceListener:onLockGSensor()
G_reader_settings:flipNilOrFalse("input_lock_gsensor")
Device:lockGSensor(G_reader_settings:isTrue("input_lock_gsensor"))
local new_text
if G_reader_settings:isTrue("input_lock_gsensor") then
new_text = _("Orientation locked.")
else
new_text = _("Orientation unlocked.")
end
Notification:notify(new_text)
return true
end
end
if not Device:isAlwaysFullscreen() then

@ -41,8 +41,11 @@ local Device = {
hasAuxBattery = no,
hasKeyboard = no,
hasKeys = no,
hasScreenKB = no, -- in practice only some Kindles
hasSymKey = no, -- in practice only some Kindles
canKeyRepeat = no,
hasDPad = no,
useDPadAsActionKeys = no,
hasExitOptions = yes,
hasFewKeys = no,
hasWifiToggle = yes,
@ -62,6 +65,7 @@ local Device = {
hasExternalSD = no, -- or other storage volume that cannot be accessed using the File Manager
canHWDither = no,
canHWInvert = no,
hasKaleidoWfm = no,
canDoSwipeAnimation = no,
canModifyFBInfo = no, -- some NTX boards do wonky things with the rotate flag after a FBIOPUT_VSCREENINFO ioctl
canUseCBB = yes, -- The C BB maintains a 1:1 feature parity with the Lua BB, except that is has NO support for BB4, and limited support for BBRGB24
@ -326,7 +330,7 @@ function Device:onPowerEvent(ev)
self:resume()
local widget_was_closed = Screensaver:close()
if widget_was_closed and self:needsScreenRefreshAfterResume() then
UIManager:scheduleIn(1, function() self.screen:refreshFull() end)
UIManager:scheduleIn(1, function() self.screen:refreshFull(0, 0, self.screen:getWidth(), self.screen:getHeight()) end)
end
self.powerd:afterResume()
end
@ -371,7 +375,7 @@ function Device:onPowerEvent(ev)
-- and on platforms where we defer to a system tool, it'd probably suspend too early!
-- c.f., #6676
if self:needsScreenRefreshAfterResume() then
self.screen:refreshFull()
self.screen:refreshFull(0, 0, self.screen:getWidth(), self.screen:getHeight())
end
-- NOTE: In the same vein as above, make sure we update the screen *now*, before dealing with Wi-Fi.
UIManager:forceRePaint()
@ -415,6 +419,15 @@ function Device:install()
end
UIManager:broadcastEvent(Event:new("Exit", save_quit))
end,
cancel_text = _("Later"),
cancel_callback = function()
local InfoMessage = require("ui/widget/infomessage")
UIManager:show(InfoMessage:new{
text = _("The update will be applied the next time KOReader is started."),
unmovable = true,
})
end,
unmovable = true,
})
end
@ -559,7 +572,7 @@ function Device:exit()
G_reader_settings:close()
-- I/O teardown
require("ffi/input"):closeAll()
self.input.teardown()
end
-- Lifted from busybox's libbb/inet_cksum.c

@ -1141,7 +1141,7 @@ function Contact:handleTwoFingerPan(buddy_contact)
ges_ev._end_pos = nil
end
ges_ev.direction = gesture_detector.DIRECTION_TABLE[tpan_dir]
-- Use the the sum of both contacts' travel for the distance
-- Use the sum of both contacts' travel for the distance
ges_ev.distance = tpan_dis + rpan_dis
-- Some handlers might also want to know the distance between the two contacts on lift & down.
ges_ev.span = end_distance

@ -139,23 +139,19 @@ local Input = {
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
"N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
"Up", "Down", "Left", "Right", "Press", "Backspace", "End",
"Back", "Sym", "AA", "Menu", "Home", "Del",
"Back", "Sym", "AA", "Menu", "Home", "Del", "ScreenKB",
"LPgBack", "RPgBack", "LPgFwd", "RPgFwd"
},
},
fake_event_set = {
IntoSS = true, OutOfSS = true,
IntoSS = true, OutOfSS = true, ExitingSS = true,
UsbPlugIn = true, UsbPlugOut = true,
Charging = true, NotCharging = true,
WakeupFromSuspend = true, ReadyToSuspend = true,
UsbDevicePlugIn = true, UsbDevicePlugOut = true,
},
-- Subset of fake_event_set for events that require passing a parameter along
complex_fake_event_set = {
UsbDevicePlugIn = true, UsbDevicePlugOut = true,
},
-- Crappy FIFO to forward parameters for those events to UIManager
-- Crappy FIFO to forward parameters to UIManager for the subset of fake_event_set that require passing a parameter along
fake_event_args = {
UsbDevicePlugIn = {},
UsbDevicePlugOut = {},
@ -175,6 +171,7 @@ local Input = {
Shift = false,
Sym = false,
Meta = false,
ScreenKB = false,
},
-- repeat state:
@ -182,6 +179,7 @@ local Input = {
-- touch state:
main_finger_slot = 0,
pen_slot = 4,
cur_slot = 0,
MTSlots = nil, -- table, object may be replaced at runtime
active_slots = nil, -- ditto
@ -198,6 +196,10 @@ local Input = {
setClipboardText = function(text)
_internal_clipboard_text = text or ""
end,
-- open'ed input devices hashmap (key: path, value: fd number)
-- Must be a class member, both because Input is a singleton and that state is process-wide anyway.
opened_devices = {},
}
function Input:new(o)
@ -223,6 +225,10 @@ function Input:init()
},
}
-- Always send pen data to a slot far enough away from our main finger slot that it can never be matched with a finger buddy in GestureDetector (i.e., +/- 1),
-- with an extra bit of leeway, since we don't even actually support three finger gestures ;).
self.pen_slot = self.main_finger_slot + 4
self.gesture_detector = GestureDetector:new{
screen = self.device.screen,
input = self,
@ -246,8 +252,9 @@ function Input:init()
end
-- set up fake event map
self.event_map[10000] = "IntoSS" -- go into screen saver
self.event_map[10001] = "OutOfSS" -- go out of screen saver
self.event_map[10000] = "IntoSS" -- Requested to go into screen saver
self.event_map[10001] = "OutOfSS" -- Requested to go out of screen saver
self.event_map[10002] = "ExitingSS" -- Specific to Kindle, SS *actually* closed
self.event_map[10010] = "UsbPlugIn"
self.event_map[10011] = "UsbPlugOut"
self.event_map[10020] = "Charging"
@ -293,14 +300,97 @@ function Input:disableRotationMap()
end
--[[--
Wrapper for FFI input open.
Wrapper for our Lua/C input module's open.
Note that we adhere to the "." syntax here for compatibility.
The `name` argument is optional, and used for logging purposes only.
--]]
function Input.open(path, name)
-- Make sure we don't open the same device twice.
if not Input.opened_devices[path] then
local fd = input.open(path)
if fd then
Input.opened_devices[path] = fd
if name then
logger.dbg("Opened fd", fd, "for input device", name, "@", path)
else
logger.dbg("Opened fd", fd, "for input device @", path)
end
end
-- No need to log failures, input will have raised an error already,
-- and we want to make those fatal, so we don't protect this call.
return fd
end
end
--[[--
Wrapper for our Lua/C input module's fdopen.
Note that we adhere to the "." syntax here for compatibility.
The `name` argument is optional, and used for logging purposes only.
`path` is mandatory, though!
--]]
function Input.fdopen(fd, path, name)
-- Make sure we don't open the same device twice.
if not Input.opened_devices[path] then
input.fdopen(fd)
-- As with input.open, it will throw on error (closing the fd first)
Input.opened_devices[path] = fd
if name then
logger.dbg("Kept fd", fd, "open for input device", name, "@", path)
else
logger.dbg("Kept fd", fd, "open for input device @", path)
end
return fd
end
end
--[[--
Wrapper for our Lua/C input module's close.
Note that we adhere to the "." syntax here for compatibility.
--]]
function Input.close(path)
-- Make sure we actually know about this device
local fd = Input.opened_devices[path]
if fd then
local ok, err = input.close(fd)
if ok or err == C.ENODEV then
-- Either the call succeeded,
-- or the backend had already caught an ENODEV in waitForInput and closed the fd internally.
-- (Because the EvdevInputRemove Event comes from an UsbDevicePlugOut uevent forwarded as an... *input* EV_KEY event ;)).
-- Regardless, that device is gone, so clear its spot in the hashmap.
Input.opened_devices[path] = nil
end
else
logger.warn("Tried to close an unknown input device @", path)
end
end
--[[--
Wrapper for our Lua/C input module's closeAll.
@todo Clean up separation FFI/this.
Note that we adhere to the "." syntax here for compatibility.
--]]
function Input.open(device, is_emu_events)
return input.open(device, is_emu_events and 1 or 0)
function Input.teardown()
input.closeAll()
Input.opened_devices = {}
end
-- Wrappers for the custom FFI implementations with no concept of paths or fd
if input.is_ffi then
-- Pass args as-is. None of 'em actually *take* arguments, but some may be invoked as methods...
function Input.open(...)
return input.open(...)
end
function Input.close(...)
return input.close(...)
end
function Input.teardown(...)
return input.closeAll(...)
end
end
--[[--
@ -532,15 +622,17 @@ function Input:resetState()
end
function Input:handleKeyBoardEv(ev)
-- Detect loss of contact for the "snow" protocol...
-- NOTE: Some ST devices may also behave similarly, but we handle those via ABS_PRESSURE
-- Detect loss of contact for the "snow" protocol, as we *never* get EV_ABS:ABS_MT_TRACKING_ID:-1 on those...
-- NOTE: The same logic *could* be used on *some* ST devices to detect contact states,
-- but we instead prefer using EV_ABS:ABS_PRESSURE on those,
-- as it appears to be more common than EV_KEY:BTN_TOUCH on the devices we care about...
if self.snow_protocol then
if ev.code == C.BTN_TOUCH then
if ev.value == 0 then
-- Kernel sends it after loss of contact for *all* slots,
-- only once the final contact point has been lifted.
if #self.MTSlots == 0 then
-- Likely, since this is usually in its own event stream,
-- Likely, since this is usually in its own input frame,
-- meaning self.MTSlots has *just* been cleared by our last EV_SYN:SYN_REPORT handler...
-- So, poke at the actual data to find the slots that are currently active (i.e., in the down state),
-- and re-populate a minimal self.MTSlots array that simply switches them to the up state ;).
@ -553,7 +645,7 @@ function Input:handleKeyBoardEv(ev)
else
-- Unlikely, given what we mentioned above...
-- Note that, funnily enough, its EV_KEY:BTN_TOUCH:1 counterpart
-- *can* be in the same initial event stream as the EV_ABS batch...
-- *can* be in the same initial input frame as the EV_ABS batch...
for _, MTSlot in ipairs(self.MTSlots) do
self:setMtSlot(MTSlot.slot, "id", -1)
end
@ -564,24 +656,30 @@ function Input:handleKeyBoardEv(ev)
end
elseif self.wacom_protocol then
if ev.code == C.BTN_TOOL_PEN then
-- Always send pen data to slot 2
self:setupSlotData(2)
-- Switch to the dedicated pen slot, and make sure it's active, as this can come in a dedicated input frame
self:setupSlotData(self.pen_slot)
if ev.value == 1 then
self:setCurrentMtSlot("tool", TOOL_TYPE_PEN)
else
self:setCurrentMtSlot("tool", TOOL_TYPE_FINGER)
-- Switch back to our main finger slot
self.cur_slot = self.main_finger_slot
end
return
elseif ev.code == C.BTN_TOUCH then
-- BTN_TOUCH is bracketed by BTN_TOOL_PEN, so we can limit this to pens, to avoid stomping on panel slots.
if self:getCurrentMtSlotData("tool") == TOOL_TYPE_PEN then
-- Make sure the pen slot is active, as this can come in a dedicated input frame
-- (i.e., we need it to be referenced by self.MTSlots for the lift to be picked up in the EV_SYN:SYN_REPORT handler).
-- (Conversely, getCurrentMtSlotData pokes at the *persistent* slot data in self.ev_slots,
-- so it can keep track of data across input frames).
self:setupSlotData(self.pen_slot)
-- Much like on snow, use this to detect contact down & lift,
-- as ABS_PRESSURE may be entirely omitted from hover events,
-- and ABS_DISTANCE is not very clear cut...
self:setupSlotData(2)
if ev.value == 1 then
self:setCurrentMtSlot("id", 2)
self:setCurrentMtSlot("id", self.pen_slot)
else
self:setCurrentMtSlot("id", -1)
end
@ -616,7 +714,7 @@ function Input:handleKeyBoardEv(ev)
-- So, we simply store it somewhere our handler can find and call it a day.
-- And we use an array as a FIFO because we cannot guarantee that insertions and removals will interleave nicely.
-- (This is all in the name of avoiding complexifying the common codepaths for events that should be few and far between).
if self.complex_fake_event_set[keycode] then
if self.fake_event_args[keycode] then
table.insert(self.fake_event_args[keycode], ev.value)
end
return keycode
@ -706,6 +804,9 @@ function Input:handlePowerManagementOnlyEv(ev)
end
if self.fake_event_set[keycode] then
if self.fake_event_args[keycode] then
table.insert(self.fake_event_args[keycode], ev.value)
end
return keycode
end
@ -719,6 +820,16 @@ function Input:handlePowerManagementOnlyEv(ev)
end
end
-- Make sure we don't leave modifiers in an inconsistent state
if self.modifiers[keycode] ~= nil then
if ev.value == KEY_PRESS then
self.modifiers[keycode] = true
elseif ev.value == KEY_RELEASE then
self.modifiers[keycode] = false
end
return
end
-- Nothing to see, move along!
return
end
@ -789,17 +900,25 @@ function Input:handleTouchEv(ev)
elseif ev.code == C.ABS_MT_TRACKING_ID then
if self.snow_protocol then
-- NOTE: We'll never get an ABS_MT_SLOT event, instead we have a slot-like ABS_MT_TRACKING_ID value...
-- This also means this may never be set to -1 on contact lift,
-- which is why we instead rely on EV_KEY:BTN_TOUCH:0 for that (c.f., handleKeyBoardEv).
self:setupSlotData(ev.value)
else
-- The Elan driver needlessly repeats unchanged ABS_MT_TRACKING_ID values,
-- which allows us to do this here instead of relying more aggressively on setCurrentMtSlotChecked.
if #self.MTSlots == 0 then
self:addSlot(self.cur_slot)
-- This also means that, unlike on sane devices, this will *never* be set to -1 on contact lift,
-- which is why we instead have to rely on EV_KEY:BTN_TOUCH:0 for that (c.f., handleKeyBoardEv).
if ev.value == -1 then
-- NOTE: While *actual* snow_protocol devices will *never* emit an EV_ABS:ABS_MT_TRACKING_ID:-1 event,
-- we've seen brand new revisions of snow_protocol devices shipping with sane panels instead,
-- so we'll need to disable the quirks at runtime to handle these properly...
-- (c.f., https://www.mobileread.com/forums/showpost.php?p=4383629&postcount=997).
-- NOTE: Simply skipping the slot storage setup for -1 would not be enough, as it would only fix ST handling.
-- MT would be broken, because buddy contact detection in GestureDetector looks at slot +/- 1,
-- whereas we'd be having the main contact point at a stupidly large slot number
-- (because it would match ABS_MT_TRACKING_ID, given the lack of ABS_MT_SLOT, at least for the first input frame),
-- while the second contact would be at slot 1, because it would immediately have required emitting a proper ABS_MT_SLOT event...
logger.warn("Input: Disabled snow_protocol quirks because your device's hardware revision doesn't appear to need them!")
self.snow_protocol = false
else
self:setupSlotData(ev.value)
end
end
self:setCurrentMtSlot("id", ev.value)
self:setCurrentMtSlotChecked("id", ev.value)
elseif ev.code == C.ABS_MT_TOOL_TYPE then
-- NOTE: On the Elipsa: Finger == 0; Pen == 1
self:setCurrentMtSlot("tool", ev.value)
@ -934,14 +1053,15 @@ end
function Input:handleTouchEvLegacy(ev)
-- Single Touch Protocol.
-- Some devices emit both singletouch and multitouch events,
-- on those devices, the 'handleTouchEv' function may not behave as expected. Use this one instead.
-- Some devices emit both singletouch and multitouch events.
-- On those devices, `handleTouchEv` may not behave as expected. Use this one instead.
if ev.type == C.EV_ABS then
if ev.code == C.ABS_X then
self:setCurrentMtSlotChecked("x", ev.value)
elseif ev.code == C.ABS_Y then
self:setCurrentMtSlotChecked("y", ev.value)
elseif ev.code == C.ABS_PRESSURE then
-- This is the least common denominator we can use to detect contact down & lift...
if ev.value ~= 0 then
self:setCurrentMtSlotChecked("id", 1)
else
@ -984,39 +1104,37 @@ end
--- (Translation should be done via registerEventAdjustHook in Device implementations).
--- This needs to be called *via handleGyroEv* in a handleMiscEv implementation (c.f., Kobo, Kindle or PocketBook).
function Input:handleMiscGyroEv(ev)
local rotation_mode, screen_mode
local rotation
if ev.value == C.DEVICE_ROTATED_UPRIGHT then
-- i.e., UR
rotation_mode = framebuffer.DEVICE_ROTATED_UPRIGHT
screen_mode = "portrait"
rotation = framebuffer.DEVICE_ROTATED_UPRIGHT
elseif ev.value == C.DEVICE_ROTATED_CLOCKWISE then
-- i.e., CW
rotation_mode = framebuffer.DEVICE_ROTATED_CLOCKWISE
screen_mode = "landscape"
rotation = framebuffer.DEVICE_ROTATED_CLOCKWISE
elseif ev.value == C.DEVICE_ROTATED_UPSIDE_DOWN then
-- i.e., UD
rotation_mode = framebuffer.DEVICE_ROTATED_UPSIDE_DOWN
screen_mode = "portrait"
rotation = framebuffer.DEVICE_ROTATED_UPSIDE_DOWN
elseif ev.value == C.DEVICE_ROTATED_COUNTER_CLOCKWISE then
-- i.e., CCW
rotation_mode = framebuffer.DEVICE_ROTATED_COUNTER_CLOCKWISE
screen_mode = "landscape"
rotation = framebuffer.DEVICE_ROTATED_COUNTER_CLOCKWISE
else
-- Discard FRONT/BACK
return
end
local old_rotation_mode = self.device.screen:getRotationMode()
local old_rotation = self.device.screen:getRotationMode()
if self.device:isGSensorLocked() then
local old_screen_mode = self.device.screen:getScreenMode()
if rotation_mode and rotation_mode ~= old_rotation_mode and screen_mode == old_screen_mode then
local matching_orientation = bit.band(rotation, 1) == bit.band(old_rotation, 1)
if rotation and rotation ~= old_rotation and matching_orientation then
-- Cheaper than a full SetRotationMode event, as we don't need to re-layout anything.
self.device.screen:setRotationMode(rotation_mode)
self.device.screen:setRotationMode(rotation)
UIManager:onRotation()
end
else
if rotation_mode and rotation_mode ~= old_rotation_mode then
return Event:new("SetRotationMode", rotation_mode)
if rotation and rotation ~= old_rotation then
-- NOTE: We do *NOT* send a broadcast manually, and instead rely on the main loop's sendEvent:
-- this ensures that only widgets that actually know how to handle a rotation will do so ;).
return Event:new("SetRotationMode", rotation)
end
end
end
@ -1053,29 +1171,29 @@ function Input:initMtSlot(slot)
end
end
function Input:getMtSlot(slot)
return self.ev_slots[slot]
end
function Input:getCurrentMtSlot()
return self.ev_slots[self.cur_slot]
end
function Input:setMtSlot(slot, key, val)
self.ev_slots[slot][key] = val
end
function Input:setCurrentMtSlot(key, val)
self:setMtSlot(self.cur_slot, key, val)
self.ev_slots[self.cur_slot][key] = val
end
-- Same as above, but ensures the current slot actually has a live ref first
function Input:setCurrentMtSlotChecked(key, val)
if #self.MTSlots == 0 then
if not self.active_slots[self.cur_slot] then
self:addSlot(self.cur_slot)
end
self:setMtSlot(self.cur_slot, key, val)
end
function Input:getMtSlot(slot)
return self.ev_slots[slot]
end
function Input:getCurrentMtSlot()
return self:getMtSlot(self.cur_slot)
self.ev_slots[self.cur_slot][key] = val
end
function Input:getCurrentMtSlotData(key)
@ -1102,22 +1220,13 @@ function Input:addSlot(value)
self.cur_slot = value
end
function Input:addSlotIfChanged(value)
if self.cur_slot ~= value then
-- We've already seen that slot in this frame, don't insert a duplicate reference!
if self.active_slots[value] then
self.cur_slot = value
else
self:addSlot(value)
end
end
end
function Input:setupSlotData(value)
if #self.MTSlots == 0 then
if not self.active_slots[value] then
self:addSlot(value)
else
self:addSlotIfChanged(value)
-- We've already seen that slot in this frame, don't insert a duplicate reference!
-- NOTE: May already be set to the correct value if the driver repeats ABS_MT_SLOT (e.g., our android/PB translation layers; or ABS_MT_TRACKING_ID for snow_protocol).
self.cur_slot = value
end
end

@ -3,7 +3,6 @@ local UIManager
local time = require("ui/time")
local lfs = require("libs/libkoreader-lfs")
local logger = require("logger")
local util = require("util")
-- We're going to need a few <linux/fb.h> & <linux/input.h> constants...
local ffi = require("ffi")
@ -11,6 +10,7 @@ local C = ffi.C
require("ffi/linux_fb_h")
require("ffi/linux_input_h")
require("ffi/posix_h")
require("ffi/fbink_input_h")
local function yes() return true end
local function no() return false end -- luacheck: ignore
@ -206,6 +206,58 @@ function Kindle:supportsScreensaver()
end
end
function Kindle:openInputDevices()
-- Auto-detect input devices (via FBInk's fbink_input_scan)
local ok, FBInkInput = pcall(ffi.load, "fbink_input")
if not ok then
-- NOP fallback for the testsuite...
FBInkInput = { fbink_input_scan = function() end }
end
local dev_count = ffi.new("size_t[1]")
-- We care about: the touchscreen, a properly scaled stylus, pagination buttons, a home button and a fiveway.
local match_mask = bit.bor(C.INPUT_TOUCHSCREEN, C.INPUT_SCALED_TABLET, C.INPUT_PAGINATION_BUTTONS, C.INPUT_HOME_BUTTON, C.INPUT_DPAD)
local devices = FBInkInput.fbink_input_scan(match_mask, 0, 0, dev_count)
if devices ~= nil then
for i = 0, tonumber(dev_count[0]) - 1 do
local dev = devices[i]
if dev.matched then
self.input.fdopen(tonumber(dev.fd), ffi.string(dev.path), ffi.string(dev.name))
end
end
C.free(devices)
else
-- Auto-detection failed, warn and fall back to defaults
logger.warn("We failed to auto-detect the proper input devices, input handling may be inconsistent!")
if self.touch_dev then
-- We've got a preferred path specified for the touch panel
self.input.open(self.touch_dev)
else
-- That generally works out well enough on legacy devices...
self.input.open("/dev/input/event0")
self.input.open("/dev/input/event1")
end
end
-- Getting the device where rotation events end up without catching a bunch of false-positives is... trickier,
-- thanks to the inane event code being used...
if self:hasGSensor() then
-- i.e., we want something that reports EV_ABS:ABS_PRESSURE that isn't *also* a pen (because those are pretty much guaranteed to report pressure...).
-- And let's add that isn't also a touchscreen to the mix, because while not true at time of writing, that's an event touchscreens sure can support...
devices = FBInkInput.fbink_input_scan(C.INPUT_ROTATION_EVENT, bit.bor(C.INPUT_TABLET, C.INPUT_TOUCHSCREEN), C.NO_RECAP, dev_count)
if devices ~= nil then
for i = 0, tonumber(dev_count[0]) - 1 do
local dev = devices[i]
if dev.matched then
self.input.fdopen(tonumber(dev.fd), ffi.string(dev.path), ffi.string(dev.name))
end
end
C.free(devices)
end
end
self.input.open("fake_events")
end
function Kindle:init()
-- Check if the device supports deep sleep/quick boot
if lfs.attributes("/sys/devices/platform/falconblk/uevent", "mode") == "file" then
@ -234,6 +286,14 @@ function Kindle:init()
self.canDeepSleep = false
end
-- If the device-specific init hasn't done so already (devices without keys don't), instantiate Input.
if not self.input then
self.input = require("device/input"):new{ device = self }
end
-- Auto-detect & open input devices
self:openInputDevices()
Generic.init(self)
end
@ -280,7 +340,18 @@ function Kindle:usbPlugIn()
-- NOTE: If the device is put in USBNet mode before we even start, everything's peachy, though :).
end
function Kindle:intoScreenSaver()
-- Hopefully, the event sources are fairly portable...
-- c.f., https://github.com/koreader/koreader/pull/11174#issuecomment-1830064445
-- NOTE: There's no distinction between real button presses and powerd_test -p or lipc-set-prop -i com.lab126.powerd powerButton 1
local POWERD_EVENT_SOURCES = {
[1] = "BUTTON_WAKEUP", -- outOfScreenSaver 1
[2] = "BUTTON_SUSPEND", -- goingToScreenSaver 2
[4] = "HALL_SUSPEND", -- goingToScreenSaver 4
[6] = "HALL_WAKEUP", -- outOfScreenSaver 6
}
function Kindle:intoScreenSaver(source)
logger.dbg("Kindle:intoScreenSaver via", POWERD_EVENT_SOURCES[source] or string.format("UNKNOWN_SUSPEND (%d)", source or -1))
if not self.screen_saver_mode then
if self:supportsScreensaver() then
-- NOTE: Meaning this is not a SO device ;)
@ -304,7 +375,8 @@ function Kindle:intoScreenSaver()
self.powerd:beforeSuspend()
end
function Kindle:outofScreenSaver()
function Kindle:outofScreenSaver(source)
logger.dbg("Kindle:outofScreenSaver via", POWERD_EVENT_SOURCES[source] or string.format("UNKNOWN_WAKEUP (%d)", source or -1))
if self.screen_saver_mode then
if self:supportsScreensaver() then
local Screensaver = require("ui/screensaver")
@ -357,18 +429,23 @@ function Kindle:outofScreenSaver()
self.powerd:afterResume()
end
-- On stock, there's a distinction between OutOfSS (which *requests* closing the SS) and ExitingSS, which fires once they're *actually* closed...
function Kindle:exitingScreenSaver() end
function Kindle:usbPlugOut()
-- NOTE: See usbPlugIn(), we don't have anything fancy to do here either.
end
function Kindle:wakeupFromSuspend()
self.powerd:wakeupFromSuspend()
function Kindle:wakeupFromSuspend(ts)
logger.dbg("Kindle:wakeupFromSuspend", ts)
self.powerd:wakeupFromSuspend(ts)
self.last_suspend_time = time.boottime_or_realtime_coarse() - self.suspend_time
self.total_suspend_time = self.total_suspend_time + self.last_suspend_time
end
function Kindle:readyToSuspend()
self.powerd:readyToSuspend()
function Kindle:readyToSuspend(delay)
logger.dbg("Kindle:readyToSuspend", delay)
self.powerd:readyToSuspend(delay)
self.suspend_time = time.boottime_or_realtime_coarse()
end
@ -382,14 +459,26 @@ function Kindle:UIManagerReady(uimgr)
end
function Kindle:setEventHandlers(uimgr)
-- These custom fake events *will* pass an argument...
self.input.fake_event_args.IntoSS = {}
self.input.fake_event_args.OutOfSS = {}
self.input.fake_event_args.WakeupFromSuspend = {}
self.input.fake_event_args.ReadyToSuspend = {}
UIManager.event_handlers.Suspend = function()
self.powerd:toggleSuspend()
end
UIManager.event_handlers.IntoSS = function()
self:intoScreenSaver()
UIManager.event_handlers.IntoSS = function(input_event)
-- Retrieve the argument set by Input:handleKeyBoardEv
local arg = table.remove(self.input.fake_event_args[input_event])
self:intoScreenSaver(arg)
end
UIManager.event_handlers.OutOfSS = function(input_event)
local arg = table.remove(self.input.fake_event_args[input_event])
self:outofScreenSaver(arg)
end
UIManager.event_handlers.OutOfSS = function()
self:outofScreenSaver()
UIManager.event_handlers.ExitingSS = function()
self:exitingScreenSaver()
end
UIManager.event_handlers.Charging = function()
self:_beforeCharging()
@ -399,11 +488,13 @@ function Kindle:setEventHandlers(uimgr)
self:usbPlugOut()
self:_afterNotCharging()
end
UIManager.event_handlers.WakeupFromSuspend = function()
self:wakeupFromSuspend()
UIManager.event_handlers.WakeupFromSuspend = function(input_event)
local arg = table.remove(self.input.fake_event_args[input_event])
self:wakeupFromSuspend(arg)
end
UIManager.event_handlers.ReadyToSuspend = function()
self:readyToSuspend()
UIManager.event_handlers.ReadyToSuspend = function(input_event)
local arg = table.remove(self.input.fake_event_args[input_event])
self:readyToSuspend(arg)
end
end
@ -427,7 +518,9 @@ local Kindle2 = Kindle:extend{
isREAGL = no,
hasKeyboard = yes,
hasKeys = yes,
hasSymKey = yes,
hasDPad = yes,
useDPadAsActionKeys = yes,
canHWInvert = no,
canModifyFBInfo = no,
canUseCBB = no, -- 4bpp
@ -440,7 +533,9 @@ local KindleDXG = Kindle:extend{
isREAGL = no,
hasKeyboard = yes,
hasKeys = yes,
hasSymKey = yes,
hasDPad = yes,
useDPadAsActionKeys = yes,
canHWInvert = no,
canModifyFBInfo = no,
canUseCBB = no, -- 4bpp
@ -453,7 +548,9 @@ local Kindle3 = Kindle:extend{
isREAGL = no,
hasKeyboard = yes,
hasKeys = yes,
hasSymKey = yes,
hasDPad = yes,
useDPadAsActionKeys = yes,
canHWInvert = no,
canModifyFBInfo = no,
canUseCBB = no, -- 4bpp
@ -464,7 +561,9 @@ local Kindle4 = Kindle:extend{
model = "Kindle4",
isREAGL = no,
hasKeys = yes,
hasScreenKB = yes,
hasDPad = yes,
useDPadAsActionKeys = yes,
canHWInvert = no,
canModifyFBInfo = no,
-- NOTE: It could *technically* use the C BB, as it's running @ 8bpp, but it's expecting an inverted palette...
@ -529,7 +628,6 @@ local KindleOasis = Kindle:extend{
model = "KindleOasis",
isTouchDevice = yes,
hasFrontlight = yes,
hasLightSensor = yes,
hasKeys = yes,
hasGSensor = yes,
display_dpi = 300,
@ -639,7 +737,7 @@ local KindleScribe = Kindle:extend{
hasGSensor = yes,
display_dpi = 300,
touch_dev = "/dev/input/touch",
canHWDither = no,
canHWDither = yes,
canDoSwipeAnimation = yes,
}
@ -653,9 +751,6 @@ function Kindle2:init()
device = self,
event_map = require("device/kindle/event_map_keyboard"),
}
self.input.open("/dev/input/event0")
self.input.open("/dev/input/event1")
self.input.open("fake_events")
Kindle.init(self)
end
@ -670,9 +765,6 @@ function KindleDXG:init()
event_map = require("device/kindle/event_map_keyboard"),
}
self.keyboard_layout = require("device/kindle/keyboard_layout")
self.input.open("/dev/input/event0")
self.input.open("/dev/input/event1")
self.input.open("fake_events")
Kindle.init(self)
end
@ -688,9 +780,6 @@ function Kindle3:init()
event_map = require("device/kindle/event_map_kindle4"),
}
self.keyboard_layout = require("device/kindle/keyboard_layout")
self.input.open("/dev/input/event0")
self.input.open("/dev/input/event1")
self.input.open("fake_events")
Kindle.init(self)
end
@ -705,9 +794,6 @@ function Kindle4:init()
device = self,
event_map = require("device/kindle/event_map_kindle4"),
}
self.input.open("/dev/input/event0")
self.input.open("/dev/input/event1")
self.input.open("fake_events")
Kindle.init(self)
end
@ -727,11 +813,6 @@ function KindleTouch:init()
-- Kindle Touch needs event modification for proper coordinates
self.input:registerEventAdjustHook(self.input.adjustTouchScale, {x=600/4095, y=800/4095})
-- event0 in KindleTouch is "WM8962 Beep Generator" (useless)
-- event1 in KindleTouch is "imx-yoshi Headset" (useless)
self.input.open("/dev/input/event2") -- Home button
self.input.open(self.touch_dev) -- touchscreen
self.input.open("fake_events")
Kindle.init(self)
end
@ -745,9 +826,6 @@ function KindlePaperWhite:init()
}
Kindle.init(self)
self.input.open(self.touch_dev)
self.input.open("fake_events")
end
function KindlePaperWhite2:init()
@ -761,9 +839,6 @@ function KindlePaperWhite2:init()
}
Kindle.init(self)
self.input.open(self.touch_dev)
self.input.open("fake_events")
end
function KindleBasic:init()
@ -776,9 +851,6 @@ function KindleBasic:init()
}
Kindle.init(self)
self.input.open(self.touch_dev)
self.input.open("fake_events")
end
function KindleVoyage:init()
@ -829,11 +901,7 @@ function KindleVoyage:init()
Kindle.init(self)
self.input.open(self.touch_dev)
self.input.open("/dev/input/event2") -- WhisperTouch
self.input.open("fake_events")
-- reenable WhisperTouch keys when started without framework
-- Re-enable WhisperTouch keys when started without framework
if self.framework_lipc_handle then
self.framework_lipc_handle:set_int_property("com.lab126.deviced", "fsrkeypadEnable", 1)
self.framework_lipc_handle:set_int_property("com.lab126.deviced", "fsrkeypadPrevEnable", 1)
@ -852,9 +920,6 @@ function KindlePaperWhite3:init()
}
Kindle.init(self)
self.input.open(self.touch_dev)
self.input.open("fake_events")
end
-- HAL for gyro orientation switches (EV_ABS:ABS_PRESSURE (?!) w/ custom values to EV_MSC:MSC_GYRO w/ our own custom values)
@ -962,21 +1027,6 @@ function KindleOasis:init()
return this:handleGyroEv(ev)
end
end
self.input.open(self.touch_dev)
self.input.open("/dev/input/by-path/platform-gpiokey.0-event")
-- get rotate dev by EV=d
local std_out = io.popen("grep -e 'Handlers\\|EV=' /proc/bus/input/devices | grep -B1 'EV=d' | grep -o 'event[0-9]'", "r")
if std_out then
local rotation_dev = std_out:read("*line")
std_out:close()
if rotation_dev then
self.input.open("/dev/input/"..rotation_dev)
end
end
self.input.open("fake_events")
end
-- HAL for gyro orientation switches (EV_ABS:ABS_PRESSURE (?!) w/ custom values to EV_MSC:MSC_GYRO w/ our own custom values)
@ -1088,21 +1138,6 @@ function KindleOasis2:init()
return this:handleGyroEv(ev)
end
end
self.input.open(self.touch_dev)
self.input.open("/dev/input/by-path/platform-gpio-keys-event")
-- Get accelerometer device by looking for EV=d
local std_out = io.popen("grep -e 'Handlers\\|EV=' /proc/bus/input/devices | grep -B1 'EV=d' | grep -o 'event[0-9]\\{1,2\\}'", "r")
if std_out then
local rotation_dev = std_out:read("*line")
std_out:close()
if rotation_dev then
self.input.open("/dev/input/"..rotation_dev)
end
end
self.input.open("fake_events")
end
function KindleOasis3:init()
@ -1171,21 +1206,6 @@ function KindleOasis3:init()
return this:handleGyroEv(ev)
end
end
self.input.open(self.touch_dev)
self.input.open("/dev/input/by-path/platform-gpio-keys-event")
-- Get accelerometer device by looking for EV=d
local std_out = io.popen("grep -e 'Handlers\\|EV=' /proc/bus/input/devices | grep -B1 'EV=d' | grep -o 'event[0-9]\\{1,2\\}'", "r")
if std_out then
local rotation_dev = std_out:read("*line")
std_out:close()
if rotation_dev then
self.input.open("/dev/input/"..rotation_dev)
end
end
self.input.open("fake_events")
end
function KindleBasic2:init()
@ -1199,9 +1219,6 @@ function KindleBasic2:init()
}
Kindle.init(self)
self.input.open(self.touch_dev)
self.input.open("fake_events")
end
function KindlePaperWhite4:init()
@ -1216,19 +1233,6 @@ function KindlePaperWhite4:init()
}
Kindle.init(self)
-- So, look for a goodix TS input device (c.f., #5110)...
local std_out = io.popen("grep -e 'Handlers\\|Name=' /proc/bus/input/devices | grep -A1 'goodix-ts' | grep -o 'event[0-9]'", "r")
if std_out then
local goodix_dev = std_out:read("*line")
std_out:close()
if goodix_dev then
self.touch_dev = "/dev/input/" .. goodix_dev
end
end
self.input.open(self.touch_dev)
self.input.open("fake_events")
end
function KindleBasic3:init()
@ -1248,29 +1252,6 @@ function KindleBasic3:init()
-- so we have to rely on contact lift detection via BTN_TOUCH:0,
-- c.f., https://github.com/koreader/koreader/issues/5070
self.input.snow_protocol = true
self.input.open(self.touch_dev)
self.input.open("fake_events")
end
local function findInputDevices()
-- Walk /sys/class/input and pick up any evdev input device with *any* EV_ABS capabilities
local devices = {}
for evdev in lfs.dir("/sys/class/input/") do
if evdev:match("event.*") then
local abs_cap = "/sys/class/input/" .. evdev .. "/device/capabilities/abs"
local f = io.open(abs_cap, "r")
if f then
local bitmap_str = f:read("l")
f:close()
if bitmap_str ~= "0" then
logger.info("Potential input device found at", evdev, "because of ABS caps:", bitmap_str)
table.insert(devices, "/dev/input/" .. evdev)
end
end
end
end
return devices
end
function KindlePaperWhite5:init()
@ -1289,25 +1270,10 @@ function KindlePaperWhite5:init()
self.screen:_MTK_ToggleFastMode(true)
Kindle.init(self)
-- Some HW/FW variants stash their input device without a by-path symlink...
if util.pathExists("/dev/input/by-path/platform-1001e000.i2c-event") then
self.touch_dev = "/dev/input/by-path/platform-1001e000.i2c-event"
self.input.open(self.touch_dev)
else
local devices = findInputDevices()
for _, touch in ipairs(devices) do
-- There should only be one match on the PW5 anyway...
self.touch_dev = touch
self.input.open(touch)
end
end
self.input.open("fake_events")
end
function KindleBasic4:init()
self.screen = require("ffi/framebuffer_mxcfb"):new{device = self, debug = logger.dbg}
-- TBD, assume PW5 for now
self.powerd = require("device/kindle/powerd"):new{
device = self,
fl_intensity_file = "/sys/class/backlight/fp9966-bl1/brightness",
@ -1321,20 +1287,6 @@ function KindleBasic4:init()
self.screen:_MTK_ToggleFastMode(true)
Kindle.init(self)
-- Some HW/FW variants stash their input device without a by-path symlink...
if util.pathExists("/dev/input/by-path/platform-1001e000.i2c-event") then
self.touch_dev = "/dev/input/by-path/platform-1001e000.i2c-event"
self.input.open(self.touch_dev)
else
local devices = findInputDevices()
for _, touch in ipairs(devices) do
-- There should only be one match on the PW5 anyway...
self.touch_dev = touch
self.input.open(touch)
end
end
self.input.open("fake_events")
end
function KindleScribe:init()
@ -1354,11 +1306,11 @@ function KindleScribe:init()
hall_file = "/sys/devices/platform/eink_hall/hall_enable",
}
Kindle.init(self)
-- Enable the so-called "fast" mode, so as to prevent the driver from silently promoting refreshes to REAGL.
self.screen:_MTK_ToggleFastMode(true)
Kindle.init(self)
--- @note The same quirks as on the Oasis 2 and 3 apply ;).
local haslipc, lipc = pcall(require, "liblipclua")
if haslipc and lipc then
@ -1369,14 +1321,10 @@ function KindleScribe:init()
logger.dbg("orientation_code =", orientation_code)
local rotation_mode = 0
if orientation_code then
if orientation_code == "U" then
if orientation_code == "U" or "L" then
rotation_mode = self.screen.DEVICE_ROTATED_UPRIGHT
elseif orientation_code == "R" then
rotation_mode = self.screen.DEVICE_ROTATED_CLOCKWISE
elseif orientation_code == "D" then
elseif orientation_code == "D" or "R" then
rotation_mode = self.screen.DEVICE_ROTATED_UPSIDE_DOWN
elseif orientation_code == "L" then
rotation_mode = self.screen.DEVICE_ROTATED_COUNTER_CLOCKWISE
end
end
if rotation_mode > 0 then
@ -1398,15 +1346,9 @@ function KindleScribe:init()
return this:handleGyroEv(ev)
end
end
-- Get accelerometer device
self.input.open("/dev/input/by-path/platform-11007000.i2c-event-joystick")
self.input.open(self.touch_dev)
self.input.open("fake_events")
-- Setup pen input
self.input.wacom_protocol = true
self.input.open("/dev/input/event4")
end
function KindleTouch:exit()
@ -1416,6 +1358,9 @@ function KindleTouch:exit()
end
if self.framework_lipc_handle then
-- Fixes missing *stock Amazon UI* screensavers on exiting out of "no framework" started KOReader
-- module was unloaded in frameworkStopped() function but wasn't (re)loaded on KOReader exit
self.framework_lipc_handle:set_string_property("com.lab126.blanket", "load", "screensaver")
self.framework_lipc_handle:close()
end

@ -243,8 +243,7 @@ function KindlePowerD:initWakeupMgr()
if not self.device:supportsScreensaver() then return end
if self.lipc_handle == nil then return end
function KindlePowerD:wakeupFromSuspend()
logger.dbg("Kindle wakeupFromSuspend")
function KindlePowerD:wakeupFromSuspend(ts)
-- Give the device a few seconds to settle.
-- This filters out user input resumes -> device will resume to active
-- Also the Kindle stays in Ready to suspend for 10 seconds
@ -252,8 +251,7 @@ function KindlePowerD:initWakeupMgr()
UIManager:scheduleIn(15, self.checkUnexpectedWakeup, self)
end
function KindlePowerD:readyToSuspend()
logger.dbg("Kindle readyToSuspend")
function KindlePowerD:readyToSuspend(delay)
if self.device.wakeup_mgr:isWakeupAlarmScheduled() then
local now = os.time()
local alarm = self.device.wakeup_mgr:getWakeupAlarmEpoch()

@ -2,10 +2,10 @@ local Generic = require("device/generic/device")
local Geom = require("ui/geometry")
local UIManager
local WakeupMgr = require("device/wakeupmgr")
local time = require("ui/time")
local ffiUtil = require("ffi/util")
local lfs = require("libs/libkoreader-lfs")
local logger = require("logger")
local time = require("ui/time")
local util = require("util")
local _ = require("gettext")
@ -15,6 +15,7 @@ local C = ffi.C
require("ffi/linux_fb_h")
require("ffi/linux_input_h")
require("ffi/posix_h")
require("ffi/fbink_input_h")
local function yes() return true end
local function no() return false end
@ -31,7 +32,7 @@ local function koboEnableWifi(toggle)
end
-- checks if standby is available on the device
local function checkStandby()
local function checkStandby(target_state)
logger.dbg("Kobo: checking if standby is possible ...")
local f = io.open("/sys/power/state")
if not f then
@ -40,11 +41,11 @@ local function checkStandby()
local mode = f:read()
f:close()
logger.dbg("Kobo: available power states:", mode)
if mode and mode:find("standby") then
logger.dbg("Kobo: standby state is supported")
if mode and mode:find(target_state) then
logger.dbg("Kobo: target standby state '" .. target_state .. "' is supported")
return yes
end
logger.dbg("Kobo: standby state is unsupported")
logger.dbg("Kobo: target standby state '" .. target_state .. "' is unsupported")
return no
end
@ -140,18 +141,19 @@ local Kobo = Generic:extend{
battery_sysfs = "/sys/class/power_supply/mc13892_bat",
-- Stable path to the NTX input device
ntx_dev = "/dev/input/event0",
ntx_fd = nil,
-- Stable path to the Touch input device
touch_dev = "/dev/input/event1",
-- Stable path to the Power Button input device
power_dev = nil,
-- Event code to use to detect contact pressure
pressure_event = nil,
-- Device features multiple CPU cores
isSMP = no,
-- Device supports "eclipse" waveform modes (i.e., optimized for nightmode).
hasEclipseWfm = no,
-- Device ships with various hardware revisions under the same device code, requiring automatic hardware detection...
-- Device ships with various hardware revisions under the same device code, requiring automatic hardware detection (PMIC & FL)...
automagic_sysfs = false,
-- The standard "standby" power state
standby_state = "standby",
unexpected_wakeup_count = 0,
}
@ -415,8 +417,6 @@ local KoboEuropa = Kobo:extend{
display_dpi = 227,
boot_rota = C.FB_ROTATE_CCW,
battery_sysfs = "/sys/class/power_supply/battery",
ntx_dev = "/dev/input/by-path/platform-ntx_event0-event",
touch_dev = "/dev/input/by-path/platform-0-0010-event",
isSMP = yes,
}
@ -446,8 +446,6 @@ local KoboCadmus = Kobo:extend{
battery_sysfs = "/sys/class/power_supply/battery",
hasAuxBattery = yes,
aux_battery_sysfs = "/sys/class/misc/cilix",
ntx_dev = "/dev/input/by-path/platform-ntx_event0-event",
touch_dev = "/dev/input/by-path/platform-0-0010-event",
isSMP = yes,
-- Much like the Libra 2, there are at least two different HW revisions, with different PMICs...
automagic_sysfs = true,
@ -501,7 +499,6 @@ local KoboGoldfinch = Kobo:extend{
nl_inverted = true,
},
battery_sysfs = "/sys/class/power_supply/battery",
power_dev = "/dev/input/by-path/platform-bd71828-pwrkey-event",
-- Board is eerily similar to the Libra 2, so, it inherits the same quirks...
-- c.f., https://github.com/koreader/koreader/issues/9552#issuecomment-1293000313
hasReliableMxcWaitFor = no,
@ -528,12 +525,77 @@ local KoboCondor = Kobo:extend{
nl_inverted = true,
},
battery_sysfs = "/sys/class/power_supply/bd71827_bat",
touch_dev = "/dev/input/by-path/platform-2-0010-event",
ntx_dev = "/dev/input/by-path/platform-ntx_event0-event",
power_dev = "/dev/input/by-path/platform-bd71828-pwrkey.6.auto-event",
isSMP = yes,
}
-- Kobo Libra Colour:
local KoboMonza = Kobo:extend{
model = "Kobo_monza",
isMTK = yes,
hasEclipseWfm = yes,
canToggleChargingLED = yes,
hasFrontlight = yes,
hasKeys = yes,
hasGSensor = yes,
display_dpi = 300,
pressure_event = C.ABS_MT_PRESSURE,
touch_mirrored_x = false,
touch_mirrored_y = true,
hasNaturalLight = yes,
frontlight_settings = {
frontlight_white = "/sys/class/backlight/mxc_msp430.0/brightness",
frontlight_mixer = "/sys/class/backlight/lm3630a_led/color",
nl_min = 0,
nl_max = 10,
nl_inverted = true,
},
battery_sysfs = "/sys/class/power_supply/bd71827_bat",
isSMP = yes,
hasColorScreen = yes,
}
-- Kobo Clara B/W:
local KoboSpaBW = Kobo:extend{
model = "Kobo_spaBW",
isMTK = yes,
hasEclipseWfm = yes,
canToggleChargingLED = yes,
hasFrontlight = yes,
touch_snow_protocol = true,
display_dpi = 300,
hasNaturalLight = yes,
frontlight_settings = {
frontlight_white = "/sys/class/backlight/mxc_msp430.0/brightness",
frontlight_mixer = "/sys/class/backlight/lm3630a_led/color",
nl_min = 0,
nl_max = 10,
nl_inverted = true,
},
battery_sysfs = "/sys/class/power_supply/bd71827_bat",
}
-- Kobo Clara Colour:
local KoboSpaColour = Kobo:extend{
model = "Kobo_spaColour",
isMTK = yes,
hasEclipseWfm = yes,
canToggleChargingLED = yes,
hasFrontlight = yes,
touch_snow_protocol = true,
display_dpi = 300,
hasNaturalLight = yes,
frontlight_settings = {
frontlight_white = "/sys/class/backlight/mxc_msp430.0/brightness",
frontlight_mixer = "/sys/class/backlight/lm3630a_led/color",
nl_min = 0,
nl_max = 10,
nl_inverted = true,
},
battery_sysfs = "/sys/class/power_supply/bd71827_bat",
isSMP = yes,
hasColorScreen = yes,
}
function Kobo:setupChargingLED()
if G_reader_settings:nilOrTrue("enable_charging_led") then
if self:hasAuxBattery() and self.powerd:isAuxBatteryConnected() then
@ -553,7 +615,7 @@ function Kobo:getKeyRepeat()
self.key_repeat = ffi.new("unsigned int[?]", C.REP_CNT)
if C.ioctl(self.ntx_fd, C.EVIOCGREP, self.key_repeat) < 0 then
local err = ffi.errno()
logger.warn("Device:getKeyRepeat: EVIOCGREP ioctl failed:", ffi.string(C.strerror(err)))
logger.warn("Device:getKeyRepeat: EVIOCGREP ioctl on fd", self.ntx_fd, "failed:", ffi.string(C.strerror(err)))
return false
else
logger.dbg("Key repeat is set up to repeat every", self.key_repeat[C.REP_PERIOD], "ms after a delay of", self.key_repeat[C.REP_DELAY], "ms")
@ -567,14 +629,14 @@ function Kobo:disableKeyRepeat()
local key_repeat = ffi.new("unsigned int[?]", C.REP_CNT)
if C.ioctl(self.ntx_fd, C.EVIOCSREP, key_repeat) < 0 then
local err = ffi.errno()
logger.warn("Device:disableKeyRepeat: EVIOCSREP ioctl failed:", ffi.string(C.strerror(err)))
logger.warn("Device:disableKeyRepeat: EVIOCSREP ioctl on fd", self.ntx_fd, "failed:", ffi.string(C.strerror(err)))
end
end
function Kobo:restoreKeyRepeat()
if C.ioctl(self.ntx_fd, C.EVIOCSREP, self.key_repeat) < 0 then
local err = ffi.errno()
logger.warn("Device:restoreKeyRepeat: EVIOCSREP ioctl failed:", ffi.string(C.strerror(err)))
logger.warn("Device:restoreKeyRepeat: EVIOCSREP ioctl on fd", self.ntx_fd, "failed:", ffi.string(C.strerror(err)))
end
end
@ -591,7 +653,7 @@ function Kobo:toggleKeyRepeat(toggle)
-- Check the current (kernel) state to know what to do
if C.ioctl(self.ntx_fd, C.EVIOCGREP, key_repeat) < 0 then
local err = ffi.errno()
logger.warn("Device:toggleKeyRepeat: EVIOCGREP ioctl failed:", ffi.string(C.strerror(err)))
logger.warn("Device:toggleKeyRepeat: EVIOCGREP ioctl on fd", self.ntx_fd, "failed:", ffi.string(C.strerror(err)))
return false
else
if key_repeat[C.REP_DELAY] == 0 and key_repeat[C.REP_PERIOD] == 0 then
@ -604,7 +666,7 @@ function Kobo:toggleKeyRepeat(toggle)
if C.ioctl(self.ntx_fd, C.EVIOCSREP, key_repeat) < 0 then
local err = ffi.errno()
logger.warn("Device:toggleKeyRepeat: EVIOCSREP ioctl failed:", ffi.string(C.strerror(err)))
logger.warn("Device:toggleKeyRepeat: EVIOCSREP ioctl on fd", self.ntx_fd, "failed:", ffi.string(C.strerror(err)))
return false
end
@ -637,8 +699,9 @@ function Kobo:init()
debug = logger.dbg,
is_always_portrait = self.isAlwaysPortrait(),
mxcfb_bypass_wait_for = mxcfb_bypass_wait_for,
no_cfa_post_processing = G_reader_settings:isTrue("no_cfa_post_processing"),
}
if self.screen.fb_bpp == 32 then
if self.screen.fb_bpp == 32 and self.screen._vinfo.red.offset ~= 0 then
-- Ensure we decode images properly, as our framebuffer is BGRA...
logger.info("Enabling Kobo @ 32bpp BGR tweaks")
self.hasBGRFrameBuffer = yes
@ -669,6 +732,8 @@ function Kobo:init()
if util.pathExists("/sys/class/power_supply/battery") then
-- Newer devices (circa sunxi)
self.battery_sysfs = "/sys/class/power_supply/battery"
elseif util.fileExists("/sys/class/power_supply/bd71827_bat") then
self.battery_sysfs = "/sys/class/power_supply/bd71827_bat"
else
self.battery_sysfs = "/sys/class/power_supply/mc13892_bat"
end
@ -686,43 +751,6 @@ function Kobo:init()
self.frontlight_settings.frontlight_mixer = "/sys/class/backlight/tlc5947_bl/color"
end
end
-- Touch panel input
if util.fileExists("/dev/input/by-path/platform-2-0010-event") then
-- Elan (HWConfig TouchCtrl is ekth6) on i2c bus 2
self.touch_dev = "/dev/input/by-path/platform-2-0010-event"
elseif util.fileExists("/dev/input/by-path/platform-1-0010-event") then
-- Elan (HWConfig TouchCtrl is ekth6) on i2c bus 1
self.touch_dev = "/dev/input/by-path/platform-1-0010-event"
elseif util.fileExists("/dev/input/by-path/platform-0-0010-event") then
-- Elan (HWConfig TouchCtrl is ekth6) on i2c bus 0
self.touch_dev = "/dev/input/by-path/platform-0-0010-event"
else
self.touch_dev = "/dev/input/event1"
end
-- Physical buttons & synthetic NTX events
if util.fileExists("/dev/input/by-path/platform-gpio-keys-event") then
-- Libra 2 w/ a BD71828 PMIC
self.ntx_dev = "/dev/input/by-path/platform-gpio-keys-event"
elseif util.fileExists("/dev/input/by-path/platform-ntx_event0-event") then
-- MTK, sunxi & Mk. 7
self.ntx_dev = "/dev/input/by-path/platform-ntx_event0-event"
elseif util.fileExists("/dev/input/by-path/platform-mxckpd-event") then
-- circa Mk. 5 i.MX
self.ntx_dev = "/dev/input/by-path/platform-mxckpd-event"
else
self.ntx_dev = "/dev/input/event0"
end
-- Power button (this usually ends up in ntx_dev, except with some PMICs)
if util.fileExists("/dev/input/by-path/platform-bd71828-pwrkey-event") then
-- Libra 2 & Nia w/ a BD71828 PMIC
self.power_dev = "/dev/input/by-path/platform-bd71828-pwrkey-event"
elseif util.fileExists("/dev/input/by-path/platform-bd71828-pwrkey.4.auto-event") then
-- Sage w/ a BD71828 PMIC
self.power_dev = "/dev/input/by-path/platform-bd71828-pwrkey.4.auto-event"
end
end
-- NOTE: i.MX5 devices have a wonky RTC that doesn't like alarms set further away that UINT16_MAX seconds from now...
@ -759,10 +787,15 @@ function Kobo:init()
self.hasNaturalLightMixer = yes
end
-- Ditto
if self:isMk7() then
if self:isMk7() or self:isMTK() then
self.canHWDither = yes
end
-- Enable Kaleido waveform modes on supported devices
if self:hasColorScreen() and self:isMTK() then
self.hasKaleidoWfm = yes
end
-- NOTE: Devices with an AW99703 frontlight PWM controller feature a hardware smooth ramp when setting the frontlight intensity.
--- A side-effect of this behavior is that if you queue a series of intensity changes ending at 0,
--- it won't ramp *at all*, jumping straight to zero instead.
@ -771,6 +804,14 @@ function Kobo:init()
self.frontlight_settings.ramp_off_delay = 0.5
end
-- I don't know how this PWM controller behaves on earlier devices, but it's... not great here.
if self:hasNaturalLightMixer() and self:isMTK() and self.frontlight_settings.frontlight_mixer:find("lm3630a_led", 12, true) then
-- First, we need a delay between ioctls
self.frontlight_settings.ramp_delay = 0.025
-- Second, it *really* doesn't like being interleaved with screen refreshes
self.frontlight_settings.delay_ramp_start = true
end
self.powerd = require("device/kobo/powerd"):new{
device = self,
battery_sysfs = self.battery_sysfs,
@ -816,14 +857,41 @@ function Kobo:init()
-- And then handle the extra shenanigans if necessary.
self:initEventAdjustHooks()
-- Various HW Buttons, Switches & Synthetic NTX events
self.ntx_fd = self.input.open(self.ntx_dev)
-- Dedicated Power Button input device (if any)
if self.power_dev then
self.input.open(self.power_dev)
-- Auto-detect input devices (via FBInk's fbink_input_scan)
local ok, FBInkInput = pcall(ffi.load, "fbink_input")
if not ok then
-- NOP fallback for the testsuite...
FBInkInput = { fbink_input_scan = NOP }
end
local dev_count = ffi.new("size_t[1]")
-- We care about: the touchscreen, the stylus, the power button, the sleep cover, and pagination buttons
-- (and technically rotation events, but we'll get it with the device that provides the buttons on NTX).
-- We exclude keyboards to play nice with the ExternalKeyboard plugin, which will handle potential keyboards on its own.
local match_mask = bit.bor(C.INPUT_TOUCHSCREEN, C.INPUT_TABLET, C.INPUT_POWER_BUTTON, C.INPUT_SLEEP_COVER, C.INPUT_PAGINATION_BUTTONS)
local devices = FBInkInput.fbink_input_scan(match_mask, C.INPUT_KEYBOARD, 0, dev_count)
if devices ~= nil then
for i = 0, tonumber(dev_count[0]) - 1 do
local dev = devices[i]
if dev.matched then
-- We need to single out whichever device provides pagination buttons or sleep cover events, as we'll want to tweak key repeat there...
-- The first one will do, as it's extremely likely to be event0, and that's pretty fairly set in stone on NTX boards.
if (bit.band(dev.type, C.INPUT_PAGINATION_BUTTONS) ~= 0 or bit.band(dev.type, C.INPUT_SLEEP_COVER) ~= 0) and not self.ntx_fd then
self.ntx_fd = self.input.fdopen(tonumber(dev.fd), ffi.string(dev.path), ffi.string(dev.name))
else
self.input.fdopen(tonumber(dev.fd), ffi.string(dev.path), ffi.string(dev.name))
end
end
end
C.free(devices)
else
-- Auto-detection failed, warn and fall back to defaults
logger.warn("We failed to auto-detect the proper input devices, input handling may be inconsistent!")
-- Various HW Buttons, Switches & Synthetic NTX events
self.ntx_fd = self.input.open(self.ntx_dev)
-- Touch panel
self.input.open(self.touch_dev)
end
-- Touch panel
self.input.open(self.touch_dev)
-- NOTE: On devices with a gyro, there may be a dedicated input device outputting the raw accelerometer data
-- (3-Axis Orientation/Motion Detection).
-- We skip it because we don't need it (synthetic rotation change events are sent to the main ntx input device),
@ -844,9 +912,12 @@ function Kobo:init()
end
-- Detect the NTX charging LED sysfs knob
if util.pathExists("/sys/class/leds/bd71828-green-led") then
-- Standard Linux LED class, wheee!
self.charging_led_sysfs_knob = "/sys/class/leds/bd71828-green-led"
if util.pathExists("/sys/class/leds/LED") then
self.charging_led_sysfs_knob = "/sys/class/leds/LED/brightness"
elseif util.pathExists("/sys/class/leds/GLED") then
self.charging_led_sysfs_knob = "/sys/class/leds/GLED/brightness"
elseif util.pathExists("/sys/class/leds/bd71828-green-led") then
self.charging_led_sysfs_knob = "/sys/class/leds/bd71828-green-led/brightness"
elseif util.pathExists("/sys/devices/platform/ntx_led/lit") then
self.ntx_lit_sysfs_knob = "/sys/devices/platform/ntx_led/lit"
elseif util.pathExists("/sys/devices/platform/pmic_light.1/lit") then
@ -876,8 +947,15 @@ function Kobo:init()
-- Only enable a single core on startup
self:enableCPUCores(1)
self.canStandby = checkStandby()
if self.canStandby() and (self:isMk7() or self:isSunxi() or self:isMTK()) then
-- On MTK, the "standby" power state is unavailable, and Nickel instead uses "mem" (and /sys/power/mem_sleep doesn't exist either)
if self:isMTK() then
self.standby_state = "mem"
end
self.canStandby = checkStandby(self.standby_state)
if self.canStandby() and (self:isMk7() or self:isSunxi()) then
-- NOTE: Do *NOT* enable this on MTK. What happens if you do can only be described as "shit hits the fan".
-- (Nickel doesn't).
self.canPowerSaveWhileCharging = yes
end
@ -1182,7 +1260,7 @@ function Kobo:standby(max_duration)
-- WiFi toggle, but (almost) everywhere.
ffiUtil.usleep(90000) -- sleep 0.09s (0.08s would also work)
local ret = ffiUtil.writeToSysfs("standby", "/sys/power/state")
local ret = ffiUtil.writeToSysfs(self.standby_state, "/sys/power/state")
self.last_standby_time = time.boottime_or_realtime_coarse() - standby_time
self.total_standby_time = self.total_standby_time + self.last_standby_time
@ -1446,6 +1524,7 @@ function Kobo:_NTXChargingLEDToggle(toggle)
end
function Kobo:_LinuxChargingLEDToggle(toggle)
-- max_brightness usually says 255 for those, but 1 does the same (and matches Nickel's behavior)
ffiUtil.writeToSysfs(toggle and "1" or "0", self.charging_led_sysfs_knob)
end
@ -1678,6 +1757,12 @@ elseif codename == "goldfinch" then
return KoboGoldfinch
elseif codename == "condor" then
return KoboCondor
elseif codename == "monza" or codename == "monzaTolino" then
return KoboMonza
elseif codename == "spaBW" or codename == "spaTolinoBW" then
return KoboSpaBW
elseif codename == "spaColour" or codename == "spaTolinoColour" then
return KoboSpaColour
else
error("unrecognized Kobo model ".. codename .. " with device id " .. product_id)
end

@ -136,8 +136,12 @@ function KoboPowerD:init()
self.device.frontlight_settings = self.device.frontlight_settings or {}
-- Does this device require non-standard ramping behavior?
self.device.frontlight_settings.ramp_off_delay = self.device.frontlight_settings.ramp_off_delay or 0.0
--- @note: Newer devices appear to block slightly longer on FL ioctls/sysfs, so we only really need a delay on older devices.
--- @note: Newer devices (or at least some PWM controllers) appear to block slightly longer on FL ioctls/sysfs,
--- so we only really need a delay on older devices.
self.device.frontlight_settings.ramp_delay = self.device.frontlight_settings.ramp_delay or (self.device:hasNaturalLight() and 0.0 or 0.025)
-- Some PWM controllers *really* don't like being interleaved between screen refreshes,
-- so we delay the *start* of the ramp on these.
self.device.frontlight_settings.delay_ramp_start = self.device.frontlight_settings.delay_ramp_start or false
-- If this device has natural light, use the sysfs interface, and ioctl otherwise.
-- NOTE: On the Forma, nickel still appears to prefer using ntx_io to handle the FL,
@ -364,8 +368,17 @@ function KoboPowerD:turnOffFrontlightHW(done_callback)
if self.device.frontlight_settings.ramp_off_delay > 0.0 and self.fl_intensity <= 2 then
UIManager:scheduleIn(self.device.frontlight_settings.ramp_delay, self._endRampDown, self, self.fl_min, done_callback)
else
self:turnOffFrontlightRamp(self.fl_intensity, self.fl_min, done_callback)
self.fl_ramp_down_running = true
-- NOTE: Similarly, some controllers *really* don't like to be interleaved with screen refreshes,
-- so we wait until the next UI frame for the refreshes to go through first...
if self.device.frontlight_settings.delay_ramp_start then
UIManager:nextTick(function()
self:turnOffFrontlightRamp(self.fl_intensity, self.fl_min, done_callback)
self.fl_ramp_down_running = true
end)
else
self:turnOffFrontlightRamp(self.fl_intensity, self.fl_min, done_callback)
self.fl_ramp_down_running = true
end
end
end
else
@ -422,8 +435,16 @@ function KoboPowerD:turnOnFrontlightHW(done_callback)
-- NOTE: Match the ramp down behavior on devices with a ramp_off_delay: jump straight to 1 or 2% intensity.
UIManager:scheduleIn(self.device.frontlight_settings.ramp_delay, self._endRampUp, self, self.fl_intensity, done_callback)
else
self:turnOnFrontlightRamp(self.fl_min, self.fl_intensity, done_callback)
self.fl_ramp_up_running = true
-- Same deal as in turnOffFrontlightHW
if self.device.frontlight_settings.delay_ramp_start then
UIManager:nextTick(function()
self:turnOnFrontlightRamp(self.fl_min, self.fl_intensity, done_callback)
self.fl_ramp_up_running = true
end)
else
self:turnOnFrontlightRamp(self.fl_min, self.fl_intensity, done_callback)
self.fl_ramp_up_running = true
end
end
end
else

@ -56,9 +56,7 @@ local PocketBook = Generic:extend{
-- Works same as input.event_map, but for raw input EV_KEY translation
keymap = { [scan] = event },
}]]
-- Runtime state: whether raw input is actually used
--- @fixme: Never actually set anywhere?
is_using_raw_input = nil,
-- We'll nil raw_input at runtime if it cannot be used.
-- InkView may have started translating button codes based on rotation on newer devices...
-- That historically wasn't the case, hence this defaulting to false.
@ -242,7 +240,9 @@ function PocketBook:init()
-- NOTE: This all happens in ffi/input_pocketbook.lua
self._model_init()
if (not self.input.raw_input) or (not pcall(self.input.open, self.input, self.raw_input)) then
-- NOTE: This is the odd one out actually calling input.open as a *method*,
-- which the imp supports to get access to self.input.raw_input
if (not self.input.raw_input) or (not pcall(self.input.open, self.input)) then
inkview.OpenScreen()
-- Raw mode open failed (no permissions?), so we'll run the usual way.
-- Disable touch coordinate translation as inkview will do that.
@ -387,6 +387,14 @@ function PocketBook:initNetworkManager(NetworkMgr)
return band(inkview.QueryNetwork(), C.NET_CONNECTED) ~= 0
end
NetworkMgr.isWifiOn = NetworkMgr.isConnected
function NetworkMgr:isOnline()
-- Fail early if we don't even have a default route, otherwise we're
-- unlikely to be online and canResolveHostnames would never succeed
-- again because PocketBook's glibc parses /etc/resolv.conf on first
-- use only. See https://sourceware.org/bugzilla/show_bug.cgi?id=984
return NetworkMgr:hasDefaultRoute() and NetworkMgr:canResolveHostnames()
end
end
function PocketBook:getSoftwareVersion()
@ -420,6 +428,25 @@ function PocketBook:setEventHandlers(uimgr)
end
end
local function getBrowser()
if util.pathExists("/usr/bin/browser.app") then
return true, "/usr/bin/browser.app"
elseif util.pathExists("/ebrmain/bin/browser.app") then
return true, "/ebrmain/bin/browser.app"
end
return false
end
function PocketBook:canOpenLink()
return inkview.MultitaskingSupported() and getBrowser()
end
function PocketBook:openLink(link)
local found, bin = getBrowser()
if not found or not link or type(link) ~= "string" then return end
inkview.OpenBook(bin, link, 0)
end
-- Pocketbook HW rotation modes start from landsape, CCW
local function landscape_ccw() return {
1, 0, 3, 2, -- PORTRAIT, LANDSCAPE, PORTRAIT_180, LANDSCAPE_180
@ -598,7 +625,6 @@ local PocketBook632 = PocketBook:extend{
local PocketBook633 = PocketBook:extend{
model = "PBColor",
display_dpi = 300,
color_saturation = 1.5,
hasColorScreen = yes,
canHWDither = yes, -- Adjust color saturation with inkview
canUseCBB = no, -- 24bpp
@ -642,6 +668,24 @@ local PocketBook700 = PocketBook:extend{
inkview_translates_buttons = true,
}
-- PocketBook Era Color (PB700K3)
local PocketBook700K3 = PocketBook:extend{
model = "PBEraColor",
display_dpi = 300,
hasColorScreen = yes,
canHWDither = yes, -- Adjust color saturation with inkview
canUseCBB = no, -- 24bpp
isAlwaysPortrait = yes,
hasNaturalLight = yes,
-- c.f., https://github.com/koreader/koreader/issues/9556
inkview_translates_buttons = true,
}
function PocketBook700K3._fb_init(fb, finfo, vinfo)
-- Pocketbook Color Lux reports bits_per_pixel = 8, but actually uses an RGB24 framebuffer
vinfo.bits_per_pixel = 24
end
-- PocketBook InkPad 3 (740)
local PocketBook740 = PocketBook:extend{
model = "PBInkPad3",
@ -672,7 +716,6 @@ local PocketBook740_2 = PocketBook:extend{
local PocketBook741 = PocketBook:extend{
model = "PBInkPadColor",
display_dpi = 300,
color_saturation = 1.5,
hasColorScreen = yes,
canHWDither = yes, -- Adjust color saturation with inkview
canUseCBB = no, -- 24bpp
@ -689,7 +732,6 @@ end
local PocketBook743C = PocketBook:extend{
model = "PBInkPadColor2",
display_dpi = 300,
color_saturation = 1.5,
hasColorScreen = yes,
canHWDither = yes, -- Adjust color saturation with inkview
canUseCBB = no, -- 24bpp
@ -708,7 +750,6 @@ local PocketBook743K3 = PocketBook:extend{
model = "PBInkPadColor3",
display_dpi = 300,
viewport = Geom:new{x=3, y=2, w=1395, h=1864},
color_saturation = 1.5,
hasColorScreen = yes,
canHWDither = yes, -- Adjust color saturation with inkview
canUseCBB = no, -- 24bpp
@ -735,7 +776,6 @@ local PocketBook743G = PocketBook:extend{
local PocketBookColorLux = PocketBook:extend{
model = "PBColorLux",
display_dpi = 125,
color_saturation = 1.5,
hasColorScreen = yes,
canHWDither = yes, -- Adjust color saturation with inkview
canUseCBB = no, -- 24bpp
@ -776,83 +816,91 @@ local PocketBook1040 = PocketBook:extend{
logger.info('SoftwareVersion: ', PocketBook:getSoftwareVersion())
local codename = PocketBook:getDeviceModel()
local full_codename = PocketBook:getDeviceModel()
-- Pocketbook codenames are all over the place:
local codename = full_codename
-- "PocketBook 615 (PB615)"
codename = codename:match(" [(]([^()]+)[)]$") or codename
-- "PocketBook 615"
codename = codename:match("^PocketBook ([^ ].*)$") or codename
-- "PB615"
codename = codename:match("^PB(.+)$") or codename
if codename == "PocketBook 515" then
if codename == "515" then
return PocketBook515
elseif codename == "PB606" or codename == "PocketBook 606" then
elseif codename == "606" then
return PocketBook606
elseif codename == "PocketBook 611" then
elseif codename == "611" then
return PocketBook611
elseif codename == "PocketBook 613" then
elseif codename == "613" then
return PocketBook613
elseif codename == "PocketBook 614" or codename == "PocketBook 614W" then
elseif codename == "614" or codename == "614W" then
return PocketBook614W
elseif codename == "PB615" or codename == "PB615W" or
codename == "PocketBook 615" or codename == "PocketBook 615W" then
elseif codename == "615" or codename == "615W" then
return PocketBook615
elseif codename == "PB616" or codename == "PB616W" or
codename == "PocketBook 616" or codename == "PocketBook 616W" then
elseif codename == "616" or codename == "616W" then
return PocketBook616
elseif codename == "PB617" or codename == "PocketBook 617" then
elseif codename == "617" then
return PocketBook617
elseif codename == "PB618" then
elseif codename == "618" then
return PocketBook618
elseif codename == "PocketBook 622" then
elseif codename == "622" then
return PocketBook622
elseif codename == "PocketBook 623" then
elseif codename == "623" then
return PocketBook623
elseif codename == "PocketBook 624" then
elseif codename == "624" then
return PocketBook624
elseif codename == "PB625" then
elseif codename == "625" then
return PocketBook625
elseif codename == "PB626" or codename == "PB626(2)-TL3" or
codename == "PocketBook 626" then
elseif codename == "626" or codename == "626(2)-TL3" then
return PocketBook626
elseif codename == "PB627" then
elseif codename == "627" then
return PocketBook627
elseif codename == "PB628" then
elseif codename == "628" then
return PocketBook628
elseif codename == "PB629" then
elseif codename == "629" then
return PocketBook629
elseif codename == "PocketBook 630" then
elseif codename == "630" then
return PocketBook630
elseif codename == "PB631" or codename == "PocketBook 631" then
elseif codename == "631" then
return PocketBook631
elseif codename == "PB632" then
elseif codename == "632" then
return PocketBook632
elseif codename == "PB633" then
elseif codename == "633" then
return PocketBook633
elseif codename == "PB634" then
elseif codename == "634" then
return PocketBook634
elseif codename == "PB640" or codename == "PocketBook 640" then
elseif codename == "640" then
return PocketBook640
elseif codename == "PB641" then
elseif codename == "641" then
return PocketBook641
elseif codename == "PB650" or codename == "PocketBook 650" then
elseif codename == "650" then
return PocketBook650
elseif codename == "PB700" or codename == "PocketBook 700" then
elseif codename == "700" then
return PocketBook700
elseif codename == "PB740" then
elseif codename == "700K3" then
return PocketBook700K3
elseif codename == "740" then
return PocketBook740
elseif codename == "PB740-2" or codename == "PB740-3" then
elseif codename == "740-2" or codename == "740-3" then
return PocketBook740_2
elseif codename == "PB741" then
elseif codename == "741" then
return PocketBook741
elseif codename == "PB743C" then
elseif codename == "743C" then
return PocketBook743C
elseif codename == "PB743K3" then
elseif codename == "743K3" then
return PocketBook743K3
elseif codename == "PB743G" or codename == "PB743g" or codename == "PocketBook 743G" or codename == "PocketBook 743g" then
elseif codename == "743G" or codename == "743g" then
return PocketBook743G
elseif codename == "PocketBook 840" or codename == "Reader InkPad" then
elseif codename == "840" or codename == "Reader InkPad" then
return PocketBook840
elseif codename == "PB970" then
elseif codename == "970" then
return PocketBook970
elseif codename == "PB1040" then
elseif codename == "1040" then
return PocketBook1040
elseif codename == "PocketBook Color Lux" then
elseif codename == "Color Lux" then
return PocketBookColorLux
else
error("unrecognized PocketBook model " .. codename)
error("unrecognized PocketBook model " .. full_codename)
end

@ -68,6 +68,7 @@ local Device = Generic:extend{
hasBattery = SDL.getPowerInfo,
hasKeyboard = yes,
hasKeys = yes,
hasSymKey = os.getenv("DISABLE_TOUCH") == "1" and yes or no,
hasDPad = yes,
hasWifiToggle = no,
hasSeamlessWifiToggle = no,
@ -120,6 +121,12 @@ local Desktop = Device:extend{
hasExitOptions = notOSX,
}
local Flatpak = Device:extend{
model = "Flatpak",
isDesktop = yes,
canExternalDictLookup = no,
}
local Emulator = Device:extend{
model = "Emulator",
isEmulator = yes,
@ -436,6 +443,8 @@ io.write("Starting SDL in " .. SDL.getBasePath() .. "\n")
-------------- device probe ------------
if os.getenv("APPIMAGE") then
return AppImage
elseif os.getenv("FLATPAK") then
return Flatpak
elseif os.getenv("KO_MULTIUSER") then
return Desktop
elseif os.getenv("UBUNTU_APPLICATION_ISOLATION") then

@ -61,7 +61,7 @@ return {
[1073741893] = "F12", -- F[12]
[1073742049] = "Shift", -- left shift
[1073742053] = "Sym", -- right shift
[1073742053] = os.getenv("DISABLE_TOUCH") == "1" and "Sym" or "Shift", -- right shift
[1073742050] = "Alt", -- left alt
[1073742054] = "AA", -- right alt key
[1073741925] = "ContextMenu", -- Context menu key

@ -14,7 +14,7 @@ local SonyPRSTUX = Generic:extend{
model = "Sony PRSTUX",
isSonyPRSTUX = yes,
hasKeys = yes,
hasOTAUpdates = yes,
hasOTAUpdates = no,
hasWifiManager = yes,
canReboot = yes,
canPowerOff = yes,

@ -32,6 +32,7 @@ local CreOptions = require("ui/data/creoptions")
local KoptOptions = require("ui/data/koptoptions")
local Device = require("device")
local Event = require("ui/event")
local FileManager = require("apps/filemanager/filemanager")
local Notification = require("ui/widget/notification")
local ReaderHighlight = require("apps/reader/modules/readerhighlight")
local ReaderZooming = require("apps/reader/modules/readerzooming")
@ -53,7 +54,8 @@ local settingsList = {
open_previous_document = {category="none", event="OpenLastDoc", title=_("Open previous document"), general=true},
history = {category="none", event="ShowHist", title=_("History"), general=true},
history_search = {category="none", event="SearchHistory", title=_("History search"), general=true},
favorites = {category="none", event="ShowColl", arg="favorites", title=_("Favorites"), general=true},
favorites = {category="none", event="ShowColl", title=_("Favorites"), general=true},
collections = {category="none", event="ShowCollList", title=_("Collections"), general=true},
filemanager = {category="none", event="Home", title=_("File browser"), general=true, separator=true},
----
dictionary_lookup = {category="none", event="ShowDictionaryLookup", title=_("Dictionary lookup"), general=true},
@ -65,7 +67,7 @@ local settingsList = {
----
-- Device
exit_screensaver = {category="none", event="ExitScreensaver", title=_("Exit screensaver"), device=true},
exit_screensaver = {category="none", event="ExitScreensaver", title=_("Exit sleep screen"), device=true},
start_usbms = {category="none", event="RequestUSBMS", title=_("Start USB storage"), device=true, condition=Device:canToggleMassStorage()},
suspend = {category="none", event="RequestSuspend", title=_("Suspend"), device=true, condition=Device:canSuspend()},
restart = {category="none", event="Restart", title=_("Restart KOReader"), device=true, condition=Device:canRestart()},
@ -82,6 +84,7 @@ local settingsList = {
----
toggle_key_repeat = {category="none", event="ToggleKeyRepeat", title=_("Toggle key repeat"), device=true, condition=Device:hasKeys() and Device:canKeyRepeat(), separator=true},
toggle_gsensor = {category="none", event="ToggleGSensor", title=_("Toggle accelerometer"), device=true, condition=Device:hasGSensor()},
lock_gsensor = {category="none", event="LockGSensor", title=_("Lock auto rotation to current orientation"), device=true, condition=Device:hasGSensor()},
toggle_rotation = {category="none", event="SwapRotation", title=_("Toggle orientation"), device=true},
invert_rotation = {category="none", event="InvertRotation", title=_("Invert rotation"), device=true},
iterate_rotation = {category="none", event="IterateRotation", title=_("Rotate by 90° CW"), device=true},
@ -119,12 +122,17 @@ local settingsList = {
----
-- File browser
set_display_mode = {category="string", event="SetDisplayMode", title=_("Set display mode"), args_func=FileManager.getDisplayModeActions, filemanager=true},
set_sort_by = {category="string", event="SetSortBy", title=_("Sort by"), args_func=FileManager.getSortByActions, filemanager=true},
set_reverse_sorting = {category="string", event="SetReverseSorting", title=_("Reverse sorting"), args={true, false}, toggle={_("on"), _("off")}, filemanager=true},
set_mixed_sorting = {category="string", event="SetMixedSorting", title=_("Folders and files mixed"), args={true, false}, toggle={_("on"), _("off")}, filemanager=true, separator=true},
----
folder_up = {category="none", event="FolderUp", title=_("Folder up"), filemanager=true},
show_plus_menu = {category="none", event="ShowPlusMenu", title=_("Show plus menu"), filemanager=true},
toggle_select_mode = {category="none", event="ToggleSelectMode", title=_("Toggle select mode"), filemanager=true},
refresh_content = {category="none", event="RefreshContent", title=_("Refresh content"), filemanager=true},
folder_shortcuts = {category="none", event="ShowFolderShortcutsDialog", title=_("Folder shortcuts"), filemanager=true},
file_search = {category="none", event="ShowFileSearch", title=_("File search"), filemanager=true, separator=true},
file_search = {category="none", event="ShowFileSearch", title=_("File search"), filemanager=true},
----
-- go_to
-- back
@ -133,12 +141,14 @@ local settingsList = {
open_next_document_in_folder = {category="none", event="OpenNextDocumentInFolder", title=_("Open next document in folder"), reader=true, separator=true},
----
show_config_menu = {category="none", event="ShowConfigMenu", title=_("Show bottom menu"), reader=true},
toggle_status_bar = {category="none", event="ToggleFooterMode", title=_("Toggle status bar"), reader=true, separator=true},
toggle_status_bar = {category="none", event="ToggleFooterMode", title=_("Toggle status bar"), reader=true},
toggle_chapter_progress_bar = {category="none", event="ToggleChapterProgressBar", title=_("Toggle chapter progress bar"), reader=true, separator=true},
----
prev_chapter = {category="none", event="GotoPrevChapter", title=_("Previous chapter"), reader=true},
next_chapter = {category="none", event="GotoNextChapter", title=_("Next chapter"), reader=true},
first_page = {category="none", event="GoToBeginning", title=_("First page"), reader=true},
last_page = {category="none", event="GoToEnd", title=_("Last page"), reader=true},
random_page = {category="none", event="GoToRandomPage", title=_("Random page"), reader=true},
page_jmp = {category="absolutenumber", event="GotoViewRel", min=-100, max=100, title=_("Turn pages"), reader=true},
go_to = {category="none", event="ShowGotoDialog", title=_("Go to page"), filemanager=true, reader=true},
skim = {category="none", event="ShowSkimtoDialog", title=_("Skim document"), reader=true},
@ -157,6 +167,7 @@ local settingsList = {
clear_location_history = {category="none", event="ClearLocationStack", arg=true, title=_("Clear location history"), reader=true, separator=true},
----
fulltext_search = {category="none", event="ShowFulltextSearchInput", title=_("Fulltext search"), reader=true},
fulltext_search_findall_results = {category="none", event="ShowFindAllResults", title=_("Last fulltext search results"), reader=true},
toc = {category="none", event="ShowToc", title=_("Table of contents"), reader=true},
book_map = {category="none", event="ShowBookMap", title=_("Book map"), reader=true, condition=Device:isTouchDevice()},
book_map_overview = {category="none", event="ShowBookMap", arg=true, title=_("Book map (overview)"), reader=true, condition=Device:isTouchDevice()},
@ -270,6 +281,7 @@ local dispatcher_menu_order = {
"history",
"history_search",
"favorites",
"collections",
"filemanager",
----
"dictionary_lookup",
@ -298,6 +310,7 @@ local dispatcher_menu_order = {
----
"toggle_key_repeat",
"toggle_gsensor",
"lock_gsensor",
"rotation_mode",
"toggle_rotation",
"invert_rotation",
@ -336,6 +349,11 @@ local dispatcher_menu_order = {
----
-- File browser
"set_display_mode",
"set_sort_by",
"set_reverse_sorting",
"set_mixed_sorting",
----
"folder_up",
"show_plus_menu",
"toggle_select_mode",
@ -351,11 +369,13 @@ local dispatcher_menu_order = {
----
"show_config_menu",
"toggle_status_bar",
"toggle_chapter_progress_bar",
----
"prev_chapter",
"next_chapter",
"first_page",
"last_page",
"random_page",
"page_jmp",
"go_to",
"skim",
@ -374,6 +394,7 @@ local dispatcher_menu_order = {
"clear_location_history",
----
"fulltext_search",
"fulltext_search_findall_results",
"toc",
"book_map",
"book_map_overview",
@ -708,7 +729,7 @@ function Dispatcher:_sortActions(caller, location, settings, touchmenu_instance)
local SortWidget = require("ui/widget/sortwidget")
local sort_widget
sort_widget = SortWidget:new{
title = _("Sort"),
title = _("Arrange actions"),
item_table = display_list,
callback = function()
if location[settings] and next(location[settings]) ~= nil then
@ -977,7 +998,7 @@ function Dispatcher:addSubMenu(caller, menu, location, settings)
end
menu[#menu].separator = true
table.insert(menu, {
text = _("Sort"),
text = _("Arrange actions"),
checked_func = function()
return location[settings] ~= nil
and location[settings].settings ~= nil

@ -35,6 +35,8 @@ local function isFile(file)
return lfs.attributes(file, "mode") == "file"
end
local is_history_location_enabled = isDir(HISTORY_DIR)
local doc_hash_cache = {}
local is_hash_location_enabled
@ -153,8 +155,9 @@ end
--- Returns path of `metadata.lua` file if it exists, or nil.
-- @string doc_path path to the document (e.g., `/foo/bar.pdf`)
-- @bool no_legacy set to true to skip check of the legacy history file
-- @treturn string
-- @treturn string (or nil on failure)
function DocSettings:findSidecarFile(doc_path, no_legacy)
if doc_path == nil or doc_path == "" then return nil end
local sidecar_filename = DocSettings.getSidecarFilename(doc_path)
local sidecar_file
for _, location in ipairs(getOrderedLocationCandidates()) do
@ -163,7 +166,7 @@ function DocSettings:findSidecarFile(doc_path, no_legacy)
return sidecar_file, location
end
end
if not no_legacy then
if is_history_location_enabled and not no_legacy then
sidecar_file = self:getHistoryPath(doc_path)
if isFile(sidecar_file) then
return sidecar_file, "hist" -- for isSidecarFileNotInPreferredLocation() used in moveBookMetadata
@ -237,7 +240,7 @@ function DocSettings:open(doc_path)
new.hash_sidecar_dir = new:getSidecarDir(doc_path, "hash")
hash_sidecar_file = new.hash_sidecar_dir .. "/" .. new.sidecar_filename
end
local history_file = new:getHistoryPath(doc_path)
local history_file = is_history_location_enabled and new:getHistoryPath(doc_path)
-- Candidates list, in order of priority:
local candidates_list = {
@ -256,9 +259,9 @@ function DocSettings:open(doc_path)
-- Backup file of new sidecar file in hashdocsettings folder
hash_sidecar_file and (hash_sidecar_file .. ".old") or "",
-- Legacy history folder
history_file,
history_file or "",
-- Backup file in legacy history folder
history_file .. ".old",
history_file and (history_file .. ".old") or "",
-- Legacy kpdfview setting
doc_path .. ".kpdfview.lua",
}

@ -33,6 +33,14 @@ local CreDocument = Document:extend{
monospace_font = "Droid Sans Mono",
header_font = "Noto Sans",
prop_to_cre_prop = { -- see cre lvtinydom.h
title = "doc.title",
authors = "doc.authors",
series = "doc.series.name",
series_index = "doc.series.number",
identifiers = "doc.identifiers",
},
-- Reasons for the fallback font ordering:
-- - Noto Sans CJK SC before FreeSans/Serif, as it has nice and larger
-- symbol glyphs for Wikipedia EPUB headings than both Free fonts)
@ -108,6 +116,15 @@ end
function CreDocument:engineInit()
if not engine_initialized then
cre = require("libs/libkoreader-cre")
-- When forking to execute any stuff in a sub-process,
-- as that stuff may not care about properly closing
-- the document, skip cre.cpp finalizer to avoid any
-- assertion failure.
require("ffi/util").addRunInSubProcessAfterForkFunc("cre_skip_teardown", function()
cre.setSkipTearDown(true)
end)
-- initialize cache
self:cacheInit()
@ -166,6 +183,7 @@ function CreDocument:init()
self.default_css = "./data/epub.css"
if file_type == "fb2" or file_type == "fb3" then
self.default_css = "./data/fb2.css"
self.is_fb2 = true -- FB2 won't look good with any html-oriented stylesheet
end
-- This mode must be the same as the default one set as ReaderView.view_mode
@ -216,6 +234,14 @@ function CreDocument:getDocumentProps()
return self._document:getDocumentProps()
end
function CreDocument:setAltDocumentProp(prop, value)
logger.dbg("CreDocument: set alt document prop", prop, value)
if type(value) == "number" then -- series index
value = tostring(value)
end
self._document:setAltDocumentProp(self.prop_to_cre_prop[prop], value)
end
function CreDocument:setupDefaultView()
if self.loaded then
-- Don't apply defaults if the document has already been loaded
@ -1352,6 +1378,11 @@ function CreDocument:setBatteryState(state)
self._document:setBatteryState(state)
end
function CreDocument:setPageInfoOverride(pageinfo)
logger.dbg("CreDocument: set page info", pageinfo)
self._document:setPageInfoOverride(pageinfo)
end
function CreDocument:isXPointerInCurrentPage(xp)
logger.dbg("CreDocument: check xpointer in current page", xp)
return self._document:isXPointerInCurrentPage(xp)
@ -1383,10 +1414,14 @@ function CreDocument:getAndClearRegexSearchError()
return retval
end
function CreDocument:findText(pattern, origin, reverse, caseInsensitive, page, regex, max_hits)
logger.dbg("CreDocument: find text", pattern, origin, reverse, caseInsensitive, regex, max_hits)
return self._document:findText(
pattern, origin, reverse, caseInsensitive and 1 or 0, regex and 1 or 0, max_hits or 200)
function CreDocument:findText(pattern, origin, direction, case_insensitive, page, regex, max_hits)
logger.dbg("CreDocument: find text", pattern, origin, direction == 1, case_insensitive, regex, max_hits)
return self._document:findText(pattern, origin, direction == 1, case_insensitive, regex, max_hits)
end
function CreDocument:findAllText(pattern, case_insensitive, nb_context_words, max_hits, regex)
logger.dbg("CreDocument: find all text", pattern, case_insensitive, regex, max_hits, true, nb_context_words)
return self._document:findAllText(pattern, case_insensitive, regex, max_hits, true, nb_context_words)
end
function CreDocument:enableInternalHistory(toggle)

@ -123,8 +123,12 @@ function DjvuDocument:getCoverPageImage()
return self.koptinterface:getCoverPageImage(self)
end
function DjvuDocument:findText(pattern, origin, reverse, caseInsensitive, page)
return self.koptinterface:findText(self, pattern, origin, reverse, caseInsensitive, page)
function DjvuDocument:findText(pattern, origin, reverse, case_insensitive, page)
return self.koptinterface:findText(self, pattern, origin, reverse, case_insensitive, page)
end
function DjvuDocument:findAllText(pattern, case_insensitive, nb_context_words, max_hits)
return self.koptinterface:findAllText(self, pattern, case_insensitive, nb_context_words, max_hits)
end
function DjvuDocument:renderPage(pageno, rect, zoom, rotation, gamma, render_mode, hinting)

@ -201,6 +201,7 @@ function Document:getProps(cached_doc_metadata)
local language = makeNilIfEmpty(props.language or props.Language)
local keywords = makeNilIfEmpty(props.keywords or props.Keywords)
local description = makeNilIfEmpty(props.description or props.Description or props.subject)
local identifiers = makeNilIfEmpty(props.identifiers)
return {
title = title,
authors = authors,
@ -209,6 +210,7 @@ function Document:getProps(cached_doc_metadata)
language = language,
keywords = keywords,
description = description,
identifiers = identifiers,
}
end
@ -373,6 +375,10 @@ function Document:findText()
return nil
end
function Document:findAllText()
return nil
end
function Document:updateColorRendering()
if self.is_color_capable and CanvasContext.is_color_rendering_enabled then
self.render_color = true

@ -86,18 +86,19 @@ end
--- Returns the preferred registered document handler or fallback provider.
-- @string file
-- @bool include_aux include auxiliary (non-document) providers
-- @treturn table provider
function DocumentRegistry:getProvider(file)
function DocumentRegistry:getProvider(file, include_aux)
local providers = self:getProviders(file)
if providers then
if providers or include_aux then
-- associated provider
local provider_key = DocumentRegistry:getAssociatedProviderKey(file)
local provider = provider_key and self.known_providers[provider_key]
if provider and not provider.order then -- excluding auxiliary
if provider and (not provider.order or include_aux) then -- excluding auxiliary by default
return provider
end
-- highest weighted provider
return providers[1].provider
return providers and providers[1].provider
end
return self:getFallbackProvider()
end

@ -12,6 +12,7 @@ local FFIUtil = require("ffi/util")
local Geom = require("ui/geometry")
local KOPTContext = require("ffi/koptcontext")
local Persist = require("persist")
local TextBoxWidget = require("ui/widget/textboxwidget")
local TileCacheItem = require("document/tilecacheitem")
local Utf8Proc = require("ffi/utf8proc")
local logger = require("logger")
@ -19,7 +20,10 @@ local util = require("util")
local KoptInterface = {
ocrengine = "ocrengine",
tessocr_data = DataStorage:getDataDir() .. "/data",
-- If `$TESSDATA_PREFIX` is set, don't override it: let libk2pdfopt honor it
-- (which includes checking for data in both `$TESSDATA_PREFIX/tessdata` and
-- in `$TESSDATA_PREFIX/` on more recent versions).
tessocr_data = not os.getenv('TESSDATA_PREFIX') and DataStorage:getDataDir().."/data/tessdata" or nil,
ocr_lang = "eng",
ocr_type = 3, -- default 0, for more accuracy use 3
last_context_size = nil,
@ -1063,6 +1067,10 @@ function KoptInterface:getWordFromReflowPosition(doc, boxes, pos)
local pageno = pos.page
local scratch_reflowed_page_boxes = self:getReflowedTextBoxesFromScratch(doc, pageno)
if not DEBUG.dassert(scratch_reflowed_page_boxes and next(scratch_reflowed_page_boxes) ~= nil, "scratch_reflowed_page_boxes shouldn't be nil/{}") then
return
end
local scratch_reflowed_word_box = self:getWordFromBoxes(scratch_reflowed_page_boxes, pos)
local reflowed_page_boxes = self:getReflowedTextBoxes(doc, pageno)
@ -1097,49 +1105,60 @@ function KoptInterface:getWordFromNativePosition(doc, boxes, pos)
return word_box
end
function KoptInterface:getSelectedWordContext(word, nb_words, pos)
local boxes = self.last_text_boxes
if not pos or not boxes or #boxes == 0 then return end
local i, j = getWordBoxIndices(boxes, pos)
if boxes[i][j].word ~= word then return end
local li, wi = i, j
local prev_count, next_count = 0, 0
local prev_text, next_text = {}, {}
local function get_prev_text(boxes, i, j, nb_words)
local prev_count = 0
local prev_text = {}
while prev_count < nb_words do
if li == 1 and wi == 1 then
if i == 1 and j == 1 then
break
elseif wi == 1 then
li = li - 1
wi = #boxes[li]
elseif j == 1 then
i = i - 1
j = #boxes[i]
else
wi = wi - 1
j = j - 1
end
local current_word = boxes[li][wi].word
local current_word = boxes[i][j].word
if #current_word > 0 then
table.insert(prev_text, 1, current_word)
prev_count = prev_count + 1
end
end
if #prev_text > 0 then
return table.concat(prev_text, " ")
end
end
li, wi = i, j
local function get_next_text(boxes, i, j, nb_words)
local next_count = 0
local next_text = {}
while next_count < nb_words do
if li == #boxes and wi == #boxes[li] then
if i == #boxes and j == #boxes[i] then
break
elseif wi == #boxes[li] then
li = li + 1
wi = 1
elseif j == #boxes[i] then
i = i + 1
j = 1
else
wi = wi + 1
j = j + 1
end
local current_word = boxes[li][wi].word
local current_word = boxes[i][j].word
if #current_word > 0 then
table.insert(next_text, current_word)
next_count = next_count + 1
end
end
if #prev_text == 0 and #next_text == 0 then return end
return table.concat(prev_text, " "), table.concat(next_text, " ")
if #next_text > 0 then
return table.concat(next_text, " ")
end
end
function KoptInterface:getSelectedWordContext(word, nb_words, pos)
local boxes = self.last_text_boxes
if not pos or not boxes or #boxes == 0 then return end
local i, j = getWordBoxIndices(boxes, pos)
if boxes[i][j].word ~= word then return end
local prev_text = get_prev_text(boxes, i, j, nb_words)
local next_text = get_next_text(boxes, i, j, nb_words)
return prev_text, next_text
end
--[[--
@ -1225,8 +1244,15 @@ function KoptInterface:getTextFromReflowPositions(doc, native_boxes, pos0, pos1)
local reflowed_page_boxes = self:getReflowedTextBoxes(doc, pageno)
local scratch_reflowed_word_box0 = self:getWordFromBoxes(scratch_reflowed_page_boxes, pos0)
if not DEBUG.dassert(scratch_reflowed_word_box0 and next(scratch_reflowed_word_box0) ~= nil, "scratch_reflowed_word_box0 shouldn't be nil/{}") then
return
end
local reflowed_word_box0 = self:getWordFromBoxes(reflowed_page_boxes, pos0)
local scratch_reflowed_word_box1 = self:getWordFromBoxes(scratch_reflowed_page_boxes, pos1)
if not DEBUG.dassert(scratch_reflowed_word_box1 and next(scratch_reflowed_word_box1) ~= nil, "scratch_reflowed_word_box1 shouldn't be nil/{}") then
return
end
local reflowed_word_box1 = self:getWordFromBoxes(reflowed_page_boxes, pos1)
local reflowed_pos_abs0 = scratch_reflowed_word_box0.box:center()
@ -1336,19 +1362,23 @@ function KoptInterface:nativeToPageRectTransform(doc, pageno, rect)
end
end
local function all_matches(boxes, pattern, caseInsensitive)
local function get_pattern_list(pattern, case_insensitive)
-- pattern list of single words
local plist = {}
-- (as in util.splitToWords(), but only splitting on spaces, keeping punctuations)
for word in util.gsplit(pattern, "%s+") do
if util.hasCJKChar(word) then
for char in util.gsplit(word, "[\192-\255][\128-\191]+", true) do
table.insert(plist, caseInsensitive and Utf8Proc.lowercase(util.fixUtf8(char, "?")) or char)
table.insert(plist, case_insensitive and Utf8Proc.lowercase(util.fixUtf8(char, "?")) or char)
end
else
table.insert(plist, caseInsensitive and Utf8Proc.lowercase(util.fixUtf8(word, "?")) or word)
table.insert(plist, case_insensitive and Utf8Proc.lowercase(util.fixUtf8(word, "?")) or word)
end
end
return plist
end
local function all_matches(boxes, plist, case_insensitive)
local pnb = #plist
-- return mached word indices from index i, j
local function match(i, j)
@ -1362,7 +1392,7 @@ local function all_matches(boxes, pattern, caseInsensitive)
end
if i > #boxes then break end
local box = boxes[i][j]
local word = caseInsensitive and Utf8Proc.lowercase(util.fixUtf8(box.word, "?")) or box.word
local word = case_insensitive and Utf8Proc.lowercase(util.fixUtf8(box.word, "?")) or box.word
local pword = plist[pindex]
local matched
if pnb == 1 then -- single word in plist
@ -1407,11 +1437,12 @@ local function all_matches(boxes, pattern, caseInsensitive)
end)
end
function KoptInterface:findAllMatches(doc, pattern, caseInsensitive, page)
function KoptInterface:findAllMatches(doc, pattern, case_insensitive, page)
local text_boxes = doc:getPageTextBoxes(page)
if not text_boxes then return end
local plist = get_pattern_list(pattern, case_insensitive)
local matches = {}
for indices in all_matches(text_boxes or {}, pattern, caseInsensitive) do
for indices in all_matches(text_boxes, plist, case_insensitive) do
for _, index in ipairs(indices) do
local i, j = unpack(index)
local word = text_boxes[i][j]
@ -1427,8 +1458,8 @@ function KoptInterface:findAllMatches(doc, pattern, caseInsensitive, page)
return matches
end
function KoptInterface:findText(doc, pattern, origin, reverse, caseInsensitive, pageno)
logger.dbg("Koptinterface: find text", pattern, origin, reverse, caseInsensitive, pageno)
function KoptInterface:findText(doc, pattern, origin, reverse, case_insensitive, pageno)
logger.dbg("Koptinterface: find text", pattern, origin, reverse, case_insensitive, pageno)
local last_pageno = doc:getPageCount()
local start_page, end_page
if reverse == 1 then
@ -1456,7 +1487,7 @@ function KoptInterface:findText(doc, pattern, origin, reverse, caseInsensitive,
end
end
for i = start_page, end_page, (reverse == 1) and -1 or 1 do
local matches = self:findAllMatches(doc, pattern, caseInsensitive, i)
local matches = self:findAllMatches(doc, pattern, case_insensitive, i)
if #matches > 0 then
matches.page = i
return matches
@ -1464,6 +1495,61 @@ function KoptInterface:findText(doc, pattern, origin, reverse, caseInsensitive,
end
end
function KoptInterface:findAllText(doc, pattern, case_insensitive, nb_context_words, max_hits)
local plist = get_pattern_list(pattern, case_insensitive)
local res = {}
for page = 1, doc:getPageCount() do
local text_boxes = doc:getPageTextBoxes(page)
if text_boxes then
for indices in all_matches(text_boxes, plist, case_insensitive) do -- each found pattern in the page
local res_item = { -- item of the Menu item_table
text = nil,
mandatory = page,
boxes = {}, -- to draw temp highlight in onMenuSelect
}
local text = {}
local i_prev, j_prev, i_next, j_next
for ind, index in ipairs(indices) do -- each word in the pattern
local i, j = unpack(index)
local word = text_boxes[i][j]
res_item.boxes[ind] = {
x = word.x0, y = word.y0,
w = word.x1 - word.x0,
h = word.y1 - word.y0,
}
text[ind] = word.word
if ind == 1 then
i_prev, j_prev = i, j
end
if ind == #indices then
i_next, j_next = i, j
end
end
-- Make this word bolder, using Poor Text Formatting provided by TextBoxWidget
-- (we know this text ends up in a TextBoxWidget).
text = TextBoxWidget.PTF_BOLD_START .. table.concat(text, " ") .. TextBoxWidget.PTF_BOLD_END
local prev_text = get_prev_text(text_boxes, i_prev, j_prev, nb_context_words)
if prev_text then
text = prev_text .. " " .. text
end
local next_text = get_next_text(text_boxes, i_next, j_next, nb_context_words)
if next_text then
text = text .. " " .. next_text
end
text = TextBoxWidget.PTF_HEADER .. text -- enable handling of our bold tags
res_item.text = text
table.insert(res, res_item)
if #res == max_hits then
return res
end
end
end
end
if #res > 0 then
return res
end
end
--[[--
Log reflow duration.
--]]

@ -211,18 +211,18 @@ local function _quadpointsFromPboxes(pboxes)
-- will also need mupdf_h.lua to be evaluated once
-- but this is guaranteed at this point
local n = #pboxes
local quadpoints = ffi.new("float[?]", 8*n)
local quadpoints = ffi.new("fz_quad[?]", n)
for i=1, n do
-- The order must be left bottom, right bottom, left top, right top.
-- https://bugs.ghostscript.com/show_bug.cgi?id=695130
quadpoints[8*i-8] = pboxes[i].x
quadpoints[8*i-7] = pboxes[i].y + pboxes[i].h
quadpoints[8*i-6] = pboxes[i].x + pboxes[i].w
quadpoints[8*i-5] = pboxes[i].y + pboxes[i].h
quadpoints[8*i-4] = pboxes[i].x
quadpoints[8*i-3] = pboxes[i].y
quadpoints[8*i-2] = pboxes[i].x + pboxes[i].w
quadpoints[8*i-1] = pboxes[i].y
quadpoints[i-1].ll.x = pboxes[i].x
quadpoints[i-1].ll.y = pboxes[i].y + pboxes[i].h - 1
quadpoints[i-1].lr.x = pboxes[i].x + pboxes[i].w - 1
quadpoints[i-1].lr.y = pboxes[i].y + pboxes[i].h - 1
quadpoints[i-1].ul.x = pboxes[i].x
quadpoints[i-1].ul.y = pboxes[i].y
quadpoints[i-1].ur.x = pboxes[i].x + pboxes[i].w - 1
quadpoints[i-1].ur.y = pboxes[i].y
end
return quadpoints, n
end
@ -232,10 +232,10 @@ local function _quadpointsToPboxes(quadpoints, n)
local pboxes = {}
for i=1, n do
table.insert(pboxes, {
x = quadpoints[8*i-4],
y = quadpoints[8*i-3],
w = quadpoints[8*i-6] - quadpoints[8*i-4],
h = quadpoints[8*i-5] - quadpoints[8*i-3],
x = quadpoints[i-1].ul.x,
y = quadpoints[i-1].ul.y,
w = quadpoints[i-1].lr.x - quadpoints[i-1].ul.x + 1,
h = quadpoints[i-1].lr.y - quadpoints[i-1].ul.y + 1,
})
end
return pboxes
@ -337,8 +337,12 @@ function PdfDocument:getCoverPageImage()
return self.koptinterface:getCoverPageImage(self)
end
function PdfDocument:findText(pattern, origin, reverse, caseInsensitive, page)
return self.koptinterface:findText(self, pattern, origin, reverse, caseInsensitive, page)
function PdfDocument:findText(pattern, origin, reverse, case_insensitive, page)
return self.koptinterface:findText(self, pattern, origin, reverse, case_insensitive, page)
end
function PdfDocument:findAllText(pattern, case_insensitive, nb_context_words, max_hits)
return self.koptinterface:findAllText(self, pattern, case_insensitive, nb_context_words, max_hits)
end
function PdfDocument:renderPage(pageno, rect, zoom, rotation, gamma, render_mode, hinting)
@ -358,16 +362,22 @@ function PdfDocument:register(registry)
registry:addProvider("cbt", "application/vnd.comicbook+tar", self, 100)
registry:addProvider("cbz", "application/vnd.comicbook+zip", self, 100)
registry:addProvider("cbz", "application/x-cbz", self, 100) -- Alternative mimetype for OPDS.
registry:addProvider("cfb", "application/octet-stream", self, 80) -- Compound File Binary, a Microsoft general-purpose file with a file-system-like structure.
registry:addProvider("docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", self, 80)
registry:addProvider("epub", "application/epub+zip", self, 50)
registry:addProvider("epub3", "application/epub+zip", self, 50)
registry:addProvider("fb2", "application/fb2", self, 80)
registry:addProvider("htm", "text/html", self, 90)
registry:addProvider("html", "text/html", self, 90)
registry:addProvider("mobi", "application/x-mobipocket-ebook", self, 80)
registry:addProvider("pdf", "application/pdf", self, 100)
registry:addProvider("pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation", self, 80)
registry:addProvider("tar", "application/x-tar", self, 10)
registry:addProvider("txt", "text/plain", self, 80)
registry:addProvider("xhtml", "application/xhtml+xml", self, 90)
registry:addProvider("xml", "application/xml", self, 10)
registry:addProvider("xps", "application/oxps", self, 100)
registry:addProvider("xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", self, 80)
registry:addProvider("zip", "application/zip", self, 20)
--- Picture types ---

@ -142,12 +142,10 @@ function FontList:_readList(dir, mark)
-- See if we're interested
if file:sub(1, 1) == "." then return end
local file_type = file:lower():match(".+%.([^.]+)") or ""
if not font_exts[file_type] then return end
if not font_exts[file_type] or isInFontsBlacklist(file) then return end
-- Add it to the list
if not isInFontsBlacklist(file) then
table.insert(self.fontlist, path)
end
table.insert(self.fontlist, path)
-- And into cached info table
mark[path] = true

@ -58,7 +58,7 @@ Returns a translation.
local translation = _("A meaningful message.")
--]]
function GetText_mt.__call(gettext, msgid)
return gettext.translation[msgid] or gettext.wrapUntranslated(msgid)
return gettext.translation[msgid] and gettext.translation[msgid][0] or gettext.translation[msgid] or gettext.wrapUntranslated(msgid)
end
local function c_escape(what_full, what)
@ -275,6 +275,8 @@ function GetText_mt.__index.changeLang(new_lang)
-- unescape \\ or msgid won't match
s = s:gsub("\\\\", "\\")
data[what] = (data[what] or "") .. s
elseif what and s == "" and fuzzy then -- luacheck: ignore 542
-- Ignore the likes of msgid "" and msgstr ""
else
-- Don't save this fuzzy string and unset fuzzy for the next one.
fuzzy = false

@ -8,11 +8,14 @@ local util = require("util")
local collection_file = DataStorage:getSettingsDir() .. "/collection.lua"
local ReadCollection = {
coll = {},
coll = nil, -- hash table
coll_order = nil, -- hash table
last_read_time = 0,
default_collection_name = "favorites",
}
-- read, write
local function buildEntry(file, order, mandatory)
file = FFIUtil.realpath(file)
if not file then return end
@ -41,6 +44,7 @@ function ReadCollection:_read()
end
logger.dbg("ReadCollection: reading from collection file")
self.coll = {}
self.coll_order = {}
for coll_name, collection in pairs(collections.data) do
local coll = {}
for _, v in ipairs(collection) do
@ -50,14 +54,23 @@ function ReadCollection:_read()
end
end
self.coll[coll_name] = coll
if not collection.settings then -- favorites, first run
collection.settings = { order = 1 }
end
self.coll_order[coll_name] = collection.settings.order
end
end
function ReadCollection:write(collection_name)
local collections = LuaSettings:open(collection_file)
for coll_name in pairs(collections.data) do
if not self.coll[coll_name] then
collections:delSetting(coll_name)
end
end
for coll_name, coll in pairs(self.coll) do
if not collection_name or coll_name == collection_name then
local data = {}
local data = { settings = { order = self.coll_order[coll_name] } }
for _, item in pairs(coll) do
table.insert(data, { file = item.file, order = item.order })
end
@ -68,20 +81,32 @@ function ReadCollection:write(collection_name)
collections:flush()
end
function ReadCollection:getFileCollectionName(file, collection_name)
-- info
function ReadCollection:isFileInCollection(file, collection_name)
file = FFIUtil.realpath(file) or file
for coll_name, coll in pairs(self.coll) do
if not collection_name or coll_name == collection_name then
if coll[file] then
return coll_name, file
end
return self.coll[collection_name][file] and true or false
end
function ReadCollection:isFileInCollections(file)
file = FFIUtil.realpath(file) or file
for _, coll in pairs(self.coll) do
if coll[file] then
return true
end
end
return false
end
function ReadCollection:hasFile(file, collection_name)
local coll_name = self:getFileCollectionName(file, collection_name)
return coll_name and true or false
function ReadCollection:getCollectionsWithFile(file)
file = FFIUtil.realpath(file) or file
local collections = {}
for coll_name, coll in pairs(self.coll) do
if coll[file] then
collections[coll_name] = true
end
end
return collections
end
function ReadCollection:getCollectionMaxOrder(collection_name)
@ -94,61 +119,74 @@ function ReadCollection:getCollectionMaxOrder(collection_name)
return max_order
end
function ReadCollection:getOrderedCollection(collection_name)
local ordered_coll = {}
for _, item in pairs(self.coll[collection_name]) do
table.insert(ordered_coll, item)
end
table.sort(ordered_coll, function(v1, v2) return v1.order < v2.order end)
return ordered_coll
end
function ReadCollection:updateCollectionOrder(collection_name, ordered_coll)
local coll = self.coll[collection_name]
for i, item in ipairs(ordered_coll) do
coll[item.file].order = i
end
self:write(collection_name)
end
-- manage items
function ReadCollection:addItem(file, collection_name)
collection_name = collection_name or self.default_collection_name
local max_order = self:getCollectionMaxOrder(collection_name)
local item = buildEntry(file, max_order + 1)
self.coll[collection_name][item.file] = item
self:write(collection_name)
end
function ReadCollection:addItems(files, collection_name) -- files = { filepath = true, }
collection_name = collection_name or self.default_collection_name
local coll = self.coll[collection_name]
local max_order = self:getCollectionMaxOrder(collection_name)
local do_write
for file in pairs(files) do
if not self:hasFile(file) then
max_order = max_order + 1
local item = buildEntry(file, max_order)
coll[item.file] = item
do_write = true
function ReadCollection:addRemoveItemMultiple(file, collections_to_add)
file = FFIUtil.realpath(file) or file
for coll_name, coll in pairs(self.coll) do
if collections_to_add[coll_name] then
if not coll[file] then
local max_order = self:getCollectionMaxOrder(coll_name)
coll[file] = buildEntry(file, max_order + 1)
end
else
if coll[file] then
coll[file] = nil
end
end
end
if do_write then
self:write(collection_name)
self:write()
end
function ReadCollection:addItemsMultiple(files, collections_to_add)
for file in pairs(files) do
file = FFIUtil.realpath(file) or file
for coll_name in pairs(collections_to_add) do
local coll = self.coll[coll_name]
if not coll[file] then
local max_order = self:getCollectionMaxOrder(coll_name)
coll[file] = buildEntry(file, max_order + 1)
end
end
end
self:write()
end
function ReadCollection:removeItem(file, collection_name, no_write)
local coll_name, file_name = self:getFileCollectionName(file, collection_name)
if coll_name then
self.coll[coll_name][file_name] = nil
if not no_write then
self:write(coll_name)
function ReadCollection:removeItem(file, collection_name, no_write) -- FM: delete file; FMColl: remove file
file = FFIUtil.realpath(file) or file
if collection_name then
if self.coll[collection_name][file] then
self.coll[collection_name][file] = nil
if not no_write then
self:write(collection_name)
end
return true
end
else
local do_write
for _, coll in pairs(self.coll) do
if coll[file] then
coll[file] = nil
do_write = true
end
end
if do_write then
if not no_write then
self:write()
end
return true
end
return true
end
end
function ReadCollection:removeItems(files) -- files = { filepath = true, }
function ReadCollection:removeItems(files) -- FM: delete files
local do_write
for file in pairs(files) do
if self:removeItem(file, nil, true) then
@ -160,12 +198,12 @@ function ReadCollection:removeItems(files) -- files = { filepath = true, }
end
end
function ReadCollection:removeItemsByPath(path)
function ReadCollection:removeItemsByPath(path) -- FM: delete folder
local do_write
for coll_name, coll in pairs(self.coll) do
for file_name in pairs(coll) do
if util.stringStartsWith(file_name, path) then
self.coll[coll_name][file_name] = nil
coll[file_name] = nil
do_write = true
end
end
@ -185,21 +223,29 @@ function ReadCollection:_updateItem(coll_name, file_name, new_filepath, new_path
coll[item.file] = item
end
function ReadCollection:updateItem(file, new_filepath)
local coll_name, file_name = self:getFileCollectionName(file)
if coll_name then
self:_updateItem(coll_name, file_name, new_filepath)
self:write(coll_name)
function ReadCollection:updateItem(file, new_filepath) -- FM: rename file, move file
file = FFIUtil.realpath(file) or file
local do_write
for coll_name, coll in pairs(self.coll) do
if coll[file] then
self:_updateItem(coll_name, file, new_filepath)
do_write = true
end
end
if do_write then
self:write()
end
end
function ReadCollection:updateItems(files, new_path) -- files = { filepath = true, }
function ReadCollection:updateItems(files, new_path) -- FM: move files
local do_write
for file in pairs(files) do
local coll_name, file_name = self:getFileCollectionName(file)
if coll_name then
self:_updateItem(coll_name, file_name, nil, new_path)
do_write = true
file = FFIUtil.realpath(file) or file
for coll_name, coll in pairs(self.coll) do
if coll[file] then
self:_updateItem(coll_name, file, nil, new_path)
do_write = true
end
end
end
if do_write then
@ -207,7 +253,7 @@ function ReadCollection:updateItems(files, new_path) -- files = { filepath = tru
end
end
function ReadCollection:updateItemsByPath(path, new_path)
function ReadCollection:updateItemsByPath(path, new_path) -- FM: rename folder, move folder
local len = #path
local do_write
for coll_name, coll in pairs(self.coll) do
@ -223,6 +269,58 @@ function ReadCollection:updateItemsByPath(path, new_path)
end
end
function ReadCollection:getOrderedCollection(collection_name)
local ordered_coll = {}
for _, item in pairs(self.coll[collection_name]) do
table.insert(ordered_coll, item)
end
table.sort(ordered_coll, function(v1, v2) return v1.order < v2.order end)
return ordered_coll
end
function ReadCollection:updateCollectionOrder(collection_name, ordered_coll)
local coll = self.coll[collection_name]
for i, item in ipairs(ordered_coll) do
coll[item.file].order = i
end
self:write(collection_name)
end
-- manage collections
function ReadCollection:addCollection(coll_name)
local max_order = 0
for _, order in pairs(self.coll_order) do
if max_order < order then
max_order = order
end
end
self.coll_order[coll_name] = max_order + 1
self.coll[coll_name] = {}
self:write(coll_name)
end
function ReadCollection:renameCollection(coll_name, new_name)
self.coll_order[new_name] = self.coll_order[coll_name]
self.coll[new_name] = self.coll[coll_name]
self.coll_order[coll_name] = nil
self.coll[coll_name] = nil
self:write(new_name)
end
function ReadCollection:removeCollection(coll_name)
self.coll_order[coll_name] = nil
self.coll[coll_name] = nil
self:write()
end
function ReadCollection:updateCollectionListOrder(ordered_coll)
for i, item in ipairs(ordered_coll) do
self.coll_order[item.name] = i
end
self:write()
end
ReadCollection:_read()
return ReadCollection

@ -3,7 +3,6 @@ local DocSettings = require("docsettings")
local datetime = require("datetime")
local dump = require("dump")
local ffiutil = require("ffi/util")
local filemanagerutil = require("apps/filemanager/filemanagerutil")
local util = require("util")
local joinPath = ffiutil.joinPath
local lfs = require("libs/libkoreader-lfs")
@ -323,36 +322,32 @@ end
--- Adds new item (last opened document) to the top of the history list.
-- If item time (ts) is passed, add item to the history list at this time position.
function ReadHistory:addItem(file, ts, no_flush)
if file ~= nil and lfs.attributes(file, "mode") == "file" then
local index = self:getIndexByFile(realpath(file))
if index then -- book is in the history already
if ts and self.hist[index].time == ts then
return -- legacy item already added
end
if not ts and G_reader_settings:isTrue("history_freeze_finished_books")
and filemanagerutil.getStatus(file) == "complete" then
return -- book marked as finished, do not update timestamps of item and file
end
end
local now = ts or os.time()
local mtime = lfs.attributes(file, "modification")
lfs.touch(file, now, mtime) -- update book access time for sorting by last read date
if index == 1 and not ts then -- last book, update access time only
self.hist[1].time = now
self.hist[1].mandatory = getMandatory(now)
else -- old or new book
if index then -- old book
table.remove(self.hist, index)
end
index = ts and self:getIndexByTime(ts, file:gsub(".*/", "")) or 1
table.insert(self.hist, index, buildEntry(now, file))
end
if not no_flush then
self:_reduce()
self:_flush()
file = realpath(file)
if not file or (ts and lfs.attributes(file, "mode") ~= "file") then
return -- bad legacy item
end
local index = self:getIndexByFile(file)
if ts and index and self.hist[index].time == ts then
return -- legacy item already added
end
local now = ts or os.time()
local mtime = lfs.attributes(file, "modification")
lfs.touch(file, now, mtime) -- update book access time for sorting by last read date
if index == 1 and not ts then -- last book, update access time only
self.hist[1].time = now
self.hist[1].mandatory = getMandatory(now)
else -- old or new book
if index then -- old book
table.remove(self.hist, index)
end
return true -- used while adding legacy items
index = ts and self:getIndexByTime(ts, file:gsub(".*/", "")) or 1
table.insert(self.hist, index, buildEntry(now, file))
end
if not no_flush then
self:_reduce()
self:_flush()
end
return true -- used while adding legacy items
end
--- Updates last book access time on closing the document.

@ -419,6 +419,13 @@ Further small adjustments can be done with 'Line Spacing' in the bottom menu.]])
},
{
title = _("Font size and families"),
{
id = "font_no_presentational_hints",
title = _("Ignore font related HTML presentational hints"),
description = _("Ignore HTML attributes that contribute to styles on the elements <body> (bgcolor, text…) and <font> (face, size, color…)."),
css = [[body, font { -cr-hint: no-presentational; }]],
separator = true,
},
{
id = "font_family_all_inherit",
title = _("Ignore publisher font families"),
@ -564,6 +571,13 @@ body, h1, h2, h3, h4, h5, h6, div, li, td, th { text-indent: 0 !important; }
title = _("Tables, links, images"),
{
title = _("Tables"),
{
id = "table_no_presentational_hints",
title = _("Ignore tables related HTML presentational hints"),
description = _("Ignore HTML attributes that contribute to styles on the <table> element and its sub-elements (ie. align, valign, frame, rules, border, cellpadding, cellspacing…)."),
css = [[table, caption, colgroup, col, thead, tbody, tfoot, tr, td, th { -cr-hint: no-presentational; }]],
separator = true,
},
{
id = "table_full_width",
title = _("Full-width tables"),
@ -965,6 +979,8 @@ This tweak can be duplicated as a user style tweak when books contain footnotes
.footnote, .footnotes, .fn,
.note, .note1, .note2, .note3,
.ntb, .ntb-txt, .ntb-txt-j,
.fnote, .fnote1,
.duokan-footnote-item, /* Common chinese books */
.przypis, .przypis1, /* Polish footnotes */
.voetnoten /* Dutch footnotes */
{
@ -1114,6 +1130,21 @@ If the footnote text uses variable or absolute font sizes, line height or vertic
font-size: inherit !important;
line-height: inherit !important;
vertical-align: inherit !important;
}
]],
},
{
id = "inpage_footnote_combine_non_linear",
title = _("Combine footnotes in a non-linear flow"),
description = _([[
This will mark each section of consecutive footnotes (at their original location in the book) as being non-linear.
The menu checkbox "Hide non-linear fragments" will then be available after the document is reopened, allowing to hide these sections from the regular flow: they will be skipped when turning pages and not considered in the various book & chapter progress and time to read features.]]),
priority = 6,
css = [[
*, autoBoxing {
-cr-hint: late;
-cr-only-if: inpage-footnote;
-cr-hint: non-linear-combining;
}
]],
},

@ -0,0 +1,57 @@
local cs_keyboard = require("util").tableDeepCopy(require("ui/data/keyboardlayouts/sk_keyboard"))
local keys = cs_keyboard.keys
keys[1][2][1] = {
"2",
north = "ě",
northeast = "Ě",
east = "~",
southeast = "/",
south = "@",
southwest = "https://",
west = "http://",
northwest = "Ĺ",
alt_label = "ě",
}
keys[1][2][2] = {
"ě",
north = "2",
northeast = "Ě",
east = "~",
southeast = "/",
south = "@",
southwest = "https://",
west = "http://",
northwest = "ĺ",
alt_label = "2",
}
keys[1][5][1] = {
"5",
north = "ř",
northeast = "Ř",
east = "¾",
southeast = "",
south = "%",
southwest = "",
west = "",
northwest = "Ŕ",
alt_label = "ř",
}
keys[1][5][2] = {
"ř",
north = "5",
northeast = "Ř",
east = "¼",
southeast = "",
south = "%",
southwest = "",
west = "½",
northwest = "ŕ",
alt_label = "5",
}
keys[5][4].label = "mezera"
return cs_keyboard

@ -0,0 +1,10 @@
-- Start with the norwegian keyboard layout (deep copy, to not alter it)
local da_keyboard = require("util").tableDeepCopy(require("ui/data/keyboardlayouts/no_keyboard"))
local keys = da_keyboard.keys
-- swap "Ø" and "Æ", and "ø" and "æ"
keys[3][10][1], keys[3][11][1] = keys[3][11][1], keys[3][10][1]
keys[3][10][2], keys[3][11][2] = keys[3][11][2], keys[3][10][2]
return da_keyboard

@ -360,4 +360,244 @@ return {
southeast = "ʒ", -- ezh, voiced palato-alveolar fricative IPA
southwest = "ż",
},
_1_ = {
"+",
north = "1",
northeast = "¡",
east = "-",
southeast = "?",
south = "!",
southwest = nil,
west = nil,
northwest = nil,
alt_label = "1",
},
_1p = {
"1",
north = "+",
northeast = "¡",
east = "-",
southeast = "?",
south = "!",
southwest = nil,
west = nil,
northwest = nil,
alt_label = "+",
},
_2_ = {
"ľ",
north = "2",
northeast = "Ľ",
east = "~",
southeast = "/",
south = "@",
southwest = "https://",
west = "http://",
northwest = "ĺ",
alt_label = "2",
},
_2p = {
"2",
north = "ľ",
northeast = "Ľ",
east = "~",
southeast = "/",
south = "@",
southwest = "https://",
west = "http://",
northwest = "Ĺ",
alt_label = "ľ",
},
_3_ = {
"š",
north = "3",
northeast = "Š",
east = "",
southeast = "",
south = "#",
southwest = "",
west = "",
northwest = "ó",
alt_label = "3",
},
_3p = {
"3",
north = "š",
northeast = "Š",
east = "",
southeast = "",
south = "#",
southwest = "",
west = "",
northwest = "Ó",
alt_label = "š",
},
_4_ = {
"č",
north = "4",
northeast = "Č",
east = "¥",
southeast = "¢",
south = "$",
southwest = "",
west = "",
northwest = "ú",
alt_label = "4",
},
_4p = {
"4",
north = "č",
northeast = "Č",
east = "¥",
southeast = "¢",
south = "$",
southwest = "",
west = "",
northwest = "Ú",
alt_label = "č",
},
_5_ = {
"ť",
north = "5",
northeast = "Ť",
east = "¼",
southeast = "",
south = "%",
southwest = "",
west = "½",
northwest = "ŕ",
alt_label = "5",
},
_5p = {
"5",
north = "ť",
northeast = "Ť",
east = "¾",
southeast = "",
south = "%",
southwest = "",
west = "",
northwest = "Ŕ",
alt_label = "ť",
},
_6_ = {
"ž",
north = "6",
northeast = "Ž",
east = { label = "◌̈", key = "̈", }, -- Combining Diaeresis (Umlaut)
southeast = { label = "◌̂", key = "̂", }, -- Combining Circumflex Accent
south = "^",
southwest = { label = "◌́", key = "́", }, -- Combining Acute Accent
west = { label = "◌̌", key = "̌", }, -- Combining Caron
northwest = "ď",
alt_label = "6",
},
_6p = {
"6",
north = "ž",
northeast = "Ž",
east = { label = "◌̈", key = "̈", }, -- Combining Diaeresis (Umlaut)
southeast = { label = "◌̂", key = "̂", }, -- Combining Circumflex Accent
south = "^",
southwest = { label = "◌́", key = "́", }, -- Combining Acute Accent
west = { label = "◌̌", key = "̌", }, -- Combining Caron
northwest = "Ď",
alt_label = "ž",
},
_7_ = {
"ý",
north = "7",
northeast = "Ý",
east = "«",
southeast = "§",
south = "&",
southwest = "¤",
west = "»",
northwest = "ň",
alt_label = "7",
},
_7p = {
"7",
north = "ý",
northeast = "Ý",
east = "«",
southeast = "§",
south = "&",
southwest = "¤",
west = "»",
northwest = "Ň",
alt_label = "ý",
},
_8_ = {
"á",
north = "8",
northeast = "Á",
east = "",
southeast = ">",
south = "*",
southwest = "<",
west = "=",
northwest = "ä",
alt_label = "8",
},
_8p = {
"8",
north = "á",
northeast = "Á",
east = "",
southeast = ">",
south = "*",
southwest = "<",
west = "=",
northwest = "Ä",
alt_label = "á",
},
_9_ = {
"í",
north = "9",
northeast = "Í",
east = "_",
southeast = "[",
south = "(",
southwest = "{",
west = "-",
northwest = "ô",
alt_label = "9",
},
_9p = {
"9",
north = "í",
northeast = "Í",
east = "_",
southeast = "[",
south = "(",
southwest = "{",
west = "-",
northwest = "Ô",
alt_label = "í",
},
_0_ = {
"é",
north = "0",
northeast = "É",
east = "",
southeast = "]",
south = ")",
southwest = "}",
west = "",
northwest = "",
alt_label = "0",
},
_0p = {
"0",
north = "é",
northeast = "É",
east = "",
southeast = "]",
south = ")",
southwest = "}",
west = "",
northwest = "",
alt_label = "é",
}
}

@ -0,0 +1,81 @@
-- Start with the english keyboard layout (deep copy, to not alter it)
local no_keyboard = require("util").tableDeepCopy(require("ui/data/keyboardlayouts/en_keyboard"))
local keys = no_keyboard.keys
-- add ' key next to numeric 0
table.insert(
keys[1],
{
{ "*", alt_label = "'", north = "'", },
{ "'", alt_label = "*", north = "*", },
{ "/", alt_label = "÷", north = "÷", },
{ "÷", alt_label = "/", north = "/", },
}
)
-- add Å key
table.insert(
keys[2],
{
{ "Å", north = "å", },
{ "å", north = "Å", },
{ "*", alt_label = "×", north = "×", },
{ "×", alt_label = "*", north = "*", },
}
)
-- add Æ key
table.insert(
keys[3],
{
{ "Æ", north = "æ", },
{ "æ", north = "Æ", },
{ "", north = "", west = "", south = "", },
{
"",
north = "",
northeast = "",
northwest = "",
east = "",
west = "",
south = "",
southeast = "",
southwest = "",
},
}
)
-- add Ø key
table.insert(
keys[4],
7,
{
{ "Ø", north = "ø", },
{ "ø", north = "Ø", },
{ "", north = "", west = "", south = "", },
{ "", alt_label = "", north = "", northwest = "", west = "", },
}
)
-- swap "Ø" and ";" / "ø" and ","
keys[4][7][1], keys[3][10][1] = keys[3][10][1], keys[4][7][1]
keys[4][7][2], keys[3][10][2] = keys[3][10][2], keys[4][7][2]
-- swap ";" and "✓" / "," and "•"
keys[4][7][3], keys[3][10][3] = keys[3][10][3], keys[4][7][3]
keys[4][7][4], keys[3][10][4] = keys[3][10][4], keys[4][7][4]
-- change order ", n m" to "n m ,"
keys[4][7][1], keys[4][8][1], keys[4][9][1] = keys[4][8][1], keys[4][9][1], keys[4][7][1]
keys[4][7][2], keys[4][8][2], keys[4][9][2] = keys[4][8][2], keys[4][9][2], keys[4][7][2]
-- Rename "space" and resize buttons
keys[5][4].label = "" -- label the Spacebar with Unicode space symbol
keys[4][1].width = 1.5 -- resize Shift
keys[4][10].width = 1.5 -- resize Backspace
keys[5][4].width = 4 -- resize Spacebar
keys[5][1].width = 1.5 -- resize Symbols
keys[5][7].width = 1.5 -- resize Enter
return no_keyboard

@ -60,44 +60,44 @@ local _y_ = sk_popup._y_
local _Z_ = sk_popup._Z_
local _z_ = sk_popup._z_
-- other
local _1_ = en_popup._1_ -- numeric key 1
local _1p = en_popup._1p -- numeric key 1, popup sibling (they have north swipe ups of each other, the rest is the same)
local _1_ = sk_popup._1_ -- numeric key 1
local _1p = sk_popup._1p -- numeric key 1, popup sibling (they have north swipe ups of each other, the rest is the same)
local _1n = en_popup._1n -- numpad key 1
local _1s = en_popup._1s -- superscript key 1
local _2_ = en_popup._2_
local _2p = en_popup._2p
local _2_ = sk_popup._2_
local _2p = sk_popup._2p
local _2n = en_popup._2n
local _2s = en_popup._2s
local _3_ = en_popup._3_
local _3p = en_popup._3p
local _3_ = sk_popup._3_
local _3p = sk_popup._3p
local _3n = en_popup._3n
local _3s = en_popup._3s
local _4_ = en_popup._4_
local _4p = en_popup._4p
local _4_ = sk_popup._4_
local _4p = sk_popup._4p
local _4n = en_popup._4n
local _4s = en_popup._4s
local _5_ = en_popup._5_
local _5p = en_popup._5p
local _5_ = sk_popup._5_
local _5p = sk_popup._5p
local _5n = en_popup._5n
local _5s = en_popup._5s
local _6_ = en_popup._6_
local _6p = en_popup._6p
local _6_ = sk_popup._6_
local _6p = sk_popup._6p
local _6n = en_popup._6n
local _6s = en_popup._6s
local _7_ = en_popup._7_
local _7p = en_popup._7p
local _7_ = sk_popup._7_
local _7p = sk_popup._7p
local _7n = en_popup._7n
local _7s = en_popup._7s
local _8_ = en_popup._8_
local _8p = en_popup._8p
local _8_ = sk_popup._8_
local _8p = sk_popup._8p
local _8n = en_popup._8n
local _8s = en_popup._8s
local _9_ = en_popup._9_
local _9p = en_popup._9p
local _9_ = sk_popup._9_
local _9p = sk_popup._9p
local _9n = en_popup._9n
local _9s = en_popup._9s
local _0_ = en_popup._0_
local _0p = en_popup._0p
local _0_ = sk_popup._0_
local _0p = sk_popup._0p
local _0n = en_popup._0n
local _0s = en_popup._0s
local sla = en_popup.sla

@ -0,0 +1,14 @@
-- Start with the norwegian keyboard layout (deep copy, to not alter it)
local sv_keyboard = require("util").tableDeepCopy(require("ui/data/keyboardlayouts/no_keyboard"))
local keys = sv_keyboard.keys
-- replace "Ø" and "ø" with "Ö" and "ö"
keys[3][10][1] = { "Ö", north = "ö", }
keys[3][10][2] = { "ö", north = "Ö", }
-- replace "Æ" and "æ" with "Ä" and "ä"
keys[3][11][1] = { "Ä", north = "ä", }
keys[3][11][2] = { "ä", north = "Ä", }
return sv_keyboard

File diff suppressed because one or more lines are too long

@ -10,7 +10,7 @@ local util = require("util")
local _ = require("gettext")
-- Date at which the last migration snippet was added
local CURRENT_MIGRATION_DATE = 20231217
local CURRENT_MIGRATION_DATE = 20240408
-- Retrieve the date of the previous migration, if any
local last_migration_date = G_reader_settings:readSetting("last_migration_date", 0)
@ -647,5 +647,16 @@ if last_migration_date < 20231217 then
end
end
-- 20240408, drop sleep screen/screensaver image_file setting in favor of document cover
if last_migration_date < 20240408 then
logger.info("Performing one-time migration for 20240408")
local image_file = G_reader_settings:readSetting("screensaver_type") == "image_file" and G_reader_settings:readSetting("screensaver_image")
if image_file then
G_reader_settings:saveSetting("screensaver_type", "document_cover")
G_reader_settings:saveSetting("screensaver_document_cover", image_file)
end
end
-- We're done, store the current migration date
G_reader_settings:saveSetting("last_migration_date", CURRENT_MIGRATION_DATE)

@ -1,37 +1,32 @@
local BD = require("ui/bidi")
local ConfirmBox = require("ui/widget/confirmbox")
local Device = require("device")
local Event = require("ui/event")
local InfoMessage = require("ui/widget/infomessage")
local Notification = require("ui/widget/notification")
local UIManager = require("ui/uimanager")
local Version = require("version")
local dbg = require("dbg")
local lfs = require("libs/libkoreader-lfs")
local _ = require("gettext")
local T = require("ffi/util").template
local common_info = {}
-- tools tab
common_info.more_tools = {
text = _("More tools"),
}
-- main tab
if Device:hasOTAUpdates() then
local OTAManager = require("ui/otamanager")
common_info.ota_update = OTAManager:getOTAMenuTable()
end
common_info.version = {
text = T(_("Version: %1"), Version:getShortVersion()),
keep_menu_open = true,
callback = function()
UIManager:show(InfoMessage:new{
text = Version:getCurrentRevision(),
})
end
}
common_info.help = {
text = _("Help"),
}
common_info.more_tools = {
text = _("More tools"),
}
common_info.device = {
text = _("Device"),
}
common_info.quickstart_guide = {
text = _("Quickstart guide"),
callback = function()
@ -40,25 +35,28 @@ common_info.quickstart_guide = {
ReaderUI:showReader(QuickStart:getQuickStart())
end
}
common_info.about = {
text = _("About"),
keep_menu_open = true,
common_info.search_menu = {
text = _("Menu search"),
callback = function()
UIManager:show(InfoMessage:new{
text = T(_("KOReader %1\n\nA document viewer for E Ink devices.\n\nLicensed under Affero GPL v3. All dependencies are free software.\n\nhttp://koreader.rocks"), BD.ltr(Version:getCurrentRevision())),
icon = "koreader",
})
end
UIManager:sendEvent(Event:new("ShowMenuSearch"))
end,
keep_menu_open = true,
}
common_info.report_bug = {
text = _("Report a bug"),
text_func = function()
local label = _("Report a bug")
if G_reader_settings:isTrue("debug_verbose") then
label = label .. " " .. _("(verbose logging is enabled)")
end
return label
end,
keep_menu_open = true,
callback_func = function()
callback = function(touchmenu_instance)
local DataStorage = require("datastorage")
local log_path = string.format("%s/%s", DataStorage:getDataDir(), "crash.log")
local common_msg = T(_("Please report bugs to \nhttps://github.com/koreader/koreader/issues\n\nVersion:\n%1\n\nDetected device:\n%2"),
Version:getCurrentRevision(), Device:info())
local log_msg = T(_("Attach %1 to your bug report."), log_path)
local log_msg = T(_("Verbose logs will make our investigations easier. If possible, try to reproduce the issue while it's enabled, and attach %1 to your bug report."), log_path)
if Device:isAndroid() then
local android = require("android")
@ -71,8 +69,56 @@ common_info.report_bug = {
else
msg = common_msg
end
UIManager:show(InfoMessage:new{
UIManager:show(ConfirmBox:new{
text = msg,
icon = "notice-info",
no_ok_button = true,
other_buttons_first = true,
other_buttons = {{
{
text = G_reader_settings:isTrue("debug_verbose") and _("Disable verbose logging") or _("Enable verbose logging"),
callback = function()
-- Flip verbose logging on dismissal
-- Unlike in the dev options, we flip everything at once.
if G_reader_settings:isTrue("debug_verbose") then
dbg:setVerbose(false)
dbg:turnOff()
G_reader_settings:makeFalse("debug_verbose")
G_reader_settings:makeFalse("debug")
Notification:notify(_("Verbose logging disabled"), Notification.SOURCE_ALWAYS_SHOW)
else
dbg:turnOn()
dbg:setVerbose(true)
G_reader_settings:makeTrue("debug")
G_reader_settings:makeTrue("debug_verbose")
Notification:notify(_("Verbose logging enabled"), Notification.SOURCE_ALWAYS_SHOW)
end
touchmenu_instance:updateItems()
-- Also unlike the dev options, explicitly ask for a restart,
-- to make sure framebuffer pulls in a logger.dbg ref that doesn't point to noop on init ;).
UIManager:askForRestart()
end,
}
}},
})
end
}
common_info.version = {
text = T(_("Version: %1"), Version:getShortVersion()),
keep_menu_open = true,
callback = function()
UIManager:show(InfoMessage:new{
text = Version:getCurrentRevision(),
})
end
}
common_info.about = {
text = _("About"),
keep_menu_open = true,
callback = function()
UIManager:show(InfoMessage:new{
text = T(_("KOReader %1\n\nA document viewer for E Ink devices.\n\nLicensed under Affero GPL v3. All dependencies are free software.\n\nhttp://koreader.rocks"), BD.ltr(Version:getCurrentRevision())),
icon = "koreader",
})
end
}

@ -271,8 +271,9 @@ common_settings.screen_eink_opt = require("ui/elements/screen_eink_opt_menu_tabl
common_settings.screen_notification = require("ui/elements/screen_notification_menu_table")
if Device:isTouchDevice() then
common_settings.menu_activate = require("ui/elements/menu_activate")
common_settings.screen_disable_double_tab = require("ui/elements/screen_disable_double_tap_table")
common_settings.taps_and_gestures = {
text = _("Taps and gestures"),
}
common_settings.ignore_hold_corners = {
text = _("Ignore long-press on corners"),
checked_func = function()
@ -282,6 +283,8 @@ if Device:isTouchDevice() then
UIManager:broadcastEvent(Event:new("IgnoreHoldCorners"))
end,
}
common_settings.screen_disable_double_tab = require("ui/elements/screen_disable_double_tap_table")
common_settings.menu_activate = require("ui/elements/menu_activate")
end
-- NOTE: Allow disabling color if it's mistakenly enabled on a Grayscale screen (after a settings import?)
@ -368,16 +371,6 @@ Please don't change any settings unless you know what you're doing.]])
end
end
if Device:isTouchDevice() then
common_settings.keyboard_layout = {
text = _("Keyboard"),
sub_item_table = require("ui/elements/menu_keyboard_layout"),
}
common_settings.taps_and_gestures = {
text = _("Taps and gestures"),
}
end
common_settings.navigation = {
text = _("Navigation"),
}
@ -398,7 +391,6 @@ local function genGenericMenuEntry(title, setting, value, default, radiomark)
end,
}
end
common_settings.back_to_exit = {
text_func = function()
local back_to_exit = G_reader_settings:readSetting("back_to_exit", "prompt") -- set "back_to_exit" to "prompt"
@ -715,17 +707,17 @@ common_settings.document_end_action = {
common_settings.language = Language:getLangMenuTable()
common_settings.font_ui_fallbacks = require("ui/elements/font_ui_fallbacks")
common_settings.device = {
text = _("Device"),
}
common_settings.screenshot = {
text = _("Screenshot folder"),
callback = function()
local Screenshoter = require("ui/widget/screenshoter")
Screenshoter:chooseFolder()
end,
keep_menu_open = true,
common_settings.keyboard_layout = {
text = _("Keyboard"),
sub_item_table = require("ui/elements/menu_keyboard_layout"),
}
common_settings.font_ui_fallbacks = require("ui/elements/font_ui_fallbacks")
common_settings.units = {
text = _("Units"),
sub_item_table = {
@ -743,10 +735,11 @@ common_settings.units = {
},
}
common_settings.search_menu = {
text = _("Menu search"),
common_settings.screenshot = {
text = _("Screenshot folder"),
callback = function()
UIManager:sendEvent(Event:new("ShowMenuSearch"))
local Screenshoter = require("ui/widget/screenshoter")
Screenshoter:chooseFolder()
end,
keep_menu_open = true,
}

@ -168,6 +168,7 @@ local order = {
"history",
"open_last_document",
"----------------------------",
"favorites",
"collections",
"----------------------------",
"mass_storage_actions", -- if Device:canToggleMassStorage()

@ -50,13 +50,13 @@ local genFallbackCandidates = function()
end
end
local more_info_text = _([[
If some book titles, dictionary entries and such are not displayed well but shown as or <EFBFBD><EFBFBD>, it may be necessary to download the required fonts for those languages. They can then be enabled as additional UI fallback fonts.
local more_info_text = T(_([[
If some book titles, dictionary entries and such are not displayed well but shown as %1 or %2, it may be necessary to download the required fonts for those languages. They can then be enabled as additional UI fallback fonts.
Fonts for many languages can be downloaded at:
https://fonts.google.com/noto
Only fonts named "Noto Sans xyz" or "Noto Sans xyz UI" (regular, not bold nor italic, not Serif) will be available in this menu. However, bold fonts will be used if their corresponding regular fonts exist.]])
Only fonts named "Noto Sans xyz" or "Noto Sans xyz UI" (regular, not bold nor italic, not Serif) will be available in this menu. However, bold fonts will be used if their corresponding regular fonts exist.]]), "￾￾", "<EFBFBD><EFBFBD>")
local getSubMenuItems = function()
genFallbackCandidates()

@ -160,15 +160,6 @@ local sub_item_table = {
end,
separator = true,
},
{
text = _("Swipe to input additional characters"),
checked_func = function()
return G_reader_settings:nilOrTrue("keyboard_swipes_enabled")
end,
callback = function()
G_reader_settings:flipNilOrTrue("keyboard_swipes_enabled")
end,
},
{
text = _("Keyboard appearance settings"),
keep_menu_open = true,
@ -233,5 +224,16 @@ local sub_item_table = {
end,
},
}
if Device:isTouchDevice() then
table.insert(sub_item_table, 4, {
text = _("Swipe to input additional characters"),
checked_func = function()
return G_reader_settings:nilOrTrue("keyboard_swipes_enabled")
end,
callback = function()
G_reader_settings:flipNilOrTrue("keyboard_swipes_enabled")
end,
})
end
return sub_item_table

@ -71,15 +71,17 @@ You can set how many lines are shown.]]),
})
local page_overlap_styles = {
arrow = _("Arrow"),
dim = _("Gray out"),
line = _("Horizontal line"),
{_("Arrow"), "arrow"},
{_("Gray out"), "dim"},
{_("Solid line"), "line"},
{_("Dashed line"), "dashed_line"},
}
for k, v in FFIUtil.orderedPairs(page_overlap_styles) do
for _, v in ipairs(page_overlap_styles) do
local style_text, style = unpack(v)
table.insert(PageOverlap.sub_item_table, {
text_func = function()
local text = v
if G_reader_settings:readSetting("page_overlap_style") == k then
local text = style_text
if G_reader_settings:readSetting("page_overlap_style") == style then
text = text .. ""
end
return text
@ -88,14 +90,14 @@ for k, v in FFIUtil.orderedPairs(page_overlap_styles) do
return ReaderUI.instance.view:isOverlapAllowed() and ReaderUI.instance.view.page_overlap_enable
end,
checked_func = function()
return ReaderUI.instance.view.page_overlap_style == k
return ReaderUI.instance.view.page_overlap_style == style
end,
radio = true,
callback = function()
ReaderUI.instance.view.page_overlap_style = k
ReaderUI.instance.view.page_overlap_style = style
end,
hold_callback = function(touchmenu_instance)
G_reader_settings:saveSetting("page_overlap_style", k)
G_reader_settings:saveSetting("page_overlap_style", style)
touchmenu_instance:updateItems()
end,
})

@ -35,6 +35,7 @@ local order = {
"----------------------------",
"toc_items_per_page",
"toc_items_font_size",
"toc_items_show_chapter_length",
"toc_items_with_dots",
"----------------------------",
"toc_alt_toc",
@ -211,18 +212,22 @@ local order = {
"----------------------------",
"find_book_in_calibre_catalog",
"fulltext_search",
"fulltext_search_findall_results",
"bookmark_search",
},
search_settings = {
"dictionary_settings",
"wikipedia_settings",
"translation_settings",
"----------------------------",
"fulltext_search_settings",
},
filemanager = {},
main = {
"history",
"open_previous_document",
"----------------------------",
"favorites",
"collections",
"----------------------------",
"book_status",

@ -57,8 +57,7 @@ If you need to do so, you'll have to use the UI toggles.]]),
return G_reader_settings:isTrue("input_lock_gsensor")
end,
callback = function()
G_reader_settings:flipNilOrFalse("input_lock_gsensor")
Device:lockGSensor(G_reader_settings:isTrue("input_lock_gsensor"))
UIManager:broadcastEvent(Event:new("LockGSensor"))
end,
})
end

@ -26,64 +26,28 @@ local function genMenuItem(text, setting, value, enabled_func, separator)
separator = separator,
}
end
return {
genMenuItem(_("Use last book's cover as screensaver"), "screensaver_type", "cover", hasLastFile),
genMenuItem(_("Use book status as screensaver"), "screensaver_type", "bookstatus", hasLastFile),
genMenuItem(_("Use random image from folder as screensaver"), "screensaver_type", "random_image"),
genMenuItem(_("Use document cover as screensaver"), "screensaver_type", "document_cover"),
genMenuItem(_("Use image as screensaver"), "screensaver_type", "image_file"),
genMenuItem(_("Use reading progress as screensaver"), "screensaver_type", "readingprogress", isReaderProgressEnabled),
genMenuItem(_("Leave screen as-is"), "screensaver_type", "disable", nil, true),
-- separator
{
text = _("Add message to screensaver"),
checked_func = function()
return G_reader_settings:isTrue("screensaver_show_message")
end,
callback = function()
G_reader_settings:toggle("screensaver_show_message")
end,
separator = true,
},
-- separator
{
text = _("Settings"),
text = _("Wallpaper"),
sub_item_table = {
genMenuItem(_("Show book cover on sleep screen"), "screensaver_type", "cover", hasLastFile),
genMenuItem(_("Show custom image or cover on sleep screen"), "screensaver_type", "document_cover"),
genMenuItem(_("Show random image from folder on sleep screen"), "screensaver_type", "random_image"),
genMenuItem(_("Show reading progress on sleep screen"), "screensaver_type", "readingprogress", isReaderProgressEnabled),
genMenuItem(_("Show book status on sleep screen"), "screensaver_type", "bookstatus", hasLastFile),
genMenuItem(_("Leave screen as-is"), "screensaver_type", "disable", nil, true),
separator = true,
{
text = _("Screensaver folder"),
keep_menu_open = true,
callback = function()
Screensaver:chooseFolder()
end,
},
{
text = _("Screensaver image"),
keep_menu_open = true,
callback = function()
Screensaver:chooseFile()
text = _("Border fill"),
enabled_func = function()
return G_reader_settings:readSetting("screensaver_type") == "cover"
or G_reader_settings:readSetting("screensaver_type") == "document_cover"
or G_reader_settings:readSetting("screensaver_type") == "random_image"
end,
},
{
text = _("Document cover"),
keep_menu_open = true,
callback = function()
Screensaver:chooseFile(true)
end,
},
{
text = _("Screensaver message"),
keep_menu_open = true,
callback = function()
Screensaver:setMessage()
end,
},
{
text = _("Covers and images settings"),
sub_item_table = {
genMenuItem(_("Black background"), "screensaver_img_background", "black"),
genMenuItem(_("White background"), "screensaver_img_background", "white"),
genMenuItem(_("Leave background as-is"), "screensaver_img_background", "none", nil, true),
genMenuItem(_("Black fill"), "screensaver_img_background", "black"),
genMenuItem(_("White fill"), "screensaver_img_background", "white"),
genMenuItem(_("No fill"), "screensaver_img_background", "none", nil, true),
-- separator
{
text_func = function()
@ -91,7 +55,7 @@ return {
if G_reader_settings:isTrue("screensaver_stretch_images") and percentage then
return T(_("Stretch to fit screen (with limit: %1 %)"), percentage)
end
return _("Stretch to fit screen")
return _("Stretch cover to fit screen")
end,
checked_func = function()
return G_reader_settings:isTrue("screensaver_stretch_images")
@ -103,38 +67,102 @@ return {
},
},
{
text = _("Message settings"),
text = _("Postpone screen update after wake-up"),
sub_item_table = {
genMenuItem(_("No delay"), "screensaver_delay", "disable"),
genMenuItem(_("1 second"), "screensaver_delay", "1"),
genMenuItem(_("3 seconds"), "screensaver_delay", "3"),
genMenuItem(_("5 seconds"), "screensaver_delay", "5"),
genMenuItem(_("Until a tap"), "screensaver_delay", "tap"),
genMenuItem(_("Until 'exit sleep screen' gesture"), "screensaver_delay", "gesture"),
},
},
{
text = _("Custom images"),
enabled_func = function()
return G_reader_settings:readSetting("screensaver_type") == "random_image"
or G_reader_settings:readSetting("screensaver_type") == "document_cover"
end,
sub_item_table = {
genMenuItem(_("Black background behind message"), "screensaver_msg_background", "black"),
genMenuItem(_("White background behind message"), "screensaver_msg_background", "white"),
genMenuItem(_("Leave background as-is behind message"), "screensaver_msg_background", "none", nil, true),
-- separator
genMenuItem(_("Message position: top"), "screensaver_message_position", "top"),
genMenuItem(_("Message position: middle"), "screensaver_message_position", "middle"),
genMenuItem(_("Message position: bottom"), "screensaver_message_position", "bottom", nil, true),
-- separator
{
text = _("Hide reboot/poweroff message"),
checked_func = function()
return G_reader_settings:isTrue("screensaver_hide_fallback_msg")
text = _("Select image or document cover"),
enabled_func = function()
return G_reader_settings:readSetting("screensaver_type") == "document_cover"
end,
keep_menu_open = true,
callback = function()
G_reader_settings:toggle("screensaver_hide_fallback_msg")
Screensaver:chooseFile()
end,
},
{
text = _("Select random image folder"),
enabled_func = function()
return G_reader_settings:readSetting("screensaver_type") == "random_image"
end,
keep_menu_open = true,
callback = function()
Screensaver:chooseFolder()
end,
},
},
},
},
},
{
text = _("Sleep screen message"),
sub_item_table = {
{
text = _("Add custom message to sleep screen"),
checked_func = function()
return G_reader_settings:isTrue("screensaver_show_message")
end,
callback = function()
G_reader_settings:toggle("screensaver_show_message")
end,
separator = true,
},
{
text = _("Edit sleep screen message"),
enabled_func = function()
return G_reader_settings:isTrue("screensaver_show_message")
end,
keep_menu_open = true,
callback = function()
Screensaver:setMessage()
end,
},
{
text = _("Background fill"),
help_text = _("This option will only become available, if you have selected 'Leave screen as-is' as wallpaper and have 'Sleep screen message' on."),
enabled_func = function()
return G_reader_settings:readSetting("screensaver_type") == "disable" and G_reader_settings:isTrue("screensaver_show_message")
end,
sub_item_table = {
genMenuItem(_("Black fill"), "screensaver_msg_background", "black"),
genMenuItem(_("White fill"), "screensaver_msg_background", "white"),
genMenuItem(_("No fill"), "screensaver_msg_background", "none", nil, true),
},
},
{
text = _("Keep the screensaver on screen after wakeup"),
text = _("Message position"),
enabled_func = function()
return G_reader_settings:isTrue("screensaver_show_message")
end,
sub_item_table = {
genMenuItem(_("Disable"), "screensaver_delay", "disable"),
genMenuItem(_("For 1 second"), "screensaver_delay", "1"),
genMenuItem(_("For 3 second"), "screensaver_delay", "3"),
genMenuItem(_("For 5 second"), "screensaver_delay", "5"),
genMenuItem(_("Until a tap"), "screensaver_delay", "tap"),
genMenuItem(_("Until 'Exit screensaver' gesture"), "screensaver_delay", "gesture"),
genMenuItem(_("Top"), "screensaver_message_position", "top"),
genMenuItem(_("Middle"), "screensaver_message_position", "middle"),
genMenuItem(_("Bottom"), "screensaver_message_position", "bottom", nil, true),
},
},
{
text = _("Hide reboot/poweroff message"),
checked_func = function()
return G_reader_settings:isTrue("screensaver_hide_fallback_msg")
end,
callback = function()
G_reader_settings:toggle("screensaver_hide_fallback_msg")
end,
},
},
},
}

@ -35,7 +35,12 @@ local Geom = {
}
function Geom:new(o)
if not o then o = {} end
if not o then
o = {
x = 0, y = 0,
w = 0, h = 0,
}
end
setmetatable(o, self)
self.__index = self
return o

@ -6,8 +6,10 @@ local Language = {
language_names = {
C = "English",
en = "English",
en_GB = "English (United Kingdom)",
ca = "Catalá",
cs = "Čeština",
da = "Dansk",
de = "Deutsch",
eo = "Esperanto",
es = "Español",
@ -22,7 +24,7 @@ local Language = {
lt_LT = "Lietuvių",
lv = "Latviešu",
nl_NL = "Nederlands",
nb_NO = "Norsk",
nb_NO = "Norsk bokmål",
pl = "Polski",
pl_PL = "Polski2",
pt_PT = "Português",
@ -117,6 +119,7 @@ function Language:getLangMenuTable()
-- NOTE: language with no translation are commented out for now
sub_item_table = {
self:genLanguageSubItem("C"),
self:genLanguageSubItem("en_GB"),
self:genLanguageSubItem("ca"),
self:genLanguageSubItem("cs"),
self:genLanguageSubItem("de"),
@ -137,7 +140,7 @@ function Language:getLangMenuTable()
--self:genLanguageSubItem("pl_PL"),
self:genLanguageSubItem("pt_PT"),
self:genLanguageSubItem("pt_BR"),
--self:genLanguageSubItem("ro"),
self:genLanguageSubItem("ro"),
self:genLanguageSubItem("ro_MD"),
self:genLanguageSubItem("sk"),
self:genLanguageSubItem("sv"),

@ -0,0 +1,68 @@
local socket = require("socket")
local logger = require("logger")
-- Reference:
-- https://lunarmodules.github.io/luasocket/tcp.html
-- Drop-in alternative to streammessagequeueserver.lua, using
-- LuaSocket instead of ZeroMQ.
-- This SimpleTCPServer is still tied to HTTP, expecting lines of headers,
-- a blank like marking the end of the input request.
local SimpleTCPServer = {
host = nil,
port = nil,
}
function SimpleTCPServer:new(o)
o = o or {}
setmetatable(o, self)
self.__index = self
if o.init then o:init() end
return o
end
function SimpleTCPServer:start()
self.server = socket.bind(self.host, self.port)
self.server:settimeout(0.01) -- set timeout (10ms)
logger.dbg("SimpleTCPServer: Server listening on port " .. self.port)
end
function SimpleTCPServer:stop()
self.server:close()
end
function SimpleTCPServer:waitEvent()
local client = self.server:accept() -- wait for a client to connect
if client then
-- We expect to get all headers in 100ms. We will block during this timeframe.
client:settimeout(0.1, "t")
local lines = {}
while true do
local data = client:receive("*l") -- read a line from input
if not data then -- timeout
client:close()
break
end
if data == "" then -- proper empty line after request headers
table.insert(lines, data) -- keep it in content
data = table.concat(lines, "\r\n")
logger.dbg("SimpleTCPServer: Received data: ", data)
-- Give us more time to process the request and send the response
client:settimeout(0.5, "t")
self.receiveCallback(data, client)
-- This should call SimpleTCPServer:send() to send
-- the response and close this connection.
else
table.insert(lines, data)
end
end
end
end
function SimpleTCPServer:send(data, client)
client:send(data) -- send the response back to the client
client:close() -- close the connection to the client
end
return SimpleTCPServer

@ -0,0 +1,86 @@
local ffi = require("ffi")
local logger = require("logger")
local MessageQueue = require("ui/message/messagequeue")
local _ = require("ffi/zeromq_h")
local czmq = ffi.load("libs/libczmq.so.1")
local C = ffi.C
local StreamMessageQueueServer = MessageQueue:extend{
host = nil,
port = nil,
}
function StreamMessageQueueServer:start()
self.context = czmq.zctx_new()
self.socket = czmq.zsocket_new(self.context, C.ZMQ_STREAM)
self.poller = czmq.zpoller_new(self.socket, nil)
local endpoint = string.format("tcp://%s:%d", self.host, self.port)
logger.dbg("StreamMessageQueueServer: Binding to endpoint", endpoint)
local rc = czmq.zsocket_bind(self.socket, endpoint)
-- If success, rc is port number
if rc == -1 then
logger.err("StreamMessageQueueServer: Cannot bind to ", endpoint)
end
end
function StreamMessageQueueServer:stop()
if self.poller ~= nil then
czmq.zpoller_destroy(ffi.new('zpoller_t *[1]', self.poller))
end
if self.socket ~= nil then
czmq.zsocket_destroy(self.context, self.socket)
end
if self.context ~= nil then
czmq.zctx_destroy(ffi.new('zctx_t *[1]', self.context))
end
end
function StreamMessageQueueServer:handleZframe(frame)
local size = czmq.zframe_size(frame)
local data = nil
if size > 0 then
local frame_data = czmq.zframe_data(frame)
if frame_data ~= nil then
data = ffi.string(frame_data, size)
end
end
czmq.zframe_destroy(ffi.new('zframe_t *[1]', frame))
return data
end
function StreamMessageQueueServer:waitEvent()
local request, id
while czmq.zpoller_wait(self.poller, 0) ~= nil do
-- See about ZMQ_STREAM and these 2 frames at http://hintjens.com/blog:42
local id_frame = czmq.zframe_recv(self.socket)
if id_frame ~= nil then
id = id_frame
end
local frame = czmq.zframe_recv(self.socket)
if frame ~= nil then
local data = self:handleZframe(frame)
if data then
logger.dbg("StreamMessageQueueServer: Received data: ", data)
request = data
end
end
end
if self.receiveCallback and request ~= nil then
self.receiveCallback(request, id)
end
end
function StreamMessageQueueServer:send(data, id_frame)
czmq.zframe_send(ffi.new('zframe_t *[1]', id_frame), self.socket, C.ZFRAME_MORE + C.ZFRAME_REUSE)
czmq.zmq_send(self.socket, ffi.cast("unsigned char*", data), #data, C.ZFRAME_MORE)
-- Note: We can't use czmq.zstr_send(self.socket, data), which would stop on the first
-- null byte in data (Lua strings can have null bytes inside).
-- Close connection
czmq.zframe_send(ffi.new('zframe_t *[1]', id_frame), self.socket, C.ZFRAME_MORE)
czmq.zmq_send(self.socket, nil, 0, 0)
end
return StreamMessageQueueServer

@ -274,6 +274,48 @@ function NetworkMgr:ifHasAnAddress()
return ok
end
-- The socket API equivalent of "ip route get 203.0.113.1 || ip route get 2001:db8::1".
--
-- These addresses are from special ranges reserved for documentation
-- (RFC 5737, RFC 3849) and therefore likely to just use the default route.
function NetworkMgr:hasDefaultRoute()
local socket = require("socket")
local s, ret, err
s, err = socket.udp()
if s == nil then
logger.err("NetworkMgr: socket.udp:", err)
return nil
end
ret, err = s:setpeername("203.0.113.1", "53")
if ret == nil then
-- Most likely "Network is unreachable", meaning there's no route to that address.
logger.dbg("NetworkMgr: socket.udp.setpeername:", err)
-- Try IPv6, may still succeed if this is an IPv6-only network.
ret, err = s:setpeername("2001:db8::1", "53")
if ret == nil then
-- Most likely "Network is unreachable", meaning there's no route to that address.
logger.dbg("NetworkMgr: socket.udp.setpeername:", err)
end
end
s:close()
-- If setpeername succeeded, we have a default route.
return ret ~= nil
end
function NetworkMgr:canResolveHostnames()
local socket = require("socket")
-- Microsoft uses `dns.msftncsi.com` for Windows, see
-- <https://technet.microsoft.com/en-us/library/ee126135#BKMK_How> for
-- more information. They also check whether <http://www.msftncsi.com/ncsi.txt>
-- returns `Microsoft NCSI`.
return socket.dns.toip("dns.msftncsi.com") ~= nil
end
-- Wrappers around turnOnWifi & turnOffWifi with proper Event signaling
function NetworkMgr:enableWifi(wifi_cb, connectivity_cb, connectivity_widget, interactive)
local status = self:requestToTurnOnWifi(wifi_cb, interactive)
@ -532,12 +574,7 @@ function NetworkMgr:isOnline()
return true
end
local socket = require("socket")
-- Microsoft uses `dns.msftncsi.com` for Windows, see
-- <https://technet.microsoft.com/en-us/library/ee126135#BKMK_How> for
-- more information. They also check whether <http://www.msftncsi.com/ncsi.txt>
-- returns `Microsoft NCSI`.
return socket.dns.toip("dns.msftncsi.com") ~= nil
return self:canResolveHostnames()
end
-- Update our cached network status

@ -16,6 +16,10 @@ local NetworkListener = EventListener:extend{
_activity_check_delay_seconds = nil,
}
if not Device:hasWifiToggle() then
return NetworkListener
end
local function enableWifi()
local toggle_im = InfoMessage:new{
text = _("Turning on Wi-Fi…"),
@ -173,11 +177,9 @@ end
function NetworkListener:onNetworkConnected()
logger.dbg("NetworkListener: onNetworkConnected")
if Device:hasWifiToggle() then
-- This is for the sake of events that don't emanate from NetworkMgr itself (e.g., the Emu)...
NetworkMgr:setWifiState(true)
NetworkMgr:setConnectionState(true)
end
-- This is for the sake of events that don't emanate from NetworkMgr itself (e.g., the Emu)...
NetworkMgr:setWifiState(true)
NetworkMgr:setConnectionState(true)
if not G_reader_settings:isTrue("auto_disable_wifi") then
return
@ -190,10 +192,8 @@ end
function NetworkListener:onNetworkDisconnected()
logger.dbg("NetworkListener: onNetworkDisconnected")
if Device:hasWifiToggle() then
NetworkMgr:setWifiState(false)
NetworkMgr:setConnectionState(false)
end
NetworkMgr:setWifiState(false)
NetworkMgr:setConnectionState(false)
NetworkListener:_unscheduleActivityCheck()
-- Reset NetworkMgr's beforeWifiAction marker

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save