Compare commits

...

1174 Commits

Author SHA1 Message Date
hius07 fa4654cdfa
readerbookmark: fix date sort (#12053) 1 day ago
NiLuJe 9fd6c82b3b
Kobo: Overengineer the manual CLOEXEC logic in the Wi-Fi scripts (#12045)
So it catches non-CLOEXEC sockets properly...

Fix #12043
2 days ago
zwim 8b88f5eb73
[ConfirmBox, MultiConfirmBox ...] remove margin around (#12039) 3 days ago
zwim 43c9ce4a9b
[time] Fix typos in documentation 4 days ago
mergen3107 16fcc38a3a
Update bug_report.md with new location of verbose logging toggle (#12042)
Closes #11276
4 days ago
hius07 fb88e8db28
Annotation list: sort by date (#12020) 5 days ago
mergen3107 a03195d524
Footer: add book authors item (#11999)
Closes #7642
5 days ago
poire-z 251da45adf
bump crengine: update Russian and Serbian hyphenation (#12036)
Includes:
- Russian hyphenation: revert "allow hyphens after не" https://github.com/koreader/crengine/pull/568
- Serbian hyphenation: combine patterns for Cyrillic and Latin scripts https://github.com/koreader/crengine/pull/566
- writeNodeEx(): fix handling of multilines attribute values https://github.com/koreader/crengine/pull/569
  See https://github.com/koreader/koreader/issues/12004#issuecomment-2156748523.
- Add getBalancedHTML() helper

Also includes:
- kobo: add missing blitbuffer library https://github.com/koreader/koreader-base/pull/1823
5 days ago
weijiuqiao e19bed3371
VocabBuilder: fix and improve non-touch device experience (#12030)
Fix #12026.
5 days ago
David 9bff98034f
ReaderHighlight: minor bug fixes (#12033)
Fixes bug reported in https://github.com/koreader/koreader/pull/11916#issuecomment-2170605117 and an issue where back button would not close widget in file manager.
5 days ago
Benoit Pierre 088ae7d4ee lint: fix issues reported by newer shellcheck / shfmt 5 days ago
Benoit Pierre c6f5db0f9e ci: fix shellchecks helper
Quote argument to `git ls-files`: ensure the glob is evaluated by git, not bash.
5 days ago
Benoit Pierre 21fd34a445 ci: improve check script
- don't stop at the first error, report all issues
- always run luacheck directly: so a luacheck install for
  another LUA version or using a shell script launcher
  works (the docker image wrapper already uses luajit)
- don't disable colors in luacheck output
- leave the terminal attributes to default at exit
5 days ago
Benoit Pierre 0eb4c2b077 ci: drop dead code 5 days ago
Benoit Pierre b7fffc9fee ci: use base shellcheck helper 5 days ago
Benoit Pierre 56f2fd48cc bump base 5 days ago
hius07 ded709e3a1
InputContainer: consistent input type in onInput() (#12012) 6 days ago
NiLuJe 57c6fb6355 ScreenSaver: Consistently prefer the event message over the default
message

i.e., we do *NOT* want to see the default "Sleeping" message shown *in
conjunction* with the "reboot/poweroff" overlay messages.

Re: https://github.com/koreader/koreader/issues/12009#issuecomment-2167176139
6 days ago
NiLuJe 94e7c5ad9e Kobo: Don't touch the fb state on shutdown/reboot.
Apparently, the refresh can be slow enough to race with the state
switch, resulting in it being displayed at the wrong bitdepth.

Fix #12009
6 days ago
Frans de Jonge d77e7b47a9
Fix Norwegian Bokmål-English Wiktionary download link (#11992)
Fixes #11970.

Apparently UTF-8 doesn't work unless it's URL-encoded.
6 days ago
Benoit Pierre 6e81162231
doc: update building documentation (#11961)
Together with koreader/koreader-base#1810, close #10116.

Supersede #10115.
7 days ago
jonnyl2 6400c3a546
Styletweaks menu: Ignore ._ files (#12022)
Ignore files starting with "._" (metafiles by MacOS), that will otherwise show up in the Styletweaks menu. (These files are alrady ignored in the File manager, even when 'show hidden files' is enabled.)
7 days ago
Benoit Pierre e3237ef8eb
bump base (#12024)
- https://github.com/koreader/koreader-base/pull/1818
- https://github.com/koreader/koreader-base/pull/1819
- https://github.com/koreader/koreader-base/pull/1821

Close #11959 & #12017.
1 week ago
ziz57 e4232078ab
Readerlink: allow registering empty scheme (#12019)
This allows plugins to handle relative and otherwise schemeless URLs.

Also: clear registered schemes on each init(), to parallel the situation
with external link buttons, and add documentation.
1 week ago
Frans de Jonge 9724dcdf4b IsoLanguage: add Tesseract language names 1 week ago
Frans de Jonge 7da60815d1 Translate Tesseract language names
Fixes #11975.
1 week ago
Benoit Pierre 41f4b1f2ac ci/macos: trim build dependencies
We actually don't need luarocks anymore for building (just testing).
1 week ago
Benoit Pierre 1568303b93 macos: sign (ad hoc) application bundle
Necessary for the arm64 variant, or macOS will complain about the
application being "damaged" (instead of the developer being not
verified), with no possible right click / ctrl+click workaround.
1 week ago
Benoit Pierre 951d9eb9c7 macos: strip application bundle tar binary
Only keep the target architecture.
1 week ago
Benoit Pierre 5dfdbd06eb ci/macos: silence brew warnings 1 week ago
Benoit Pierre 1105f83557 ci/macos: add arm64 job 1 week ago
Benoit Pierre e757f2bf18 macos: add architecture name to update bundle 1 week ago
Marek Veselý ab2f545075
Add kindlehf target (#11986)
for kindles after fw 5.16.3

* Add kindlehf mk file
* Update kodev to support kindlehf
* Update OTAManager logic to support kindlehf
1 week ago
hius07 e1f2acf68f
Screensaver: fix menu wordings (#11993) 2 weeks ago
mergen3107 0a021d5e5e
Statistics Plugin: Skip adding never opened marked as finished books (#11991)
Closes #11201
Thank you very much @hius07 !
2 weeks ago
hius07 786a7e1a4e
Collections: refresh list of files in FileManager (#11994) 2 weeks ago
zwim 09822ba552
[AltStatusBar] Fix when showing (almost) empty (#12003)
With enabled battery percentage shown and some other (e.g. reading percentage) the alt status bar shows the expected line. But when disabling everything except battery percentage, then the reading percentage keeps being shown.

So when using this `self:page_info_override` caching trick, we have to restore the cre default again.
2 weeks ago
mergen3107 9984f3cb7b
Fix WebDAV cloud error (#12000)
Fixes #11985
2 weeks ago
NiLuJe 206de629d8 Bump base
https://github.com/koreader/koreader-base/pull/1815
https://github.com/koreader/koreader-base/pull/1817
https://github.com/koreader/koreader-base/pull/1816
2 weeks ago
NiLuJe 0a5dcf885d Calibre Metadata: Flag array & objects as such in the "safe" parser 2 weeks ago
NiLuJe 8d80acf720 Calibre Metadata: Make sure self.books is always flagged as a JSON array
Ditto for self.driveinfo
2 weeks ago
NiLuJe 1ca443195b Calibre Metadata: Don't lose rapidjson object/array tagging in slim 2 weeks ago
zwim c8f4008e9b
AltStatusBar: fix battery percentage not shown (#11976) 2 weeks ago
David 81d2db150f
ReaderThumbnail: change base class to support key events (#11987)
fixing bug reported here https://github.com/koreader/koreader/pull/11916#issuecomment-2151903891
2 weeks ago
hius07 585afda4be
File searcher: group operations (#11980) 2 weeks ago
Martín Fernández 79c13bee0c
Calibre: Metadata parser improvements (#11922)
* Added a safe pure-Lua SAX JSON parser (via LunaJSON).
* Updated RapidJSON.
* Also implemented a calibre-specific SAX parser in Lua-RapidJSON, and use it by default instead of the full RapidJSON one.
* Raised the file-size threshold to switch between the fast & safe parsers to 50MB.
* Added an UI option to switch between the three parsers.
2 weeks ago
David 04eec52eee
BookMap on devices with useDPadAsActionKeys (#11916)
as first discussed here #11908. This PR brings the book map to non-touch devices that useDPadAsActionKeys().

Book map can be accessed from the menu or by using the following shortcut: ScreenKB + Down or Shift + Down depending on whether you use a K4 device or a kindle with keyboard respectively.

Inside the book map, a user can toggle the hamburger menu by pressing the Menu key and make any adjustment from there. ScreenKB (or Shift) + Up/Down allows it to scroll and Page turn buttons to move by whole full page turns. Back key allows user to exit the map.
2 weeks ago
David 21213f35af
Minor wording corrections (#11981)
see #11978
2 weeks ago
Frans de Jonge e2682e2d98
[plugin] AutoDim: reset counter on page turn (#11984)
Even if not from input.

Closes #11979.
2 weeks ago
David f330593233
Fixing button invert on start (#11982)
fixing issue described here https://github.com/koreader/koreader/pull/11963#issuecomment-2147876605
2 weeks ago
David 7a84cfef4a
Individually invert left or right side page turn buttons on Kindle (#11963)
K4 is ergonomically designed to be held with one hand (one's hand wrapped around the back and both thumb and middle finger on either PgFwd buttons).

This PR allows users to individually invert left and right page turners such that it can be operated just with one hand. It also closes #9350

Not sure if there are any other devices with two sets of page turn buttons, so currently limited to kindle, excluding kindle Voyage, but could be added too, albeit with some gymnastics.
2 weeks ago
David 2d07a82ea2
Add menu key event to Menu widget (#11966)
As seen here https://github.com/koreader/koreader/pull/11918#issuecomment-2137290809
2 weeks ago
Benoit Pierre 86fa488508
bump base, unbreak MuPDF edition (#11974)
- https://github.com/koreader/koreader-base/pull/1806
- https://github.com/koreader/koreader-base/pull/1808
- https://github.com/koreader/koreader-base/pull/1809
- https://github.com/koreader/koreader-base/pull/1810
- https://github.com/koreader/koreader-base/pull/1811
- https://github.com/koreader/koreader-base/pull/1812
2 weeks ago
hius07 ed550948a9
mosaicmenu: fix collection mark (#11969) 2 weeks ago
hius07 bd42f6984e
Gesture manager: make long-pressing on top left corner non adjustable in FM (#11971)
It is overridden by the folder tree, so the Gesture manager setting doesn't work.
2 weeks ago
poire-z 14b7971529
AltStatusBar: fix page info when not all 3 items enabled (#11965)
Fix issue noticed at https://github.com/koreader/koreader/pull/11873#issuecomment-2144485909.
2 weeks ago
NiLuJe 08f02bf915 OPDS: Don't append Search twice for feeds with an OpenSearchDescription 2 weeks ago
NiLuJe 8a37917e5c OPDS: Fixup the last commit
We don't want to actually change the page when we request a fill on the
initial fetch.
2 weeks ago
NiLuJe 04ccd80b86 OPDS: Fix progressive fill on pagination
I have... no idea how this could have ever worked, the Menu handler has
*always* been called OnNextPage o_O.
2 weeks ago
NiLuJe f3a67a4f4d OPDS: Handle searching in Calibre's OPDS server
Re: #11968
2 weeks ago
Frans de Jonge f2691b833e MuPDF: add mobi filetype 3 weeks ago
Frans de Jonge b46f03f092 [minor] Fix typo in XLSX
Dumb typo in #11955.
3 weeks 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.
3 weeks 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.
3 weeks 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>.
3 weeks 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
3 weeks ago
SomeGuy c5600ffe19
SortWidget, DictQuickLookup: rename some functions (#11949)
addressing concerns found #11933
3 weeks 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
3 weeks ago
ziz57 93407c8947
Readerlink: fix scheme detection for external links (#11942)
Following RFC 3986.
3 weeks 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
3 weeks ago
Benoit Pierre 10e6f489d0 kopt: honor `TESSDATA_PREFIX` environment variable
Don't override it by default, but honor it if present.
3 weeks ago
hius07 14519bc076
Dispatcher: add File browser actions 'Set display mode', 'Sort By' (#11921) 3 weeks ago
hius07 b06272592d
Annotation list: flexible item heights (#11918) 3 weeks ago
zwim d2ff789543
[plugin] AutoWarmth: add more gestures for autowarmth activation (#11946)
Fixes #11274.
3 weeks ago
SomeGuy 1b37aa1bd6
Keyboard menu: remove swipe setting from NT (#11944) 3 weeks ago
hius07 84d28dc5d9
FontList: skip Kindle blacklisted fonts (#11932)
Closes #11927.
3 weeks 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
3 weeks 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.
3 weeks 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
3 weeks ago
Benoit Pierre 2f0e456a42 ci/macos: dump binaries runtime path & dependencies
Useful information for investigating issues with the bundle.
3 weeks 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.
3 weeks ago
Benoit Pierre 4750b4a4cd ci/macos: drop unnecessary build dependencies 3 weeks ago
Benoit Pierre 404c7c0dfe ci: update build directory trimming before caching 3 weeks 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
3 weeks ago
Benoit Pierre 1c9c35dcb3 bump base 3 weeks ago
nairyo c7a59145a3
[fix] VocabBuilder: restore erroneously removed onShowVocabBuilder function (#11915)
Fixes #11913.
3 weeks ago
Piotrek Marciniak 9b1a21ef82
Export highlights: empty annotations as nil instead of empty string (#11912)
Fixes Readwise exports
3 weeks ago
SomeGuy 1aefd80ea6
ReaderBookmark: "bookmark current page" removed from NT kindles (#11907)
Discussed https://github.com/koreader/koreader/issues/11834#issuecomment-2132411929 onwards.
4 weeks ago
Frans de Jonge 94372c2adf
Remove ShowFontMenu (#11904)
Some clean up, as suggested in <https://github.com/koreader/koreader/pull/11864#discussion_r1615312993>.
4 weeks ago
SomeGuy eb63cf655f
CoverBrowser: update MosaicMenu FocusManager grid view to 2D (#11906)
This PR [and #11884] are for #11834.
4 weeks ago
Frans de Jonge ffc43030ec
ReaderLink: change (hasScreenKB or hasSymKey) add to notification stack shortcut to Press instead of Down (#11905)
See 316c6a0624 (r1615314584)
4 weeks ago
hius07 9223cde2bd
Status bar: fix book progress (#11885) 4 weeks 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.
4 weeks 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
4 weeks ago
Frans de Jonge a21db40745
[i18n] ReaderBookmark: don't bother translators with a space (#11902) 4 weeks ago
SomeGuy 00d0affd70
Remove very long press settings from NT (#11898) 4 weeks ago
SomeGuy d217f5c161
Menu widget: limit right as hold to hasFewKeys (#11890)
Addresses concerns in #11884.
4 weeks ago
hius07 076f77282b
FileManager copy/move: do not paste to itself (#11878) 4 weeks ago
hius07 c6e6d72cf3
Collections: add collection mark to books (#11868) 4 weeks 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.
4 weeks ago
Frans de Jonge b222900cb9
Enable Alt+Shift+G for screenshots on all devices with a keyboard (#11888) 4 weeks ago
hius07 556e5bd6b4
VirtualKeyboard: delete word on backspace swipe west (#11843)
Closes #11831.
4 weeks ago
SomeGuy 81575ae24f
Global long press on K4, 'ScreenKB' + 'Press' (this time for real) (#11884)
discussed #11834

then merged #11872, then reverted #11881
4 weeks 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
4 weeks ago
Frans de Jonge 07b507370f
Revert "Global long press on K4, 'ScreenKB' + 'Press' (#11872)" (#11881)
This reverts commit 3f64ecfd28.
4 weeks 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.
4 weeks 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.
4 weeks 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.
4 weeks ago
Frans de Jonge 20d8a5c313
FocusManager: enable more keyboard shortcuts by default (#11871) 4 weeks ago
SomeGuy 3f64ecfd28
Global long press on K4, 'ScreenKB' + 'Press' (#11872)
discussed #11834
1 month 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>.
1 month ago
SomeGuy e859109885
Status bar: major UI makeover (#11678) 1 month 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).
1 month 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
1 month ago
SomeGuy 577c5d454f
Non-touch DPad improvements (#11749)
Closes #11295.
1 month ago
Martín Fernández 36d2e3cf74
Add README.md to Linux package (#11859) 1 month ago
yparitcher 59fb906921
NetworkListener: fix FM integration & properly gate behind hasWifiToggle (#11858) 1 month 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.
1 month ago
Martín Fernández 1eb2095ead
Desktop: add all supported mimetypes. (#11847)
Change the comment to "ebook reader"
1 month ago
Martín Fernández 89a6ae28a6
appstream: update metadata (#11851)
proper requires, supports and recommends
all supported mimetypes
add branding colors
1 month ago
Martín Fernández 635d243152
debian: update metadata (#11854)
Slightly modified to avoid: E: koreader: description-starts-with-package-name
1 month 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).
1 month ago
Martín Fernández ba8891082c
fix after #11833 (#11849) 1 month 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
1 month ago
Martín Fernández 7bded465eb
SDL: add flatpak flavor (#11833)
* ship metadata with generic linux binaries
* append release version & data at build time
1 month 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
1 month ago
hius07 daf0fa4b4b
MultiInputDialog: fix keyboard height changed (#11832) 1 month ago
hius07 6b192c346a
Annotations: Bookmark list improvement (#11825) 1 month ago
hius07 46449eb06e
Page overlap: add dashed line marker (#11804)
Requested by our contributor in #11734 (comment).
1 month ago
nairyo 126c01e1b5
Replace tweak_buttons_func with an event instead (#11777)
Also see https://github.com/Ajatt-Tools/anki.koplugin/issues/22
1 month ago
hius07 db63db11b2
FileManager: safe initial path (#11774)
We do not like trailing slash in the path (except root).
Closes #11772.
1 month 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
1 month ago
Philip Chan 5d63907cae
fix #11810: only set FocusManager layout if hasDPod (#11811) 1 month 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.
1 month ago
Martín Fernández 79be8a10b1
appimage: keep metadata in sync with flathub (#11819)
Because it is actually pulled from here :p
1 month ago
Benoit Pierre a9a023c062
doc: fix android NDK / SDK instructions (#11818) 1 month ago
Benoit Pierre ea538900ba ci/macos: install ninja 1 month ago
Benoit Pierre b2d495fcdd ci: force color output 1 month ago
Benoit Pierre 0aaa8e7c5b ci/macos: drop unnecessary permissions stanza 1 month ago
Benoit Pierre f82cc31717 ci/macos: add caching to speedup workflow 1 month ago
Benoit Pierre a12b075e07 ci/macos: avoid concurrent workflow runs on the same ref 1 month ago
Benoit Pierre b28c58b902 ci/circle: tweak resource classes 1 month 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.
1 month ago
mergen3107 8a316f928a
Kindle: remove separate L and R orientations (#11780)
Fixes #11743
1 month 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.
1 month 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
1 month 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`
1 month ago
sdasda7777 eb7af994e1
Check if /usr/bin symlink is present to fix instance reuse issue on older Pocketbooks (#11764)
Fixes #11760.
1 month ago
Nico Hirsch 3e04184638
calibre: add wireless connectionstart/stop actions (#11806)
* Register start stop connection actions

* Match code order

* Title to lowercase
1 month ago
Philip Chan f0a3bcf05b
NT: add Hot swapping dictionaries button and edit button into FocusManager's layout (#11803)
fix #11783
1 month ago
Benoit Pierre 7ba42579eb bump base 1 month ago
Benoit Pierre 8c0362f9ed make: ignore thirdparty directory when installing 1 month ago
Benoit Pierre 5a146414db make: symlink fonts / plugins on install 1 month ago
Benoit Pierre 49e3251e7b make: support changing the build / install directory location 1 month ago
Benoit Pierre f749fc2fd9 make: move target specific rules to dedicated files 1 month 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`).
1 month ago
Benoit Pierre a4526633dd make: reduce the number of `$(shell …)` calls
Through use of `$(INSTALL_DIR)`.
1 month ago
Benoit Pierre 5efba26d80 make: ensure `base/Makefile.defs` can be included
Automatically run `fetchthirdparty`.
1 month ago
Benoit Pierre 3f8f87d294 make: minor cleanup 1 month 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`)
1 month ago
Benoit Pierre 2d4b12c99c tools/mk7z: tweak manifest handling code
To support mawk (default awk on debian unstable).
1 month 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
```
1 month ago
SomeGuy baab326332
[ReaderHighlight] Remove duplicated setting on NT devices (#11750)
Closes #11747.
1 month ago
Joshua Bullock 1398154546
Gesture: Add toggle for orientation lock (#11795) 1 month ago
Predrag Đokić eb6e5e3c20
Kindle: Fix missing Amazon UI screensaver after exiting KOreader (#11794) 1 month ago
hius07 12c3c190b0
Annotations: fixes 2 (#11788)
* readerbookmark: fix index for reverse sorting

* readerhighlight: fix pdf tap on highlight detection
1 month ago
Benoit Pierre 075edf9980 tests/readerhighlight: speedup
Reduce time to run those tests by 74% on my machine (~8.8s → ~2.3s).
1 month ago
Benoit Pierre 554e8daf99 tests/readerhighlight: avoid changing versioned files
Work on a copy of `test/sample.pdf`.
1 month ago
Benoit Pierre 089c19cb39 tests/readerbookmark: avoid changing versioned files
Work on a copy of `test/sample.pdf`.
1 month ago
Benoit Pierre 3e809b6c7b tests/readerlink: speedup
Reduce time to run those tests by 95% on my machine (~17s → 0.8s).
1 month ago
Benoit Pierre fdf19e98c3 tests/readerlink: factorize setup/teardown code 1 month ago
Benoit Pierre d0ca04c48d tests/readerlink: reorganize
Group tests by file type (EPUB or PDF).
1 month ago
Benoit Pierre b67c6147d5 tests: ensure successive testsuite runs work
Don't carry over some old settings. Additionally, avoid clobbering the standard reader settings.
1 month ago
peicuiping a7e34673e6
chore: remove repetitive words (#11785) 1 month 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
1 month 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
1 month ago
sdasda7777 8530282d38
Add Czech keyboard layout (#11769)
Fixes #11339.
1 month ago
hius07 725df17c45
Annotations: fixes (#11761) 1 month ago
hius07 c47d3b3177
Menu widget: cleanup (#11759) 1 month 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.
2 months ago
SomeGuy e51b71f463
[Gestures.plugin] Clarify some rotation gesture strings (#11753) 2 months 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.
2 months 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).
2 months 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.
2 months ago
poire-z ca90b982b4 ReaderStyleTweak: reword CSS suggestions info text 2 months ago
poire-z 1c9a6509a2 HttpInspector: allow browsing global variables 2 months 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
2 months ago
hius07 d82815952e
Annotations, part 1 (#11563)
New format to handle annotations (page bookmarks, highlights, notes) and store them in the book metadata files.
2 months ago
hius07 6b0d97bf22
Multiple collections (#11693) 2 months ago
Benoit Pierre 087ddd1510 ci: small macOS cleanups
Reduce differences with the configuration used on koreader-base.
2 months ago
Benoit Pierre 7392dd2ad4 ci: tweak macOS CI checkout phase
Use the same parameters as for the koreader-base job (faster).
2 months 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).
2 months ago
Benoit Pierre ad6e3b34c4 bump luajit-launcher 2 months ago
Benoit Pierre 64213bf3d9 bump base 2 months ago
Benoit Pierre 6d5ad05e9f ci: bump docker images 2 months ago
hius07 8ff846ba6e
SortWidget: sort alphabetically (#11705) 2 months 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.
2 months ago
vyaus f793c6a36c
Gesture: add jump to random page (#11727) 2 months ago
Frans de Jonge 34abb4e22b
Update translations for v2024.04 (#11726) 2 months ago
Galunid ca14420372
Add relevant nil guards to prevent reflow crashes (#11715)
closes #10854 #9272 #4481
2 months ago
hius07 f5be04a738
Keyboard: detect tap if swipes are disabled (#11699)
Discussed in #11668.
2 months ago
mergen3107 caea0e8fb2
Kindle Scribe: improve gyro detection, replace accel with acc keyword (#11696)
Fixes #11691.
2 months ago
ElimGarak1 bfc84795c8
Add PocketBook Era Color (PB700K3) (#11695)
Fixes #11684.
2 months 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.
3 months ago
hius07 c415aea67e
File manager: fix classic mode item font size (#11626)
Closes #11625.
3 months ago
Frans de Jonge 601925ab87
[minor] CSS tweaks: fix typo cellpading → celpadding (#11618) 3 months ago
Frans de Jonge ec54afcc0d
[minor] ReaderStyletweak: fix typo (#11616) 3 months ago
Frans de Jonge a7edf213a5
[minor] FileChooser: use en-dash instead of dash for UI (#11615) 3 months ago
SomeGuy f6588e95e8
ReaderHighlight: ~~strikeout~~ → Strikethrough (#11619) 3 months ago
Frans de Jonge b1a80921ad
Revert "ReaderPaging: enable Kindle page-turn animations (#11424)" (#11617)
This reverts commit ee7c3ab551.
3 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.'
3 months ago
Benoit Pierre dce92020c1
bump base: fix sdcv binaries on non-Android/macOS platforms (#11609)
Closes #11604.
3 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
3 months ago
SomeGuy fbd3e822fe
ReaderView: increase underline thickness (#11586)
Closes #11581.
3 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
3 months ago
dkabot ee7c3ab551
ReaderPaging: enable Kindle page-turn animations (#11424)
Fixes #11423.
3 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
3 months ago
Denis Malinovsky f4a8514545
ReaderTypography: update menu features symbols for Russian (#11570) 3 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.
3 months ago
poire-z 67cd647d1a
ReaderToc: add option to show chapter lengths (#11546) 3 months ago
hius07 c8c4e0301a
covermenu: fix file dialog (#11576) 3 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 4 months ago
zwim 764f8ba647 bump base: Use Bresenham's algorithm and anti-aliasing for rounded corners 4 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
4 months ago
hius07 7edb64ecee
Upper menu: fix generating the menu (#11513)
revert all changes to the upper menu made in #11495
4 months ago
Frans de Jonge c4a09b3076
Remove bountysource from README (#11385)
Closes #11384.
4 months ago
Frans de Jonge d239623c46
Update translations for v2024.03 (#11508) 4 months ago
hius07 ec98c6334b
MenuSearch: open upper menu when needed only (#11495) 4 months ago
hasezoey ade1daca3a
FileChooser: "percent - unopened - finished last" consider status "complete" as 100% (#11472)
re #11369
4 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.
4 months ago
hius07 2fca0ee989
Reader: use existing module instances on start (#11485) 4 months ago
hius07 72a6fa1e64
FileManager: less lfs calls when selecting files (#11476) 4 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) 5 months ago
hius07 962477e7c6
TextViewer: make find result bold (#11427) 5 months ago
zwim f836f6a237
Clear UI leftovers before doing an OTA-install, add unmovable to InfoMessage and ConfirmBox (#11412) 5 months ago
hius07 38a14ff3a0
PathChooser: fix calling Folder shortcuts (#11367) 5 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.
5 months ago
hius07 b832d43d98
CoverBrowser: fix classic mode update cache (#11398) 5 months ago
hius07 f246b3d19c
Text editor: refresh path on saving file (#11396)
File size was changed.
5 months ago
NiLuJe 75ffc3bb76 Terminal: Better yet, disable it properly on prereq failure 5 months ago
NiLuJe a906838123 Terminal: Clearer error message on prereq failures
Pluginloader uses pcall, so, throw with an actual error message

Re: #11394
5 months ago
NiLuJe c9da681874 Bump base
https://github.com/koreader/koreader-base/pull/1732
5 months ago
NiLuJe a8a944cbaf ScreenSaverWidget: Simplify the setDirty call
No need for a closure, there's no dimen involved.
5 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.
5 months ago
NiLuJe 4785df48a9 PRSTux: Disable OTAs
We haven't been offering any for years, and I've just killed zsync
support there anyway.
5 months ago
hius07 925aa728c9
Fulltext search: action to show last findall results (#11388) 5 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
Frans de Jonge cd903d66e5
Update translations for v2024.01 (#11333) 5 months ago
mergen3107 4a9473ed45
Kindle Oasis 1,2,3, Scribe: fix startup orientation (#11277)
Closes #11269
5 months ago
hius07 4ddc0f2ef8
FileChooser: fixes (#11300) 5 months ago
lbesnard 5a3f73766c
Exporter: memos, replace space with underscore for # tag creation (#11107)
Creating tags on memos looked like "#This is a book title" meaning only "#This" would become a Tag. Replacing spaces with underscore to be more usable so that the following tag would be created "#This_is_a_book_title"
5 months ago
NiLuJe 9826615f06
i18n: Don't use Lua unicode escapes (#11330)
Something in the gettext/weblate pipeline doesn't like it...

These are real unicode codepoints, not custom nerdfont ones, so just
render the actual glyph instead of escaping it.

Fix #11328

Cf. #10845.
5 months ago
NiLuJe cc52c022dc Notification: Take three, take the guesswork away by making every
specific mask visible
5 months ago
NiLuJe 4a15dce3e4 Kindle: Disable HW dithering on Scribe
Pending the actual fix from
https://github.com/koreader/koreader-base/pull/1718 that'll hit after
the release.
5 months ago
NiLuJe 3131475e39 Notification: refactor settings
Simplify the code, because the bit trickery was fairly nasty to follow.
KISS: flip one value at a time, either in or out.

Actually allow flipping *all* of the things via the UI, to help track
what the hell is actually happening when you touch a button.

Make some of 'em radio to make it clear when flipping one might affect
the other(s).

Brought on by https://www.mobileread.com/forums/showthread.php?t=358166
5 months ago
Tomáš Janoušek ccbfbabb25
Fix "Move current book to archive" (#11321)
updateItemByPath has recently been replaced with updateItem in both
readhistory and readcollection.

Fixes: aabd6d7a26 ("File browser, Collection: improve group actions (#11178)")
Fixes: https://github.com/koreader/koreader/issues/11320
6 months ago
Mihai Vasiliu 5d2a441064
Add viewport for PB743K3 (#11302)
The bezel covers a couple of pixels of the screen, so adjust not to show content under the bezel.
6 months ago
mergen3107 63329569eb
[android] toggle warmth on onyx devices (#11275) 6 months ago
mergen3107 6c85547ce6
Add HW dither on Kindle Scribe (#11292) 6 months ago
hius07 3533356ffd
filemanagerhistory: fix untranslated strings (#11282) 6 months ago
mergen3107 4c2fc1eb81
Kindle Scribe: fix touch input (#11285) 6 months ago
mergen3107 57cbde5901
[plugin] Add folder check in move-to-archive (#11262) 6 months ago
NiLuJe ac5d662eb4
Statistics: Also skip hidden flows stuff in continuous mode (#11284)
Followup to #11279
6 months ago
greatyingzi c12b4f2e14
Statistics: fix a crash in continuous mode opening statistics page (#11279) 6 months ago
hius07 7a421ea3ab
Fix "Open next file" (#11272) 6 months ago
hius07 b949d07f4f
filemanagerbookinfo: proper text_type of description (#11260) 6 months ago
mergen3107 9ba66ac382
Exporter - fix android shares (#11259) 6 months ago
NiLuJe 33b54f5574
Kindle: Add a hasLightSensor devcap, and use it in the AutoFrontlight plugin (#11255)
Add the Scribe to the list while we're there
6 months ago
NiLuJe e7ee900cbd Bump android/luajit-launcher
https://github.com/koreader/android-luajit-launcher/pull/451
https://github.com/koreader/android-luajit-launcher/pull/431
https://github.com/koreader/android-luajit-launcher/pull/439
https://github.com/koreader/android-luajit-launcher/pull/452
6 months ago
NiLuJe d151dd5f12 Bump base
https://github.com/koreader/koreader-base/pull/1716
https://github.com/koreader/koreader-base/pull/1714
https://github.com/koreader/koreader-base/pull/1717
6 months ago
hius07 042a529fab
File manager: fix height of menu widget (#11251) 6 months ago
Frans de Jonge 75d3c10496
Debian package: add X-Purism-FormFactor to koreader.desktop (#11249)
Fixes #11118.
6 months ago
hius07 5a4e70a19d
History, Collection: title bar in FM style (#11243)
These changes make the title bar in File manager, History and Collection windows of the same height.
The items and book covers thumbnail dimensions in Mosaic and Detailed list display modes are now equal, and up/down scaling of the thumbnails is not needed when switching between File manager, History and Collection.
6 months ago
dependabot[bot] 9d94ab38f3
[CI] Bump actions/upload-artifact from 3 to 4 (#11242)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 3 to 4.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
6 months ago
poire-z 48197805aa
bump crengine: fix stylesheet possibly not applied (#11239)
Includes:
- Uplift German hyphenation patterns
- LVStyleSheet: fix stylesheet possibly not applied
and base:
- android sdk: remove patcher
6 months ago
hius07 bf59f53e18
Folder shortcuts: speed up (#11221) 6 months ago
NiLuJe 675f5a062f Dispatcher: Allow custom intensity/warmth gesture to be setup in gesture
distance mode, like the default edge swipe, and not only with a fixed
increment.

Document the behavior in a slightly less confusing way.
6 months ago
zwim c30c1ff11f DeviceListener: Rejig calculateGestureDelta algorithm
Get rid of the silly precomputed tables, and do More Maths(TM) instead!
Thanks to @zwim for the magic sauce ;).

Minor simplification of the API while I'm in there, and unify the warmth
computations, do everything in the native scale (much like what
effectively happens for intensity) to workaround the silly public API
being an unhelpful PITA, ensuring consistent & effective changes.
6 months ago
NiLuJe a2e0642998 Cervantes: Make sure the warmth scaling is accurate
For some mysterious reason, we init fl_warmth_max to 100 on Kobo &
Cervantes, despite this being the case on absolutely none of them.

TL;DR: We update it according to nl_max during init, but this was
missing on Cervantes.
6 months ago
NiLuJe 9ab866598a ReaderProgress: Ensure a sane dimen
Otherwise, depending on the DPI, the size computation might not cover
the full-screen, leaving gaps to what was behind the widget.

Fix #11177
6 months ago
NiLuJe bc01394f8f SortWidget: Abbreviate the pagination text
Helps avoid truncation, given the way the widget is currently built.
Closes #11222
6 months ago
NiLuJe ff58ac7a65 DeviceListener: Handle non-gesture warmth increment in the native scale
Re: #11220
Followup to #6468
6 months ago
hius07 f765fe3070
Screenshot: set as book custom cover (#11227) 6 months ago
mergen3107 0d2f8fbb3f
Terminal Emulator: Fix former_buffer crash (#11230)
Closes #11229
6 months ago
hius07 f4a5a2b60a
TextViewer: add dialog to set font size and justify text (#11210) 6 months ago
poire-z fe02b83b6a
ImageViewer: menu to invert rotations and set auto rotation (#11206)
Add some menu items under Screen>Rotation to allow inverting
the default rotation of ImageViewer, and to auto-rotate for
best fit on launch, so landscape images are auto-rotated to
the preferred rotation.
6 months ago
hius07 aabd6d7a26
File browser, Collection: improve group actions (#11178)
Maintain correct records in History and Favorites when moving/deleting folders or group of files.
Optimize Collection module to minimize storage requests.
6 months ago
Frans de Jonge 13a521c398
AppImage: remove hasMultiTouch = no (#11212)
SDL supports multitouch since https://github.com/koreader/koreader-base/pull/1599
7 months ago
NiLuJe 9ebe7714ec Bump base
https://github.com/koreader/koreader-base/pull/1708
https://github.com/koreader/koreader-base/pull/1706
https://github.com/koreader/koreader-base/pull/1707
https://github.com/koreader/koreader-base/pull/1711
https://github.com/koreader/koreader-base/pull/1710
https://github.com/koreader/koreader-base/pull/1709
https://github.com/koreader/koreader-base/pull/1712
7 months ago
NiLuJe 18fff200ac GestureDetector: Be even *more* defensive with initial_tev records
Apparently, #11198 wasn't defensive enough...

Make those visible warnings, so we can see how much they actually happen
in the wild...

Closes #11209
7 months ago
Wim de With e7780c09f3 fixup! Process directory list items for mixed collation 7 months ago
Wim de With 128accafe3 fixup! Process directory list items for mixed collation 7 months ago
Wim de With 01642659d2 Process directory list items for mixed collation 7 months ago
Wim de With 98d92d37ab Refactor FileManager collation for easier patching 7 months ago
NiLuJe 3c63a5d8a8
GestureDetector: Switch buddy contacts to voidState in a safer manner (#11198)
Some platforms with horribly broken MT handling (hai, PB) will manage to
screw something up so bad that we end up with slots never running
through initialState on a contact down, so they never get an initial_tev
recorded.

Ensure we do that if necessary when switching a buddy to voidState to
avoid crashes throughout the code, which assumes everything is sane and
doesn't guard accesses to initial_tev

Re: #11196, #10950 & #11111
Closes: #11111
7 months ago
hugleo f990937f9f
Autocrop fix (#11194)
After some tests with the said document from https://github.com/koreader/koreader/issues/970 it seems like that the visual defect occurs for only semi-auto and manual crop modes.

I've removed auto-crop from the rule so fixes https://github.com/koreader/koreader/issues/4106 when using auto-crop.
7 months ago
hius07 498193c26d
InputText: Delete all and fix Select (#11182) 7 months ago
poire-z b361cec4ff ReaderStyleTweak: minor CSS suggestions popup fixes 7 months ago
poire-z c32ace4611 bump crengine: various optimizations and fixes
Includes:
- Avoid unnecessary string modifications
- Optimize LVHTMLParser::CheckFormat()
- lvfntman: fix stupid mistake
- css: speedup class matching
- fb2.css: empty paragraphs should have some line height
- CJK: fix bad ruby drawing on last paragraph line
7 months ago
hius07 b4424b4685
filemanagerhistory: fix non standard status (#11188) 7 months ago
hius07 dce12de09d
Profiles: fix update actions in gestures (#11175) 7 months ago
NiLuJe 4a64e02c68
ScreenSaverLock: Hide the popup on suspend (#11174)
Fix #11164 and involves a drive-by fix:

Kindle: Send Suspend/Resume event regardless of the screen saver state

If we get the events, it means stuff happened, we can't just only honor
it in the most common workflows ;).

This effectively reverts a tiny bit of #10426 (I was sort of expecting
this to be problematic at the time, and I most likely hadn't tested it).
7 months ago
mergen3107 33c7f05158
Kindle scribe gyro and pen support (#11159)
* Kindle Scribe: add G-sensor and pen support, fix startup orientation

* Fixes #11144 and #11156
7 months ago
hius07 d3787eee64
filemanagershortcuts: fix menu (#11167) 7 months ago
Monirzadeh 6527d7f1ab
Fix English-Persian dictionary link (#11165)
Spaces should be encoded.
7 months ago
NiLuJe 5ef4db6176 Bump android/luajit-launcher
https://github.com/koreader/android-luajit-launcher/pull/449
7 months ago
NiLuJe f42893d3d1 Bump base
https://github.com/koreader/koreader-base/pull/1704
7 months ago
hius07 f6fb35fd98
QuickMenu: keep open on apply (#11135) 7 months ago
NiLuJe 82e7ebd6df
ReaderView: Flash when paging *away* from images, too (#11152)
Reword the `Always flash around pages with images` Menu/Dispatcher entry
accordingly.

Fix #11143
7 months ago
NiLuJe 9af3e95d9d Kindle: Fix a smattering of frontlight bugs
* afterResume had *two* different implementations, so the historical one
  that handled frontlight fixups no longer ran
  (regression since #10426)
* isFrontlightOn was completely broken, for a couple of reasons:
  * There was no is isFrontlightOnHW implementation, so when it ran, it
    mostly always thought the frontlight was on, because
    self.fl_intensity doesn't change on toggle off.
  * _decideFrontlightState was never called on Kindle,
    so isFrontlightOnHW was never really called, making isFrontlightOn
    completely useless. Call it in setIntensityHW's coda, as it ought to
    be. And properly document that.

Generic *was* calling _decideFrontlightState is setIntensity, but
*before* actually setting the frontlight, which makes no goddamn sense,
so get rid of that, too.

* Also fix frontlight toggle notifications (regression since #10305)

TL;DR: The PowerD API being a mess strikes again.
7 months ago
NiLuJe 2554aee7a3 Gestures: Make the edge swipe zones configurable
As "global defaults" (i.e., "advanced settings" in the UI).

These were the only zones not available there, for some reason (probably
they were new and we try to avoid adding new defaults).

Fix #11142
7 months ago
Monirzadeh fbd012b928
Add English to Persian dictionary (#11160) 7 months ago
NiLuJe bf03f40ef2
Dispatcher: Allow toggling USBMS (#11123)
It's right next to actual exit/restart actions, so it'll never ask for confirmation.
7 months ago
NiLuJe bba48fc1bf
Widgets: Unschedule timeouts on early close (#11126)
Affects Notification, InfoMessage & QRMessage

Includes a drive-by fix for Trapper interactions that have been broken for... a long while ;).
7 months ago
NiLuJe bc7ea8602e
UIManager: Don't block gestures for new widgets when input is disabled (#11122)
They'll be disabled again when the widget in question is dismissed.

This exposes a couple of semi-obvious but edge-casey footguns to the user, but a hardened implementation is way uglier. See PR for details.
7 months ago
Ben Bell f92c0eae3b
Exporter: fix XMNote export, add author field (#11134)
Optimize #11087 :

1. In `isReadyToExport`, check if the ip is set. If not, then the user cannot enable xmnote export.
2. Add author field in highlights.
7 months ago
WangKe dea94026f1
Exporter: add XMNote export (#11087) 7 months ago
Frans de Jonge 7870e83410
Bump luajit-luancher: support Onyx Nova, warmth support Tolino Vision 4, LuaJIT 20231109 (#11128)
* Use Tolino NTX on Tolino Vision 4 to add warmth support <https://github.com/koreader/android-luajit-launcher/pull/446> @ailin-nemui
* Support for Onyx Nova <https://github.com/koreader/android-luajit-launcher/pull/443> @hugleo
* Bump LuaJIT to 20231109 <https://github.com/koreader/android-luajit-launcher/pull/447> @NiLuJe (same as in <https://github.com/koreader/koreader/pull/11127>)
7 months ago
Frans de Jonge 51b4d5e646
Bump base: some dependencies, minor Makefile fixes (#11127)
* Bump HB to 8.3.0 (koreader/koreader-base#1694)
* Bump LuaJIT to 20231109 (koreader/koreader-base#1694)
* crengine: drop unnecessary MuPDF dependency (koreader/koreader-base#1700)
* cmake: fix ko_write_gitclone_script function (koreader/koreader-base#1702)
* fix crengine, koreader-cre, and mupdf rules (koreader/koreader-base#1701)
* sqlite: don't set HAVE_LOG2=0 on Android (koreader/koreader-base#1703)
7 months ago
hius07 d99c70b5e1
ReaderHighlight: adjustable highlight dialog position (#11116) 7 months ago
hius07 88d6613fed
Folder shortcuts: new markers, buttons (#11108)
Buttons to add/remove folder shortcut from Plus menu and from folder popup dialog.
A marker ("empty star") for folders with shortcuts.
7 months ago
hius07 ed46dca886
MoveToArchive: fix DocSettings:updateLocation() call (#11103)
Closes #11101.
7 months ago
hius07 da8e23c011
InputDialog: tap outside to close dialog if keyboard is hidden (#11094) 7 months ago
hius07 082ef9b545
FileChooser: fix reverse sorting of folders (#11093)
Do not apply "reverse sorting" to folders in sorting modes "type", "size", "percentage".
In that modes folders are sorted by name.
7 months ago
poire-z 894cb3190d
Page browser: show nightmode thumbnails when in nightmode (#11091)
Just prevent page thumbnails ImageWidgets to be nightmode
inverted, unlike all other ones which are expected to be
double inverted to get their original colors shown.
The same thumbnail can be used and cached in both day and
night modes, unless "nightmode_images" is enabled and have
crengine itself invert images, making thumbnails different.
7 months ago
hius07 4044c81a5a
History: update on deleting a folder (#11090) 7 months ago
Wim de With 17a4aa962f
Fix connection bug with non-ASCII SSIDs in wpa_supplicant (#11089)
* Bump base

includes:

koreader/koreader-base#1691
koreader/koreader-base#1692
koreader/koreader-base#1689
koreader/koreader-base#1690
koreader/koreader-base#1693

* Integrate decoding of SSIDs within wpa_supplicant

The UTF-8 decoding of SSIDs is specific to wpa_supplicant. In this
patch, we move all of this decoding logic to the wpa_supplicant module.
We expose the raw bytes of the SSID to the NetworkMgr code, and make
sure to always fix bad UTF-8 before we display the SSID to the user.

Within the wpa_supplicant module, we replace the call to the
wpa_passphrase binary to get the PSK with a direct function call to
OpenSSL. This allows us to calculate the PSK over any arbitrary bytes,
including UTF-8. In the same vein, we use the hex-encoded SSID to
communicate with wpa_supplicant when setting up the network to support
arbitrary bytes in the SSID.

Unfortunately, we also remove the tests, as there is no way to unit test
local functions.
7 months ago
hius07 d0d3cf78f9
History: search (#11084) 7 months ago
hius07 51f3115b90
Profiles: fix updating gestures (#11072) 8 months ago
ElimGarak1 2e2ca76a03
Add new PocketBook InkPad Color 3 (743K3) (#11079)
To add the new PocketBook InkPad Color 3 (743K3) with E Ink Kaleido™ 3 screen.
8 months ago
hius07 68aa209a6c
Open with... improvement (#11056)
Allows associating filetypes with non-document providers (like ImageViewer or TextViewer) to "open" (view) these files by a tap in file browser.
8 months ago
NiLuJe 94a82087de
Device: Don't leave hasSeamlessWifiToggle enabled when hasWifiToggle is disabled (#11060)
Fix #11059
8 months ago
NiLuJe fee2b79829
Kindle: The KT5 requires the same input delving shenanigans as the PW5 (#11064) 8 months ago
hius07 4391267dfa menu_keyboard_layout: add setting to disable swipes 8 months ago
hius07 414e74f626 virtualkeyboard: add setting to disable swipes 8 months ago
Frans de Jonge 48dfe6ce05
Bump base for improvements by @benoit-pierre (#11069)
Includes:

* Input: Ensure clearTimer won't compute a lower nfds value than one of our opened fds <https://github.com/koreader/koreader-base/pull/1685>

Also includes various dependency bumps.

<16a856a637...master>
8 months ago
zwim 58cb1ff42b
[AutoWarmth] Fix sanity checks with disabled schedule entries (#11062) 8 months ago
hius07 b70f866656
DocSettings (again) (#11020)
Cleaning and optimizing Docsettings code.
8 months ago
Frans de Jonge 873503369c
Update translations for v2023.10 (#11053) 8 months ago
NiLuJe d805a69446 Open input device outside of the readdir loop to workaround a bug in the
input backend
8 months ago
NiLuJe 84e942a326 Kindle: Auto-detect the input device on the PW5
Apparently, the by-path symlink went poof
8 months ago
NiLuJe 6e08809419
Kindle: Amend #11049 comments (#11051)
Turns out it's not really specific to the Signature Edition
8 months ago
hius07 6b892a65a3
ReaderHighlight: delete highlight in view note dialog (#11039) 8 months ago
NiLuJe 12bea3b14b
Kindle: Handle the PW5 SE properly (#11049)
Need to poke elsewhere for its Input device

Fix #11048
8 months ago
NiLuJe 2c5d618f6b
Kindle: Initial Scribe support (#11047)
Assuming Bellatrix3 boards are extremely similar to their Bellatrix brethren...

Fix #11045
8 months ago
hius07 39d54956ec
webdav: do not allow trailing slash in start folder (#11026) 8 months ago
Frans de Jonge 9e82761c45
[lang] Exporter: minor stylistic changes to Memos capitalization and dialog titles (#11032) 8 months ago
Frans de Jonge edf9798bb7
[lang] Exporter: add translator's note about Flomo (#11031) 8 months ago
Frans de Jonge 9274a3febd
[lang] Minor string fixes (calibre should be lowercase, remove stray newline) (#11030)
Follow-up to #10945.
8 months ago
hius07 3b2fc7a551
Reader: re-enable File search, Folder shortcuts (#11028) 8 months ago
Frans de Jonge f3520effd6
README: update user guide link (#11000)
See https://github.com/koreader/koreader.github.io/pull/21
8 months ago
hius07 c92d94af4d
writeToFile: centralize (#11012) 8 months ago
NiLuJe d8a48d9e1c Bump base
https://github.com/koreader/koreader-base/pull/1666
https://github.com/koreader/koreader-base/pull/1667
https://github.com/koreader/koreader-base/pull/1668
https://github.com/koreader/koreader-base/pull/1665
https://github.com/koreader/koreader-base/pull/1664
https://github.com/koreader/koreader-base/pull/1669
8 months ago
NiLuJe fed24ba28c UIManager: Always send PowerOff & Reboot events
They were only sent when said action was triggered manually.

Note that this is perfectly harmless, since, currently,
nothing actually responds to those events ;).
8 months ago
NiLuJe c47ce45fb8 WakeupMgr: Forward rtc_dev & dodgy_rtc to the backend 8 months ago
hius07 2ed2c2c23d
md5: centralize and deduplicate (#11003)
Document partial md5 hash is calculated by util.partialMD5() and stored in doc_settings as "partial_md5_checksum" on the first document opening.
8 months ago
hius07 e9051353a2
Vocabbuilder: fix docless title (#11008)
Closes #11006.
8 months ago
hius07 6ccf19b99f
History: update on renaming/moving a folder (#10999) 8 months ago
hasezoey ee7a6455ce
Device:Android: always call "_decideFrontlightState" in "setIntensityHW" (#10737)
re https://github.com/koreader/koreader/pull/10731#discussion_r1271505121

This PR changes so that androids implementation of `setIntensityHW` always calls `_decideFrontlightState`
8 months ago
夏鲁豫 ff6ee69753
Exporter: fix memos export, add flomo export (#10988) 8 months ago
ElimGarak1 5b916cccff
Add new PocketBook PB-Basic Lux 4 (PB618) (#10987) 8 months ago
François Gannaz 11083022b0
Minor code quality improvements (#10905) 8 months ago
Frans de Jonge 6e57ccaf4f
[doc] Building targets: remove mentions of Ubuntu Touch (#10974)
Deprecated or at least untested for years. Noticed due to <https://github.com/koreader/koreader/discussions/10972#discussion-5701647>
8 months ago
yparitcher 0e26d4499b
util.htmlEntitiesToUtf8: add some more semi common entities (#10979) 8 months ago
Benoit Pierre d4421130f8
otamanager: fix OTA model name for Android x86_64 (#10997) 8 months ago
hius07 e577c79d95
File search, FileChooser and others (#10994) 8 months ago
NiLuJe 16e96969c5
TimeSync: Double-check that ntpd is busybox (#10992)
Kindle ships another implementation, with incompatible syntax...

Regression since #10935
Thanks to @yparitcher ;).
8 months ago
Ryan W West 27104ea011
Add hash-based document metadata storage option (#10945)
This option saves metadata sidecar (sdr) directories not next to the book or in koreader/docsettings/, but in koreader/hashdocsettings/ using the partial md5 hash of each documents, allowing users to move, rename, and copy their documents outside of KOReader without accidentally losing their highlights/notes/progress. Included are various warnings and info to users of the benefits and drawbacks of this non-default option.

Closes #10892.
8 months ago
NiLuJe 4eac18f9b9 Bump android/luajit-launcher
https://github.com/koreader/android-luajit-launcher/pull/441

Fix #10533, #10150, #10587, #10501

https://github.com/koreader/android-luajit-launcher/pull/442
9 months ago
NiLuJe bb492f5d78 Bump base
https://github.com/koreader/koreader-base/pull/1662
https://github.com/koreader/koreader-base/pull/1647
https://github.com/koreader/koreader-base/pull/1663
9 months ago
NiLuJe d039aa5f3a Statistics: Minor style-nit followup to #10968
Too late to avoid the whitespace diff churn, unfortunately ;).
9 months ago
hius07 517731dbbb
Finished books: freeze history timestamp and statistics (#10968) 9 months ago
poire-z ea3f8951a3 Add ReaderHandmade: support for custom TOC and hidden flows
Checkboxes under TOC>Settings> allow enabling Custom TOC
and Custom hidden flows (similar to "Hide non-linear fragments"
available with some EPUBs).
Handled by a new ReaderHandmade module.
Adding and removing TOC chapters is done in PageBrowser
with long-press on a page thumbnail, and for chapters also
in the popup dialog after selecting some text in a page.

ReaderToc: add a symbol in title when the TOC is the
alternative TOC or the custom TOC.
9 months ago
poire-z c51b0c7bb9 BookMap: add option Alternative theme
When enabled in BookMap, both BookMap and PageBrowser get:
- alternating gray background on chapter spans
- hatched background instead of uni-gray on hidden flows.
Also, with both normal look and this alternative theme,
show some hatched overlay on thumbnails part of hidden flows.

ReaderToc: for each TOC item, have it carry its sequence/number
in that level (we do this in validateAndFixToc() where we are
already iterating all the items).
9 months ago
poire-z 5406fdf2e0 BookMap/PageBrowser: update upper instance when leaving lower one
BookMap and PageBrowser instances can be stacked; when
toggling a bookmark in a lower PageBrowser and closing it,
make sure the bookmark is shown in the BookMap we get back to.
Also consider TOC and hidden flows as a thing that can be
edited and needs refreshing, so we're ready for next commits
about custom TOC and custom hidden flows.
Also update Reader footer when closing the last BookMap
or PageBrowser.
9 months ago
poire-z 2b75ef108e PageBrowser: allow tap/hold on thumbnail before it is loaded
There was no real reason for having to wait.
Also avoid possible crash when a background generated
thumbnail would be displayed after other things happened.
9 months ago
poire-z 1c128f1089 PageBrowser: taller vertical marks for thumbnail rows in viewfinder
Instead of small markers below the baseline, use
translucent vertical lines over most of the ribbon's
height to mark thumbnail rows, so we can more easily
relate chapter spans to thumbnail rows.
9 months ago
poire-z fb6bc0391f PageBrowser: add option to preload next/prev page thumbnails 9 months ago
poire-z bbf923c7a8 BookMap/PageBrowser: allow mousewheel scrolling
Also fix SDL to use sendEvent() instead of broadcastEvent(),
so such BookMap scrolling doesn't also scroll pages in the
book below.
9 months ago
poire-z 35f16f87a4 ReaderThumbnail: fix slow thumbnails when in scroll mode
If originally with two-columns in page mode, and switching
to scroll mode, thumbnails generation would be slow as
the document would be rerendered for each thumbnail.
9 months ago
poire-z 5ec736ae96 ReaderToc: rework getChapterPageCount/Left/Done()
Rework the way they are computed, fixing issues with hidden
flows when they don't coincide with chapter starts.
9 months ago
poire-z e6ac74c1df ReaderPaging: use document:getNext/PrevPage()
instead of doing arithmetic (ie. new_page=cur_page+1).
This makes it ready to work with custom hidden flows
where these document:getNextPage()/getPrevPage() will
be overloaded to skip pages in hidden flows.

Also fix some odd issues (page truncated or with parts
duplicated) with scrolling/page turning when at start
or end of the document.
9 months ago
poire-z 52c45ef6db InputDialog: add param to setInputText() to set cursor pos
When not provided, the cursor stays at its initial position,
which might not be the best if replacing the whole content,
where we would prefer to have it at start or end.
9 months ago
poire-z da424486ef FrameContainer: allow for hatched background or overlay
Bump base for BlitBuffer:hatchRect().
base bump also includes:
- Update to HarfBuzz 8.2.1
- tests and ci updates
9 months ago
NiLuJe 2ff60ce04f
Kobo: Deal with some more frontlight edge cases on devices with the AW99703 PWM controller (#10971)
Namely, skip ramping when going to/from <= 2% frontlight, otherwise we just eat the delay for no good reason (1%), or it just stutters and looks bad (2%).

Fix #10970
9 months ago
François Gannaz 128302873d
New menu option and filemanager filter to hide finished books #7158 (#10895)
* New menu option and filemanager filter to hide finished books #7158

The default behavior is to display the finished books (no change on
upgrade). For consistency with the two similar options, it represented
by a checkbox "Show hidden books" that is checked by default.

The implementation is straightforward, meaning that, when the option is
unchecked, each file will require a call to `filemanagerutil.getStatus`
that checks its status.

For clarity, the code uses the "finished books" expression because the
condition is relevant to the *book* metadata, while the other settings
are about *file* attributes.
9 months ago
Benoit Pierre 5b5b4d9ebc
readerhighlight: fix OCRed text dictionary lookups (#10967)
Cf. #10966.
9 months ago
hius07 0ac258fcb7
Coverbrowser: event for cache refreshing (#10956) 9 months ago
Benoit Pierre 08555ad68b coverbrowser: make showing some metadata fields optional 9 months ago
Benoit Pierre e8bc28db33 coverbrowser: minor simplification
Add a `toggleSetting` helper.
9 months ago
Benoit Pierre 7d3456edc1
[plugin] CoverBrowser: improve title & authors layout (#10942)
- handle possible height overflow
- now that labels don't overflow, increase the minimum font size
9 months ago
hius07 0c5240074c
screen_rotation_menu_table: deduplicate (#10954) 9 months ago
hius07 40e2a838ae
Status: setting status of current book in History and Collections (#10946) 9 months ago
hrdl 4875f63f09
sdl: ensure dialogs are displayed after window change events (#10929)
Fix #10927

Co-authored-by: NiLuJe <ninuje@gmail.com>
9 months ago
ElimGarak1 ed885a79b2
[PocketBook] Add PB-Verse_(PB629) and PB-Verse Pro_(PB634) (#10938)
Closes #10913.
9 months ago
Benoit Pierre 8f6fae81fa
tests: tag tests relying on a working internet access (#10939)
So they can easily be filtered-out when offline.
9 months ago
NiLuJe 741302445b
TimeSync: Probe for root & ntpd/ntpdate dynamically (#10935)
And, on Kindle, make sure we update the native UI's time, too.

Fix #10932
9 months ago
NiLuJe e5535a3a3a
ReaderRolling: Guard against races between scheduled tasks and CloseDocument (#10934)
Fix #10932

---------

Co-authored-by: Frans de Jonge <fransdejonge@gmail.com>
9 months ago
hius07 cafe565ade
screensaver_menu: radio buttons (#10931) 9 months ago
hius07 9f39614e8a
Custom metadata: fixes 2 (#10919) 9 months ago
NiLuJe 55ea10655f
Revert "Kobo: Disable HW inversion on MTK (#10841)" (#10928)
This reverts commit e7e0d2edb6.

Whatever's actually going on with nightmode, this isn't it.
This was independently confirmed to behave as expected,
and the issue that prompted this was *not* fixed by this commit.
9 months ago
hius07 ed5c1cef20
ButtonTable: remove unused (#10926) 9 months ago
NiLuJe 34ba2fab30
NetworkMgr: Handle non-blocking turnOnWifi implementations better (#10863)
* Device: Add a `hasSeamlessWifiToggle` devcap to complement `hasWifiToggle`, to denote platforms where we can toggle WiFi without losing focus, as this has obvious UX impacts, and less obvious technical impacts on some of the NetworkMgr innards...
* Android: Mark as `!hasSeamlessWifiToggle`, as it requires losing focus to the system settings. Moreover, `turnOnWifi` returns *immediately* and we *still* run in the background during that time, for extra spiciness...
* NetworkMgr: Ensure only *one* call to `turnOnWifi` will actually go on when stuff gets re-scheduled by the `beforeWifiAction` framework.
* NetworkMgr: Ensure the `beforeWifiAction` framework will not re-schedule the same thing *ad vitam aeternam* if a previous connection attempt is still ongoing. (i.e., previously, on Android, if you backed out of the system settings, you entered the Benny Hill dimension, as NetworkMgr would keep throwing you back into the system settings ;p). This has a few implications on callbacks requested by subsequent connection attempts, though. Generally, we'll try to honor *explicitly interactive* callbacks, but `beforeWifiAction` stuff will be dropped (only the original cb is preserved). That's what prevents the aforementioned infinite loop, as the `beforeWifiAction` framework was based on the assumption that `turnOnWifi` somewhat guaranteed `isConnected` to be true on return, something which is only actually true on `hasWifiManager` platforms.
* NetworkMgr: In `prompt` mode, the above implies that the prompt will not even be shown for concurrent attempts, as it's otherwise extremely confusing (KOSync on Android being a prime example, as it has a pair of Suspend/Resume handlers, so the initial attempt trips those two because of the focus switch >_<").
* NetworkMgr: Don't attempt to kill wifi when aborting a connection attempt on `!hasSeamlessWifiToggle` (because, again, it'll break UX, and also because it might run at very awkward times (e.g., I managed to go back to KOReader *between* a FM/Reader switch at one point, which promptly caused `UIManager` to exit because there was nothing to show ;p).
* NetworkMgr: Don't drop the connectivity callback when `beforeWifiAction` is set to prompt and the target happens to use a connectivity check in its `turnOnWifi` implementation (e.g., on Kindle).
* Android: Add an `"ignore"` `beforeWifiAction` mode, that'll do nothing but schedule the connectivity check with its callback (with the intent being the system will eventually enable wifi on its own Soon(TM)). If you're already online, the callback will run immediately, obviously. If you followed the early discussions on this PR, this closely matches what happens on `!hasWifiToggle` platforms (as flagging Android that way was one of the possible approaches here).
* NetworkMgr: Bail out early in `goOnlineToRun` if `beforeWifiAction` isn't `"turn_on"`. Prompt cannot work there, and while ignore technically could, it would serve very little purpose given its intended use case.
* KOSync: Neuter the Resume/Suspend handlers early on `CloseDocument`, as this is how focus switches are handled on Android, and if `beforeWifiAction` is `turn_on` and you were offline at the time, we'd trip them because of the swap to system settings to enable wifi.
* KOSync: Allow `auto_sync` to be enabled regardless of the `beforeWifiAction` mode on `!hasSeamlessWifiToggle` platforms. Prompt is still a terrible idea, but given that `goOnlineToRun` now aborts early if the mode is not supported, it's less of a problem.
9 months ago
hugleo 7ecd94b26a
Bump LuaJIT for Onyx Poke5 support (#10917) 9 months ago
Serge Baranov 55daf9fbf4
Bump LuaJIT (#10914)
New device: Onyx Boox Palma
9 months ago
hius07 684fc22ffc
TextViewer: font size (#10911) 9 months ago
NiLuJe 2b13cd7dcb
Device: Make sure `input_no_key_repeat` survives a suspend/resume cycle (#10904)
I somehow assumed the repeat state snapshot used for restore was taken *after* disabling repeat, but obviously not ;o).

Fix #10902
9 months ago
Frans de Jonge f3d959a324
Bump base (#10909)
Mainly to get zlib to build, also includes some third-party bumps.
9 months ago
Wim de With 1773b62ce8
Check for Noto Emoji font in UI fallback fonts (#10908) 9 months ago
hius07 be125af949
Actions for Go to first/last bookmark (#10899) 9 months ago
Frans de Jonge 1f03e76aea
[doc] Building: add libtool-bin to Debian/Ubuntu prerequisites as a workaround for ancient libzmq (#10897)
Fixes #10896.

It's because of an extremely old version of libzmq, fixed since <https://github.com/zeromq/libzmq/pull/1497>.
9 months ago
poire-z f78a8c5315
Custom metadata: update Statistics db on metadata change (#10894) 9 months ago
hius07 3f677a7fdd
copt/kopt options: deduplicate some defaults (#10893) 9 months ago
hius07 108d87742a
Custom metadata: fixes (#10889) 9 months ago
dependabot[bot] df19c2a0da
Bump actions/checkout from 3 to 4 (#10890)
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
9 months ago
hius07 cc82ead981
copt deduplicate: font settings (#10885) 10 months ago
hius07 6efcf96b75
copt deduplicate: font_size, visible_pages (#10882) 10 months ago
hius07 7d626456a3
copt deduplicate: render_dpi, margins, rotation_mode (#10880) 10 months ago
hius07 73378cd9d7
copt deduplicate: line_spacing (#10768) 10 months ago
hius07 5e74f29fba
copt deduplicate: embedded_css, embedded_fonts (#10876) 10 months ago
hius07 e4ba8c7909
Custom metadata: event (#10869) 10 months ago
NiLuJe 2a79ad918c Bump android-luajit-launcher
https://github.com/koreader/android-luajit-launcher/pull/434
10 months ago
NiLuJe 6d9d3ee8c2 Bump base
https://github.com/koreader/koreader-base/pull/1649
https://github.com/koreader/koreader-base/pull/1648
10 months ago
NiLuJe ba5c7df0db DocSettings: Minor cleanup following #10861
Allows me to fix an old pet-peeve of mine re: the terrible variable
names used in there ;o).
10 months ago
hius07 f46f341b9b
Reset settings/cover/metadata separately (#10866) 10 months ago
hius07 cbcf03b1af
Exporter: custom metadata (#10874) 10 months ago
hius07 912ae156f7
copt deduplicate: smooth_scaling, nightmode_images (#10801)
Main discussion in #10763.
10 months ago
hius07 a767ad44db
PDF contrast: incorrect set by a gesture (#10798) 10 months ago
NiLuJe 4cc620b702
VirtualKeyboard: Revamp visibility handling (#10852)
Move as much of the state tracking as possible inside VirtualKeyboard itself.
InputDialog unfortunately needs an internal tracking of this state because it needs to know about it *before* the VK is shown, so we have to keep a bit of duplication in there, although we do try much harder to keep everything in sync (at least at function call edges), and to keep the damage contained to, essentially, the toggle button's handler.

(Followup to #10803 & #10850)
10 months ago
NiLuJe 4d620d9fd2
Device: Try harder to properly setup the charging LED on plug/unplug (#10855)
By debouncing the call and delaying it some more.
10 months ago
zwim bedd422669
AutoSuspend: Handle a few corner-cases better (#10797)
* Enforce a minimal standby timer for the first standby after a resume.
* On Kobo, sleep a bit before standby.

This aims to alleviate race conditions causing visible refresh glitches on sunxi, especially when using an extremely low standby timer (i.e., below the defaults).
10 months ago
hius07 ed2ea6803f
Custom metadata (#10861) 10 months ago
poire-z 0ef7729678
bump crengine: revamp CSS 'inherit' handling, other fixes (#10865)
Includes:
- SVG: <epub:switch/case/default>: accept SVG
- Text: don't adjust spacing of leading no-break-space
- Block rendering: fix "break-before: always" sometimes not ensured
- Styles: parse <style> as white-space: pre
- CSS: revamp 'inherit' handling, now fully per CSS specs
- CSS/colors: handle 'transparent' as a real color
Also includes:
- android: forward play and pause to the system
10 months ago
Wim de With 599a3034ca
NetworkManager: Decode SSID to UTF-8 (#10864)
wpa_supplicant returns all non-ASCII SSIDs as raw bytes in the form
\x0a. We interpret these bytes as UTF-8, and make sure that all invalid
characters are replaced with a �.
10 months ago
hius07 1ef7821b66
getProps: centralize 2 (#10837)
Centralize and optimize handling document properties.
10 months ago
Frans de Jonge 38d468f36c
Update translations for v2023.08 (#10851) 10 months ago
poire-z 9d5d5d4a30
InputText/InputDialog: fix keyboard issue (#10850)
Fix issue introduced by 976aaf5f: with full screen
text editor and hiding the keyboard, a tap in the
text area would show a new keyboard hiding the
buttons allowing to save/close the text editor,
getting us stuck there.
10 months ago
NiLuJe a736a3ebe0
Calibre: Add reading status to the metadata search popups (#10842)
We're getting old, remembering whether you've actually read stuff is hard ;).
10 months ago
Frans de Jonge ea433d78dd
[i18n] Revert Unicode escape sequence to utf-8 bytes for translation (#10845)
Cf. <https://github.com/koreader/koreader/discussions/10774#discussioncomment-6618167>, reverts #10769 for direct use in translations.
10 months ago
NiLuJe e7e0d2edb6
Kobo: Disable HW inversion on MTK (#10841)
Apparently, that doesn't quite work...

Followup to #10719
10 months ago
NiLuJe 6fa8e1d2fd
KOSync: Set sane socket timeouts properly (#10835)
An attempt was made in the original code, but the whole thing was designed in the hope of actually switching to turbo, so it was super janky without it.
Anyway, we now actually have a sane way to set socket timeouts, so, use that, and set them very tight for now.

This is fairly critical right now, because the server is down, and the default timeouts are ~30s. That happens to be *above* the debounce threshold, so you can't even hope for that to help you. Meaning, right now, you get a 2 * 30s block on resume with auto sync. That's... Very Not Good(TM).

That becomes a single 2s one after this.
10 months ago
NiLuJe 21de1a832e Main: Move the G_reader_settings flush to Device:exit
I don't trust PB's input.closeAll not to be doing something wonky that
would trip an early abort.
10 months ago
NiLuJe d1bbdb5dad PocketBook: Run Generic:exit *last*
Makes more sense this way.

Re: #10828

Also clarify the UIManager "no dialogs left" message, and drop the
return value, as it's meaningless, we just want to break & return.
10 months ago
hius07 33467098dd
filemanager: minor tweaks (#10821) 10 months ago
NiLuJe 910b67d35b NetworkListener: Only disable Wi-Fi on suspend on hasWifiManager platforms
Assume the host system does things right otherwise.
Should be sane on Kindle
On PocketBook, who knows, but assuming the device actually suspends,
that should effectively kill our keepalive
Irrelevant for Android, as we skipped it because the wifi toggling
methods are interactive.
10 months ago
NiLuJe d52e0a0dfe NetworkManager: Always flip wifi_was_on when interactively tearing down Wi-Fi
Fix #10823

(For reference, we *enable* wifi_was_on no matter *how* wifi is enabled,
but we only toggle it off when it's killed by a *direct* user interaction,
the intent being that if *something* non-interactive enabled wifi,
you'll probably silently need it on resume too).
10 months ago
hius07 f4f5af5f2d
Exporter: fix order for Readwise (#10824)
Exported highlights are good sorted by position, page numbers could be wrong.
Closes #10817.

For reference: https://readwise.io/api_deets
10 months ago
NiLuJe 7e628958b6 Misc: review pass for #10803 10 months ago
NiLuJe 6d47b3241c NetworkManager: Fully tear down WiFi on aborted connection attempts
The proper connectivity check did it, now every early or not so early
abort do.
10 months ago
NiLuJe 5146358605 NetworkManager: Allow backends to report connection failures early
As early as turnOnWifi.
Implement it on hasWifiManager platforms, preventing useless
connectivity checks to run when they're obviously never going to work
because you're out of range of your AP.

Also implemented a flag to notify the backend if the connection attempt
was interactive or not.
Right now, interactive is extremely restricted, it basically means the
menu checkmark, or a gesture.
The intent being that for stuff like the beforeWifiAction framework, we
don't want the backend to spawn extra UI.
Specifically, for hasWifiManager platforms, we no longer spawn the AP
scan list on failure unless the caller was interactive.

TL;DR: beforeWifiAction is now much less obnoxious when you're obviously
not able to connect.
10 months ago
NiLuJe bc235b3d19 NetworkManager: Make goOnlineToRun abort on tap (anywhere)
Assuming it's past the blocking portion of this endeavor, if any.

Fix #10807
10 months ago
NiLuJe a15fc0e9a5 Revert "Test: Fiw readerhighlight test"
This reverts commit c23c422f62.
10 months ago
NiLuJe 66d2930e5e Revert "Tests: "Unbreak" some more tests"
This reverts commit f4dd0f92b2.
10 months ago
NiLuJe e63c613626 Tests: "Unbreak" some more tests
This is horrible and needs to burn in a fire.
10 months ago
NiLuJe ae7e2f14aa Test: Fiw readerhighlight test
With actual ReaderUI teardowns, and fix the overlapping highlight
handling, so we can drop the nocov flags
10 months ago
NiLuJe 55534fcec2 ReaderView: Keep the terrible readerhilight_spec test happy
This thing is a horrible mess.
10 months ago
NiLuJe 0686ee594c ReaderView: Make sure we unschedule the hinting task on actual teardown.
onCloseDocument is way too early, if another, later onCloseDocument
handler trips a ReaderUI repaint, a new task will be scheduled, and that
one will never be cancelled, resulting in it running post-teardown,
crashing horribly.

This was for instance very easy to trigger via KOSync.

Fix #10806
10 months ago
NiLuJe 976aaf5f53 InputText: Actually flip the is_keyboard_hidden flag properly
MultiInputDialog does a close -> show in the same callback,
so if we don't actually keep it in sync with the actual state, we lose
the keyboard and essentially softlock the UI, which is Very Bad(TM).

NOTE: InputDialog has its own keyboard_hidden flag, and it looks...
fairly nightmarish.
10 months ago
NiLuJe 7a5216a0a6 NetworkManager: Plug a logic hole in the beforeWifiAction logic
The *runWhenOnline wrappers may not pass a callback when connected but
not online yet, so we need nil guards all the way down ;).

Re: #10806
10 months ago
NiLuJe 22678ee5ff Misc: Review pass on the previous commits
* Unbreak the Device test
* Rename a few things
* Tweak a few comments
10 months ago
NiLuJe f44f92a864 Kobo: Run Generic:init dead last, like every other platform
This is semantically more correct, and should prevent platform-specific
weirdness if an ordering concern ever comes up (e.g., like it did for
the viewport stuff).
10 months ago
NiLuJe 71378b0b50 Startup: Move Device ctor/dtor shenanigans to the actual Device
constructors and destructors

No need to leak implementation details in there.
10 months ago
NiLuJe e9a84ca434 Reader: Move the "print exit code in a file" hack to the PB
implementation, where this mess belongs.

No other platform relies on this (because, spoiler alert, InkView \o/).
10 months ago
NiLuJe 4acf131071 ReaderActivityIndicator: Oh god, my eyes, they buuuuurn.
Make this a real boy, with a transient lipc handle.
And get rid of the insane 1s sleep on affected ReaderView paints,
because ouchy.

This is completely deprecated anyway, so this is entirely pointless,
and mainly to prevent implementation details from creeping into
reader.lua.
10 months ago
NiLuJe ac1358e0f0 Menu: Actually hide PlatformNav if !hasKeys
Returning {} just leads to a present but blank entry

(Also, nobody notice that the capcheck was wrong inside platnav anyway ;p).
10 months ago
NiLuJe f4aca567ef Kobo: Allow disabling key repeats
Re #10743

Note that this only makes faulty switches slightly less annoying: for a
stuck switch, instead of a string of page turns, you'll get a single
missed page turn on the tap that actually releases the stuck contact...
10 months ago
NiLuJe ddf6f3616c Menu: Amend the previous commit
Kindle animations are Reader only ;).
10 months ago
NiLuJe 6283674c47 Menu: Split off "Page Turns" submenu in two sections
The Kindle swipe animations & physical key inversion apply *everywhere*,
so we need this accessible in the FM, too.

Move it to Navigation, which is where it already was on !Touch devices.
10 months ago
NiLuJe 0db7509fe2
Kobo: Fix the input translation on the Elipsa 2E (#10802)
Apparently it mirrors on the Y axis. Or the panel vs. touch rotations are wonkier than expected or ...something.
10 months ago
hius07 0499b5109a
Profiles: fix New profile with current pdf document settings (#10778) 10 months ago
hius07 1544ca28af
FileManager: truncate left in selected files list (#10783)
Truncate the path to view the filename in full. Closes #10760.
11 months ago
poire-z cc391514b5 Translator: explicite wording above each section 11 months ago
poire-z e641c89eee View HTML & CSS: fix minor spacing issues 11 months ago
zwim ac143ca9d6
Add forgotten f:close() (#10796) 11 months ago
yparitcher f3fc2ac353 do not allow clipboard to be nil
we use it as text anyway and test if it is the empty string also.

this fixes a crash in viewhtml when holding a selector with an empty clipboard, we try to contcate getClipboardText which is nil (on !SDL & !Android)
11 months ago
melyux 493a60faef
Exporter: Replace newlines with commas in HTML, Readwise (#10788)
Resolves #10755.
11 months ago
NiLuJe 38aa74ec7c
KOSync: Only nag about "Action when Wi-Fi is off" when toggling (#10785)
auto-sync *on*

Fix #10784
11 months ago
NiLuJe ccd29d36c0 Bump base
https://github.com/koreader/koreader-base/pull/1646
11 months ago
NiLuJe 4a0fceeffd OTM: Don't keep it in scope forever.
First, don't require it so as not to pin it in package.loaded; just use
dofile
And, second, do that in an explicitly scoped block because nothing in
there should persist, when we named it "one time", we meant it ;p.
11 months ago
zwim 2ba3353d8a
[AutoWarmth] use CheckButton for night mode toggle (#10762) 11 months ago
NiLuJe 724db0b924
Statistics: Close the DB handle in the vacuum OTM block (#10775)
Followup to #10749
11 months ago
weijiuqiao 416237e526
Statistics: Attempt to prevent rampant replication of null id_books (#10749)
And add an OTM block to do a cleanup pass on existing DBs (which might take a while if you're severely affected, because we've seen reports of DBs north of 2GB).
11 months ago
poire-z 626864f856 [chore] replace utf8 bytes with Unicode escape sequence 11 months ago
poire-z 7bff61150a util.prettifyCSS(): do better with more complex CSS 11 months ago
poire-z b03992c077 ViewHTML: fix content messed up when consecutive nbsp
Fix bug in replace_in_html() when consecutive
matches.
Also ensure content after leading indentation is not
wrapped on the next line.
11 months ago
poire-z 08e9c27a32 Wikipedia EPUBs: re-add ToC in content
The ToC is no longer in the HTML we get from the
Wikipedia API. So, add it ourselves.
Also, as we can't get anywhere the Wikipedia localized
string for "Contents" (ie. "Sommaire" in French), use
thick <HR> to mark the start and end of this ToC.
11 months ago
ichnilatis-gr 583ae40e46
Greek keyboard: update to the new keyboard layout (#10734) 11 months ago
NiLuJe 9c8e55b3e5
Kobo: Yet another attempt at working around the hangs on the latest NXP boards (#10771)
* Notification: Drop the fencing from #10083; it never actually helped, and had subtle side-effects we could do without.
* VirtualKeyBoard: Flash on close, otherwise, some of the fast refresh glitches may be burned into the working buffer until a flash. Making sure we flash ourselves prevent it from sticking around on the page ;).
* util: Move `writeToSysfs` to base (i.e., `ffi/util`), as we need it there (and it actually makes more sense there anyway ;p).
* Bump base for https://github.com/koreader/koreader-base/pull/1645, which is where the actual workaround (hopefully) lives.

Re #8414, #9806, #10558
11 months ago
NiLuJe 689d9e2976
Kobo: Initial Elipsa 2E support (#10719)
Untested, but mostly everything should be there ;p.

Bumps base for MTK support (https://github.com/koreader/koreader-base/pull/1642)
11 months ago
NiLuJe ef0bcbad3f
ReaderFooter: Don't reset the initial position marker like a cowboy (#10767)
Take a more tactful approach instead, should hopefully prevent issues like in https://github.com/koreader/koreader/pull/10114#issuecomment-1658913661
11 months ago
hius07 7164bc2bd9
Exporter: fixes (#10765) 11 months ago
NiLuJe 9ceb07a4c6
Kobo: Unbreak Aura One support (#10758)
Regression since #10728, Fix #10757
11 months ago
hius07 b36ccc7249
QuickMenu: add long-press on profile (#10671) 11 months ago
clach04 500eadf895
[plugin] Wallabag: strip trailing slashes from server URL (#10715)
Fixes #9187.
11 months ago
poire-z 4fa278ff0b Wikipedia: handle images in changed Wikipedia HTML
The HTML we get from Wikipedia has recently changed,
which has caused images to not be detected and shown
in DictQuickLookup, and to be missing or ugly in
saved EPUBs. Update our code and CSS to display them
again.
11 months ago
poire-z eecdac930e View HTML: Show matched rules: ignore style & tweaks on long-press
On long-press on the "Show matched stylesheets rules"
buttons, ignore those from our epub.css/html5.css and
from styletweaks, which may make investigating a book
style simpler without their noise.
11 months ago
poire-z e6d21ad371 bump crengine: CSS caption-side, SVG, CSS and table fixes
Includes:
- SVG: support embedded base64 images when no doc
- epub.css: hide <nav hidden="">
- Tables: fix possible specified width overflow
- Tables: fix caption and table width interactions
- CSS/Tables: add support for "caption-side: top/bottom"
- gatherNodeMatchingRulesets(): avoid messing document cache
- gatherStylesheetsMatchingRulesets(): add with_m_stylesheet param
11 months ago
hius07 47dae6c5dd
Dictionary: fix Fuzzy search setting appearance (#10721) 11 months ago
hius07 a311de6d8e
readerview: do not read/save inapplicable doc_settings items (#10710) 11 months ago
NiLuJe 21949c5521 Kobo: Apply the AW99703 smooth ramp off fix to all affected devices
e.g., at least the Clara 2E features the same controller & driver,
but it is likely found on a few other models.
11 months ago
NiLuJe 79b836b726 Build: Don't create the legacy history dir
Fix OTAs on fresh installs (via
https://github.com/koreader/koreader-base/pull/1643)
11 months ago
NiLuJe f5e871d4ed NetworkInfo: Never show the token index
Now that I've seen it in action on a device, it's mostly useless, and,
if anything, confusing, because it's not static.
11 months ago
hasezoey d087710140
Autowarmth: Change strings "set" / "unset" to "turn on" / "turn off" where appropriate (#10733) 11 months ago
Martín Fernández 0d40d9c99d
bump luajit-launcher: more devices (#10732) 11 months ago
hasezoey 618f065de4
DeviceListener:onToggleFrontlight: add notification of "unchanged" (#10727) 11 months ago
hasezoey b1109a729e
Android: call "_decideFrontlightState" to keep "is_fl_on" in sync (#10731) 11 months ago
hasezoey 14347f0c18
Android - lights: return to previous level on cancel (#10726) 11 months ago
NiLuJe d350418367 Env: Actually use the full ffi.load signature in our overload
We *do* actually use the optional global argument sometime...
Namely, for librt in ffi/posix_h.
c.f., https://github.com/koreader/koreader-base/pull/1586#issuecomment-1637184501
11 months ago
NiLuJe 8e063d5b39 macOS: Comment on the expected gettext behavior
Someone who can actually run otool will have to confirm whether
the intended behavior (glib being linked against a static gettext copy)
is actually what's happening...
11 months ago
NiLuJe 17bdd56e02 Never export LD_LIBRARY_PATH
It's been made redundant by the RPATH changes

The only platform that gets the dubious honor of actually needing an
LD_LIBRARY_PATH is PocketBook, because of InkView.

Co-authored-by: Benoit Pierre <benoit.pierre@gmail.com>

Bump base to pull in the aforementioned RPATH changes ;).

https://github.com/koreader/koreader-base/pull/1638
11 months ago
poire-z 8a240dd635
bump LunaSVG, crengine: add support for .docm (#10702)
Includes:
- bump LunaSVG: intermediate upstream bump, cleanup
- bump LunaSVG: minor upstream tweaks
crengine:
- DocX: add support for similar DocM format
- LVStyleSheet: fix LVCssDeclaration::getHash()
- CSS parsing: accept Unicode values for ID and classnames
- update for Harfbuzz 8, fix some compiler warning
Also fix input not restored when loading failed,
and KOReader not able to exit.
11 months ago
NiLuJe dacc3c2f72
NetworkManager: Bypass isOnline on !hasWifiToggle platforms (#10697)
Followup to #10669, fix #10694
11 months ago
Frans de Jonge 3bec20fc25
NetworkManager: improve clarity for chatty framework user-facing explanation (#10696)
See https://github.com/koreader/koreader/pull/10669/files#r1263385527
11 months ago
hius07 3e43a21cf2
ArchiveViewer: view files (#10683) 11 months ago
NiLuJe 890dc81081 Bump base for webp rpath fixes
https://github.com/koreader/koreader-base/pull/1637
11 months ago
NiLuJe f61dc0cdb5 macOS: We ship our own webp, don't pull brew's
c.f., https://github.com/koreader/koreader/pull/10687#issuecomment-1633450159
11 months ago
NiLuJe 9e1197c489
LuaJIT update (#10689)
* Bump base (https://github.com/koreader/koreader-base/pull/1636)
* Bump android/luajit-launcher (https://github.com/koreader/android-luajit-launcher/pull/424)
11 months ago
Glen Sawyer 73c4f09a88
FileChooser: re-add file sort by "last date read" (#10682)
Revert bits from eb299c30.
11 months ago
poire-z d0e705a582
Font list menu: allow sorting by more recently set (#10662)
- Add a toggle in Font settings> allowing showing font
  ordered by most recently selected (long-press on
  it allows clearing this history).
- Keep in G_reader_settings a list of known fonts, so
  we can notice newly added user fonts, and put them
  at the start of the most recently selected. Show
  these new fonts with a symbol in the menu.
- TouchMenu: allows for a flag to trigger menu
  refresh when going up.
11 months ago
Frans de Jonge c72b3dbae4
Bump base: update to HarfBuzz 8.0.1 (#10688)
https://github.com/koreader/koreader-base/pull/1635
11 months ago
NiLuJe 42235266e0
Ship all libwebp depencencies — Unbreak all the things ;o) (#10687)
https://github.com/koreader/koreader-base/pull/1634
11 months ago
Christian Kilb 6d7dccda13
[PocketBook]Add support for InkPad Color 2 (#10685)
Co-authored-by: Christian Kilb <christian.kilb@gmail.com>
11 months ago
hius07 e1ed3a71c5
Style tweaks: notification on toggling style tweak with a gesture (#10674) 11 months ago
NiLuJe d0899f24e3 Bump android/luajit-launcher
https://github.com/koreader/android-luajit-launcher/pull/422

https://github.com/koreader/android-luajit-launcher/pull/423
11 months ago
NiLuJe eb4a82a48b Bump base
https://github.com/koreader/koreader-base/pull/1631

https://github.com/koreader/koreader-base/pull/1633
11 months ago
NiLuJe d57325aaf6
NetworkManager: Enable "before wifi" action support on every hasWifiToggle platform (#10669)
* Enable before_wifi_action & after_wifi_action on hasWifiToggle platforms (which is basically all of 'em except naked SDL).
* Decouple restoreWifiAsync from hasWifiManger, because we can do that on other platforms (namely, Kindle. Probably PB, too, but WiFi is already a mess there, and I can't test it).
* Implement restoreWifiAsync on Kindle.
* Properly flag rM as hasWifiManager & hasFastWifiStatusQuery, because it is actually both of those (it uses our wpa_supplicant backend).
* Update the KOSync checks to take these changes into account, to properly disable auto_sync if necessary.
* Really made the Network* event signaling consistent. For realz this time.
* In an effort to make the whole beforeWifiAction framework somewhat usable there, we now assume connectivity is always available on !hasWifiToggle platforms...
12 months ago
Benoit Pierre 41a07a2a66
crengine: fix clang release build (#10680)
https://github.com/koreader/crengine/pull/522
12 months ago
Benoit Pierre 20bd7f9a6d
Bump base (#10677)
* https://github.com/koreader/koreader-base/pull/1617
12 months ago
hius07 b1c90260e4
DropBox: unify error messages (#10666) 12 months ago
hius07 b33971b92d
OPDS fixes (#10657) 12 months ago
Benoit Pierre d849d0e657
Bump base (#10673)
* https://github.com/koreader/koreader-base/pull/1624
* https://github.com/koreader/koreader-base/pull/1630
12 months ago
Benoit Pierre 3ac8a67c6d android: update to use NDK 23c 12 months ago
Benoit Pierre ded5558b88 android: speed up development cycle
Don't recreate `assets.7z` from scratch every time, and also
ensure it's reproducible to avoid busting gradle's cache.
12 months ago
Benoit Pierre 7e98241b7e kodev: add `dlopen` traces to android logcat 12 months ago
NiLuJe 4194244951
GestureDetector: Make two_finger pans & swipes report the same sort of data than their standard brethren (#10649)
Namely, that swipes report the gesture's *starting* coordinates as `pos` (for hit-detection purposes), while pans (and basically everything else) use the *lift* coordinates (as well as provide a relative vector).

In each case, we now also provide a separate `start_pos`/`end_pos`, in case handlers need finer-grained data (e.g., anchoring UI elements to a swipe's lift coordinates).
12 months ago
Frans de Jonge b4000d84a5
Update translations for v2023.06.1 (#10665) 12 months ago
hius07 86f4b3c18d
QuickMenu: anchor to gesture position 3 (#10646) 12 months ago
ElimGarak1 4b475bee04
Recognize PocketBook InkPad 4 with new firmware (PB743G)/(PB743g) (#10655)
Extended addition of PocketBook InkPad 4 with firmware later U743g.6.8.885 (PB743G) because its device ID was corrected from PB743g to PB743G by firmware U743g.6.8.1719.
12 months ago
Frans de Jonge 2b18a7e90f
[i18n]KOSync: use already translated "Not available" instead of "Unavailable" (#10648)
Cf. https://github.com/koreader/koreader/pull/10605#discussion_r1252639047
12 months ago
Martín Fernández 5336e2f11d
[ota]: deprecation check & notice (#10639) 12 months ago
Benoit Pierre 346d8eb83e
Decrease fonts memory use (#10618)
Instead of opening the same font multiple times for each different
size (multiple face instances), share one face instance and create
multiple size instances.
12 months ago
Alistair Francis fc89b1affa
Remarkable: support the mainline touchscreen (#10585) 12 months ago
poire-z a4720b44cd Text search: various Kopt search fixes
- Properly parse input text for words (the previous
  code wasn't working with Greek letters)
- With multiple words search, don't allow "substring
  matching" for words in the middle
- Remove support for Lua pattens, so to get proper
  substring matching (as we have with cre text search)
12 months ago
poire-z dffc2404ca Text search: normalize input text
So that Unicode NFC normalization can combine diacritics
entered via our keyboard with their base into their
canonical form (we can expect books text to be normalized).
12 months ago
hius07 6decb71521
ReaderPaging: do not store zeroes in page_positions (#10602)
Page_position value depends on the view mode parameters, but we can assume that zero is a frequent case.
12 months ago
Benoit Pierre f661726fef tests: fix listing tests with `busted --list` 12 months ago
Benoit Pierre fdd645af73 tests: minor cleanup 12 months ago
yparitcher 27f7f1d5a9 QuickMenu: anchor to gesture position
Fixes: #10631
12 months ago
Benoit Pierre 4b9de16c51
bump base: libk2pdfopt, fix ffi cdecls, improved ccache (#10638)
- https://github.com/koreader/koreader-base/pull/1619
- https://github.com/koreader/koreader-base/pull/1623
- https://github.com/koreader/koreader-base/pull/1628
12 months ago
hius07 dee1c902ec
ReaderUI: minor optimization (#10634) 12 months ago
hius07 eb299c300d
FileChooser: Sort by date (#10627) 12 months ago
hius07 adfbbd9903
Disabled touch input: always active gestures (#10624) 12 months ago
NiLuJe 08dd97384c
KOSync: Clarify settings, plus refactor & fixes to make "auto-sync" more reliable (#10605)
Fix: #10539, and for context #6489, #6733, #6534

Reorganize and reword most of the settings to make it clear what actually ties into auto sync, and what doesn't. (Specifically, what happens when a pull attempts to sync forward or backward has nothing to do with auto sync, it applies in all cases; while the periodic sync *does* require auto sync).

The main point of contention, though, is that auto sync will now *always* attempt to setup network connectivity (i.e., on resume/suspend/close). Periodic sync will *not* though (the intent being that, if you use periodic sync, you're relying on the activity check to actually keep wifi on at all times)).

Since this may lead to a large amount of nagging about wifi toggles on devices w/ NetworkManager support, it is now *disabled* by default on those devices. (And given that it wouldn't have worked because of the lack of connectivity, that doesn't really make any practical difference ;p).

Additionally, given the fact that there's no way to make this behavior viable if the "before wifi" action is left at its default of "prompt", this feature now *requires* that to be set to "turn_on" (on devices where it can, of course); attempting to toggle it on will warn about that if necessary.
This change is retroactive (OTM).

Includes an assortment of fixes and cleanups, including migrating to the new LuaSettings API, which is why there's no longer a smattering of superfluous flushes.
12 months ago
NiLuJe 7fddb1dedf Bump android/luajit-launcher
https://github.com/koreader/android-luajit-launcher/pull/417
https://github.com/koreader/android-luajit-launcher/pull/420
12 months ago
NiLuJe 05bbbafefd Bump base
https://github.com/koreader/koreader-base/pull/1622
12 months ago
NiLuJe 752f23c85e
Input: Be more through in resetState (#10612)
Drop the actual slot data storage, in addition to the active references
12 months ago
Frans de Jonge b24059b2cb
Update translations for v2023.06 (#10626) 12 months ago
kosmoski 72c1905110
Add 'Content-Length' header to WebDAV upload request (#10567)
Co-authored-by: wojtek <wojtek@pop-os.localdomain>
12 months ago
hius07 5db765e4e5
correct parsing unzip output (#10625) 12 months ago
Frans de Jonge b8af326836
[i18n,plugin]Statistics: differentiate different types of 'reset' (#10621)
References <https://github.com/koreader/koreader/discussions/10479>.
12 months ago
NiLuJe 2f1435f1a7 Bump base
https://github.com/koreader/koreader-base/pull/1616
https://github.com/koreader/koreader-base/pull/1618
https://github.com/koreader/koreader-base/pull/1621
https://github.com/koreader/koreader-base/pull/1627
12 months ago
NiLuJe 5c8143d9ac SDL: Fix initial position & size
Also keep sdl_window in sync with enforced dimensions on the emu.
12 months ago
NiLuJe 4ae25794a8
Add a config file for the Lua LSP (#10611) 1 year ago
NiLuJe 55869b82cb
PowerD: Unbreak frontlight toggle notifications outside of Kobo (#10597)
Fix #10588
Regression since #10305

While we're there, rejig the FL toggle callback shenanigans so that implementation details don't leak through to *other* implementations.
(i.e., leave the Kobo mess in Kobo land, with only a minimal impact on the public API and its implementation).
1 year ago
hius07 4a775b03f1
History: update short date/time correctly (#10601)
Further to #10594, fix updating the mandatory.
1 year ago
hius07 45428dda49
History: shorten date/time field (#10594) 1 year ago
hius07 54ab0a04f8
Archive viewer (#10584) 1 year ago
Frans de Jonge 64f778d97d
PocketBook: add alternative definition for PB840 (#10582)
Cf. #10579.
1 year ago
hius07 eef99dafd6
Overlap: fix detecting reflow mode (#10576) 1 year ago
hius07 1d4e256c82
credocument: epub alternative mimetype (#10575)
Used by Flibusta OPDS.
1 year ago
yparitcher 1f62091b73
HtmlboxWidget: pairs->ipairs (#10574)
thanks @poire-z
Closes: #10565
1 year ago
zwim 3e3cc690f3
Fix regression after 10557 (#10570) 1 year ago
Benoit Pierre f7d5828f08
persist: fix possible leak on error (#10568) 1 year ago
hius07 46933035c5
Open with: images (#10561) 1 year ago
Benoit Pierre c95b17410b
tests: fix EPUB test (#10566)
Otherwise the test fail in my environment because of some user fonts.
1 year ago
Benoit Pierre bb7ae20697
kindle: show require error trace (#10569) 1 year ago
Frans de Jonge f7d633cc3d
readerhighlight_spec: add #nocov to some highlight tests (#10559)
The problem stems from <https://github.com/koreader/koreader/pull/10492> but we need the translations to go through.
1 year ago
hius07 3eee5dd14c
OPDS: view book cover (#10555)
(1) New "Book cover" button in the download dialog.
(2) Detect book that can be borrowed only (Internet Archive).
There is no support for borrowing, just showing a disabled button.
(3) Fixed some libraries, all of them work except Gallica.
1 year ago
zwim c9e1b76a14
[plugin] AutoWarmth: Fix nightmode issue on non natural light devices (#10556)
This should fix #10491.
1 year ago
zwim 1ce1fe1780
[plugin] AutoWarmth: Fix resume and frontlight issue (#10557)
Fixes a regression introduced in #10426, when suspending during night and resuming during daylight.
1 year ago
hius07 fc7181419f
CoverImage: custom cover support (#10553) 1 year ago
poire-z 976284e44d
bump crengine: minor NCX ToC and createXPointer(pt) fixes (#10554)
Includes https://github.com/koreader/crengine/pull/518 : 
- EPUB NCX ToC: don't ignore items with invalid targets
- ldomDocument::createXPointer(pt): handle some edge case
  Closes #10505.
1 year ago
hius07 23feb0c9cd
Bookmarks: filter by highlight style (#10549) 1 year ago
hius07 01e6593dd4
File popup dialog buttons (#10537) 1 year ago
yparitcher f7fd1ee0ce
bump base: expose the kindle mxcfb temperature constants (#10548) 1 year ago
poire-z 18cd6b79d8
BookMap: fix help text wording (#10544)
See https://github.com/koreader/koreader/pull/10530#discussion_r1217635869.
1 year ago
poire-z 04425b74fc BookMap: add "Overview" mode
Add a restricted but convenient mode showing BookMap like
in initial view, while still allowing chapter levels to be
tweaked. This allows getting back to this view with another
gesture to see the overall progress in the book, while
still having the normal BookMap in flat mode acting as
an alternative ToC.
Available as an action to associate to a gesture, and
with long-press on the "Book map" menu item.
1 year ago
poire-z b7078caf29 BookMap: add option "10-page markers"
Allows hairy-boxes amateurs to get small markers (under
the baseline) every 10 pages (with -/+ to get small or
medium markers, with optionally smaller ones every 5 pages).
1 year ago
poire-z 5f4cd9df00 BookMap: add option "Page browser on tap"
Enabled by default, to get the current behaviour.
Disabling it allows people with small fingers to jump
directly to that page in Reader.
1 year ago
poire-z ac159928dd BookMap: nicer alignment of the vertical page number
Align page numbers (the vertical ones, on the left of
a BookMapRow) on the baseline of the BookMapRow, where
page slots rise from.
Also tweak the page number spike below the baseline used
in PageBrowser.
1 year ago
poire-z fa68c1e488 PageBrowser: smaller margins if no thumbnail page numbers
Also fix possibly unbalanced left and right margins.
1 year ago
hius07 38bb4a1983
QuickMenu: scrollable (#10528) 1 year ago
hius07 6cb245f86f
MoveToArchive register to Dispatcher (#10524) 1 year ago
hius07 5405fff837
Profiles: disable inapplicable actions in QuickMenu (#10500) 1 year ago
Frans de Jonge 1c0905bee2
Update translations for v2023.05.1 (#10523) 1 year ago
NiLuJe 2797a74a96
Kobo: Handle the newer Nia HW revision (#10520)
Fix #9590, closes #9591
1 year ago
hius07 66b661b726
Update readerhighlight_spec.lua (#10515) 1 year ago
hius07 1944f54aa0
Custom cover: fix (#10513)
Somehow have overlooked in testing.
Closes #10512.
1 year ago
hius07 a8244ddd23
FileChooser: fix legacy "Files and folders mixed" collate setting (#10514)
Closes #10509.
1 year ago
Glen Sawyer 128f205324
[plugin] Statistics: fix sync caused by semicolon in SQL comment (#10503)
Follow-up to <https://github.com/koreader/koreader/pull/10498>.
1 year ago
Glen Sawyer 2aa11b13ad
[plugin] Statistics: sync the `last_open` field (#10498) 1 year ago
yparitcher 8034858180 WakeupMgr: kinde: use a 90 sec proximity
sometimes my kindle fires the rtc alarm a tad too late so use a more generous timeout

the real fix would be to have validateWakeupAlarmByProximity return differently if the alarm is past or in the future, but i am too lazy
1 year ago
hius07 eadf76e992
Profiles: fix showing some bad values (#10495) 1 year ago
hius07 57f4ff2c68
ReaderHighlight: manage overlapped highlights (#10492) 1 year ago
hius07 177092243f
readcollection: fix mandatory for deleted files (#10488) 1 year ago
hius07 46578c0e12
documentregistry: fix DocSettings (#10478) 1 year ago
ElimGarak1 a18afc24b8
PB: Fix Inkpad 4 device id (#10481)
Turns out it's a lowercase G, and that matters ;).
1 year ago
Sunny 9e0f185174
rM: Work around LD_PRELOAD weirdness on forks on rM 2 (#10490) 1 year ago
roshavagarga d54c5acc11
PB: Add Inkpad 4 (#10468)
Fix #10444
1 year ago
NiLuJe 17d5934712
Notification: NOP conflicting event handlers from our base class (#10471)
Notification is a toast, so it doesn't stop event popagation.
If we don't disable thoses handlers inside Notification, we get spurious
duplicate handlers being fired ;).

Fix #10461
1 year ago
hius07 6c0bfb1c0a
FileManagerCollection: Fix crash on "Execute shell script" 1 year ago
hius07 919d67656d
readerhighlight: fix translator auto lang (#10469) 1 year ago
Frans de Jonge 710614752d
[doc, plugin] Exporter: manually specify module name (#10465)
Follow-up to <https://github.com/koreader/koreader/pull/10464>.
1 year ago
hius07 09425ea729
Exporter: fix comment for ldoc (#10464) 1 year ago
poire-z 011c168686
Wikipedia EPUBs: strip out invalid-XHTML <link> (#10462) 1 year ago
hius07 3d5775241d
Exporter: selected files (#10453)
Export highlights for selected files.
Having a button "Select all files in folder", it is easy to export the whole folder.
So, closes #10402.

To keep even number of buttons, added a feature "Show selected files list". May be useful to check selections before an operation. Just a sorted list, no titlebar or popup menu, tapping a file jumps to its folder.
1 year ago
NiLuJe 7e98b9de4b
PM: Minor refactor to suspend/resume code flow (#10426)
Make sure we only send Suspend/Resume events when we *actually* suspend/resume. This is done via the Device `_beforeSuspend`/`_afterResume` methods, and those were called by the *input handlers*, not the PM logic; which means they would fire, while the PM logic could actually take a smarter decision and *not* do what the event just sent implied ;).

(i.e., sleep with a cover -> suspend + actual suspend, OK; but if you then resume with a button -> input assumes resume, but PM will actually suspend again!).

Existing design issue made more apparent by #9448 ;).

Also fixes/generalizes a few corner-cases related to screen_saver_lock handling (e.g., don't allow USBMS during a lock).

And deal with the fallout of the main change to the Kobo frontlight ramp behavior ;).
1 year ago
hius07 7ab832081b
[plugin] Exporter: choose folder (#10448)
Default is still koreader/clipboard.
1 year ago
hrdl 20101baef6
pdfdocument: cache page text (#10450)
Updating the selection queries the same page tens of times per second. This prevents KOReader from keeping up with high-frequency input event streams.

Fixes #10443. Relies on koreader/koreader-base#1612.
1 year ago
Frans de Jonge 796574e5cd
Bump base (#10458)
* https://github.com/koreader/koreader-base/pull/1613
* https://github.com/koreader/koreader-base/pull/1612
* https://github.com/koreader/koreader-base/pull/1533
1 year ago
hius07 5f59ee6417
readerhighlight: fix translate (#10451)
Fix translation for books without "language" in properties (it is "", Translator expects nil).
Regression after #10438.
1 year ago
夏鲁豫 4c9231a8f8
[plugin] Exporter: add memos export (#10411)
Add an export method for memos, which originates from [flomo](https://flomoapp.com/) and now has an open-source self-deployment solution:[memos](https://github.com/usememos/memos)
1 year ago
Frans de Jonge b8c5ec7d42
Update translations for v2023.05 (#10452) 1 year ago
hius07 3dce41269d
Translator: translate current page (#10438) 1 year ago
yparitcher 946f7931ae bump android-luajit-launcher
Fix cutout on android 13

Closes: #9446
1 year ago
yparitcher f588edd9b1
mac: github actions macos-13 (#10404)
github no longer supports 10.15 so use 13 just to ensure the build does not break
1 year ago
NiLuJe ea2aeb917b Bump android-luajit-launcher
https://github.com/koreader/android-luajit-launcher/pull/416
1 year ago
NiLuJe 0ef741a9f7 Bump base
https://github.com/koreader/koreader-base/pull/1607
https://github.com/koreader/koreader-base/pull/1610
https://github.com/koreader/koreader-base/pull/1609
1 year ago
NiLuJe b989a6ff15
NaturalLightWidget: Fallback to previous values instead of `nil` (#10425)
Happens in case the InputText field is emptied by the user.
The backend code makes rather strong assumptions that it'll *always* get a number of of it ;).

Fix #10352
1 year ago
NiLuJe 5c353c2289
OTM: Attempt to deal with wonky config states (#10429)
Such states (not quite sure how you could actively get in such a broken state to begin with, mind you)  could lead to an attempt to load an old fontlist cache in an incompatible format.

Re: #9771
1 year ago
NiLuJe fdea39c2d6
Input: Leave EV_SDL mostly alone in inhibitInput (#10430)
It's mostly not used for actual input, but sideband stuff that we don't want to miss.

Fix #10427
1 year ago
NiLuJe 38727d882b
kodev: Catch TAG_SURFACE in the debug logcat filter (#10431) 1 year ago
poire-z fc81c7db24 DictQuickLookup: add button to show list of results
Add a left button to the title bar to show the list
of results as a popup.
Dictionary: tap or long-press on that button give
different view of the results.
Wikipedia: request 30 results instead of 20, so we
can show 15, 10 or 6 of them per page of that popup.
1 year ago
poire-z 8c8a032269 ButtonDialog: allow for step/page scrolling
With tall ButtonDialog with many rows, allows for a more
natural and readable scrolling without any truncated row
(like in Excel, we previously behave as web browsers).

ButtonTable:
- make the span and separator layout more explicite
- add some small horizontal padding when button text
  is centered
1 year ago
poire-z 7a95d11f07 ScrollableContainer: add support for step/grid scrolling
When the containee is row-based, this can ensure that when
scrolling with swipes, we get the a full row at top, and
that any truncated row at top or bottom is fully visible
after a swipe.
1 year ago
poire-z 0ee10e5049 Button: fix unstable height
The final height of a button could be different whether
a smaller font size or multiline end up being used.
Also fix 2-lines logical error making it less favored.
Also forward any lang property to the underlying text widgets.
1 year ago
poire-z 4fa7e718a2 TextBoxWidget:getFontSizeToFitHeight(): allow for more accuracy
By providing the font that will be used.
As it's more expensive, use only when really needed.
1 year ago
hrdl 552787db89
SDL: use render buffer size instead of window size (#10428)
Related to koreader/koreader-base#1610. Not sure if this is actually
needed, but for consistency the render buffer size should be used.
1 year ago
yparitcher a52a4158a9
Android: improve kodev run (#10418)
launch & uninstall the correct version

do not rely on setup_env

When paired with wireless debugging, this makes android debugging not such a pain.
1 year ago
poire-z d8b3878e8a
bump base/LunaSVG: minor upstream fixes (#10412) 1 year ago
hius07 8665929a5e
Exporter: correct access to doc settings (#10407) 1 year ago
zwim d718682502
Autowarmth: suntime.lua minimal documentation update (#10405) 1 year ago
hius07 b4f453eb58
Page overlap: add "Horizontal line" marker (#10401) 1 year ago
Frans de Jonge 46f0bba74b
ReaderLink: fix capitalization typo in "Go Forward" (#10399) 1 year ago
Frans de Jonge 3da24046c0
[plugin] Exporter: avoid passing newlines to translators (#10400) 1 year ago
poire-z bf6a7e8873
bump crengine: BiDi, floats, and FB2 cover related fixes (#10398)
Includes:
- DrawBuf::DrawRescaled(): account for _drawnImagesCount/Surface
- renderBlockElementEnhanced: ignore margins set on table cells
- writeNodeEx: also return @import'ed css file names
- Text: fix interactions of standalone floats and <br/>
- BiDi: fix HTML driven BiDi possibly wrong after <br/>
- Text: fix spaces not collapsing when interleaved with ignorables
- BiDi: ensure proper ordering when white-space: pre
- BiDi: ensure proper ordering when white-space: pre (2)
1 year ago
hius07 2ba694d394
FileChooser: fix percent sorting (#10393) 1 year ago
yparitcher b18ab634ab Dispatcher: consistent ordering 1 year ago
yparitcher de5c872dcb Dispatcher: add `addCurrentLocationToStack`
This is useful when i want to reference something a few pages away while keeping track of my current page
1 year ago
poire-z 2aeb4a87df
Statistics: fix ReaderProgress bad layout in RTL UI (#10389) 1 year ago
Frans de Jonge 84e094de36
DataStorage: fix typo and actually create $HOME/.config if it doesn't exist yet (#10390)
Fixes #10380.
1 year ago
Frans de Jonge 2f7923c513
DataStorage: create parent config home if it doesn't exist yet (#10382)
This situation might potentially occur in something like a Docker/Podman container or WSL.

Fixes #10380.
1 year ago
yparitcher 1102c030fa
Kindle: toggle cover events
Allow disabling the hall efect sensor via the sysfs knob, so the kindle system wont sleep & wake the device

for those of use that stay in koreader, are caseless and have get spurious wakeups
1 year ago
hius07 4f23a6fafa
Custom book covers (#10329) 1 year ago
John Hackett 3f9820052d
Add esperanto-english dictionary (#10297) 1 year ago
kpopconnoisseur 0533ec46d8
kindle voyage: no framework: whispertouch button activation
kindle: "no framework" voyage page turn buttons
kindle: update device tests
1 year ago
hius07 9db74f1f01
ConfirmBox: add widgets (#10364) 1 year ago
yparitcher 64b0f5e7cf ReaderFooter: separate ToggleFooterMode from TapFooter
the footer can be toggled by gestures and menu, and should only be locked or skim in flipping mode when the footer is tapped

also allow toggling when only displaying the progress bar

rename to TapFooter to show it is not an event

Fixes: #10355
1 year ago
hius07 a5320acf4f
koptoptions: fix default margin (#10376)
Regression from #10208.
Closes #10373.
1 year ago
zwim 00b3594d44
Kobo: Refactor frontlight toggle ramp behavior (#10305)
* Rewrite the loop mechanism to use scheduled tasks instead of a single blocking-ish subprocess.
* Change the actual logic to be more pleasing to the eye, especially on newer devices, as those *may* natively ramp on set; and fix a bad interaction with that behavior that could lead to no ramp at all on ramp down.
* Simplify Generic's Suspend handling to deal with the refresh ordering in a saner manner. The screensaver might be visible a tad longer than before this change before the frontlight actually ramps off.
1 year ago
zwim 662bd65a5f
Fix some screen refresh effects (#10306) 1 year ago
poire-z 04fba2205d PageBrowser: show page number alongside thumbnails
Add a top left menu item -/+ to show none, only on the
first thumbnail of a row, or on all thumbnails.
Also make the page slot separator longer in the bottom
ribbon before pages that start a thumbnails row.
Also show a little spike in the bottom ribbon below page
slots that get their page number displayed, to ease
figuring out the connection.
1 year ago
poire-z 7bae2ed920 Dispatcher: add "Save book metadata" 1 year ago
Frans de Jonge 744f2d185f
[fix] Reintroduce "Start with" default to prevent nil string on startup (#10369)
Regression from #10198.

Fixes #10368.
1 year ago
Mochitto 0db042fd0c
Fix crash exporting to Markdown with missing authors (#10278) 1 year ago
Mochitto 53079441f9
Fix: exporting all notes and single doc now both use metadata for author and title (#10280) 1 year ago
Mochitto c0615c3bda
Feature: Export to kindle's myClippings (#10263)
* Feature: Added the possibility to export using kindle's myClippings formatting
* Fix: files that have the same extension don't collide anymore
1 year ago
weijiuqiao cb9cbff10e
KeyValuePage: fix corner-case alignment bug (#10326)
Change algorithm to comparing all kv pairs.
Also ignore any pairs where the value is "" when computing
the alignment, since they usually are intended more as
a title and should not interfere.
1 year ago
yparitcher b5fda00a8b
kodev check: enforce default submodule.recurse=false for shellcheck & shfmt on submodules (#10362)
Otherwise results might be unexpected for people who set submodule.recurse to true

Regression of sorts since #10344
1 year ago
Frans de Jonge 6c2f230639
Bump luajit-launcher (#10360)
* Add Boyue S62 root light controller (koreader/android-luajit-launcher#408) @RamKromberg
* Onyx Leaf: fix lights (koreader/android-luajit-launcher#414) @pazos
1 year ago
hius07 fd8cb14c00
CoverBrowser: fix getting cached info (#10346) 1 year ago
Frans de Jonge b04639a9e6
Update translations for v2023.04 (#10359) 1 year ago
Frans de Jonge e6f1804694
Bump luajit-launcher (#10358)
Adds Tagus Gea

* https://github.com/koreader/android-luajit-launcher/pull/412 @Alfedi
1 year ago
NiLuJe 53e6cf563a
Deal with table.pack corner-cases properly (#10350)
c.f., https://github.com/koreader/koreader-base/pull/1603 for more
details.

Re: #9624
1 year ago
zwim 60849aed12
kodev: don't check shfm, same as in #8636 (#10344) 1 year ago
Frans de Jonge a8ab5e84e5
Bump base for HiDPI support (#10341)
Fixes <https://github.com/koreader/koreader/issues/8630>. Might require the `SDL_VIDEODRIVER=wayland` environment variable.

Includes:

* SDL: add idiotic workaround to be less blurry on HiDPI (koreader/koreader-base#1602)
* SDL: avoid silly ABS_MT_TRACKING_ID for mouse button presses (koreader/koreader-base#1600)
1 year ago
hius07 99265ba1b7
BookInfo: fix crash on show cover (#10315) 1 year ago
Frans de Jonge 5dd88535ba
Bump base for SDL: add multitouch support (#10334)
Includes:

* Fix build on Mac OS X 13.3+ (missing ifr_netmask) (koreader/koreader-base#1596)
* Add C declarations needed for zmq http server (koreader/koreader-base#1595)
* SDL: add multitouch support (koreader/koreader-base#1599)
1 year ago
hius07 af45ec75c3
ReaderThumbnails: update cached page thumbnail on bookmark note change (#10303) 1 year ago
hius07 be7d6008d1
Reader: do not apply line spacing out of range (#10307)
* readercoptlistener: fix line spacing out of range

* readerkoptlistener: fix line spacing out of range

* creoptions: fix line spacing limit
1 year ago
weijiuqiao 34c2dab54b
statistic.koplugin: fix today's timeline showing next day when within custom offset (#10299)
Continuing https://github.com/koreader/koreader/pull/10254#issuecomment-1494697269
1 year ago
zwim 4dbaca180a
PM: Optimize task queue handling around standby (#10203)
Instead of firing on(Enter|Leave)Standby Events, and having every other piece of code that might care about that handle re-scheduling their stuff themselves; simply make the standby implementation (i.e., AutoSuspend's) shift the whole task queue by the amount of time spent in standby to re-sync everything automatically.

(This is necessary in the first place because Linux, as the task queue ticks in CLOCK_MONOTONIC, which does *not* tick during suspend/standby; while we expect most of the tasks scheduled to actually reflect real world clock delays).
1 year ago
weijiuqiao 86ddfc856d
SSH.koplugin: fix cant stop SSH server bug when pid file's stale (#10300)
Fixes #9812.
1 year ago
hius07 483a005cc2
File browser: show Folder Menu on long-press on Home icon (#10298) 1 year ago
hius07 b01e1c5918
Reader: do not apply font size out of range (#10295) 1 year ago
hius07 14de514ae8
filechooser: fix show files in bold (#10296) 1 year ago
Frans de Jonge 89b4c737e2
Bump base for M1 building (#10291)
* Makefile / library path modifications to make M1 building work (koreader-base#1570)
* thirdparty/luasec 1.3.1 (koreader-base#1591)
* [CI] Update all but Android to new CMake 3.16 image (koreader-base#1487)
1 year ago
yparitcher 3452462fa7
Make `kodev check` feature complete (#8682) 1 year ago
poire-z 960671a16c PageBrowser: tweak scrolling behaviour at book start/end
Handle scroll by row or page a bit differently, so we dont
constrain and readjust the focus page when reaching book
start or end: when later scrolling in the other direction,
we'll find exactly the view as it was (this means that
we allow a single thumbnail in the view, but it's less
confusing this way).
1 year ago
poire-z 7c5c7bb9cf Calendar view's day view: thicker separator at 00:00
When using the new option "Daily timeline starts at",
make the separator line between 23:00 and 00:00 thicker.
Also tweak time picker to pick minutes by units of 10,
and remove max hour (06:00) limitation.
1 year ago
hius07 b7c4e1c21b
ListMenu: show mark for books with highlights (#10276) 1 year ago
hius07 34ab0d8ac6
Folder Menu: sign for Home folder (#10288) 1 year ago
hius07 827581ac72
readerbookmark: fix writing pdf annotation (#10287) 1 year ago
Mochitto d39cc78f9c
[plugin] Exporter: use util.getSafeFilename() to remove illegal characters from output filename (#10282)
Fixes #10281.
1 year ago
Piotrek Marciniak 2e630286df
Sync book statistics: add to dispatcher (#10285) 1 year ago
hius07 b98fee3dd0
filechooser: fix bug with "unreadable content" (#10283) 1 year ago
weijiuqiao 5648c942c1
Calendar view: add options to change start time of days (#10254)
Can be set for example to 04:00 to see after-midnight readings
with those of the previous evening in day timeline.
Also fix possible shifts in day start when crossing DST changes
with prev/next.
Also fix sorting (by reverse reading duration) of books at top
1 year ago
hius07 59a0139a60
File browser: add Folder Menu (#10275) 1 year ago
nairyo 983e083a0b
Center pdf manual zoom mode (#10246)
Center PDF content when its width is smaller than the screen

fix #9967
1 year ago
hius07 a3080b042a
BookInfo: add page information (#10255) 1 year ago
hius07 d992fd3cfe
Dispatcher: translate QuickMenu title (#10271)
Closes #10270.
1 year ago
hius07 3a37d30ad6
Dispatcher: use translated labels for Zoom Mode actions (#10269) 1 year ago
yparitcher bc5d354225
ReaderLink: allow a forward location stack (#10228)
this allows going back and forth from links (think of undo / redo)

when going back and no forward locations and when we are not on the same page as the last saved location, add the current location to the forward stack, helping if one goes back by mistake they can jump back to their current location

when going back and no forward locations and when we are not on the same page as the last saved location, add the current location to the forward stack, helping if one goes back by mistake they can jump back to thier current location
1 year ago
Mochitto 3a894f954c
Fix: Updated legacy directory, which crashed the program (#10260)
Fixes #10259
1 year ago
hius07 91fc8a0061
readhistory: fix clear missing (#10257) 1 year ago
hius07 4d26650ad6
Filesearcher: add search in book metadata (#10198) 1 year ago
hius07 df9c2bcb7f
History: fix book info buttons disable (#10244) 1 year ago
hius07 c6a7f083c9
readerstyletweak: update profiles on unregistering in dispatcher (#10247) 1 year ago
hius07 aedb713f82
Menu widget: cleanup (#10241) 1 year ago
poire-z b3fe90c927 bump crengine: fix parsing of multibytes encodings
Includes:
- LVTextFileBase: fix parsing of multibytes encodings
1 year ago
poire-z f16fbf1d0b Book style tweak: add button with CSS suggestions
Mostly non-standard CSS declarations (so, possibly unknown
to users) that can be useful for solving certain issues.
1 year ago
poire-z fd0759e351 Book map, Page browser: add top left menu
Split original very long help text (which was very
slow to display) into 2 parts: About... and Available
gestures.
Also add -/+ buttons to change things (which can already
and more practically be done with swipes along the edges)
to give a bit of meat to these menus.
1 year ago
poire-z 8036ca0d56 MovableContainer: add support for anchoring initial position
When passing as 'anchor' a Geom object (ie. a widget dimen
or a ges.pos), or a function returning such an object,
a MovableContainer will be initially positionned near this
point/widget, instead of being centered on the screen.
Allow that for ButtonDialog and ButtonDialogTitle, so we
can make them behave as context menus (ie. for a titlebar
top left/right icons).
1 year ago
poire-z 25a7ca9a60 ButtonTable: allow shrinking unneeded width
If buttons and their text would fit in a smaller width,
reduce the whole ButtonTable width.
May be used for properly sized context menus with
ButtonDialog, with the width required depending on
the translations for a language.
1 year ago
poire-z a04244091f ButtonDialog/ButtonDialogTitle: consistent 'width' handling
Make these 2 widget behave similarly, and don't rely on
ButtonTable default width for their own default width.
ButtonDialogTitle: also properly size its title.
1 year ago
poire-z 7d62f639cc ButtonTable: allow buttons to request a fixed width
It is expected some button(s) in a row do not specify
such a width, so they get distributed the remaining
space from the ButtonTable specified width.
1 year ago
poire-z 02d6097dca Button: fix enableDisable when multilines
Also add support for a enabled_func() property, and
ensure its enable/disable state on each repaint.
1 year ago
poire-z e3156c7b25 FrontLightWidget: cleanup buttons layout
Properly compute Button and separator widths,
instead of using magic numbers (which lost their
magic over the years :)
Ensure buttons and progress widgets are properly
aligned on the sides.
Move the optional Warmth "Configure" button in the
middle of Warmth Min and Max.
Fix keyboard navigation layout, which was not working
on devices with Warmth.
1 year ago
poire-z f0122cf457 Button: handle 'width' as the final outer width
All our widgets are considering their provided 'width'
as the outer width, except Button which considered it
as some 'inner width', to which padding/border/margin
were added. Let's have them all consistent.
Some other widgets using Button had tweaks to account
for that odd behaviour: fix and simplify them.
Also fix Button layout when text is left aligned.
1 year ago
hius07 1e6b22a60e
Readerzooming: fix use of default settings (#10205)
(1) Fix default direction. Closes #10201. Fixes #9321.
(2) Fix default overlap (unreported bug).
(3) Do not write unused settings to doc_settings.
1 year ago
Frans de Jonge 068809bb93
Update translations for v2023.03 (#10226) 1 year ago
Frans de Jonge 1dfb766ff1
[i18n] Fix untranslated "Screensaver message" (#10217)
Reported in <https://www.mobileread.com/forums/showpost.php?p=4306749&postcount=107>.
1 year ago
poire-z 3006348585 PageBrowser: toggle page bookmark with long-press on thumbnail 1 year ago
poire-z 4f76a208b0 bump crengine: minor fixes, add getPageXPointer()
Includes:
- Update German hyphenation patterns
- CSS stylesheets cache: also clear it after initial load
- getSurroundingAddedHeight(): use interline_scale_factor
- LVDocView::getNodeByPoint(): tweak again for text selection
- LVDocView::getPageBookmark(): behave as getBookmark()
1 year ago
poire-z 683452fd57 KOpt: switch page_margin to buttonprogress and fine tuning
Allow for larger margins (which may exhibit other issues
that were less noticable with small margins).
1 year ago
nairyo 65031ae14f
Dictionary lookup window: use from/to language to determine glyph form (#10184)
For example, to determine whether the Japanese or Chinese form should be shown.

* Dictionaries.lua - use iso3 codes everywhere
readerdictionary.lua - convert iso to bcp tag, to construct ifo_lang
isolanguage.lua - map from iso3 to full language name, map from iso3 to bcp language tag

* Make the full language names translatable

* Store ifo information from dictionaries.lua in downloaded dictionaries
1 year ago
hius07 cc3e48fa3a
Coverbrowser: unify display modes (#10188) 1 year ago
hius07 536d71970f
Bookmark flipping mode fix (#10196)
(1) Changed indicator icon to "bookmark".
(2) Added toggling action to dispatcher.
1 year ago
weijiuqiao 6267249b1e
vocabbuilder: fix reset word progress bug (#10195)
fixes #10194.
1 year ago
hius07 a3d42ac344
Classic display mode: add popup file dialog buttons (#10185) 1 year ago
poire-z eeb3c08457 View HTML: add CSS helpers with long-press
Move View html code from ReaderHighlight to a new
dedicated module.
Long-press on an element or its text in the HTML will
show a popup with a list of selectors related to this
element that can be copied to clipboard (to be pasted
in Find or in a Book style tweak).
2 addtional buttons in this popup allow seeing all the
CSS rulesets in all stylesheets that would be matched by
this element, which should make it easier understanding
the publisher stylesheets and using or creating style
tweaks.
1 year ago
poire-z 945a1cc76f TextViewer: add support for long-press on text
As done in DictQuickLookup (more event handlers are
needed to cohabitate well with MovableContainer).
By default, selected word or text is copied to clipboard.
Also provide indexes to any long-press callback, as we'll
need them for View HTML.
1 year ago
poire-z f063155166 bump crengine: optimizations, fixes, View HTML helpers
Includes:
- LVStyleSheet: "const" some accessors
- LVStyleSheet: optimize `LVStyleSheet::merge`
- LVStyleSheet: simplify `insert_into_selectors` implementation
- Update German hyphenation patterns
- Header: render title/authors with book language glyphs
- Text: fix issues when hyphenation happened on a ligature
- getDocumentFileStream() support percent-encoded path
- getHtml(): add flag to get CSS selectors for met elements
- Add gatherStylesheetsMatchingRulesets(node)
Also: Bump SQLite to 3.41.0
1 year ago
hius07 3640842bc2
BookStatusWidget: relocate status toggle buttons (#10179)
Same order as in the new file dialog status buttons.
1 year ago
hius07 d2ed7402da
FileChooser: fix sorting and getNextFile() issues (#10176)
- Fix sorting folders when collate is "type", "size", "percentage":
  folders are sorted by name now.
- Fix getting next file in folder when collate is "mixed files and
  folders": returned nil when next item was a folder.
1 year ago
Frans de Jonge 5fce41d7d9
[i18n] FileManagerMenu: remove stray newline from translated sting (#10174) 1 year ago
Frans de Jonge 4e78b081f9
[i18n] Calibre plugin: improve clarity for translators (#10172)
The path list will start with \n already, but an extra newline
won't hurt and this string is really weird (i.e., people will
naturally put a space or a newline in front of it).
1 year ago
zwim efd2df6f05
Version log and (limited) notifications log (#10178)
This PR will close #1257 and #3418.
1 year ago
NiLuJe 9f37863f00
rM: Handle input shenanigans on mainline kernels (#10168)
Add a dedicated handler to handle mixed pen + panel, when panel sends both mt + st and stylus only st
Don't stomp on panel slots in the Wacom protocol handler
Get rid of a few superfluous nil guards
Return early in the EV_KEY handler for touch-related events (i.e., BTN_)
1 year ago
hius07 fe7b10f18d
Datastorage: do not create unnecessary folders (#10162) 1 year ago
NiLuJe 825d812f93
Button: don't dump the widget in a debug print (#10171)
Just display its ref.

Re: https://github.com/koreader/koreader/pull/7166#issuecomment-1445177124
1 year ago
hius07 38bd768d5e
DocSettings: Move book metadata to preferred location (#10149) 1 year ago
NiLuJe 189739ac4c
ReaderFooter: Don't reset session marker on standby (#10167) 1 year ago
hius07 55f1b8c974
UX: a set of minor fixes (#10165)
- AutoFrontlight plugin: update checkmark on toggling
- KeepAlive plugin: update checkmark on toggling
- ReaderPageMap: font size menu entry
- ReaderStatus, common_settings_menu_table: book status "read" -> "finished"
1 year ago
Frans de Jonge 26dc43553b
[fix] CoverBrowser: fix calling upvalue crash in MosaicMenu (#10164)
Regression introduced in #10140.
1 year ago
Frans de Jonge 03a9551565
[i18n, plugin] Exporter: fix some incorrect uses of translation (#10159)
Fixes #9231.
1 year ago
hius07 5c9ba53353
History: update last book access time (#10156) 1 year ago
melyux 81c0bc396a
Duration format: Add spaces, remove lead zeros for Letters (#10141)
* Add thinspaces between d/h/m/s
* Remove lead zeros
* Make letter `d` for days translatable
* Make thinspace into hairspace when compact
* Adjust and add unit tests
1 year ago
poire-z b610facfdc
Partial rerendering: really avoid flash on reload (#10155)
Also restore input if ReaderUI crashed.
Also avoid refresh_count management when "Full refresh
rate" is set to "never".
1 year ago
Frans de Jonge 6ad3371cd8
Bump luajit-launcher for Onyx Poke 4 lite (#10137)
See <https://github.com/koreader/android-luajit-launcher/pull/406>.
1 year ago
hius07 54fc6533b1
MoveToArchive fix DocSettings (#10152) 1 year ago
NiLuJe d0b78c01aa
Hotfix followup to #10139 (#10151)
Don't blow up if we fall back to exec'ing ping
Fix https://github.com/koreader/koreader/pull/10139#issuecomment-1437678458
1 year ago
NiLuJe 5330c24b77
Device: Handle network info data gathering ourselves (#10139)
i.e., we now query routes, interfaces, wireless extensions & ping ourselves, dropping the dependency on specific CLI tools altogether.
1 year ago
hius07 d38c71a7bc
Wallabag: fix doc settings (#10147) 1 year ago
ichnilatis-gr 0f3aeb7628
Keyboard: some transpositions in el_popup for practical reasons (#10144) 1 year ago
melyux 55259846a8
Fix N_() usages, rearrange a SQL select (#10146)
Fix some of my early blunders in using the `N_()` gettext function. Mini-PR from https://github.com/koreader/koreader/pull/9924#discussion_r1104298501 (@Frenzie).

There was also one line for generating this same `%1 (%2 pages)` text that confusingly uses different ordering in the SQL query output; switched the two SELECT arguments around to make it match the other 5 usages. Works the same as before
1 year ago
hius07 5cf72b6eea
MosaicMenu: correct access to doc settings (#10140) 1 year ago
hius07 54b3b5a8cc
ReaderHighlight: minor fixes (#10142)
* readerui: save highlight to pdf requires button press

* readerhighlight: fix select mode confirmbox icon
1 year ago
NiLuJe e79f161516 Dictionaries: Update URL for pt_PT
Fix #10134 (sorta ;p)
1 year ago
NiLuJe 203094d47d Device:retrieveNetworkInfo: Drop the "HWaddr" bit from the parsed output
i.e., just display "ifname MAC"
1 year ago
NiLuJe 6dc41fe02a Device:retrieveNetworkInfo: Make "Default gateway" translatable
re: #8739
1 year ago
NiLuJe 08b0c088e5
util: Add removePath (#10129)
Will attempt to prune empty directories from the given path, going as far back up as possible.
1 year ago
Andrej Shadura 328670b515
Device:retrieveNetworkInfo: Add a fallback to net-tools (#8739)
Some devices don't ship with iproute2 compatible tools, but may ship
with net-tools compatible ones.

Unify code w/ SDL's similar codepaths for its NetworkMgr:isConnected
implementation
1 year ago
hius07 15605291c2
DocSettings: add support of centralized sdr storage (#10132) 1 year ago
melyux e55b60175b
CoverBrowser Mosaic: improve book status, floating progress bar (#9939)
- New dogear icons in Mosaic cover view to indicate each
  possible book status: 'Reading', 'On hold', 'Finished'
- Progress bar redesigned to be floating, taller, thicker
  bordered in Mosaic cover view
- Don't show progress bar if book is finished
- FakeCover bottom text (filename) adjusted to not overlap
  with progress bar and dogear if they exist
- Mosaic book shortcut letter moved from bottom left to
  top left
1 year ago
yparitcher 706c8c5610 fix broken escaping 1 year ago
hius07 d1081fa982
Docsettings: add centralized sdr storage (#10074)
Added an option to choose a new location to save document settings, highlights and bookmarks 
(koreader/docsettings folder).
1 year ago
NiLuJe c5d606a7f4
ProgressWidget: Add an optional marker on the initial position (#10114)
* Enable it on SkimToWidget
* Optional on ReaderFooter (toggle in the progress bar > style submenu)
1 year ago
poire-z 81f2aed086 ReaderRolling: quicker partial rerenderings with EPUBs
Only available with EPUBs containing 2 or more fragments,
and a file size large enough to ensure a cache file is used.
The idea is simply, on any rendering setting change, to
skip the rerendering of the full book and to defer any
rerendering to the moment we draw a DocFragment, and
render only it.
So, on a setting change, only the fragment containing the
current page will be rerendered, and the new fragments we
may cross while turning pages.
When having done so, KOReader is in a degraded state (the
full page count is incorrect, the ToC is invalid...).
So, a full rerendering is needed, and one will happen
in the background, and when the user is idle, we reload
seamlessly and quickly from the cache file it has made.
ReaderFlipping will show some icons in the top left
corner to let it know at which steps in this procress
we are.
1 year ago
poire-z 15499434df bump crengine: support for partial rerenderings
Includes:
- Cache: minor simplifications
- Cache: improve cache file reproducibility
- CHM: minor cleanup
- LVStream: minor cleanups
- LVStream: fix and improve `LVZipDecodeStream` implementation
- LVStream: disable CRC checks for ZIP streams
- CSS: optimize :nth-child/of-type() and :nth-last-child/of-type()
- Page splitting: fix oversight in flags handling
- Update _doc_rendering_hash also when loading from cache
- getDocumentRenderingHash(extended): add parameter
- Toggable support for partial rerenderings of individual DocFragments
Also:
- util.isSubProcessDone(): add 'wait' option
- Also add getppid and some contants to ffi/posix.
1 year ago
poire-z c8616f1f35 CreDocument: enable crengine cache with smaller documents 1 year ago
poire-z e4b97802a5 ImageWidget: account for alpha in the cache hash 1 year ago
poire-z 8d3cda532b MultiConfirmBox: allow changing icon 1 year ago
poire-z af1de5b6a3 TouchMenu: allow help_text on disabled menu items
Also don't propagate tap/hold on disabled menu items.
(When top and bottom menu displayed at the same time,
tap on a disabled top menu item could active some
bottom toggle change.)
1 year ago
poire-z 2f0954fa71 Book style tweak: revamp sample tweak 1 year ago
NiLuJe b9950597e0
Icons: CRe partial rendering icons (#10086)
Re #10124

----

Also includes:

* Resize the Zoom-to-Page icon to match the size of its brethren

It was one pixel smaller on all sides, for... some reason?

(Possibly I borked the resize the first time around, don't remember if I
worked on that one).
1 year ago
zwim 8861955aeb
Recalculate timezone, (DST) (#10125) 1 year ago
hius07 da841f3d87
ListMenu: correct access to doc settings (#10119) 1 year ago
yparitcher 237f44deda Dispatcher: add word spacing & expansion 1 year ago
NiLuJe 15b236e3e9
Kobo: Mimic Nickel's poweroff behavior on sunxi (#10122)
It goes through init, unlike on other models.

Re: #10121
1 year ago
NiLuJe 8e9fc9953f
Calibre: Allow authors/title metadata browse/searches (#10113)
c.f., https://www.mobileread.com/forums/showthread.php?t=352142

This adds dedicated "browse by" buttons for authors & tags, and adds series & tags to the search settings for the meta "Search books" button.
1 year ago
hius07 91ff6ce2d8
ReaderBookmark: fix comparing positions (#10109) 1 year ago
hius07 db9cb542e4
FileChooser: optimize sorting functions (#10091) 1 year ago
NiLuJe 318a22d913
util: Rewrite makePath (#10111)
The previous iteration, besides failing to handle leaf-only input,
was relying on splitFilePathName, which just doesn't do what's required
to incrementally build the directory tree the right way around ;).

This should more closely match mkdir -p, i.e., it will *fail* if any
part (or all of it) of the path exists but is *not* a directory.

Re #10074
1 year ago
Frans de Jonge dbad14e20b
ExternalKeyboard: make metadata capitalization abide by styleguide (#10081) 1 year ago
Utsob Roy c9007b1e29
Exporter: add seconds in markdown export (#10065) 1 year ago
clach04 5c81d72a2d
Readme: update list of supported formats (#10004) 1 year ago
melyux 8b99c50b2d
Duration format: add "Letters" format (1h30m10s) (#9924) 1 year ago
Melik 881d8ed406 Fix spelling of filemanagerutil.genStatusButtonsRow 1 year ago
Melik bc92ad923e Move cache update logic into CoverMenu:updateCache 1 year ago
Melik 7d6da81233 Compact covermenu addn. to for loop, fix collections bug, further gimp genStatusButton to use upvalues 1 year ago
Melik 0e470aaed9 Remove leftover function, arg 1 year ago
Melik d9b0785a96 Reset button gen to util, collections current file check
- filemanagerutil.resetDocumentSettings()'s doc_settings:close() -> doc_settings:flush()
- Remove current_status from filemanagerutil.getStatusButtonsRow() args, get it inside from file
- Move genStatusButton() inside filemanagerutil.getStatusButtonsRow()
- Move "Reset settings" button generation to filemanagerutil
- Rename "Reset settings" button to "Reset" and update popup box text
- Disable "Reset settings" for file if it's currently open in Collections (same as History)
1 year ago
Melik fb7ec830c6 Create getStatusButtonsRow() for status buttons, hide row if history item deleted 1 year ago
Melik 31cfffc289 Pull genStatusButton() to filemanagerutil 1 year ago
Melik 502bb0ccbf Create genStatusButton() for buttons, use item.dim for deleted in history 1 year ago
Melik 9e7e68beb6 Shorten status button names 1 year ago
Melik c0d0b36f98 Get rid of luacheck warning 1 year ago
Melik 1b2adfd201 Return to callback fudging, fix book info callback replacement 1 year ago
Melik 628cacf1e6 Disable status buttons for deleted files in history 1 year ago
Melik 77b110a987 Add 'Reset settings' button to collections 1 year ago
Melik 8642509f26 Move 'Put on hold' between reading & read 1 year ago
Melik a9313dda1a Add buttons to collections 1 year ago
Melik 57849b3f7c Add buttons to history, use id for button-getting 1 year ago
Melik 086d4622e6 Add buttons to file manager, lay groundwork 1 year ago
NiLuJe 9fd5bbd7cb Bump android/luajit-launcher
https://github.com/koreader/android-luajit-launcher/pull/405
https://github.com/koreader/android-luajit-launcher/pull/404
1 year ago
NiLuJe 03c0fd7f9e Bump base
https://github.com/koreader/koreader-base/pull/1580
1 year ago
NiLuJe 96850c23a0
NetworkMgr: Refine isConnected check (#10098)
Much easier to deal with thanks to the cleanup work done in #10062 ;).

* `carrier` is set to 1 as soon as the device is *administratively* up (in practice, as soon as we run `ifconfig up`). This is perfectly fine for `isWifiOn`, but absolutely not for `isConnected`, because we are not, actually, connected to *anything*, no attempt at associating has even been made at that point. Besides being semantically wrong, in practice, this will horribly break the connectivity check, because it expects that `isConnected` means we can talk to at least the LAN.
* Delving into the Linux docs reveals that `operstate` looks like a better candidate, as it reflects *operational status*; for Wi-Fi, that means associated and successfully authenticated. That's... closer, but still not it, because we still don't have an IP, so we technically can't talk to anything other than the AP.
* So, I've brought out the big guns (`getifaddrs`), and replicated a bit of code that I already use in the USBNetwork hack on Kindle, to detect whether we actually have an IP assigned. (Other approaches, like `/proc/net/route`, may not be entirely fool-proof, and/or get complicated when IPv6 enters the fray (which it does, on Kobo, Mk. 8+ devices are IPv6-enabled)).

TL;DR: Bunch of C via ffi, and `isConnected` now returns true only when the device is operationally up *and* we have an IP assigned.

Pulls in https://github.com/koreader/koreader-base/pull/1579 & https://github.com/koreader/lj-wpaclient/pull/10
1 year ago
hius07 03454a89d8
Screensaver: correct access to doc settings (#10105) 1 year ago
NiLuJe b1c30a7b1e
Input: Don't set unnecessary input translations when viewports are involved (#10104) 1 year ago
yparitcher d60c75e00d network: Allow auto_wifi_off on kindle & remarkable
They expose a network sysfs entry so we can track usage

Works for me on kindle, but i stop the amazon framework
1 year ago
yparitcher 0e53631f48 NetworkManager: isWifiOn isConnected consistent usage.
cervantes kindle kobo remarkable: use sysfs carrier file to determine connection state

cleanup hasWifiManager checks

gateway check: use ip if available

Fixes: #10087
Closes: #10092
1 year ago
NiLuJe 67d0808494
Misc: Make --verbose do what it should be doing ;). (#10095)
Stupid c/p typo exists since its inception in #6976
1 year ago
proletarius101 9d2ac3a0fc
improve appstream metadata (#10094)
- add developer name (can be shown on flathub.org)
- set `Translate` link to the translation repository
1 year ago
Alexander Schlemmer 849687f2d0
[fix] kodev wbuilder crashes with "attempt to index global 'G_defaults' (a nil value)" (#10088)
Fixes #10084.
1 year ago
NiLuJe 45a4aac3d3
Notification: Fence the *display* update in an attempt to avoid upsetting some boards... (#10083)
Re: https://github.com/koreader/koreader/issues/9806#issuecomment-1416827447

Depends on https://github.com/koreader/koreader-base/pull/1576

Includes assorted cosmetics tweaks related to duplicate `setDirty` calls when instantiating widgets that already have an `onShow` handler doing it. (I left widgets doing it in `update` instead of `init` alone, on the assumption that callers *may* be relying on that behavior when updating widgets at runtime. This might actually never matter, and it certainly didn't for ScreenSaverWidget, which is why I removed it from there ;p).
1 year ago
zwim 4e944dc918 patch management
Use texteditor if available

Auto disable if patches folder doesnt exist

And show a restart message after file edit

Sort menu entries in execution order

Warning sign for failing patches
1 year ago
zwim cf9d3a0d70 Add quickEditFile method to texteditor 1 year ago
zwim 50ab620da1
Screen Warmth: Fix a bug with syncing screen warmth on start (#10066) 1 year ago
hius07 3881899338
Dispatcher: fix dynamically built menu (#10080)
Applicable for Set font and Set highlight action actions.
Closes #10077.
1 year ago
zwim c5997a6246
[Document] Consistent menu entries in auto-save menu (#9995) 1 year ago
proletarius101 50b2267b74
AppImage AppData: fix not shown as a desktop application (#10076)
To make it shown as a desktop application and listed on flathub.org, the app data must mark it as a desktop application: https://github.com/flathub/flathub/wiki/AppData-Guidelines#header
1 year ago
yparitcher 731b802a0e
kodev: fix grep: warning: stray \ before - (#10071)
New warning in 3.8.
1 year ago
Sargun Vohra 15fb73384f
EInk: Add a setting to toggle flashing on pages with images (#10049)
With minor code cleanups along the way ;).
1 year ago
Frans de Jonge 1faf0a1786 OTAManager: pick the right binaries on Android x86_64 1 year ago
Frans de Jonge c713df1dc7 OTAManager: pick the right binaries on Android aarch64 (arm64) 1 year ago
NiLuJe 92ebed577b OTAManager: Pick the right binaries on Kindles running on the Bellatrix
platform

Re: https://github.com/koreader/koreader/pull/4539#issuecomment-1411707712
1 year ago
hius07 c403b3bd25
Profiles: create profile with current document settings (#10036) 1 year ago
NiLuJe 21210800c1
ReaderFooter/Header: Refine autorefresh repaint-or-not checks (#10045)
Use both a whitelist for targeted widget repaints, a blacklist for no repaint at all, and a fallback for a full in-order ReaderUI repaint when unsure.

Use a similar approach in ReaderHeader (i.e., prevent explicit refreshes while ReaderMenu is open).

Re #9979, re #9768
1 year ago
yparitcher 747c3eaf9d
Kindle: NetworkMgr: isWifiOn isConnected (#10059)
isWifiOn for kindle currently returns if the interface is connected, change this to doing what is says isWifiOn the file is only present if the wireless interface is up.

isConnected pings the gateway, rely on the kernel for a more reliable check.

Whan connecting to my android phone's wifi hotspot to remote debug from my phone, the network is connected yet the phone(gateway) does not respond to pings leading koreader to shut down the connection thinking it is unsuccessful
1 year ago
Frans de Jonge b070a121d6
Update translations for v2023.01 (#10061) 1 year ago
poire-z d2f344becf
bump crengine: various minor fixes and tweaks (#10044)
Includes:
- LVHashTable: fix iterator implementation
- Text: fix mismatched `free` on `new`-allocated memory
- Fonts: fix possible memory leak in RegisterExternalFont()
- Avoid computing document CRC32 if metadataOnly
- LVStream: fix `LVStreamFragment` implementation
- (crengine-ng) Various small picks: unused variables, cast & types
- (crengine-ng) LVStream: minor fixes
- (crengine-ng) LVStream: cleanup zip processing
- (crengine-ng) ldomDataStorageManager: fix cast & types
- (crengine-ng) ldomDataStorageManager: fix possible segfault
- (crengine-ng) DOM/MathML: more currNode/parentNode sanity checks
- (crengine-ng) Default style: use cr3.ini fg & bg colors
- (crengine-ng) Support regular documents inside zip
- HyphMan: silence some clang warnings
- Stylesheets: support percent-encoded stylesheet links
- LVFontDef::CalcMatch(): don't use the bias if typeface matches

Also includes:
- LRU: Add a couple of APIs to get/set the cache size
- xtext: fix inconsistent rendering of arabic text
1 year ago
Mnkach 730857fc5e
Keyboard: add Ukrainian virtual keyboard (#10037) 1 year ago
hius07 b83a009dff
Profiles: various fixes related to registration and gestures (#10033)
- Keep profile registration in a duplicate.
- Fix profile menu update on register/unregister.
- Update gestures and profiles after renaming a profile.
- Update gestures and profiles after deleting a profile.
- Update gestures and profiles after unregistering a profile.
- Fix Profiles touchmenu stack update.
1 year ago
Utsob Roy 64fefc7a22
Exporter: add highlight markdown style (#9976) 1 year ago
Martín Fernández 34f92bc196
bump android-luajit-launcher: new devices + versionCode (#10042)
Onyx Boox Note X2, thanks to @hghwng
Warmth light support for Tolino Shine3, thanks to @Nepochal

Appends an integer to versionCode, which is used to identify the android ABI. Fixes #9687
1 year ago
Jej 198a84a772
Minor updates in comments (#10038) 1 year ago
NiLuJe 7863a7ad70
Misc: Natural sorting refactor (#10023)
* Move natural sorting algo to a dedicated sort module to avoid code duplication
* Use a slightly more accurate algorithm, and speed it up by caching intermediary strings
* Calibre: Use natural sorting in metadata search (fix #10009)
1 year ago
NiLuJe bb900aa9a7
WakeupMgr: Explicitly invalidate the current alarm in unsetWakeupAlarm (#10032)
Apparently, toggling the rtc interrupt doesn't quite work on some boards? (fix #10031)

(Drive-by tweak to UIManager: Less confusing logging when quit gets tripped both via quit
and _gated_quit (i.e., on poweroff))
1 year ago
NiLuJe 16fb32d5a4
Kobo: Unbreak a slew of devices (#10019)
Fix #10020, Regression since #10008

(hasMultiTouch is set by `Generic.init`, but we use it before that;
so just set it properly ourselves in our superclass, because we don't
actually need to rely on Generic's autodetection).
1 year ago
Frans de Jonge 1699711a0b
[i18n] Enable Farsi (#10017)
Cf. <https://github.com/koreader/koreader/discussions/10016>.
1 year ago
NiLuJe 4732bc9aed ReaderView: Recompute page layout when toggling ReaderFooter in
non-reflowable documents.

The code only handled setups with "reclaim bar height" enabled (because
that's my usual workflow, har har), but would have broken in various
more or less obvious ways without it, depending on the exact layout/zoom
settings.
The previous attempts at handling the no-reclaim case were focused
on scroll mode, which is a bit of a nightmare. This approach should
deal as well/badly as the previous one in scroll mode, but actually
handle page mode properly ;).

Re #9979
1 year ago
NiLuJe f74921724a Android: Don't pile on event hooks to handle viewport changes
Untested, hopefully sane :s
1 year ago
NiLuJe 64af750c6c Kobo: Unbreak input translation when a viewport is active
Generic is responsible for setting up the translation via input hooks;
since our own input hooks involve swapping or mirroring X/Y, we need to
run our own hooks *first*, so that the viewport translation actually
does the right thing...

Probably broken for a good long while, I'd just assumed the inaccuracies
on the H2O were due to the IR grid... :/.

Reported @ https://www.mobileread.com/forums/showthread.php?t=351340
1 year ago
NiLuJe 843c4bfe66
Bump base (#10003)
https://github.com/koreader/koreader-base/pull/1569
https://github.com/koreader/koreader-base/pull/1571
1 year ago
poire-z abbcd4ddbe
Exit menu: restore long-press to exit directly (#10000)
Unwillingly removed by 8ef426d79.
1 year ago
hius07 d5a4ac14e1
ReaderUI: fix status and history (#9993) 1 year ago
NiLuJe 5c1305719a
Kobo: Unbreak automagic_sysfs on the Libra 2 (#9998)
Regression since #9902, because I'd forgotten that the default Kobo block *does* set those...
Reported by @ptrm on Gitter, thanks!

Take two, essentially revert 8a16518918
We have defaults for most of those in the Kobo superclass, so these can never really be `nil`.
1 year ago
zwim b3a7d32d54
[AutoWarmth] Simplify translations (#9996) 1 year ago
hius07 20ac8004d6
ReadHistory: further minor refactoring (#9991) 1 year ago
NiLuJe 4ce0058e2d
ReaderLink: Minor logic simplification in onGoToPageLink (#9987)
Abort earlier if the nearest link is too far, instead of computing stuff and creating an object we'll never actually use.

Includes minor logging tweaks to vaguely related codepaths ;p.
1 year ago
hius07 a8b333e4f9
Dictionaries: add Ukrainian explanatory dictionary (#9982) 1 year ago
zwim f0b007bb31
Footer: avoid footer auto refresh over bottom menu (#9983) 1 year ago
poire-z 581a943c42 TextEditor: use a file symbol instead of numbers in history 1 year ago
poire-z 7448ad23e7 ReaderDogear: fix no y-offset after load with top status bar 1 year ago
poire-z bbbcdffd3b ButtonTable: reset MovableContainer state on button tap
Prevent any later hold_release event from being handled
by MovableContainer as a moving touch+hold_release.
This issue was noticable when closing DictQuickLookup
with long-press on close, resulting in the movable
highlight actions ButtonTable moving to where the
long-press happened.
1 year ago
georgeto cbe7775bc8
CheckMark: Fix on tap toggling of checkmarks in SortWidget (#9941)
The x and y coordinates of SortItemWidget's checkmark widgets were not updated, thus remained at their initial value 0.
Consequently the intersectWith check in SortItemWidget:onTap always evaluated to false, resulting in taps on checkmarks in the sort widget not being recognized.

Co-authored-by: NiLuJe <ninuje@gmail.com>
1 year ago
NiLuJe 8d133507ad Device: Handle screen_saver_lock + WiFi corner-cases
If Wi-Fi is restored on resume, going back to suspend while the
screensaver is shown via the screen_saver_lock flag would have attempted
to suspend *without* killing Wi-Fi first.

This implodes on the vast majority of NTX boards, so take the usual
precautions.
1 year ago
NiLuJe 144706654d Device: Drop unused device capabilities 1 year ago
NiLuJe 554520463d ScreenSaver: Get rid of gratuitous complexity
Screensaver isn't instantiated, so we don't really have any reason to go
through fancy class trickery to do this (as cool as it looked ;p)...
1 year ago
NiLuJe 9eac47e0df ScreenSaver: Refactor gesture lock to behave regardless of configuration
Go through a dedicated sticky invisible widget instead of piggybacking
on ScreenSaverWidget, so that we behave if there are other InputContainers
in ScreenSaverWidget, or if there isn't any ScreenSaverWidget at all.

Fix #9911, fix #9955
1 year ago
hius07 6e1683e313
KOSync: use MultiInputDialog instead of LoginDialog (#9962) 1 year ago
zwim 180cebe7ee
[CoverImage] use units in SpinWidget (#9938) 1 year ago
weijiuqiao 89af6d9385
KVPage: fix crash from empty page due to item deletion (#9974)
Fixes #9972.
1 year ago
zwim 54a105c24f
TouchMenu: simplify/remove enabled_func in some cases (#9966) 1 year ago
NiLuJe d6ec148548 CenterContainer: Use an elseif instead of two ifs checking the same
variable
2 years ago
NiLuJe 78a21a0ecc WidgetContainer: Nitty cosmetic tweak 2 years ago
NiLuJe 3743229ffe TextWidget: Feed Geom a full-size table in getSize()
Might save a realloc down the line.
2 years ago
NiLuJe 04c8bc903e IconButton: Fix RTL highlighting
I'm not *quite* sure what's responsible for inverting the padding
values, since the only Widget I can see doing that is FrameContainer,
and I can't find any in that Widget chain, but, oh, well.

Easily reproducible with the FileManager's TitleBar left/right buttons
(i.e., Home & Plus).
2 years ago
zwim ebaef61536
Make translateable: SystemStat, custom footer (#9945)
Fixes #9942.
2 years ago
Robert-Jan de Dreu c9df3a000f
PB: Implement GSensor support for more moderm devices (#9873)
(Bumps base for https://github.com/koreader/koreader-base/pull/1566)
2 years ago
proletarius101 a33617d760
Better compliance with appstream data standard (#9940)
* better compliance with appstream data standard 

Changed according to https://github.com/flathub/flathub/pull/3724#discussion_r1051620485

* update screenshots

* there can only be 1 default screenshot
2 years ago
NiLuJe 788ccac561
Input/Device: Refactor Gyro events handling (#9935)
* Get rid of the `canToggleGSensor` Device cap, it's now mandatory for `hasGSensor` devices. (This means Kindles can now toggle the gyro, fix #9136).
* This also means that `Device:toggleGSensor` is now implemented by `Generic`.
* Update the Screen & Gyro rotation constants to be clearer (c.f., https://github.com/koreader/koreader-base/pull/1568) (/!\ This might conceivably break some `rotation_map` user-patches).
* Input: Move the platform-specific gyro handling to Device implementations, and let Input only handle a single, custom protocol (`EV_MSC:MSC_GYRO`).
* Input: Refine the `rotation_map` disable method implemented in 43b021d37c. Instead of directly poking at the internal field, use a new method, `disableRotationMap` (/!\ Again, this might break some `rotation_map` user-patches).
* Input: Minor tweaks to event adjust hooks to make them more modular, allowing the Kobo implementation to build and use a single composite hook. API compatibility maintained with wrappers.
2 years ago
Frans de Jonge 99df719562
[i18n] Make gateway strings translateable (#9933)
See <https://www.mobileread.com/forums/showthread.php?p=4282773#post4282773>.
2 years ago
NiLuJe 5e28ab10a4
Bump base (#9934)
(https://github.com/koreader/koreader-base/pull/1565)

Re: #9918
2 years ago
hius07 ee75abd4a0
Statistics: show correct number of notes in a book (#9928) 2 years ago
proletarius101 f5a049212f
AppImage: make appdata standard compliant (#9929)
- license: use SPDX license identifier: https://spdx.org/licenses/
- content rating: add OARS content rating: https://hughsie.github.io/oars/generate.html (required by flathub: https://github.com/flathub/flathub/wiki/AppData-Guidelines#oars-information)
2 years ago
poire-z b1b7773237
TouchMenu: tweak menu search (#9926)
- Cleanup search and animation codes, fix inconsistencies
  between animation/no-animation opening, and refreshes
  glitches on eInk.
- Show menu item on tap, with buttons to either open
  directly, or to walk there (removed earlier "Animation"
  checkbox, so the choice can be decided later).
- Move event handlers into ReaderMenu/FileManagerMenu.
- Avoid duplicated and confusing results from gestures
  and font-family submenus.
2 years ago
NiLuJe f6421abab0
Calibre: Log errors on wireless connection failures (#9914)
Also, get rid of the weird and clunky `failed_connect_callback` thingy, because it makes no sense?

Re: #9908
2 years ago
Glen Sawyer 932ed44a9f
[plugin] Statistics: do not increment db sequences unnecessarily during cloud sync (#9921)
When you do an insert using either ON CONFLICT [...] DO NOTHING or INSERT OR IGNORE to prevent duplicate rows, sqlite still increments the sequence counter for each of the duplicate rows that it did not insert.
The only way around that is to explicitly write the SQL statement so that it doesn't try to insert the duplicates in the first place.
2 years ago
melyux a53e9847fa
Footer: some menu items rewording (#9915)
Change footer status bar type setting label from 'Prefix' to 'Item style'.
2 years ago
melyux b1bbd590db
Statistics: cleanup and tweaks (#9904)
Many tweaks (alignment, dates...), reordering and
rewording in "Current statistics", per-book stats,
and Reading progress.
2 years ago
melyux 6f39f6cb7b
Screensaver: exclude content when excluding cover, random image as fallback (#9912)
Exclude some screensaver option showing book content
(title, page image) when "Exclude this book cover" is checked. 
Random image as fallback, KOReader logo as 2nd-order fallback.
2 years ago
melyux e3f6404eb2
Screensaver: tweak "Screensaver message" description text (#9909) 2 years ago
hius07 7719ad04f3
Reader: add open next file in folder to Dispatcher (#9916)
Closes https://www.mobileread.com/forums/showthread.php?t=350683.
2 years ago
NiLuJe be42cac98e Bump base
https://github.com/koreader/koreader-base/pull/1564
2 years ago
NiLuJe f71d1b4e9d Bump android-luajit-launcher
https://github.com/koreader/android-luajit-launcher/pull/396
2 years ago
NiLuJe 9b936d3f3c AutoSuspend: On Kindle, periodically reset the system's timeout so that
our own timeout can actually be honored.

Fix #7874, fix #9868
2 years ago
NiLuJe 3a8e9f0333 DocSettings: Refine the primary/backup mtime shenanigans
Use the same mtime (the newest) for both entries, so that priority order
wins the tie, giving priority to the primary.

Avoids preferring an older legacy sidecar in case of issues with the new
files.
2 years ago
NiLuJe 3a8d94ad56 Kindle: Unbreak ScreenSaver tracking on SO devices
Fix #9883, regression since c7f5bfb72a
2 years ago
NiLuJe 1ca0748ca7 DocSettings: Make sure that between a main/backup pair of sidecar files,
the main one *always* has priority, regardless of mtime.

Should help workaround weird local/UTC issues when USBMS is involved,
c.f., https://github.com/koreader/koreader/issues/9227#issuecomment-1345263324
2 years ago
NiLuJe 8a16518918 Kobo: Only probe necessary entries in automagic_sysfs codepath
e.g., on the Sage, only the power_dev stuff changes between HW revs,
the rest of the hardcoded stuff is still relevant, so skip probing
those.
2 years ago
NiLuJe b16b215cbb ReaderHighlight: Appease newer versions of LuaCheck
Good to know: it now defaults to assuming that function arguments
prefixed with an underscore would be unused, which is... nice in theory ;).
2 years ago
NiLuJe 649af0be59 Startup: Unbreak last_file when the file is missing
Fix #9887, likely a regression since #9669
2 years ago
melyux 6c2d5c66da
Add linear flow wisdom to screensaver message magic variables (#9905)
If there are hidden flows, this calculates the current page, total pages, percent, time left in chapter, and time left in the magic variables used in screensaver message only for the current flow. If there are no hidden flows, the behavior is the same as before.
2 years ago
Robert-Jan de Dreu 9a573ae812
PocketBook: remove old input selection global (#9871) 2 years ago
bbshelper 6b388fe061
bump base/libunibreak for performance (#9906)
The upstream release has faster line-break property lookup.
Also includes:
- Input: remove old Pocketbook input selection
- Pocketbook: add ffi/inkview GSensor functions for modern Pocketbooks
2 years ago
charlesangus cad4c24118
Kobo: Handle power button input device on new Sage HW revision (#9896)
Co-authored-by: Frans de Jonge <fransdejonge@gmail.com>
Co-authored-by: NiLuJe <ninuje@gmail.com>
2 years ago
Roygbyte 3e49cf9a18
ReaderLink: allow buttons to be added to the External link dialog (#9746)
So plugins can register and advertize themselves
as handlers of external links (ie. Wallabag).
2 years ago
poire-z 24a3c722c8
Calendar view: properly use datetime module (#9893)
No need to pass the translations from main.lua
to calendarview.lua.
Also use datetime translations in ReaderProgress.
2 years ago
Frans de Jonge c01a9fd213
[i18n] Remove stray newline from ExternalKeyboard string (#9890) 2 years ago
weijiuqiao a76f3f5bf5
Vocabulary builder: add search ability (#9881)
And allow circle multiswipe to reload words.
2 years ago
Frans de Jonge e1fe897c9b
[i18n] Add a couple of explanatory comments for translators (#9878) 2 years ago
hius07 c53dd55b16
ReaderHighlight: new Select mode icon (#9869) 2 years ago
weijiuqiao 49d8ec2f4f
Cloudsync: dropbox token re-generation (#9862) 2 years ago
greatyingzi a6f0c3a31c
ReadTimer: allow repeat when time is up (#9844) 2 years ago
hius07 05cd59ebe5
ReaderBookmark: indicate current page with dimmed page numbers after current (#9872) 2 years ago
Frans de Jonge e026598f72
TouchMenu: menu search without patterns (#9884)
See <https://github.com/koreader/koreader/pull/9876#issuecomment-1340090208>.
2 years ago
Frans de Jonge e58a12ba04
TouchMenu: Search menu to search the menu (#9876)
Fixes #9800.
2 years ago
zwim 57ef9e6a55 TouchMenu: Button for date&time and battery 2 years ago
zwim 192a243b4d Add datetime.lua
Move date and time related functions from util.lua
(and the statistics plugin) to a new datetime.lua.
2 years ago
Martín Fernández 2e5284051a
bump android-luajit-launcher: new devices (#9880)
meebook p6
onyx nova air 2
onyx tab ultra
2 years ago
Frans de Jonge 1a3a210392
Revert "[CI] Set macOS Github Action to macOS 12.00 (#9856)" (#9864)
This reverts commit 991b6ab48b.
2 years ago
NiLuJe b9473fa600 Bump android-luajit-launcher
https://github.com/koreader/android-luajit-launcher/pull/393
2 years ago
NiLuJe 4e7a6d6526 Bump base
https://github.com/koreader/koreader-base/pull/1558
https://github.com/koreader/koreader-base/pull/1559
https://github.com/koreader/koreader-base/pull/1561 (fix #9860)
2 years ago
poire-z 5696a7cf00
bump crengine: fix styles under boxing elements (#9865)
Includes:
- getTextLangCfg(lang_tag): add 'force' parameter
  (and use it in cre.cpp's softHyphenateText()).
- Styles: check when inheritable CSS set on boxing elements
cre.cpp: docToWindowRect(): ignore rect over page top
2 years ago
zwim d1cd2a1c8d
Restart KOReader: add a ConfirmBox instead of a plain InfoMessage (#9853) 2 years ago
Steady Quad fa9f0acf5e
cloudstorage: Fix directory listing for some WebDAV servers (#9847)
(sub)folders on some WebDAV servers are not listed in cloudstorage due to strict xml element matching of <d:collection> elements.
2 years ago
zwim 7b2ac4769f
[plugin] AutoWarmth: cleanup code (#9788) 2 years ago
Jason Benwell 991b6ab48b
[CI] Set macOS Github Action to macOS 12.00 (#9856)
Doing a debug build using MACOSX_DEPLOYMENT_TARGET=12.00 produces a build that seems to work. The debug part isn't optimal but at least it builds and runs.
2 years ago
poire-z 35e8a0249d
bump base: LunaSVG: various upstream fixes, fix edge pixels bleeding (#9858) 2 years ago
hius07 cd56dd2edf
ReaderHighlight: pdf multi-page highlights (#9850) 2 years ago
Frans de Jonge b5e7ade880
[i18n] Fix Arabic translations (#9857)
They were still on the old ar_AA, notified in <https://github.com/koreader/koreader/pull/9822#issuecomment-1335439759>.
2 years ago
weijiuqiao 02c9f26f6c
Keyboard: add Chinese pinyin input-method (#9843) 2 years ago
melyux cc53ceb039
History: remember status filter, show filtered count in title (#9822) 2 years ago
Frans de Jonge fdbba7b020
Update translations for v2022.11 (#9851) 2 years ago
greatyingzi 61364ffc33
[chore] CoverImage: unified call method for Device.screen (#9839)
Co-authored-by: greatyingzi <greatyingzi@github.com>
2 years ago
yparitcher 513c88d661
Dispatcher: only suspend if Device:canSuspend (#9842)
Closes #9838
2 years ago
Frans de Jonge 84be6fc226
[i18n] Fix 'Choose new folder to set as home?' can't be translated (#9841)
Reported in <https://www.mobileread.com/forums/showthread.php?p=4276962#post4276962>.
2 years ago
poire-z b1f77ac085
Calendar view's day view: tweak SQL data extraction (#9834)
Ensure any reading at 0h00 or 23h59 looks like it
continues from previous or to next page if DB data
seems to indicate so.
2 years ago
Martín Fernández ee94a575f1
calibre: fix attempt to index path_entry (#9837)
Fixes #9836
2 years ago
poire-z 3ef2811e59
Calendar view's day view: visual tweaks (#9832)
- Add vertical lines every 10mn
- If today, show an arrow indicator at current time
- Add "Today's timeline" to menu
- Rename badly named self.hour_width, which is a height
2 years ago
hius07 905d5610ba
OPDSbrowser: accept catalogs without header in response (#9829)
Such catalogs are not cached.
Closes #6088.
2 years ago
hius07 c35140e8d2
ReaderHighlight: new icon in select mode ConfirmBox (#9830) 2 years ago
weijiuqiao 1c42641733
Calendar view: fancier day details with timeline (#9813) 2 years ago
melyux 78772fcbb1
BatteryStat: show current battery percentage (#9814) 2 years ago
melyux 38797498da
ScreenSaver: allow battery % in screensaver message (#9810) 2 years ago
hius07 90f6dd178e
CloudStorage: add DropBox starting folder (#9821) 2 years ago
hius07 5b889a0145
Reader style tweaks: register in Dispatcher manually (#9816)
Style tweaks can be applied with a gesture or added to a profile.
2 years ago
zwim 486d7071c7
[StatusBar] Allow status bars to be repainted if they are not covered (#9768) 2 years ago
weijiuqiao 81b2f22b04
ReaderStatistics: fix sync sql command (#9811)
Fixes sync sql command that uses left join instead of
inner join, which adds unnecessary comparisons and
might result in incorrect mapping between databases.
2 years ago
poire-z fc19b3ded7
bump base: mupdf cbz chapters patch: fix crash on flat archives (#9808) 2 years ago
hius07 1b066cf1de
Exporter: fix orphan highlights (#9802) 2 years ago
Robert-Jan de Dreu f953f041ea
PocketBook: use inkview to adjust image colors to look more bright (#9756)
Adjust image colors for PocketBook color devices to supply
brighter image colors. Make it a config option so we can
adjust it to a sensible default for all devices later.
Also enable HW Dithering for color devices: the dithering
flag allows us to figure out if what is viewed is an
image or text. This way we can enable color rendering
or not based on the dithering flag.
2 years ago
hius07 d95c692c78
ReaderHighlight: add abort select mode (#9786) 2 years ago
poire-z 93a0895b0b
bump base: inkview colors adjustment, mupdf cbz chapters patch (#9804)
Includes:
- Use inkview to adjust image colors to look more bright
- mupdf: add patch to support chapters in comic book archives
2 years ago
weijiuqiao e8983d335d VocabBuilder: fix not updating book title in the more dialogue when the book title is changed but not reassigned. 2 years ago
weijiuqiao efb335e195 KeyValuePage: fix key value too close when keys are short and values are long 2 years ago
weijiuqiao e481e60aa1 Syncservice: fix not updating services after adding new ones 2 years ago
NiLuJe df890105e4 ReaderMenu: Use a dedicated handler for the Menu key
Piggybacking on the touch gesture handlers implied that the gesture
filter (tap and/or swipe) was still applied, which made no sense.

Also, stop the weird Event roundtrip for events we're actually the only
ones to handle, just call the handler ourselves directly.

Fix #9792
2 years ago
NiLuJe 3a92f3385c PB: Handle KEY_HOME
It possibly historically never made it out of InkView in the past, but
apparently that's no longer the case and/or every other key behaves the
same way anyway, so it's harmless at worst.

Fix #9791
2 years ago
Hannes Krumbiegel a23c30f862
Add dictionaries for languages with over 1000 entries on Wiktionary (#9767) 2 years ago
hius07 9338b2e524
Dispatcher: show values (#9777) 2 years ago
hius07 06cec98d09
Filemanagerutil: let abbreviate manage extra slash (#9785) 2 years ago
Khoi Le 01975366b6
Add Vietnamese dictionaries (#9783) 2 years ago
NiLuJe 779a8e00de ExternalKeyboard: Harden the debugfs check
* Don't even attempt to run mount if we're not root (e.g., emu or PB)
* Deal with sane init systems that mount debugfs as debugfs and not none
  (e.g., Kindle).

Also, switch to debug logging for those errors, pluginloader is already
verbose enough.

Fix #9774
2 years ago
NiLuJe 304d393949 Kobo: Unbreak KSM detection
There isn't actually a kbmenu binary, the process names are all shell
scripts, kbmenu just happens to be the default folder name,
so we need full commandline matching ;).

Fix https://github.com/koreader/koreader/pull/9738#issuecomment-1312473323
Regression since #9738

Bump base

https://github.com/koreader/koreader-base/pull/1552
2 years ago
hius07 80f818e757
Button: fix long wrapped text alignment (#9778) 2 years ago
weijiuqiao 77e0ac57de
Vocabulary builder: make a word's book title changeable (#9776)
Adds the ability to re-assign which book a word belongs to
and change the name of an existing book. It can be used as
a way to regroup words and hide/show them by book.
2 years ago
weijiuqiao 5082289aad
Vocabulary builder: allow longer intervals (#9764) 2 years ago
hius07 71c65d18f1
Page turns: add backward tap zone width setting (#9772) 2 years ago
Dylan Calvin c5cd87f09a
(OPDS) Progess Sync Support for Kavita & Various Refactor Fixes (#9750) 2 years ago
weijiuqiao 678d0c911c
Vocabulary builder: safer db migration (#9702)
Refactor db migration to allow restarting from
an intermediate stage.
2 years ago
zwim d6b67f42d8
[plugin, AutoWarmth] Add time offset for toggling frontlight off (#9755) 2 years ago
poire-z 1fa706a2c5
Fix page stats messed up after rerendering (#9775)
Fix regression introduced by 48eb0231 #9651: on re-rerendering, the new pages count didn't reach the Statistics plugin.
So, if you change font size or margins, the stats for the page read since then were badly accounted, until a change of book or restart... I noticed that in Book map, strange nobody else did over the last 2.5 weeks.
2 years ago
weijiuqiao 8500fdd519
Cloud-based sync for 2 plugins: reading statistics and vocabulary builder (#9709)
This commit adds cross-device sync ability for two plugins: reading statistics and vocabulary builder. It relies on user setting up a Cloud server (DropBox and WebDAV but not FTP though) and designating a path. Behind the curtains sqlite databases are being passed around and updated.

UI-wise, for the statistics plugin, two new menu items Synchronize now and Cloud sync to set it up (might not be the best wording) are added. As for vocabulary builder, a similar Cloud sync button is added to the menu and a shortcut icon button to Synchronize now is pinned at the bottom corner.

CloudStorage new features: WebDAV creating folders and uploading files. And a new widget-like sync server chooser. In the end I decided not to add automatic sync, as the SQL commands part seem a bit much.
2 years ago
hius07 4f3000e882
ReaderHighlight: fix ending fragment check (#9769) 2 years ago
NiLuJe c5700b718d Android: Get rid of the Camera key hack
Unnecessary now that we have a standard mechanism to disable touch input
;).
2 years ago
NiLuJe 58ba4076a8 Kobo: Detect the original Touch "A" variant properly.
Because, of course, it has different input quirks than the B...

Fix #9742
2 years ago
NiLuJe c7f5bfb72a ScreenSaver: Handle Power button presses properly if the screensaver
lock is enabled

Fix #9744
2 years ago
NiLuJe bf0f8d66f0 Kobo: Don't exit the startup script on shutdown when started via KSM.
This ensures we keep displaying our own stuff, without KSM interference.

Fix #9748
2 years ago
NiLuJe 1ec6fb5fcf InputContainer/Dispatcher: Allow toggling touch input
This is made easier by the fact that only a single method in a single
widget actually handles Gesture, and that we barely ever overload it.
So, apply a bit of monkey-patching trickery to handle the magic :).

Fix #9695
2 years ago
NiLuJe 47734eefef DeviceListener: Unify Notification usage 2 years ago
NiLuJe ce02e92937 ExternalKeyboard: Map KEY_ESC to Back, to ease navigation 2 years ago
NiLuJe 6f29b48587 ExternalKeyboards: Drop the startup FM workaround
Unnecessary since 2db294d0b7,
as the delayed event takes care of our problem ;).
2 years ago
Martín Fernández 5e1932bf38
bump android-luajit-launcher: a bunch of devices (#9763)
- crema carta g
- onyx leaf 2
- onyx lomonosov
- onyx montecristo 3
- onyx nova pro
- ridi paper 3
- fix: onyx poke 3 lights

https://github.com/koreader/koreader/issues/8482
2 years ago
weijiuqiao 74eb001eca
Calendar view: adjust color palette to avoid pure black bars (#9739) 2 years ago
Roygbyte eb1e084d93
NewsDownloader: avoid some crashes (#9697)
Keep nil reference from being passed to UIManager.
2 years ago
zwim a1256f6758
Fix regression (introduced in #9657) (#9759)
This should fix #9741.
2 years ago
poire-z e3f1642308
bump crengine: speed optimisations, support obfuscated fonts (#9758)
Includes:
- CSS: cache parse results
- LVContainer: optimize filename lookups with a hashtable
- EPUB: optimize OPF parsing
- EPUB: support obfuscated embedded fonts
- Fonts/CSS: handle "oblique" as "italic"
2 years ago
Martín Fernández 9473c70dcc
exporter: prevent exporting documents when they're not open (#9753)
Fixes #9740

thanks to @hius07 for the hint!
2 years ago
zwim cd69382934
[plugin] AutoWarmth: fix frontlight toggle during day (#9735) 2 years ago
Frans de Jonge 0f52499d37
Dispatcher: add unused singular to action plurals (#9734)
Tools and humans alike are confused by leaving it empty. It should be inoffensive to resolve the problem this by having an unused singular. Doing anything else would make it so we couldn't have the correct plurals in Slovak, among others.

Follow-up to https://github.com/koreader/koreader/pull/9726
2 years ago
zwim 6717852825
AutoWarmth: add message on user change of night mode (#9715) 2 years ago
zwim 48a1f53d21
[doc] RadioButtonWidget (#9733) 2 years ago
zwim 07748b0999
[UIManager] Reverse order of _task_queue (#9706) 2 years ago
hius07 c3ed51aded
ReaderHighlight: set default long-press action with a gesture (#9723) 2 years ago
zwim 629304adce
AutoWarmth: Fix frontlight flash after resume and weird true midnight settings (#9730) 2 years ago
NiLuJe 4a355f789d ExternalKeyboard: Add a note about the special drivers sysfs knob
Just so that I don't forget about it entirely, as I haven't had to use
it so far...
2 years ago
NiLuJe 683d14c3fb Bump base
https://github.com/koreader/koreader-base/pull/1549
2 years ago
NiLuJe 8e31bc8a68 ExternalKeyboard: Simplify the menu shenanigans
checked_func implies keep_menu_open anyway
2 years ago
NiLuJe 925fd647dc InputContainer/FocusManager: Simplify key_events clearing on unplug
Thanks to @poire-z for the idea, it's indeed much nicer this way ;).
2 years ago
NiLuJe d585cd5d86 ExternalKeyboard: Don't keep the menu open on toggle, the event cascade
may reinit stuff and close it anyway.
2 years ago
NiLuJe 09498d4bbb ReaderUI: Refresh key_events handlers on keyboard hotplug 2 years ago
NiLuJe e0bfebb0b5 FileManager: Refresh key_events handlers on keyboard hotplug 2 years ago
NiLuJe 2db294d0b7 ExternalKeyboard: Debounce the Connected/Disconnected events
Until we find a way to fine-tune the evdev device parsing, we'll often
have at least two devices per keyboard, but we only need a single event
;).
2 years ago
NiLuJe b28d52a124 ExternalKeyboard: Map KEY_COMPOSE to "Menu" 2 years ago
NiLuJe 5a939553fc ExternalKeyboard: Flip the hasKeys Device cap, too
This allows navigation to work as expected ;).
2 years ago
NiLuJe 8cba303b96 ReaderPanning: Don't crash in the key event handler.
I'm unclear on where this is supposed to do anything, though.
But at least it no longer crashes, so, yay?
2 years ago
NiLuJe 53c0a65a5b ExternalKeyboard: Don't spam the "Keyboard connected" InfoMessage
Only show it for the first keyboard
2 years ago
NiLuJe 5b6d2ab66f ExternalKeyboard: Workaround bad interaction with coverbrowser when
enabled on startup
2 years ago
NiLuJe 02020176a9 ExternalKeyboard: Only disable OTG on exit
We currently did so on plugin destruction, which was a bit
overkill/annoying, as plugin instances are torn down on view
changes (i.e., when FileManager/ReaderUI are torn down) ;).
2 years ago
NiLuJe 9863a9c0bb ExternalKeyboard: Use the evdev number passed along by base to avoid sweeping the full list of input devices
This required some... creative thinking to avoid complexifying common
Input/UIManager codepaths ;p.
2 years ago
NiLuJe 25dce4d1b5 WebDAV: Fix a warning with the latest LuaCheck version
(error-prone negation)
2 years ago
NiLuJe 1e24a1c7a3 ReaderUI: Properly neuter gesture handling from InputContainer modules
None[1] of them actually rely on their own onGesture handler, they
all register their own stuff to ReaderUI's.
Hotfix #9710 revealed that the way this was handled didn't exactly
work as expected ;).

The only thing that consumes ges_events is InputContainer's onGesture
method. All these modules *extend* InputContainer, but none of them
*implement* a custom onGesture, so self.onGesture = nil was just a NOP,
they always access InputContainer's method via inheritance.
If we actively want to neuter it, we *have* to implement it in that
module (with a NOP).

[1] The exception being ReaderZooming, but that only when in flip mode.
2 years ago
Frans de Jonge 71cad1e49c
Fix broken translation metadata (#9726)
Should fix https://github.com/koreader/koreader/discussions/9719

Regression introduced in https://github.com/koreader/koreader/pull/9682 by my fault.

See the xgettext error message:

```
frontend/dispatcher.lua:573: warning: Empty msgid.  It is reserved by GNU gettext:
                                      gettext("") returns the header entry with
                                      meta information, not the empty string.
xgettext: warning: msgid '' is used without plural and with plural.
                   xgettext.c:1975: Here is the occurrence without plural.
                   frontend/dispatcher.lua:573: Here is the occurrence with plural.
                   Workaround: If the msgid is a sentence, change the wording of the sentence; otherwise, use contexts for disambiguation.
```

Co-authored-by: Frans de Jonge <frans@clevercast.com>
2 years ago
hius07 e3f134da78
KeyValuePage: enhance TitleBar (#9713)
Add left button.
Allow title centering (may be useful with the left button).
2 years ago
Martín Fernández 8b8d79275c
calibre metadata: prevent duplicates (#9725)
Fixes #9722
2 years ago
zwim 94d3d3b487
[timeval] RIP on All Saints Day (#9686) 2 years ago
poire-z f10ea7d339
TextViewer: allow close on any multiswipe (#9720)
Similar to most fullscreen widgets.
2 years ago
weijiuqiao 42d4056f8f
Virtual keyboard: auto releasable Shift and keypopups (#9659)
Long-press on Shift to keep it non-auto-released.
2 years ago
hius07 e1a52b5881
OPDSbrowser: fix uninitialized calibre (#9718) 2 years ago
NiLuJe f58d1c0449
Cleanup: Track color constant name change (#9716)
* Bump base

https://github.com/koreader/koreader-base/pull/1547
https://github.com/koreader/koreader-base/pull/1548
2 years ago
yparitcher 4d8e2f0ea1 ges_events must be a table
regression in #9691 b523c2e8b9
2 years ago
Roygbyte cf643c453b
KeyValuePage: Handle nil/empty kvp arrays (#9705) 2 years ago
NiLuJe c740ee3174 Kindle: Make sure Device:untar actually works
The fuse proxy strikes again... Fix #9704
2 years ago
NiLuJe f08f6fc0bf Bump base
https://github.com/koreader/koreader-base/pull/1545
https://github.com/koreader/koreader-base/pull/1546
https://github.com/koreader/koreader-base/pull/1520
2 years ago
NiLuJe 0382e0d4c2 OTM: Handle syntax errors at *both* stages when migrating
defaults.persistent.lua

We were pcall'ing the parsing, but not the execution...
The funky Lua syntax quirks means that it is possible to pass the former
but not the latter ;).

Fix #9700, de-facto regression since #9546
2 years ago
NiLuJe 5cb358f653 PocketBook: Move low-level input event type translation to the low-level
input modules.

There was a weird mix of touch being handled there, and key/msc here,
which was weird and made the logs extremely confusing to read.
2 years ago
NiLuJe 3dd87a38b3 Kindle: Oh, hey, turns out that was the new Basic
I'd completely forgotten they'd announced one ;o).
2 years ago
NiLuJe 43b021d37c Input: Allow disabling rotation_map entirely
(This involves moving it to the instance object to avoid inheritance).

Pocketbook: Disable rotation_map on the Era (fix #9556)
It would appear that InkView handles the translation for us, now...
2 years ago
NiLuJe 4e233aafc0 AutoSuspend: Update the standby help message.
Tone it down for everyone, as it's been running smoothly for a while
now, but add a dedicated extra warning on buggy boards that it might
randomly implode.
2 years ago
NiLuJe e98bf342e7 GH: Mention that toggling verbose debug logging requires a restart.
Otherwise, stuff that inherits logger functions on require don't get
logged (i.e., framebuffer), and the debug guards aren't hooked.
2 years ago
NiLuJe 9470d473da Kindle: Lower some PM log messages to debug 2 years ago
NiLuJe b1504ea718 Kobo: Downgrade some suspend logging messages to debug, and *all*
standby logging messages to debug (except for warnings/errors, of course).
2 years ago
NiLuJe dbc559c0e0 USBMS: Add a human-readable log message when entering USBMS
Because apparently people are confused by the UIManager:quit ones...
2 years ago
NiLuJe cac37ad018 Kobo: Flag all the boards similar to the Libra 2 as unreliable (Clara 2E
& Nia).

And also disable the jump marker on those, like we did for the Libra 2.
2 years ago
NiLuJe bf574bfaaf GestureDetector: Only show the final adjustGesCoordinate log if the gesture actually was adjusted. 2 years ago
NiLuJe b656b9f5af Persist: Make sure data is flushed to disk on save
(i.e., do the usual flush & fsync dance, because FAT32 is the worst).
2 years ago
NiLuJe b523c2e8b9 InputContainer: Fall cleanup ;).
Get rid of the doc & seqtext fields, as they are not actually used (nor
are they particularly useful, the event handler's name should be pretty
self-explanatory).

Also, tweak the key_events documentation to highlight the quirks of the
API, especially as far as array nesting is involved...

Random drive-by cleanup of the declarations of key_events & ges_events
to re-use the existing instance object (now that we know they're sane
;p) for tables with a single member (less GC pressure).
2 years ago
Borys Lykah 9b2201a438
Initial hotpluggable keyboard handling (#9540)
* Added a new plugin external-keyboard. It listens to USB events. When keyboard is plugged in or plugged out, it updates device and input configuration accordingly.
* Added new fake events UsbDevicePlugIn and UsbDevicePlugOut that are emitted when a device is connected to a book reader that plays the role of USB host. The usage of the existing events UsbPlugIn and UsbPlugOut has not changed - they are used when a reader is connected to a host. The koreader-base has a related PR for those events.
* Did a small refactoring of initialization for the modules FocusManager and InputText. They check device keyboard capabilities on their when the module is first loaded and store it. Some of the initialization code has been extracted into functions, so that we can re-initialize them when keyboard is (dis)connected.
* Initial implementation centered around text input, and tested with USB keyboards on devices with OTG support.
* Said OTG shenanigans are so far supported on devices with debugfs & the chipidea driver, or sunxi devices.
2 years ago
hius07 c36a2929ac
OPDSbrowser refactoring (#9703)
Refactoring:
-removed duplicated code
-removed passing of username/password through all the modules
-logical order of the methods
-some optimizing and drying
-comments

New features:
-subcatalog link can be saved to the list of servers (eg: direct link to the catalog of books by an author)
-more book information can be fetched and shown (Book information)
-fixed access to the Standard library (by @KGKopli, closes #9372)
2 years ago
hius07 a8ff3a6571 readerzoomin: add zoom mode translation, simplify zoom mode validation 2 years ago
hius07 0919c4ef17 readerkoptlistener: simplify zoom mode validation 2 years ago
weijiuqiao e8cd2ba498
WebDAV: fix bugs when start folder starts with "/" (#9688) 2 years ago
zwim d7c63edca6
Random optimizations (#9657) 2 years ago
poire-z e72211a5bf
bump base: xtext, cre, ffi, harfbuzz (#9696)
Includes:
- Update to HarfBuzz 5.3.1, libffi 3.4.4
- LPeg: Minor cosmetic tweaks
- cre.cpp: add cre.softHyphenateText(lang, text)
- xtext: properly support soft-hyphens
- xtext: option to allow limiting usage of soft-hyphens
- xtext: better ellipsis support with Arabic text
- ffi: Add fflush to posix_h
- fb_mxcfb: Don't wraparound update markers until UINT32_MAX
2 years ago
zwim 31b16ba3e1
UIManager: get rid of self._running and self._run_forever (#9669) 2 years ago
zwim 88add4ed6f
[spec] _task_queue has to be sorted! (#9694) 2 years ago
hius07 d37803a8e7
Dispatcher: preserve profile name, show actions count (#9682)
(1) Preserve profile name when selecting the "Nothing" action.
(2) Show number of actions instead of "Many".
2 years ago
Dylan Calvin 07be320722
[plugin] OPDS: Add stream from page (#9681) 2 years ago
Martín Fernández 3530aef891
exporter.koplugin: send the right mimetype when sharing text. (#9194)
Add an option to send title too. Currently unused.
2 years ago
zwim db8e2a9403
[spec] Get rid of argc in the _task_queue (#9684)
argc was eliminated in #9624

Thanks to @hius07  https://github.com/koreader/koreader/pull/9680#issuecomment-1290440793

Plus a minor change in the scheduling test, so that the _task_queue is not filled only from one end.
2 years ago
weijiuqiao ad89742e9b
Keyvaluepage: better alignment (#9672) 2 years ago
hius07 1d9c81de6e
ScrollableContainer: fix 'argc' (#9680) 2 years ago
Roygbyte 724db87755
ButtonDialog: add documentation about layout (#9505) 2 years ago
sonix-github eb56948fac
Keyboard: add Slovak virtual keyboard (#9656) 2 years ago
zwim dc2cc5cba2
AutoDim: close trapper widget explicitly (#9675) 2 years ago
poire-z 48eb02318d
Dispatcher: avoid multiple cre rerenderings when many settings changed (#9651)
When a gesture/profile was updating multiple cre settings,
each setting handler would emit UpdatePos which each would
force a re-rendering.
When this might be happening, postpone the rerendering
until all are set.
Needs some bit of refactoring to the events at play:
introduce "DocumentRerendered" event, and use it where
we used "UpdatePos" or "UpdateToc" to mean exactly that.
2 years ago
hius07 d1abbbfdd8
Document settings submenu: add Save as default (#9643) 2 years ago
hius07 b8d9944a07
Profiles: optional registration in Dispatcher (#9623) 2 years ago
weijiuqiao 6bd7dfdf41
Vocabulary builder: add "Open..." to Dispatcher (#9634)
So it can be called from a gesture.
Also accommodate reset all in reversed order.
2 years ago
weijiuqiao edf7cc9a61
Vocabulary builder: support extracting context from pdfs (#9622)
Move getSelectedWordContext(), now document specific,
from ReaderHighlight into each document module.
2 years ago
weijiuqiao 58613d66e7
UI font fallbacks: use bold fonts if present for bold (#9621) 2 years ago
hius07 4d4b04359c
ReadHistory: refactoring with binary search (#9603)
Get rid of indexing and sorting, reduce flushing.
2 years ago
NiLuJe 54fae2987e
Support Android AArch64 (#9645)
c.f., https://github.com/koreader/koreader-base/pull/1540 & https://github.com/koreader/android-luajit-launcher/pull/390
2 years ago
NiLuJe f04beb157b Bump base
https://github.com/koreader/koreader-base/pull/1541
https://github.com/koreader/koreader-base/pull/1539
2 years ago
NiLuJe 82b5f64178 time: Fix another subtle FP issue in split_s_us
The us part wasn't actually truncated properly.
2 years ago
NiLuJe 318d0e0ff0 UIManager: Modernize commented out debugging code
If you've ever enabled the main loop debugging, you'll know that
actually dumping the full window stack was *hilarious*.
Just print table counts, it's often good enough to debug what's
happening in the exceedingly rare cases you need this ;).
Also, it'll actually be readable, unlike the previous insanity ^^.
2 years ago
NiLuJe 390c875c80 Fix: key_events cannot be nil anymore since #9586 2 years ago
NiLuJe d2ac2ca6e7 Cleanup: Remove unused onAnyKeyPressed handlers
And simplify the few we do catch by using aliases instead of duplicated
functions;).
2 years ago
NiLuJe 78381c3afa ScreenSaverWidget: Actually enable the onAnyKeyPressed handler
Which allows us to exit the screensaver on, well, any key press ;o).
2 years ago
NiLuJe 854cbe9f94 Kobo: Unbreak frontlight toggle for some specific values
Because of floating point computery math stuff.

Regression since #9609
c.f., https://github.com/koreader/koreader/pull/9609#issuecomment-1288187080
2 years 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

@ -4,10 +4,12 @@ CI_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=/dev/null
source "${CI_DIR}/common.sh"
echo -e "\n${ANSI_GREEN}shellcheck results"
"${CI_DIR}/helper_shellchecks.sh"
exit_code=0
echo -e "\\n${ANSI_GREEN}Checking for unscaled sizes"
echo -e "\n${ANSI_GREEN}shellcheck results${ANSI_RESET}"
"${CI_DIR}/helper_shellchecks.sh" || exit_code=1
echo -e "\\n${ANSI_GREEN}Checking for unscaled sizes${ANSI_RESET}"
# stick `|| true` at the end to prevent Travis exit on failed command
unscaled_size_check=$(grep -nr --include=*.lua --exclude=koptoptions.lua --exclude-dir=base --exclude-dir=luajit-rocks --exclude-dir=install --exclude-dir=keyboardlayouts --exclude-dir=*arm* "\\(padding\\|margin\\|bordersize\\|width\\|height\\|radius\\|linesize\\) = [0-9]\\{1,2\\}" | grep -v '= 0' | grep -v '= [0-9]/[0-9]' | grep -Ev '(default_option_height|default_option_padding)' | grep -v scaleBySize | grep -v 'unscaled_size_check: ignore' || true)
# Also check Geom objects; for legibility two regular expressions rather than
@ -15,26 +17,28 @@ unscaled_size_check=$(grep -nr --include=*.lua --exclude=koptoptions.lua --exclu
unscaled_size_check_geom=$(grep -E -nr --include=*.lua --exclude=gesturerange_spec.lua --exclude-dir=base --exclude-dir=luajit-rocks --exclude-dir=*arm* 'Geom:new{.+ [wh] = [0-9]{1,4}' | grep -Ev '[wh] = 0' | grep -v '= [0-9]/[0-9]' | grep -v scaleBySize || true)
if [ "${unscaled_size_check}" ] || [ "${unscaled_size_check_geom}" ]; then
echo -e "\\n${ANSI_RED}Warning: it looks like you might be using unscaled sizes.\\nIt is almost always preferable to defer to one of the predefined sizes in ui.size in the following files:"
echo -e "\\n${ANSI_RED}Warning: it looks like you might be using unscaled sizes.\\nIt is almost always preferable to defer to one of the predefined sizes in ui.size in the following files:${ANSI_RESET}"
echo "${unscaled_size_check}"
echo "${unscaled_size_check_geom}"
exit 1
exit_code=1
fi
tab_detected=$(grep -P "\\t" --include \*.lua --exclude={dateparser.lua,xml.lua} --recursive {reader,setupkoenv,datastorage}.lua frontend plugins spec || true)
if [ "${tab_detected}" ]; then
echo -e "\\n${ANSI_RED}Warning: tab character detected. Please use spaces."
echo -e "\\n${ANSI_RED}Warning: tab character detected. Please use spaces.${ANSI_RESET}"
echo "${tab_detected}"
exit 1
exit_code=1
fi
untagged_todo=$(grep -Pin "[^\-]\-\-(\s+)?@?(todo|fixme|warning)" --include \*.lua --exclude={dateparser.lua,xml.lua} --recursive {reader,setupkoenv,datastorage}.lua frontend plugins spec || true)
if [ "${untagged_todo}" ]; then
echo -e "\\n${ANSI_RED}Warning: possible improperly tagged todo, fixme or warning detected."
echo -e "\\n${ANSI_RED} use --- followed by @todo, @fixme or @warning."
echo -e "\\n${ANSI_RED} use --- followed by @todo, @fixme or @warning.${ANSI_RESET}"
echo "${untagged_todo}"
exit 1
exit_code=1
fi
echo -e "\n${ANSI_GREEN}Luacheck results"
luajit "$(command -v luacheck)" --no-color -q {reader,setupkoenv,datastorage}.lua frontend plugins spec
echo -e "\n${ANSI_GREEN}Luacheck results${ANSI_RESET}"
luacheck -q {reader,setupkoenv,datastorage}.lua frontend plugins spec || exit_code=1
exit ${exit_code}

@ -7,8 +7,6 @@ ANSI_RED="\033[31;1m"
# shellcheck disable=SC2034
ANSI_GREEN="\033[32;1m"
ANSI_RESET="\033[0m"
# shellcheck disable=SC2034
ANSI_CLEAR="\033[0K"
travis_retry() {
local result=0
@ -33,43 +31,3 @@ travis_retry() {
set -e
return ${result}
}
retry_cmd() {
local result=0
local count=1
set +e
retry_cnt=$1
shift 1
while [ ${count} -le "${retry_cnt}" ]; do
[ ${result} -ne 0 ] && {
echo -e "\n${ANSI_RED}The command \"$*\" failed. Retrying, ${count} of ${retry_cnt}${ANSI_RESET}\n" >&2
}
"$@"
result=$?
[ ${result} -eq 0 ] && break
count=$((count + 1))
sleep 1
done
[ ${count} -gt "${retry_cnt}" ] && {
echo -e "\n${ANSI_RED}The command \"$*\" failed ${retry_cnt} times.${ANSI_RESET}\n" >&2
}
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

@ -5,28 +5,6 @@ CI_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${CI_DIR}/common.sh"
# shellcheck disable=2016
mapfile -t shellscript_locations < <({ git grep -lE '^#!(/usr)?/bin/(env )?(bash|sh)' | sed "/^plugins\/terminal.koplugin\/shfm$/d" && git submodule --quiet foreach '[ "$path" = "base" -o "$path" = "platform/android/luajit-launcher" ] || git grep -lE "^#!(/usr)?/bin/(env )?(bash|sh)" | sed "s|^|$path/|"' && git ls-files ./*.sh; } | sort | uniq)
mapfile -t shellscript_locations < <({ git grep -lE '^#!(/usr)?/bin/(env )?(bash|sh)' | sed "/^plugins\/terminal.koplugin\/shfm$/d" && git submodule --quiet foreach '[ "$path" = "base" -o "$path" = "platform/android/luajit-launcher" ] || git grep -lE "^#!(/usr)?/bin/(env )?(bash|sh)" | sed "s|^|$path/|"' && git ls-files './*.sh'; } | sort | uniq)
SHELLSCRIPT_ERROR=0
SHFMT_OPTIONS="-i 4 -ci"
for shellscript in "${shellscript_locations[@]}"; do
echo -e "${ANSI_GREEN}Running shellcheck on ${shellscript}"
shellcheck "${shellscript}" || SHELLSCRIPT_ERROR=1
echo -e "${ANSI_GREEN}Running shfmt on ${shellscript}"
# shellcheck disable=2086
if ! shfmt ${SHFMT_OPTIONS} -kp "${shellscript}" >/dev/null 2>&1; then
echo -e "${ANSI_RED}Warning: ${shellscript} contains the following problem:"
# shellcheck disable=2086
shfmt ${SHFMT_OPTIONS} -kp "${shellscript}" || SHELLSCRIPT_ERROR=1
continue
fi
# shellcheck disable=2086
if [ "$(cat "${shellscript}")" != "$(shfmt ${SHFMT_OPTIONS} "${shellscript}")" ]; then
echo -e "${ANSI_RED}Warning: ${shellscript} does not abide by coding style, diff for expected style:"
# shellcheck disable=2086
shfmt ${SHFMT_OPTIONS} -d "${shellscript}" || SHELLSCRIPT_ERROR=1
fi
done
exit "${SHELLSCRIPT_ERROR}"
./base/utils/shellcheck.sh "${shellscript_locations[@]}"

@ -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

@ -1,17 +0,0 @@
#!/usr/bin/env bash
CI_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=/dev/null
source "${CI_DIR}/common.sh"
echo -e "\\n${ANSI_GREEN}make fetchthirdparty"
bash "${CI_DIR}/fetch.sh"
echo -e "\\n${ANSI_GREEN}static checks"
bash "${CI_DIR}/check.sh"
echo -e "\\n${ANSI_GREEN}make all"
bash "${CI_DIR}/build.sh"
echo -e "\\n${ANSI_GREEN}make testfront"
bash "${CI_DIR}/test.sh"

@ -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

@ -28,7 +28,7 @@ Please try to include the relevant sections in your issue description.
You can upload the whole `crash.log` file (zipped if necessary) on GitHub by dragging and dropping it onto this textbox.
If your issue doesn't directly concern a Lua crash, we'll quite likely need you to reproduce the issue with *verbose* debug logging enabled before providing the logs to us.
To do so, from the file manager, go to [Tools] → More tools → Developer options, and tick both `Enable debug logging` and `Enable verbose debug logging`.
To do so, go to `Top menu → Hamburger menu → Help → Report a bug` and tap `Enable verbose logging`. Restart as requested, then repeat the steps for your issue.
If you instead opt to inline it, please do so behind a spoiler tag:
<details>

@ -1,46 +1,170 @@
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-11 and macos-12 are broken at this time being.
# https://github.com/koreader/koreader/issues/8686,
# https://github.com/koreader/koreader/issues/8686#issuecomment-1172950236
macos:
strategy:
fail-fast: false
matrix:
platform: ['arm64', 'x86_64']
runs-on: ${{ matrix.platform == 'arm64' && 'macos-14' || 'macos-13' }}
# Please don't update to newer macOS version unless you can test that the new
# action produces working binaries.
runs-on: macos-10.15
env:
# Bump number to reset all caches.
CACHE_EPOCH: '0'
CLICOLOR_FORCE: '1'
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.platform == 'arm64' && '11.0' || '10.15' }}
MAKEFLAGS: 'OUTPUT_DIR=build INSTALL_DIR=install TARGET=macos'
steps:
- name: XCode version
run: xcode-select -p
- name: Check out Git repository
uses: actions/checkout@v3
# 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 }}-${{ runner.arch }}-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 }}-${{ runner.arch }}-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 gettext pkg-config wget gnu-getopt grep p7zip ninja
)
brew install --formula --quiet "${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
uses: actions/upload-artifact@v3
- 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:
name: koreader-macos
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-${{ matrix.platform }}
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

@ -0,0 +1,5 @@
{
"$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json",
"runtime.version": "LuaJIT",
}

@ -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,37 +1,49 @@
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,)
ANDROID_ARCH?=arm
ifeq ($(ANDROID_ARCH), x86)
ANDROID_ABI:=$(ANDROID_ARCH)
IS_RELEASE := $(if $(or $(IS_RELEASE),$(APPIMAGE),$(LINUX),$(MACOS)),1,)
ifeq ($(ANDROID_ARCH), arm64)
ANDROID_ABI?=arm64-v8a
else ifeq ($(ANDROID_ARCH), x86)
ANDROID_ABI?=$(ANDROID_ARCH)
else ifeq ($(ANDROID_ARCH), x86_64)
ANDROID_ABI?=$(ANDROID_ARCH)
else
ANDROID_ARCH?=arm
ANDROID_ABI?=armeabi-v7a
endif
ANDROID_ABI?=armeabi-v7a
# Use the git commit count as the (integer) Android version code
ANDROID_VERSION?=$(shell git rev-list --count HEAD)
ANDROID_NAME?=$(VERSION)
# set PATH to find CC in managed toolchains
ifeq ($(TARGET), android)
PATH:=$(ANDROID_TOOLCHAIN)/bin:$(PATH)
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=$(shell PATH='$(PATH)' $(CC) -dumpmachine 2>/dev/null)
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) ."
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,410 +178,16 @@ 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
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
# in runtime luajit-launcher's libluajit.so will be loaded
-rm $(INSTALL_DIR)/koreader/libs/libluajit.so
# fresh APK assets
rm -rfv $(ANDROID_ASSETS) $(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
# 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 && 7z a -l -m0=lzma2 -mx=9 \
../../$(ANDROID_ASSETS)/koreader.7z * \
-xr!*cache$ \
-xr!*clipboard$ \
-xr!*data/dict$ \
-xr!*data/tessdata$ \
-xr!*history$ \
-xr!*l10n/templates$ \
-xr!*libs$ \
-xr!*ota$ \
-xr!*resources/fonts* \
-xr!*resources/icons/src* \
-xr!*rocks/bin$ \
-xr!*rocks/lib/luarocks$ \
-xr!*screenshots$ \
-xr!*share/man* \
-xr!*spec$ \
-xr!*tools$ \
-xr!*COPYING$ \
-xr!*Makefile$ \
-xr!*NOTES.txt$ \
-xr!*NOTICE$ \
-xr!*README.md$ \
-xr!*sdcv \
-xr'!.*'
# make the android APK
$(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)
# Include target specific rules.
ifneq (,$(wildcard make/$(TARGET).mk))
include make/$(TARGET).mk
endif
androiddev: androidupdate
$(MAKE) -C $(ANDROID_LAUNCHER_DIR) dev
android-toolchain:
$(MAKE) -C $(KOR_BASE) android-toolchain
android-ndk:
$(MAKE) -C $(KOR_BASE)/toolchain $(ANDROID_NDK_HOME)
android-sdk:
$(MAKE) -C $(KOR_BASE)/toolchain $(ANDROID_HOME)
# for gettext
DOMAIN=koreader
@ -619,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) $@

@ -11,7 +11,7 @@
[![Weblate Status][badge-weblate]][link-weblate]
[Download](https://github.com/koreader/koreader/releases) •
[User guide](http://koreader.rocks/koreader-user-guide.pdf) •
[User guide](http://koreader.rocks/user_guide/) •
[Wiki](https://github.com/koreader/koreader/wiki) •
[Developer docs](http://koreader.rocks/doc/)
@ -19,7 +19,7 @@
* **portable**: runs on embedded devices (Cervantes, Kindle, Kobo, PocketBook, reMarkable), Android and Linux computers. Developers can run a KOReader emulator in Linux and MacOS.
* **multi-format documents**: supports fixed page formats (PDF, DjVu, CBT, CBZ) and reflowable e-book formats (EPUB, FB2, Mobi, DOC, CHM, TXT). Scanned PDF/DjVu documents can also be reflowed with the built-in K2pdfopt library.
* **multi-format documents**: supports fixed page formats (PDF, DjVu, CBT, CBZ) and reflowable e-book formats (EPUB, FB2, Mobi, DOC, RTF, HTML, CHM, TXT). Scanned PDF/DjVu documents can also be reflowed with the built-in K2pdfopt library. [ZIP files][link-wiki-zip] are also supported for some formats.
* **full-featured reading**: multi-lingual user interface with a highly customizable reader view and many typesetting options. You can set arbitrary page margins, override line spacing and choose external fonts and styles. It has multi-lingual hyphenation dictionaries bundled into the application.
@ -33,7 +33,7 @@
* **and much more**: look up words with StarDict dictionaries / Wikipedia, add your own online OPDS catalogs and RSS feeds, over-the-air software updates, an FTP client, an SSH server, …
Please check the [user guide](http://koreader.rocks/koreader-user-guide.pdf) and the [wiki][link-wiki] to discover more features and to help us document them.
Please check the [user guide](http://koreader.rocks/user_guide/) and the [wiki][link-wiki] to discover more features and to help us document them.
## Screenshots
@ -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
@ -101,3 +101,4 @@ Right now we only support [liberapay](https://liberapay.com/KOReader) donations,
[link-issues-features]:https://github.com/koreader/koreader/issues?q=is%3Aopen+is%3Aissue+label%3Aenhancement
[link-weblate]:https://hosted.weblate.org/engage/koreader/?utm_source=widget
[link-wiki]:https://github.com/koreader/koreader/wiki
[link-wiki-zip]:https://github.com/koreader/koreader/wiki/ZIP

@ -1 +1 @@
Subproject commit fc31b4d98bb267946a3080e07f1a0f8d50dfa2a3
Subproject commit fd9dc60997518c607d45cdd75d68ed7e7c004db0

@ -1,4 +1,4 @@
-- need low-level mechnism to detect android to avoid recursive dependency
-- need low-level mechanism to detect android to avoid recursive dependency
local isAndroid, android = pcall(require, "android")
local lfs = require("libs/libkoreader-lfs")
@ -17,18 +17,25 @@ 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
lfs.mkdir(os.getenv("XDG_CONFIG_HOME"))
end
else
local user_rw = jit.os == "OSX" and "Library/Application Support" or ".config"
data_dir = string.format("%s/%s/%s", os.getenv("HOME"), user_rw, "koreader")
local user_rw = string.format("%s/%s", os.getenv("HOME"), jit.os == "OSX" and "Library/Application Support" or ".config")
if lfs.attributes(user_rw, "mode") ~= "directory" then
lfs.mkdir(user_rw)
end
data_dir = string.format("%s/%s", user_rw, "koreader")
end
else
data_dir = "."
end
if lfs.attributes(data_dir, "mode") ~= "directory" then
lfs.mkdir(data_dir)
local ok, err = lfs.mkdir(data_dir)
if not ok then error(err .. " " .. data_dir) end
end
return data_dir
@ -42,6 +49,13 @@ function DataStorage:getSettingsDir()
return self:getDataDir() .. "/settings"
end
function DataStorage:getDocSettingsDir()
return self:getDataDir() .. "/docsettings"
end
function DataStorage:getDocSettingsHashDir()
return self:getDataDir() .. "/hashdocsettings"
end
function DataStorage:getFullDataDir()
if full_data_dir then return full_data_dir end
@ -57,13 +71,24 @@ end
local function initDataDir()
local sub_data_dirs = {
"cache", "clipboard",
"data", "data/dict", "data/tessdata",
"history", "ota", "plugins",
"screenshots", "settings", "styletweaks",
"cache",
"clipboard",
"data",
"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
"plugins",
"screenshots",
"settings",
"styletweaks",
}
local datadir = DataStorage:getDataDir()
for _, dir in ipairs(sub_data_dirs) do
local sub_data_dir = string.format("%s/%s", DataStorage:getDataDir(), dir)
local sub_data_dir = string.format("%s/%s", datadir, dir)
if lfs.attributes(sub_data_dir, "mode") ~= "directory" then
lfs.mkdir(sub_data_dir)
end

@ -36,14 +36,6 @@ DCREREADER_VIEW_MODE = "page",
-- default to false
DSHOWOVERLAP = false,
-- show hidden files in filemanager
-- default to false
DSHOWHIDDENFILES = false,
-- landscape clockwise rotation
-- default to true, set to false for counterclockwise rotation
DLANDSCAPE_CLOCKWISE_ROTATION = true,
-- default minimum screen height for reading with 2 pages in landscape mode
DCREREADER_TWO_PAGE_THRESHOLD = 7,
@ -73,6 +65,10 @@ DTAP_ZONE_BOTTOM_LEFT = {x = 0, y = 7/8, w = 1/8, h = 1/8},
DTAP_ZONE_BOTTOM_RIGHT = {x = 7/8, y = 7/8, w = 1/8, h = 1/8},
DDOUBLE_TAP_ZONE_NEXT_CHAPTER = {x = 1/4, y = 0, w = 3/4, h = 1},
DDOUBLE_TAP_ZONE_PREV_CHAPTER = {x = 0, y = 0, w = 1/4, h = 1},
DSWIPE_ZONE_LEFT_EDGE = { x = 0, y = 0, w = 1/8, h = 1},
DSWIPE_ZONE_RIGHT_EDGE = { x = 7/8, y = 0, w = 1/8, h = 1},
DSWIPE_ZONE_TOP_EDGE = { x = 0, y = 0, w = 1, h = 1/8},
DSWIPE_ZONE_BOTTOM_EDGE = { x = 0, y = 7/8, w = 1, h = 1/8},
-- koptreader config defaults
DKOPTREADER_CONFIG_FONT_SIZE = 1.0, -- range from 0.1 to 3.0
@ -180,13 +176,11 @@ DMINIBAR_CONTAINER_HEIGHT = 14, -- Larger means more padding at the bottom, at
-- insensitive sort
DALPHA_SORT_CASE_INSENSITIVE = true,
-- Light parameter for Kobo
KOBO_LIGHT_ON_START = -2, -- -1, -2 or 0-100.
-- -1 uses previous koreader session saved brightness
-- -2 uses 'Kobo eReader.conf' brighness,
-- other sets light on start to a fix brighness
KOBO_SYNC_BRIGHTNESS_WITH_NICKEL = true, -- Save brightness set in KOreader
-- with nickel's 'Kobo eReader.conf'
-- Frontlight behavior on Kobo
KOBO_LIGHT_ON_START = -2, -- -1, -2 or 0-100.
-- -1 uses the brightness set by KOReader (if any, 20% otherwise)
-- -2 uses the brightness set in Nickel
KOBO_SYNC_BRIGHTNESS_WITH_NICKEL = true, -- Update Nickel's config to match our own
-- Network proxy settings
-- proxy url should be a string in the format of "http://localhost:3128"

@ -11,19 +11,47 @@ You can skip most of the following instructions if desired, and use our premade
## Prerequisites
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`
or `clang`.
To get and compile the source you must have:
- `autoconf`: version greater than 2.64
- `bash`: version 4.0 or greater
- `ccache`: optional, but recommended
- `cmake`: version 3.15 or greater, 3.20 or greater recommended
- `gettext`
- `gcc/g++` or `clang/clang++`: with C11 & C++17 support
- `git`
- `make`: version 4.1 or greater
- `nasm`
- `ninja`: optional, but recommended
- `patch`
- `pkg-config` or `pkgconf`
- `unzip`
- `wget`
### Debian/Ubuntu and derivates
For testing:
- `busted`
- `lua`: version 5.1
- `luarocks`
- `SDL2`
### Alpine Linux
Install the prerequisites using apk:
```
sudo apk add autoconf automake bash cmake coreutils curl diffutils g++ \
gcc gettext-dev git grep gzip libtool linux-headers lua5.1-busted \
luarocks5.1 make ninja-build ninja-is-really-ninja patch pkgconf \
procps-ng sdl2 tar unzip wget
```
### Debian/Ubuntu
Install the prerequisites using APT:
```
sudo apt-get install build-essential git patch wget unzip \
gettext autoconf automake cmake libtool nasm ragel luarocks lua5.1 libsdl2-dev \
libssl-dev libffi-dev libsdl2-dev libc6-dev-i386 xutils-dev linux-libc-dev:i386 zlib1g:i386
sudo apt-get install autoconf automake build-essential ca-certificates cmake \
gcc-multilib gettext git libsdl2-2.0-0 libtool libtool-bin lua-busted \
lua5.1 luarocks nasm ninja-build patch pkg-config unzip wget
```
### Fedora/Red Hat
@ -31,7 +59,14 @@ libssl-dev libffi-dev libsdl2-dev libc6-dev-i386 xutils-dev linux-libc-dev: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 autoconf automake cmake gettext gcc gcc-c++ git libtool \
lua5.1 luarocks nasm ninja-build patch perl-FindBin procps-ng SDL2 \
unzip wget
```
And for busted:
```
luarocks --lua-version=5.1 --local install busted
```
### macOS
@ -39,13 +74,13 @@ 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 \
sdl2 lua@5.1 luarocks gettext pkg-config wget gnu-getopt grep bison
brew install autoconf automake binutils cmake coreutils gettext \
gnu-getopt grep libtool make nasm ninja pkg-config sdl2 wget
```
You will also have to ensure Homebrew's gettext, gnu-getopt, bison & grep are in your path, e.g., via
You will also have to ensure Homebrew's gettext, gnu-getopt, grep are in your path, e.g., via
```
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}"
export PATH="$(brew --prefix)/opt/gettext/bin:$(brew --prefix)/opt/gnu-getopt/bin:$(brew --prefix)/opt/grep/libexec/gnubin:${PATH}"
```
See also `brew info gettext` for details on how to make that permanent in your shell.
@ -85,9 +120,6 @@ To run KOReader on your development machine:
./kodev run
```
*Note:* On macOS and possibly other non-Linux hosts, you might want to pass `--no-build` to prevent re-running the buildsystem, as incremental builds may not behave properly.
You can specify the size and DPI of the emulator's screen using
`-w=X` (width), `-h=X` (height), and `-d=X` (DPI).
@ -118,11 +150,6 @@ Once you have the emulator ready to rock you can [build for other platforms too]
## Testing
You may need to check out the [circleci config file][circleci-conf] to setup up
a proper testing environment.
Briefly, you need to install `luarocks` and then install `busted` and `ansicolors` with `luarocks`. The "eng" language data file for tesseract-ocr is also need to test OCR functionality. Finally, make sure that `luajit` in your system is at least of version 2.0.2.
To automatically set up a number of primarily luarocks-related environment variables:
```
@ -193,17 +220,15 @@ De auteur van het boek is Plato en de titel is The Republic.
Ccache can speed up recompilation by caching previous compilations and detecting
when the same compilation is being repeated. In other words, it will decrease
build time when the sources have been built before. Ccache support has been added to
KOReader's build system. To install ccache:
* in Ubuntu use:`sudo apt-get install ccache`
* in Fedora use:`sudo dnf install ccache`
* from source:
* download the latest ccache source from http://ccache.samba.org/download.html
* extract the source package in a directory
* `cd` to that directory and use:`./configure && make && sudo make install`
* to disable ccache, use `export USE_NO_CCACHE=1` before make.
* for more information about ccache, visit: https://ccache.samba.org/
build time when the sources have been built before. To install ccache use:
* Alpine Linux: `sudo apk add ccache`
* Debian/Ubuntu: `sudo apt-get install ccache`
* Fedora/Red Hat: `sudo dnf install ccache`
* macOS: `brew install ccache`
* or from an official release or source: https://github.com/ccache/ccache/releases
To disable ccache, use `export USE_NO_CCACHE=1` before make.
[circleci-conf]:https://github.com/koreader/koreader/blob/master/.circleci/config.yml
[koreader-weblate]:https://hosted.weblate.org/engage/koreader/

@ -17,28 +17,15 @@ 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
Cross compile toolchains are available for Ubuntu users through these commands:
##### Ubuntu Touch
```
sudo apt-get install gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf
```
**NOTE 1:** The packages `pkg-config-arm-linux-gnueabihf` and `pkg-config-arm-linux-gnueabi` may
block you from building. Remove them if you get the following ld error
```
/usr/lib/gcc-cross/arm-linux-gnueabihf/4.8/../../../../arm-linux-gnueabihf/bin/ld: cannot find -lglib-2.0
```
##### e-Ink devices (e.g., Kindle, Kobo, Cervantes, reMarkable, PocketBook)
**NOTE:** While, for some targets (specifically, Cervantes, Kindle & Kobo), we make *some* effort to support Linaro/Ubuntu TCs,
@ -69,20 +56,6 @@ sudo apt-get install openjdk-8-jdk p7zip-full
Building a debian package requires the `dpkg-deb` tool. It should be already installed if you're on a Debian/Ubuntu based distribution.
#### for Ubuntu Touch
Building for Ubuntu Touch requires the `click` package management tool.
Ubuntu users can install it with:
```
sudo apt-get install click
```
**NOTE**: The Ubuntu Touch build won't start anymore, and none of the currently active developers have any physical devices. Please visit [#4960](https://github.com/koreader/koreader/issues/4960) if you want to help.
The Ubuntu Touch builds are therefore no longer published under releases on GitHub, but they are still available from [the nightly build server](http://build.koreader.rocks/download/nightly/).
## Building
You can check out our [nightlybuild script][nb-script] to see how to build a package from scratch.
@ -167,14 +140,6 @@ See [Building](https://github.com/koreader/koreader/blob/master/doc/Building.md)
./kodev release remarkable
```
### Embedded Linux devices
#### Ubuntu Touch
```
./kodev release ubuntu-touch
```
## Porting to a new target.
See [Porting.md](Porting.md)

@ -41,7 +41,7 @@ for _, widget in ipairs(self) do
end
end
-- If not consumed by children, consume it ourself
return self["on"..event.name](self, unpack(event.args))
return self["on"..event.name](self, unpack(event.args, 1, event.args.n))
```
## Event system

@ -1,6 +1,5 @@
local BD = require("ui/bidi")
local ButtonDialog = require("ui/widget/buttondialog")
local ButtonDialogTitle = require("ui/widget/buttondialogtitle")
local CheckButton = require("ui/widget/checkbutton")
local ConfirmBox = require("ui/widget/confirmbox")
local DataStorage = require("datastorage")
@ -19,18 +18,9 @@ local lfs = require("libs/libkoreader-lfs")
local logger = require("logger")
local _ = require("gettext")
local N_ = _.ngettext
local Screen = require("device").screen
local T = require("ffi/util").template
local CloudStorage = Menu:extend{
cloud_servers = {
{
text = _("Add new cloud storage"),
title = _("Choose cloud type"),
url = "add",
editable = false,
},
},
no_title = false,
show_parent = nil,
is_popout = false,
@ -54,8 +44,6 @@ function CloudStorage:init()
else
self.item_table = self:genItemTableFromRoot()
end
self.width = Screen:getWidth()
self.height = Screen:getHeight()
self.title_bar_left_icon = "plus"
self.onLeftButtonTap = function() -- add new cloud storage
self:selectCloudType()
@ -131,7 +119,7 @@ function CloudStorage:selectCloudType()
},
})
end
self.cloud_dialog = ButtonDialogTitle:new{
self.cloud_dialog = ButtonDialog:new{
title = _("Add new cloud storage"),
title_align = "center",
buttons = buttons,
@ -173,11 +161,11 @@ function CloudStorage:openCloudServer(url)
if NetworkMgr:willRerunWhenConnected(function() self:openCloudServer(url) end) then
return
end
tbl, e = WebDav:run(self.address, self.username, self.password, url)
tbl, e = WebDav:run(self.address, self.username, self.password, url, self.choose_folder_mode)
end
if tbl then
self:switchItemTable(url, tbl)
if self.type == "dropbox" then
if self.type == "dropbox" or self.type == "webdav" then
self.onLeftButtonTap = function()
self:showPlusMenu(url)
end
@ -239,7 +227,7 @@ function CloudStorage:downloadFile(item)
})
end
local function createTitle(filename_orig, filename, path) -- title for ButtonDialogTitle
local function createTitle(filename_orig, filename, path) -- title for ButtonDialog
return T(_("Filename:\n%1\n\nDownload filename:\n%2\n\nDownload folder:\n%3"),
filename_orig, filename, BD.dirpath(path))
end
@ -255,7 +243,6 @@ function CloudStorage:downloadFile(item)
text = _("Choose folder"),
callback = function()
require("ui/downloadmgr"):new{
show_hidden = G_reader_settings:readSetting("show_hidden"),
onConfirm = function(path)
self.cs_settings:saveSetting("download_dir", path)
self.cs_settings:flush()
@ -277,6 +264,7 @@ function CloudStorage:downloadFile(item)
{
{
text = _("Cancel"),
id = "close",
callback = function()
UIManager:close(input_dialog)
end,
@ -329,7 +317,7 @@ function CloudStorage:downloadFile(item)
},
}
self.download_dialog = ButtonDialogTitle:new{
self.download_dialog = ButtonDialog:new{
title = createTitle(filename_orig, filename, download_dir),
buttons = buttons,
}
@ -359,7 +347,7 @@ function CloudStorage:onMenuHold(item)
local title = T(_("Choose this folder?\n\n%1"), BD.dirpath(item.url))
local onConfirm = self.onConfirm
local button_dialog
button_dialog = ButtonDialogTitle:new{
button_dialog = ButtonDialog:new{
title = title,
buttons = {
{
@ -539,7 +527,7 @@ function CloudStorage:synchronizeSettings(item)
local syn_dialog
local dropbox_sync_folder = item.sync_source_folder or "not set"
local local_sync_folder = item.sync_dest_folder or "not set"
syn_dialog = ButtonDialogTitle:new {
syn_dialog = ButtonDialog:new {
title = T(_("Dropbox folder:\n%1\nLocal folder:\n%2"), BD.dirpath(dropbox_sync_folder), BD.dirpath(local_sync_folder)),
title_align = "center",
buttons = {
@ -648,7 +636,11 @@ function CloudStorage:uploadFile(url)
end)
local url_base = url ~= "/" and url or ""
UIManager:tickAfterNext(function()
DropBox:uploadFile(url_base, self.password, file_path, callback_close)
if self.type == "dropbox" then
DropBox:uploadFile(url_base, self.password, file_path, callback_close)
elseif self.type == "webdav" then
WebDav:uploadFile(url_base, self.address, self.username, self.password, file_path, callback_close)
end
end)
end
end
@ -686,7 +678,11 @@ function CloudStorage:createFolder(url)
end
self:openCloudServer(url)
end
DropBox:createFolder(url_base, self.password, folder_name, callback_close)
if self.type == "dropbox" then
DropBox:createFolder(url_base, self.password, folder_name, callback_close)
elseif self.type == "webdav" then
WebDav:createFolder(url_base, self.address, self.username, self.password, folder_name, callback_close)
end
end,
},
}
@ -711,8 +707,8 @@ function CloudStorage:configCloud(type)
name = fields[1],
password = fields[2],
address = fields[3],
url = fields[4],
type = "dropbox",
url = "/"
})
elseif type == "ftp" then
table.insert(cs_servers,{
@ -758,6 +754,7 @@ function CloudStorage:editCloudServer(item)
server.name = fields[1]
server.password = fields[2]
server.address = fields[3]
server.url = fields[4]
cs_servers[i] = server
break
end

@ -58,11 +58,7 @@ end
function DropBox:downloadFileNoUI(url, password, path)
local code_response = DropBoxApi:downloadFile(url, password, path)
if code_response == 200 then
return true
else
return false
end
return code_response == 200
end
function DropBox:uploadFile(url, password, file_path, callback_close)
@ -98,14 +94,15 @@ end
function DropBox:config(item, callback)
local text_info = _([[
Dropbox access tokens are short-lived (4 hours).
To generate new access token please use Dropbox refresh token and <APP_KEY>:<APP_SECRET> Base64 encoded string.
To generate new access token please use Dropbox refresh token and <APP_KEY>:<APP_SECRET> string.
Some of the previously generated long-lived tokens are still valid.]])
local text_name, text_token, text_appkey
local text_name, text_token, text_appkey, text_url
if item then
text_name = item.text
text_token = item.password
text_appkey = item.address
text_url = item.url
end
self.settings_dialog = MultiInputDialog:new {
title = _("Dropbox cloud storage"),
@ -122,6 +119,10 @@ Some of the previously generated long-lived tokens are still valid.]])
text = text_appkey,
hint = _("Dropbox <APP_KEY>:<APP_SECRET>\n(leave blank for long-lived token)"),
},
{
text = text_url,
hint = _("Dropbox folder (/ for root)"),
},
},
buttons = {
{

@ -13,14 +13,14 @@ local _ = require("gettext")
local DropBoxApi = {
}
local API_TOKEN = "https://api.dropbox.com/oauth2/token"
local API_URL_INFO = "https://api.dropboxapi.com/2/users/get_current_account"
local API_TOKEN = "https://api.dropbox.com/oauth2/token"
local API_URL_INFO = "https://api.dropboxapi.com/2/users/get_current_account"
local API_GET_SPACE_USAGE = "https://api.dropboxapi.com/2/users/get_space_usage"
local API_LIST_FOLDER = "https://api.dropboxapi.com/2/files/list_folder"
local API_DOWNLOAD_FILE = "https://content.dropboxapi.com/2/files/download"
local API_UPLOAD_FILE = "https://content.dropboxapi.com/2/files/upload"
local API_CREATE_FOLDER = "https://api.dropboxapi.com/2/files/create_folder_v2"
local API_LIST_FOLDER = "https://api.dropboxapi.com/2/files/list_folder"
local API_LIST_ADD_FOLDER = "https://api.dropboxapi.com/2/files/list_folder/continue"
local API_CREATE_FOLDER = "https://api.dropboxapi.com/2/files/create_folder_v2"
local API_DOWNLOAD_FILE = "https://content.dropboxapi.com/2/files/download"
local API_UPLOAD_FILE = "https://content.dropboxapi.com/2/files/upload"
function DropBoxApi:getAccessToken(refresh_token, app_key_colon_secret)
local sink = {}
@ -29,24 +29,23 @@ function DropBoxApi:getAccessToken(refresh_token, app_key_colon_secret)
url = API_TOKEN,
method = "POST",
headers = {
["Authorization"] = "Basic " .. require("ffi/sha2").bin_to_base64(app_key_colon_secret),
["Content-Type"] = "application/x-www-form-urlencoded",
["Authorization"] = "Basic " .. require("ffi/sha2").bin_to_base64(app_key_colon_secret),
["Content-Type"] = "application/x-www-form-urlencoded",
["Content-Length"] = string.len(data),
},
source = ltn12.source.string(data),
sink = ltn12.sink.table(sink),
}
socketutil:set_timeout()
local code = socket.skip(1, http.request(request))
local code, _, status = socket.skip(1, http.request(request))
socketutil:reset_timeout()
if code == 200 then
local headers = table.concat(sink)
if headers ~= "" then
local _, result = pcall(JSON.decode, headers)
return result["access_token"]
end
local result_response = table.concat(sink)
if code == 200 and result_response ~= "" then
local _, result = pcall(JSON.decode, result_response)
return result["access_token"]
end
logger.info("Dropbox: cannot get access token")
logger.warn("DropBoxApi: cannot get access token:", status or code)
logger.warn("DropBoxApi: error:", result_response)
end
function DropBoxApi:fetchInfo(token, space_usage)
@ -61,16 +60,15 @@ function DropBoxApi:fetchInfo(token, space_usage)
sink = ltn12.sink.table(sink),
}
socketutil:set_timeout()
local code = socket.skip(1, http.request(request))
local code, _, status = socket.skip(1, http.request(request))
socketutil:reset_timeout()
if code == 200 then
local headers = table.concat(sink)
if headers ~= "" then
local _, result = pcall(JSON.decode, headers)
return result
end
local result_response = table.concat(sink)
if code == 200 and result_response ~= "" then
local _, result = pcall(JSON.decode, result_response)
return result
end
logger.info("Dropbox: cannot get account info")
logger.warn("DropBoxApi: cannot get account info:", status or code)
logger.warn("DropBoxApi: error:", result_response)
end
function DropBoxApi:fetchListFolders(path, token)
@ -78,7 +76,6 @@ function DropBoxApi:fetchListFolders(path, token)
local data = "{\"path\": \"" .. path .. "\",\"recursive\": false,\"include_media_info\": false,"..
"\"include_deleted\": false,\"include_has_explicit_shared_members\": false}"
local sink = {}
socketutil:set_timeout()
local request = {
url = API_LIST_FOLDER,
method = "POST",
@ -90,34 +87,29 @@ function DropBoxApi:fetchListFolders(path, token)
source = ltn12.source.string(data),
sink = ltn12.sink.table(sink),
}
local headers_request = socket.skip(1, http.request(request))
socketutil:set_timeout()
local code, _, status = socket.skip(1, http.request(request))
socketutil:reset_timeout()
if headers_request == nil then
return nil
end
local result_response = table.concat(sink)
if result_response ~= "" then
if code == 200 and result_response ~= "" then
local ret, result = pcall(JSON.decode, result_response)
if ret then
-- Check if more results, and then get them
if result.has_more then
logger.dbg("Found additional files")
logger.dbg("DropBoxApi: found additional files")
result = self:fetchAdditionalFolders(result, token)
end
return result
else
return nil
end
else
return nil
end
logger.warn("DropBoxApi: cannot get folder content:", status or code)
logger.warn("DropBoxApi: error:", result_response)
end
function DropBoxApi:downloadFile(path, token, local_path)
local data1 = "{\"path\": \"" .. path .. "\"}"
socketutil:set_timeout(socketutil.FILE_BLOCK_TIMEOUT, socketutil.FILE_TOTAL_TIMEOUT)
local code, _, status = socket.skip(1, http.request{
local code, headers, status = socket.skip(1, http.request{
url = API_DOWNLOAD_FILE,
method = "GET",
headers = {
@ -128,14 +120,16 @@ function DropBoxApi:downloadFile(path, token, local_path)
})
socketutil:reset_timeout()
if code ~= 200 then
logger.warn("DropBoxApi: Download failure:", status or code or "network unreachable")
logger.warn("DropBoxApi: cannot download file:", status or code)
end
return code
return code, (headers or {}).etag
end
function DropBoxApi:uploadFile(path, token, file_path)
function DropBoxApi:uploadFile(path, token, file_path, etag, overwrite)
local data = "{\"path\": \"" .. path .. "/" .. BaseUtil.basename(file_path) ..
"\",\"mode\": \"add\",\"autorename\": true,\"mute\": false,\"strict_conflict\": false}"
"\",\"mode\":" .. (overwrite and "\"overwrite\"" or "\"add\"") ..
",\"autorename\": " .. (overwrite and "false" or "true") ..
",\"mute\": false,\"strict_conflict\": false}"
socketutil:set_timeout(socketutil.FILE_BLOCK_TIMEOUT, socketutil.FILE_TOTAL_TIMEOUT)
local code, _, status = socket.skip(1, http.request{
url = API_UPLOAD_FILE,
@ -143,14 +137,15 @@ function DropBoxApi:uploadFile(path, token, file_path)
headers = {
["Authorization"] = "Bearer ".. token,
["Dropbox-API-Arg"] = data,
["Content-Type"] = "application/octet-stream",
["Content-Length"] = lfs.attributes(file_path, "size"),
["Content-Type"] = "application/octet-stream",
["Content-Length"] = lfs.attributes(file_path, "size"),
["If-Match"] = etag,
},
source = ltn12.source.file(io.open(file_path, "r")),
source = ltn12.source.file(io.open(file_path, "r")),
})
socketutil:reset_timeout()
if code ~= 200 then
logger.warn("DropBoxApi: Upload failure:", status or code or "network unreachable")
logger.warn("DropBoxApi: cannot upload file:", status or code)
end
return code
end
@ -162,15 +157,15 @@ function DropBoxApi:createFolder(path, token, folder_name)
url = API_CREATE_FOLDER,
method = "POST",
headers = {
["Authorization"] = "Bearer ".. token,
["Content-Type"] = "application/json",
["Authorization"] = "Bearer ".. token,
["Content-Type"] = "application/json",
["Content-Length"] = #data,
},
source = ltn12.source.string(data),
source = ltn12.source.string(data),
})
socketutil:reset_timeout()
if code ~= 200 then
logger.warn("DropBoxApi: Folder creation failure:", status or code or "network unreachable")
logger.warn("DropBoxApi: cannot create folder:", status or code)
end
return code
end
@ -294,4 +289,5 @@ function DropBoxApi:fetchAdditionalFolders(response, token)
return out
end
return DropBoxApi

@ -0,0 +1,191 @@
local DataStorage = require("datastorage")
local Font = require("ui/font")
local InfoMessage = require("ui/widget/infomessage")
local LuaSettings = require("luasettings")
local Menu = require("ui/widget/menu")
local NetworkMgr = require("ui/network/manager")
local Notification = require("ui/widget/notification")
local Screen = require("device").screen
local UIManager = require("ui/uimanager")
local ffiutil = require("ffi/util")
local util = require("util")
local _ = require("gettext")
local server_types = {
dropbox = _("Dropbox"),
webdav = _("WebDAV"),
}
local indent = ""
local SyncService = Menu:extend{
no_title = false,
show_parent = nil,
is_popout = false,
is_borderless = true,
title = _("Cloud sync settings"),
title_face = Font:getFace("smallinfofontbold"),
}
function SyncService:init()
self.item_table = self:generateItemTable()
self.width = Screen:getWidth()
self.height = Screen:getHeight()
Menu.init(self)
end
function SyncService:generateItemTable()
local item_table = {}
-- select and/or add server
local added_servers = LuaSettings:open(DataStorage:getSettingsDir().."/cloudstorage.lua"):readSetting("cs_servers") or {}
for _, server in ipairs(added_servers) do
if server.type == "dropbox" or server.type == "webdav" then
local item = {
text = indent .. server.name,
address = server.address,
username = server.username,
password = server.password,
type = server.type,
url = server.url,
mandatory = server_types[server.type],
}
item.callback = function()
require("ui/downloadmgr"):new{
item = item,
onConfirm = function(path)
server.url = path
self.onConfirm(server)
self:onClose()
end,
}:chooseCloudDir()
end
table.insert(item_table, item)
end
end
if #item_table > 0 then
table.insert(item_table, 1, {
text = _("Choose cloud service:"),
bold = true,
})
end
table.insert(item_table, {
text = _("Add service"),
bold = true,
callback = function()
local cloud_storage = require("apps/cloudstorage/cloudstorage"):new{}
local onClose = cloud_storage.onClose
cloud_storage.onClose = function(this)
onClose(this)
self:switchItemTable(nil, self:generateItemTable())
end
UIManager:show(cloud_storage)
end
})
return item_table
end
function SyncService.getReadablePath(server)
local url = util.stringStartsWith(server.url, "/") and server.url:sub(2) or server.url
url = util.urlDecode(url) or url
url = util.stringEndsWith(url, "/") and url or url .. "/"
if server.type == "dropbox" then
url = "/" .. url
elseif server.type == "webdav" then
url = (server.address:sub(-1) == "/" and server.address or server.address .. "/") .. url
end
if url:sub(-2) == "//" then url = url:sub(1, -2) end
return url
end
-- Prepares three files for sync_cb to call to do the actual syncing:
-- * local_file (one that is being used)
-- * income_file (one that has just been downloaded from Cloud to be merged, then to be deleted)
-- * cached_file (the one that was uploaded in the previous round of syncing)
--
-- How it works:
--
-- If we simply merge the local file with the income file (ignore duplicates), then items that have been deleted locally
-- but not remotely (on other devices) will re-emerge in the result file. The same goes for items deleted remotely but
-- not locally. To avoid this, we first need to delete them from both the income file and local file.
--
-- The problem is how to identify them, and that is when the cached file comes into play.
-- The cached file represents what local and remote agreed on previously (was identical to local and remote after being uploaded
-- the previous round), by comparing it with local file, items no longer in local file are ones being recently deleted.
-- The same applies to income file. Then we can delete them from both local and income files to be ready for merging. (The actual
-- deletion and merging procedures happen in sync_cb as users of this service will have different file specifications)
--
-- After merging, the income file is no longer needed and is deleted. The local file is uploaded and then a copy of it is saved
-- and renamed to replace the old cached file (thus the naming). The cached file stays (in the same folder) till being replaced
-- in the next round.
function SyncService.sync(server, file_path, sync_cb, is_silent)
if NetworkMgr:willRerunWhenOnline(function() SyncService.sync(server, file_path, sync_cb, is_silent) end) then
return
end
local file_name = ffiutil.basename(file_path)
local income_file_path = file_path .. ".temp" -- file downloaded from server
local cached_file_path = file_path .. ".sync" -- file uploaded to server last time
local fail_msg = _("Something went wrong when syncing, please check your network connection and try again later.")
local show_msg = function(msg)
if is_silent then return end
UIManager:show(InfoMessage:new{
text = msg or fail_msg,
timeout = 3,
})
end
if server.type ~= "dropbox" and server.type ~= "webdav" then
show_msg(_("Wrong server type."))
return
end
local code_response = 412 -- If-Match header failed
local etag
local api = server.type == "dropbox" and require("apps/cloudstorage/dropboxapi") or require("apps/cloudstorage/webdavapi")
local token = server.password
if server.type == "dropbox" and not (server.address == nil or server.address == "") then
token = api:getAccessToken(server.password, server.address)
end
while code_response == 412 do
os.remove(income_file_path)
if server.type == "dropbox" then
local url_base = server.url:sub(-1) == "/" and server.url or server.url.."/"
code_response, etag = api:downloadFile(url_base..file_name, token, income_file_path)
elseif server.type == "webdav" then
local path = api:getJoinedPath(server.address, server.url)
path = api:getJoinedPath(path, file_name)
code_response, etag = api:downloadFile(path, server.username, server.password, income_file_path)
end
if code_response ~= 200 and code_response ~= 404
and not (server.type == "dropbox" and code_response == 409) then
show_msg()
return
end
local ok, cb_return = pcall(sync_cb, file_path, cached_file_path, income_file_path)
if not ok or not cb_return then
show_msg()
if not ok then require("logger").err("sync service callback failed:", cb_return) end
return
end
if server.type == "dropbox" then
local url_base = server.url == "/" and "" or server.url
code_response = api:uploadFile(url_base, token, file_path, etag, true)
elseif server.type == "webdav" then
local path = api:getJoinedPath(server.address, server.url)
path = api:getJoinedPath(path, file_name)
code_response = api:uploadFile(path, server.username, server.password, file_path, etag)
end
end
os.remove(income_file_path)
if type(code_response) == "number" and code_response >= 200 and code_response < 300 then
os.remove(cached_file_path)
ffiutil.copyFile(file_path, cached_file_path)
UIManager:show(Notification:new{
text = _("Successfully synchronized."),
timeout = 2,
})
else
show_msg()
end
end
return SyncService

@ -7,17 +7,18 @@ local UIManager = require("ui/uimanager")
local ReaderUI = require("apps/reader/readerui")
local WebDavApi = require("apps/cloudstorage/webdavapi")
local util = require("util")
local ffiutil = require("ffi/util")
local _ = require("gettext")
local T = require("ffi/util").template
local WebDav = {}
function WebDav:run(address, user, pass, path)
return WebDavApi:listFolder(address, user, pass, path)
function WebDav:run(address, user, pass, path, folder_mode)
return WebDavApi:listFolder(address, user, pass, path, folder_mode)
end
function WebDav:downloadFile(item, address, username, password, local_path, callback_close)
local code_response = WebDavApi:downloadFile(address .. WebDavApi:urlEncode( item.url ), username, password, local_path)
local code_response = WebDavApi:downloadFile(WebDavApi:getJoinedPath(address, item.url), username, password, local_path)
if code_response == 200 then
local __, filename = util.splitFilePathName(local_path)
if G_reader_settings:isTrue("show_unsupported") and not DocumentRegistry:hasProvider(filename) then
@ -48,26 +49,44 @@ function WebDav:downloadFile(item, address, username, password, local_path, call
end
end
function WebDav:uploadFile(url, address, username, password, local_path, callback_close)
local path = WebDavApi:getJoinedPath(address, url)
path = WebDavApi:getJoinedPath(path, ffiutil.basename(local_path))
local code_response = WebDavApi:uploadFile(path, username, password, local_path)
if type(code_response) == "number" and code_response >= 200 and code_response < 300 then
UIManager:show(InfoMessage:new{
text = T(_("File uploaded:\n%1"), BD.filepath(address)),
})
if callback_close then callback_close() end
else
UIManager:show(InfoMessage:new{
text = T(_("Could not upload file:\n%1"), BD.filepath(address)),
timeout = 3,
})
end
end
function WebDav:createFolder(url, address, username, password, folder_name, callback_close)
local code_response = WebDavApi:createFolder(address .. WebDavApi:urlEncode(url .. "/" .. folder_name), username, password, folder_name)
if code_response == 201 then
if callback_close then
callback_close()
end
else
UIManager:show(InfoMessage:new{
text = T(_("Could not create folder:\n%1"), folder_name),
})
end
end
function WebDav:config(item, callback)
local text_info = _([[Server address must be of the form http(s)://domain.name/path
This can point to a sub-directory of the WebDAV server.
The start folder is appended to the server path.]])
local hint_name = _("Server display name")
local text_name = ""
local hint_address = _("WebDAV address, for example https://example.com/dav")
local text_address = ""
local hint_username = _("Username")
local text_username = ""
local hint_password = _("Password")
local text_password = ""
local hint_folder = _("Start folder")
local text_folder = ""
local title
local text_button_ok = _("Add")
local title, text_name, text_address, text_username, text_password, text_folder
if item then
title = _("Edit WebDAV account")
text_button_ok = _("Apply")
text_name = item.text
text_address = item.address
text_username = item.username
@ -81,29 +100,24 @@ The start folder is appended to the server path.]])
fields = {
{
text = text_name,
input_type = "string",
hint = hint_name ,
hint = _("Server display name"),
},
{
text = text_address,
input_type = "string",
hint = hint_address ,
hint = _("WebDAV address, for example https://example.com/dav"),
},
{
text = text_username,
input_type = "string",
hint = hint_username,
hint = _("Username"),
},
{
text = text_password,
input_type = "string",
text_type = "password",
hint = hint_password,
hint = _("Password"),
},
{
text = text_folder,
input_type = "string",
hint = hint_folder,
hint = _("Start folder, for example /books"),
},
},
buttons = {
@ -123,18 +137,17 @@ The start folder is appended to the server path.]])
end
},
{
text = text_button_ok,
text = _("Save"),
callback = function()
local fields = self.settings_dialog:getFields()
-- make sure the URL is a valid path
if fields[5] ~= "" then
if string.sub(fields[5], 1, 1) ~= '/' then
fields[5] = '/' .. fields[5]
end
end
if fields[1] ~= "" and fields[2] ~= "" then
-- make sure the URL is a valid path
if fields[5] ~= "" then
if not fields[5]:match('^/') then
fields[5] = '/' .. fields[5]
end
fields[5] = fields[5]:gsub("/$", "")
end
if item then
-- edit
callback(item, fields)
@ -153,11 +166,9 @@ The start folder is appended to the server path.]])
},
},
},
input_type = "text",
}
UIManager:show(self.settings_dialog)
self.settings_dialog:onShowKeyboard()
end
function WebDav:info(item)

@ -7,10 +7,18 @@ local socketutil = require("socketutil")
local util = require("util")
local _ = require("gettext")
local logger = require("logger")
local lfs = require("libs/libkoreader-lfs")
local WebDavApi = {
}
function WebDavApi:getJoinedPath( address, path )
local path_encoded = self:urlEncode( path ) or ""
local address_strip = address:sub(-1) == "/" and address:sub(1, -2) or address
local path_strip = path_encoded:sub(1, 1) == "/" and path_encoded:sub(2) or path_encoded
return address_strip .. "/" .. path_strip
end
function WebDavApi:isCurrentDirectory( current_item, address, path )
local is_home, is_parent
local home_path
@ -32,6 +40,7 @@ function WebDavApi:isCurrentDirectory( current_item, address, path )
is_home = true
else
local temp_path = string.sub( item, string.len(home_path) + 1 )
if string.sub( path, -1 ) == "/" then path = string.sub( path, 1, -2 ) end
if temp_path == path then
is_parent = true
end
@ -51,7 +60,7 @@ function WebDavApi:urlEncode(url_data)
return url_data
end
function WebDavApi:listFolder(address, user, pass, folder_path)
function WebDavApi:listFolder(address, user, pass, folder_path, folder_mode)
local path = self:urlEncode( folder_path )
local webdav_list = {}
local webdav_file = {}
@ -61,10 +70,10 @@ function WebDavApi:listFolder(address, user, pass, folder_path)
if string.sub( address, -1 ) == "/" then has_trailing_slash = true end
if path == nil or path == "/" then
path = ""
elseif string.sub( path, 1, 2 ) == "/" then
elseif string.sub( path, 1, 1 ) == "/" then
if has_trailing_slash then
-- too many slashes, remove one
path = string.sub( path, 1 )
path = string.sub( path, 2 )
end
has_leading_slash = true
end
@ -72,7 +81,7 @@ function WebDavApi:listFolder(address, user, pass, folder_path)
address = address .. "/"
end
local webdav_url = address .. path
if not has_trailing_slash then
if string.sub(webdav_url, -1) ~= "/" then
webdav_url = webdav_url .. "/"
end
@ -116,7 +125,7 @@ function WebDavApi:listFolder(address, user, pass, folder_path)
if string.sub( item_fullpath, -1 ) == "/" then
item_fullpath = string.sub( item_fullpath, 1, -2 )
end
local is_current_dir = self:isCurrentDirectory( item_fullpath, address, path )
local is_current_dir = self:isCurrentDirectory( util.urlDecode(item_fullpath), address, folder_path )
local item_name = util.urlDecode( FFIUtil.basename( item_fullpath ) )
item_name = util.htmlEntitiesToUtf8(item_name)
@ -125,7 +134,7 @@ function WebDavApi:listFolder(address, user, pass, folder_path)
item:find("<[^:]*:resourcetype></[^:]*:resourcetype>")
local item_path = (path == "" and has_trailing_slash) and item_name or path .. "/" .. item_name
if item:find("<[^:]*:collection/>") then
if item:find("<[^:]*:collection[^<]*/>") then
item_name = item_name .. "/"
if not is_current_dir then
table.insert(webdav_list, {
@ -161,6 +170,14 @@ function WebDavApi:listFolder(address, user, pass, folder_path)
type = files.type,
})
end
if folder_mode then
table.insert(webdav_list, 1, {
text = _("Long-press to choose current folder"),
url = folder_path,
type = "folder_long_press",
bold = true
})
end
return webdav_list
end
@ -179,7 +196,43 @@ function WebDavApi:downloadFile(file_url, user, pass, local_path)
logger.warn("WebDavApi: Download failure:", status or code or "network unreachable")
logger.dbg("WebDavApi: Response headers:", headers)
end
return code, (headers or {}).etag
end
function WebDavApi:uploadFile(file_url, user, pass, local_path, etag)
socketutil:set_timeout(socketutil.FILE_BLOCK_TIMEOUT, socketutil.FILE_TOTAL_TIMEOUT)
local code, _, status = socket.skip(1, http.request{
url = file_url,
method = "PUT",
source = ltn12.source.file(io.open(local_path, "r")),
user = user,
password = pass,
headers = {
["Content-Length"] = lfs.attributes(local_path, "size"),
["If-Match"] = etag,
}
})
socketutil:reset_timeout()
if type(code) ~= "number" or code < 200 or code > 299 then
logger.warn("WebDavApi: upload failure:", status or code or "network unreachable")
end
return code
end
function WebDavApi:createFolder(folder_url, user, pass, folder_name)
socketutil:set_timeout(socketutil.FILE_BLOCK_TIMEOUT, socketutil.FILE_TOTAL_TIMEOUT)
local code, _, status = socket.skip(1, http.request{
url = folder_url,
method = "MKCOL",
user = user,
password = pass,
})
socketutil:reset_timeout()
if code ~= 201 then
logger.warn("WebDavApi: create folder failure:", status or code or "network unreachable")
end
return code
end
return WebDavApi

File diff suppressed because it is too large Load Diff

@ -3,106 +3,245 @@ This module provides a way to display book information (filename and book metada
]]
local BD = require("ui/bidi")
local ButtonDialog = require("ui/widget/buttondialog")
local ConfirmBox = require("ui/widget/confirmbox")
local Device = require("device")
local DocSettings = require("docsettings")
local Document = require("document/document")
local DocumentRegistry = require("document/documentregistry")
local ImageViewer = require("ui/widget/imageviewer")
local Event = require("ui/event")
local InfoMessage = require("ui/widget/infomessage")
local KeyValuePage = require("ui/widget/keyvaluepage")
local InputDialog = require("ui/widget/inputdialog")
local TextViewer = require("ui/widget/textviewer")
local UIManager = require("ui/uimanager")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local Utf8Proc = require("ffi/utf8proc")
local filemanagerutil = require("apps/filemanager/filemanagerutil")
local lfs = require("libs/libkoreader-lfs")
local util = require("util")
local _ = require("gettext")
local N_ = _.ngettext
local T = require("ffi/util").template
local BookInfo = WidgetContainer:extend{
bookinfo_menu_title = _("Book information"),
title = _("Book information"),
props = {
"title",
"authors",
"series",
"series_index",
"language",
"keywords",
"description",
},
prop_text = {
cover = _("Cover image:"),
title = _("Title:"),
authors = _("Authors:"),
series = _("Series:"),
series_index = _("Series index:"),
language = _("Language:"),
keywords = _("Keywords:"),
description = _("Description:"),
pages = _("Pages:"),
},
}
function BookInfo:init()
if self.ui then -- only for Reader menu
if self.document then -- only for Reader menu
self.ui.menu:registerToMainMenu(self)
end
end
function BookInfo:addToMainMenu(menu_items)
menu_items.book_info = {
text = self.bookinfo_menu_title,
text = self.title,
callback = function()
self:onShowBookInfo()
end,
}
end
function BookInfo:isSupported(file)
return lfs.attributes(file, "mode") == "file"
end
-- Shows book information.
function BookInfo:show(file, book_props)
self.prop_updated = nil
local kv_pairs = {}
local directory, filename = util.splitFilePathName(file)
local filename_without_suffix, filetype = util.splitFileNameSuffix(filename) -- luacheck: no unused
if filetype:lower() == "zip" then
local filename_without_sub_suffix, sub_filetype = util.splitFileNameSuffix(filename_without_suffix) -- luacheck: no unused
sub_filetype = sub_filetype:lower()
local supported_sub_filetypes = { "fb2", "htm", "html", "log", "md", "txt" }
for __, t in ipairs(supported_sub_filetypes) do
if sub_filetype == t then
filetype = sub_filetype .. "." .. filetype
break
end
end
end
local file_size = lfs.attributes(file, "size") or 0
local file_modification = lfs.attributes(file, "modification") or 0
-- File section
local folder, filename = util.splitFilePathName(file)
local __, filetype = filemanagerutil.splitFileNameType(filename)
local attr = lfs.attributes(file)
local file_size = attr.size or 0
local size_f = util.getFriendlySize(file_size)
local size_b = util.getFormattedSize(file_size)
local size = string.format("%s (%s bytes)", size_f, size_b)
table.insert(kv_pairs, { _("Filename:"), BD.filename(filename) })
table.insert(kv_pairs, { _("Format:"), filetype:upper() })
table.insert(kv_pairs, { _("Size:"), size })
table.insert(kv_pairs, { _("File date:"), os.date("%Y-%m-%d %H:%M:%S", file_modification) })
table.insert(kv_pairs, { _("Folder:"), BD.dirpath(filemanagerutil.abbreviate(directory)), separator = true })
table.insert(kv_pairs, { _("Size:"), string.format("%s (%s bytes)", size_f, size_b) })
table.insert(kv_pairs, { _("File date:"), os.date("%Y-%m-%d %H:%M:%S", attr.modification) })
table.insert(kv_pairs, { _("Folder:"), BD.dirpath(filemanagerutil.abbreviate(folder)), separator = true })
-- Book section
-- book_props may be provided if caller already has them available
-- but it may lack "pages", that we may get from sidecar file
if not book_props or not book_props.pages then
-- check there is actually a sidecar file before calling DocSettings:open()
-- that would create an empty sidecar directory
if DocSettings:hasSidecarFile(file) then
local doc_settings = DocSettings:open(file)
if doc_settings then
if not book_props then
-- Files opened after 20170701 have a "doc_props" setting with
-- complete metadata and "doc_pages" with accurate nb of pages
book_props = doc_settings:readSetting("doc_props")
end
if not book_props then
-- File last opened before 20170701 may have a "stats" setting.
-- with partial metadata, or empty metadata if statistics plugin
-- was not enabled when book was read (we can guess that from
-- the fact that stats.page = 0)
local stats = doc_settings:readSetting("stats")
if stats and stats.pages ~= 0 then
-- Let's use them as is (which was what was done before), even if
-- incomplete, to avoid expensive book opening
book_props = stats
end
end
-- Files opened after 20170701 have an accurate "doc_pages" setting.
local doc_pages = doc_settings:readSetting("doc_pages")
if doc_pages and book_props then
book_props.pages = doc_pages
book_props = BookInfo.getDocProps(file, book_props)
end
-- cover image
self.custom_book_cover = DocSettings:findCustomCoverFile(file)
local key_text = self.prop_text["cover"]
if self.custom_book_cover then
key_text = "\u{F040} " .. key_text
end
table.insert(kv_pairs, { key_text, _("Tap to display"),
callback = function()
self:onShowBookCover(file)
end,
hold_callback = function()
self:showCustomDialog(file, book_props)
end,
separator = true,
})
-- metadata
local custom_props
local custom_metadata_file = DocSettings:findCustomMetadataFile(file)
if custom_metadata_file then
self.custom_doc_settings = DocSettings.openSettingsFile(custom_metadata_file)
custom_props = self.custom_doc_settings:readSetting("custom_props")
end
local values_lang, callback
for _i, prop_key in ipairs(self.props) do
local prop = book_props[prop_key]
if prop == nil or prop == "" then
prop = _("N/A")
elseif prop_key == "title" then
prop = BD.auto(prop)
elseif prop_key == "authors" or prop_key == "keywords" then
if prop:find("\n") then -- BD auto isolate each entry
prop = util.splitToArray(prop, "\n")
for i = 1, #prop do
prop[i] = BD.auto(prop[i])
end
prop = table.concat(prop, "\n")
else
prop = BD.auto(prop)
end
elseif prop_key == "language" then
-- Get a chance to have title, authors... rendered with alternate
-- glyphs for the book language (e.g. japanese book in chinese UI)
values_lang = prop
elseif prop_key == "description" then
-- Description may (often in EPUB, but not always) or may not (rarely in PDF) be HTML
prop = util.htmlToPlainTextIfHtml(prop)
callback = function() -- proper text_type in TextViewer
self:showBookProp("description", prop)
end
end
key_text = self.prop_text[prop_key]
if custom_props and custom_props[prop_key] then -- customized
key_text = "\u{F040} " .. key_text
end
table.insert(kv_pairs, { key_text, prop,
callback = callback,
hold_callback = function()
self:showCustomDialog(file, book_props, prop_key)
end,
})
end
-- pages
local is_doc = self.document and true or false
table.insert(kv_pairs, { self.prop_text["pages"], book_props["pages"] or _("N/A"), separator = is_doc })
-- Page section
if is_doc then
local lines_nb, words_nb = self.ui.view:getCurrentPageLineWordCounts()
if lines_nb == 0 then
lines_nb = _("N/A")
words_nb = _("N/A")
end
table.insert(kv_pairs, { _("Current page lines:"), lines_nb })
table.insert(kv_pairs, { _("Current page words:"), words_nb })
end
local KeyValuePage = require("ui/widget/keyvaluepage")
self.kvp_widget = KeyValuePage:new{
title = self.title,
value_overflow_align = "right",
kv_pairs = kv_pairs,
values_lang = values_lang,
close_callback = function()
self.custom_doc_settings = nil
self.custom_book_cover = nil
if self.prop_updated then
UIManager:broadcastEvent(Event:new("InvalidateMetadataCache", file))
UIManager:broadcastEvent(Event:new("BookMetadataChanged", self.prop_updated))
end
end,
}
UIManager:show(self.kvp_widget)
end
function BookInfo.getCustomProp(prop_key, filepath)
local custom_metadata_file = DocSettings:findCustomMetadataFile(filepath)
return custom_metadata_file
and DocSettings.openSettingsFile(custom_metadata_file):readSetting("custom_props")[prop_key]
end
-- Returns extended and customized metadata.
function BookInfo.extendProps(original_props, filepath)
-- do not customize if filepath is not passed (eg from covermenu)
local custom_metadata_file = filepath and DocSettings:findCustomMetadataFile(filepath)
local custom_props = custom_metadata_file
and DocSettings.openSettingsFile(custom_metadata_file):readSetting("custom_props") or {}
original_props = original_props or {}
local props = {}
for _, prop_key in ipairs(BookInfo.props) do
props[prop_key] = custom_props[prop_key] or original_props[prop_key]
end
props.pages = original_props.pages
-- if original title is empty, generate it as filename without extension
props.display_title = props.title or filemanagerutil.splitFileNameType(filepath)
return props
end
-- Returns customized document metadata, including number of pages.
function BookInfo.getDocProps(file, book_props, no_open_document)
if DocSettings:hasSidecarFile(file) then
local doc_settings = DocSettings:open(file)
if not book_props then
-- Files opened after 20170701 have a "doc_props" setting with
-- complete metadata and "doc_pages" with accurate nb of pages
book_props = doc_settings:readSetting("doc_props")
end
if not book_props then
-- File last opened before 20170701 may have a "stats" setting.
-- with partial metadata, or empty metadata if statistics plugin
-- was not enabled when book was read (we can guess that from
-- the fact that stats.page = 0)
local stats = doc_settings:readSetting("stats")
if stats and stats.pages ~= 0 then
-- title, authors, series, series_index, language
book_props = Document:getProps(stats)
end
end
-- Files opened after 20170701 have an accurate "doc_pages" setting.
local doc_pages = doc_settings:readSetting("doc_pages")
if doc_pages and book_props then
book_props.pages = doc_pages
end
end
-- If still no book_props (book never opened or empty "stats"), open the
-- document to get them
-- If still no book_props (book never opened or empty "stats"),
-- but custom metadata exists, it has a copy of original doc_props
if not book_props then
local custom_metadata_file = DocSettings:findCustomMetadataFile(file)
if custom_metadata_file then
book_props = DocSettings.openSettingsFile(custom_metadata_file):readSetting("doc_props")
end
end
-- If still no book_props, open the document to get them
if not book_props and not no_open_document then
local document = DocumentRegistry:openDocument(file)
if document then
local loaded = true
@ -130,169 +269,415 @@ function BookInfo:show(file, book_props)
end
end
-- If still no book_props, fall back to empty ones
if not book_props then
book_props = {}
return BookInfo.extendProps(book_props, file)
end
function BookInfo:findInProps(book_props, search_string, case_sensitive)
for _, key in ipairs(self.props) do
local prop = book_props[key]
if prop then
if key == "series_index" then
prop = tostring(prop)
elseif key == "description" then
prop = util.htmlToPlainTextIfHtml(prop)
end
if not case_sensitive then
prop = Utf8Proc.lowercase(util.fixUtf8(prop, "?"))
end
if prop:find(search_string) then
return true
end
end
end
end
-- Shows book information for currently opened document.
function BookInfo:onShowBookInfo()
if self.document then
self.ui.doc_props.pages = self.ui.doc_settings:readSetting("doc_pages")
self:show(self.document.file, self.ui.doc_props)
end
end
local title = book_props.title
if title == "" or title == nil then title = _("N/A") end
table.insert(kv_pairs, { _("Title:"), BD.auto(title) })
local authors = book_props.authors
if authors == "" or authors == nil then
authors = _("N/A")
elseif authors:find("\n") then -- BD auto isolate each author
authors = util.splitToArray(authors, "\n")
for i=1, #authors do
authors[i] = BD.auto(authors[i])
function BookInfo:showBookProp(prop_key, prop_text)
UIManager:show(TextViewer:new{
title = self.prop_text[prop_key],
text = prop_text,
text_type = prop_key == "description" and "book_info" or nil,
})
end
function BookInfo:onShowBookDescription(description, file)
if not description then
if file then
description = BookInfo.getDocProps(file).description
elseif self.document then -- currently opened document
description = self.ui.doc_props.description
end
authors = table.concat(authors, "\n")
end
if description then
self:showBookProp("description", util.htmlToPlainTextIfHtml(description))
else
authors = BD.auto(authors)
UIManager:show(InfoMessage:new{
text = _("No book description available."),
})
end
table.insert(kv_pairs, { _("Authors:"), authors })
end
local series = book_props.series
if series == "" or series == nil then
series = _("N/A")
function BookInfo:onShowBookCover(file, force_orig)
local cover_bb = self:getCoverImage(self.document, file, force_orig)
if cover_bb then
local ImageViewer = require("ui/widget/imageviewer")
local imgviewer = ImageViewer:new{
image = cover_bb,
with_title_bar = false,
fullscreen = true,
}
UIManager:show(imgviewer)
else
-- If we were fed a BookInfo book_props (e.g., covermenu), series index is in a separate field
if book_props.series_index then
-- Here, we're assured that series_index is a Lua number, so round integers are automatically displayed without decimals
series = book_props.series .. " #" .. book_props.series_index
else
-- But here, if we have a plain doc_props series with an index, drop empty decimals from round integers.
series = book_props.series:gsub("(#%d+)%.0+$", "%1")
UIManager:show(InfoMessage:new{
text = _("No cover image available."),
})
end
end
function BookInfo:getCoverImage(doc, file, force_orig)
local cover_bb
-- check for a custom cover (orig cover is forcibly requested in "Book information" only)
if not force_orig then
local custom_cover = DocSettings:findCustomCoverFile(file or (doc and doc.file))
if custom_cover then
local cover_doc = DocumentRegistry:openDocument(custom_cover)
if cover_doc then
cover_bb = cover_doc:getCoverPageImage()
cover_doc:close()
return cover_bb, custom_cover
end
end
end
table.insert(kv_pairs, { _("Series:"), BD.auto(series) })
local pages = book_props.pages
if pages == "" or pages == nil then pages = _("N/A") end
table.insert(kv_pairs, { _("Pages:"), pages })
local language = book_props.language
if language == "" or language == nil then language = _("N/A") end
table.insert(kv_pairs, { _("Language:"), language })
local keywords = book_props.keywords
if keywords == "" or keywords == nil then
keywords = _("N/A")
elseif keywords:find("\n") then -- BD auto isolate each keywords
keywords = util.splitToArray(keywords, "\n")
for i=1, #keywords do
keywords[i] = BD.auto(keywords[i])
-- orig cover
local is_doc = doc and true or false
if not is_doc then
doc = DocumentRegistry:openDocument(file)
if doc and doc.loadDocument then -- CreDocument
doc:loadDocument(false) -- load only metadata
end
keywords = table.concat(keywords, "\n")
else
keywords = BD.auto(keywords)
end
table.insert(kv_pairs, { _("Keywords:"), keywords })
if doc then
cover_bb = doc:getCoverPageImage()
if not is_doc then
doc:close()
end
end
return cover_bb
end
function BookInfo:updateBookInfo(file, book_props, prop_updated, prop_value_old)
if self.document and prop_updated == "cover" then
self.ui.doc_settings:getCustomCoverFile(true) -- reset cover file cache
end
self.prop_updated = {
filepath = file,
doc_props = book_props,
metadata_key_updated = prop_updated,
metadata_value_old = prop_value_old,
}
self.kvp_widget:onClose()
self:show(file, book_props)
end
function BookInfo:setCustomCover(file, book_props)
if self.custom_book_cover then -- reset custom cover
if os.remove(self.custom_book_cover) then
DocSettings.removeSidecarDir(util.splitFilePathName(self.custom_book_cover))
self:updateBookInfo(file, book_props, "cover")
end
else -- choose an image and set custom cover
local PathChooser = require("ui/widget/pathchooser")
local path_chooser = PathChooser:new{
select_directory = false,
file_filter = function(filename)
return DocumentRegistry:isImageFile(filename)
end,
onConfirm = function(image_file)
if DocSettings:flushCustomCover(file, image_file) then
self:updateBookInfo(file, book_props, "cover")
end
end,
}
UIManager:show(path_chooser)
end
end
local description = book_props.description
if description == "" or description == nil then
description = _("N/A")
function BookInfo:setCustomCoverFromImage(file, image_file)
local custom_book_cover = DocSettings:findCustomCoverFile(file)
if custom_book_cover then
os.remove(custom_book_cover)
end
DocSettings:flushCustomCover(file, image_file)
if self.ui.doc_settings then
self.ui.doc_settings:getCustomCoverFile(true) -- reset cover file cache
end
UIManager:broadcastEvent(Event:new("InvalidateMetadataCache", file))
UIManager:broadcastEvent(Event:new("BookMetadataChanged"))
end
function BookInfo:setCustomMetadata(file, book_props, prop_key, prop_value)
-- in file
local custom_doc_settings, custom_props, display_title, no_custom_metadata
if self.custom_doc_settings then
custom_doc_settings = self.custom_doc_settings
else -- no custom metadata file, create new
custom_doc_settings = DocSettings.openSettingsFile()
display_title = book_props.display_title -- backup
book_props.display_title = nil
custom_doc_settings:saveSetting("doc_props", book_props) -- save a copy of original props
end
custom_props = custom_doc_settings:readSetting("custom_props", {})
local prop_value_old = custom_props[prop_key] or book_props[prop_key]
custom_props[prop_key] = prop_value -- nil when resetting a custom prop
if next(custom_props) == nil then -- no more custom metadata
os.remove(custom_doc_settings.sidecar_file)
DocSettings.removeSidecarDir(util.splitFilePathName(custom_doc_settings.sidecar_file))
no_custom_metadata = true
else
-- Description may (often in EPUB, but not always) or may not (rarely
-- in PDF) be HTML.
description = util.htmlToPlainTextIfHtml(book_props.description)
if book_props.pages then -- keep a copy of original 'pages' up to date
local original_props = custom_doc_settings:readSetting("doc_props")
original_props.pages = book_props.pages
end
custom_doc_settings:flushCustomMetadata(file)
end
-- (We don't BD wrap description: it may be multi-lines, and the value we set
-- here may be viewed in a TextViewer that has auto_para_direction=true, which
-- will show the right thing, that'd we rather not mess with BD wrapping.)
table.insert(kv_pairs, { _("Description:"), description })
-- Cover image
local viewCoverImage = function()
local widget
local document = DocumentRegistry:openDocument(file)
if document then
if document.loadDocument then -- CreDocument
document:loadDocument(false) -- load only metadata
end
local cover_bb = document:getCoverPageImage()
if cover_bb then
widget = ImageViewer:new{
image = cover_bb,
with_title_bar = false,
fullscreen = true,
}
end
document:close()
book_props.display_title = book_props.display_title or display_title -- restore
-- in memory
prop_value = prop_value or custom_doc_settings:readSetting("doc_props")[prop_key] -- set custom or restore original
book_props[prop_key] = prop_value
if prop_key == "title" then -- generate when resetting the customized title and original is empty
book_props.display_title = book_props.title or filemanagerutil.splitFileNameType(file)
end
if self.document and self.document.file == file then -- currently opened document
self.ui.doc_props[prop_key] = prop_value
if prop_key == "title" then
self.ui.doc_props.display_title = book_props.display_title
end
if not widget then
widget = InfoMessage:new{
text = _("No cover image available"),
}
if no_custom_metadata then
self.ui.doc_settings:getCustomMetadataFile(true) -- reset metadata file cache
end
UIManager:show(widget)
end
table.insert(kv_pairs, { _("Cover image:"), _("Tap to display"), callback=viewCoverImage })
self:updateBookInfo(file, book_props, prop_key, prop_value_old)
end
-- Get a chance to have title, authors... rendered with alternate
-- glyphs for the book language (e.g. japanese book in chinese UI)
local values_lang = nil
if book_props.language and book_props.language ~= "" then
values_lang = book_props.language
function BookInfo:showCustomEditDialog(file, book_props, prop_key)
local prop = book_props[prop_key]
if prop and prop_key == "description" then
prop = util.htmlToPlainTextIfHtml(prop)
end
local widget = KeyValuePage:new{
title = _("Book information"),
value_overflow_align = "right",
kv_pairs = kv_pairs,
values_lang = values_lang,
local input_dialog
input_dialog = InputDialog:new{
title = _("Edit book metadata:") .. " " .. self.prop_text[prop_key]:gsub(":", ""),
input = prop,
input_type = prop_key == "series_index" and "number",
allow_newline = prop_key == "authors" or prop_key == "keywords" or prop_key == "description",
buttons = {
{
{
text = _("Cancel"),
id = "close",
callback = function()
UIManager:close(input_dialog)
end,
},
{
text = _("Save"),
callback = function()
local prop_value = input_dialog:getInputValue()
if prop_value and prop_value ~= "" then
UIManager:close(input_dialog)
self:setCustomMetadata(file, book_props, prop_key, prop_value)
end
end,
},
},
},
}
UIManager:show(widget)
UIManager:show(input_dialog)
input_dialog:onShowKeyboard()
end
function BookInfo:onShowBookInfo()
if not self.document then return end
-- Get them directly from ReaderUI's doc_settings
local doc_props = self.ui.doc_settings:readSetting("doc_props")
-- Make a copy, so we don't add "pages" to the original doc_props
-- that will be saved at some point by ReaderUI.
local book_props = {}
for k, v in pairs(doc_props) do
book_props[k] = v
function BookInfo:showCustomDialog(file, book_props, prop_key)
local original_prop, custom_prop, prop_is_cover
if prop_key then -- metadata
if self.custom_doc_settings then
original_prop = self.custom_doc_settings:readSetting("doc_props")[prop_key]
custom_prop = self.custom_doc_settings:readSetting("custom_props")[prop_key]
else
original_prop = book_props[prop_key]
end
if original_prop and prop_key == "description" then
original_prop = util.htmlToPlainTextIfHtml(original_prop)
end
prop_is_cover = false
else -- cover
prop_key = "cover"
prop_is_cover = true
end
book_props.pages = self.ui.doc_settings:readSetting("doc_pages")
self:show(self.document.file, book_props)
local button_dialog
local buttons = {
{
{
text = _("Copy original"),
enabled = original_prop ~= nil and Device:hasClipboard(),
callback = function()
UIManager:close(button_dialog)
Device.input.setClipboardText(original_prop)
end,
},
{
text = _("View original"),
enabled = original_prop ~= nil or prop_is_cover,
callback = function()
if prop_is_cover then
self:onShowBookCover(file, true)
else
self:showBookProp(prop_key, original_prop)
end
end,
},
},
{
{
text = _("Reset custom"),
enabled = custom_prop ~= nil or (prop_is_cover and self.custom_book_cover ~= nil),
callback = function()
local confirm_box = ConfirmBox:new{
text = prop_is_cover and _("Reset custom cover?\nImage file will be deleted.")
or _("Reset custom book metadata field?"),
ok_text = _("Reset"),
ok_callback = function()
UIManager:close(button_dialog)
if prop_is_cover then
self:setCustomCover(file, book_props)
else
self:setCustomMetadata(file, book_props, prop_key)
end
end,
}
UIManager:show(confirm_box)
end,
},
{
text = _("Set custom"),
enabled = not prop_is_cover or (prop_is_cover and self.custom_book_cover == nil),
callback = function()
UIManager:close(button_dialog)
if prop_is_cover then
self:setCustomCover(file, book_props)
else
self:showCustomEditDialog(file, book_props, prop_key)
end
end,
},
},
}
button_dialog = ButtonDialog:new{
title = _("Book metadata:") .. " " .. self.prop_text[prop_key]:gsub(":", ""),
title_align = "center",
buttons = buttons,
}
UIManager:show(button_dialog)
end
function BookInfo:onShowBookDescription()
if not self.document then return end
local description = self.document:getProps().description
if description and description ~= "" then
-- Description may (often in EPUB, but not always) or may not (rarely
-- in PDF) be HTML.
description = util.htmlToPlainTextIfHtml(description)
local TextViewer = require("ui/widget/textviewer")
UIManager:show(TextViewer:new{
title = _("Description:"),
text = description,
})
else
UIManager:show(InfoMessage:new{
text = _("No book description available."),
})
function BookInfo:moveBookMetadata()
-- called by filemanagermenu only
local file_chooser = self.ui.file_chooser
local function scanPath()
local sys_folders = { -- do not scan sys_folders
["/dev"] = true,
["/proc"] = true,
["/sys"] = true,
}
local books_to_move = {}
local dirs = { file_chooser.path }
while #dirs ~= 0 do
local new_dirs = {}
for _, d in ipairs(dirs) do
local ok, iter, dir_obj = pcall(lfs.dir, d)
if ok then
for f in iter, dir_obj do
local fullpath = "/" .. f
if d ~= "/" then
fullpath = d .. fullpath
end
local attributes = lfs.attributes(fullpath) or {}
if attributes.mode == "directory" and f ~= "." and f ~= ".."
and file_chooser:show_dir(f) and not sys_folders[fullpath] then
table.insert(new_dirs, fullpath)
elseif attributes.mode == "file" and not util.stringStartsWith(f, "._")
and file_chooser:show_file(f)
and DocSettings.isSidecarFileNotInPreferredLocation(fullpath) then
table.insert(books_to_move, fullpath)
end
end
end
end
dirs = new_dirs
end
return books_to_move
end
UIManager:show(ConfirmBox:new{
text = _("Scan books in current folder and subfolders for their metadata location?"),
ok_text = _("Scan"),
ok_callback = function()
local books_to_move = scanPath()
local books_to_move_nb = #books_to_move
if books_to_move_nb == 0 then
UIManager:show(InfoMessage:new{
text = _("No books with metadata not in your preferred location found."),
})
else
UIManager:show(ConfirmBox:new{
text = T(N_("1 book with metadata not in your preferred location found.",
"%1 books with metadata not in your preferred location found.",
books_to_move_nb), books_to_move_nb) .. "\n" ..
_("Move book metadata to your preferred location?"),
ok_text = _("Move"),
ok_callback = function()
UIManager:close(self.menu_container)
for _, book in ipairs(books_to_move) do
DocSettings.updateLocation(book, book)
end
file_chooser:refreshPath()
end,
})
end
end,
})
end
function BookInfo:onShowBookCover()
if not self.document then return end
local cover_bb = self.document:getCoverPageImage()
if cover_bb then
UIManager:show(ImageViewer:new{
image = cover_bb,
with_title_bar = false,
fullscreen = true,
})
else
UIManager:show(InfoMessage:new{
text = _("No cover image available."),
})
function BookInfo.showBooksWithHashBasedMetadata()
local header = T(_("Hash-based metadata has been saved in %1 for the following documents. Hash-based storage may slow down file browser navigation in large directories. Thus, if not using hash-based metadata storage, it is recommended to open the associated documents in KOReader to automatically migrate their metadata to the preferred storage location, or to delete %1, which will speed up file browser navigation."),
DocSettings.getSidecarStorage("hash"))
local file_info = { header .. "\n" }
local sdrs = DocSettings.findSidecarFilesInHashLocation()
for i, sdr in ipairs(sdrs) do
local sidecar_file, custom_metadata_file = unpack(sdr)
local doc_settings = DocSettings.openSettingsFile(sidecar_file)
local doc_props = doc_settings:readSetting("doc_props")
local custom_props = custom_metadata_file
and DocSettings.openSettingsFile(custom_metadata_file):readSetting("custom_props") or {}
local doc_path = doc_settings:readSetting("doc_path")
local title = custom_props.title or doc_props.title or filemanagerutil.splitFileNameType(doc_path)
local author = custom_props.authors or doc_props.authors or _("N/A")
doc_path = lfs.attributes(doc_path, "mode") == "file" and doc_path or _("N/A")
local text = T(_("%1. Title: %2; Author: %3\nDocument: %4"), i, title, author, doc_path)
table.insert(file_info, text)
end
local doc_nb = #file_info - 1
UIManager:show(TextViewer:new{
title = T(N_("1 document with hash-based metadata", "%1 documents with hash-based metadata", doc_nb), doc_nb),
title_multilines = true,
text = table.concat(file_info, "\n"),
})
end
return BookInfo

@ -1,20 +1,26 @@
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 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 BaseUtil = require("ffi/util")
local util = require("util")
local filemanagerutil = require("apps/filemanager/filemanagerutil")
local _ = require("gettext")
local T = BaseUtil.template
local T = require("ffi/util").template
local util = require("util")
local FileManagerCollection = WidgetContainer:extend{
coll_menu_title = _("Favorites"),
title = _("Collections"),
default_collection_title = _("Favorites"),
checkmark = "\u{2713}",
}
function FileManagerCollection:init()
@ -22,112 +28,159 @@ 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.coll_menu_title,
text = self.title,
callback = function()
self:onShowColl("favorites")
self:onShowCollList()
end,
}
end
function FileManagerCollection:updateItemTable()
-- Try to stay on current page.
local select_number = nil
if self.coll_menu.page and self.coll_menu.perpage then
select_number = (self.coll_menu.page - 1) * self.coll_menu.perpage + 1
-- 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:refreshFileManager()
if self.files_updated then
if self.ui.file_chooser then
self.ui.file_chooser:refreshPath()
end
self.files_updated = nil
end
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()
self:refreshFileManager()
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
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)
if self.ui.document then
if self.ui.document.file ~= item.file then
self.ui:switchDocument(item.file)
end
else
self.ui:openFile(item.file)
end
self.coll_menu:switchItemTable(self.coll_menu_title,
ReadCollection:prepareList(self.coll_menu.collection), select_number)
end
function FileManagerCollection:onMenuHold(item)
local file = item.file
self.collfile_dialog = nil
local buttons = {
{
{
text = _("Sort"),
callback = function()
UIManager:close(self.collfile_dialog)
local item_table = {}
for i=1, #self._manager.coll_menu.item_table do
table.insert(item_table, {text = self._manager.coll_menu.item_table[i].text, label = self._manager.coll_menu.item_table[i].file})
end
local SortWidget = require("ui/widget/sortwidget")
local sort_item
sort_item = SortWidget:new{
title = _("Sort favorites"),
item_table = item_table,
callback = function()
local new_order_table = {}
for i=1, #sort_item.item_table do
table.insert(new_order_table, {
file = sort_item.item_table[i].label,
order = i
})
end
ReadCollection:writeCollection(new_order_table, self._manager.coll_menu.collection)
self._manager:updateItemTable()
end
}
UIManager:show(sort_item)
self.book_props = self.ui.coverbrowser and self.ui.coverbrowser:getBookInfo(file)
end,
},
{
text = _("Remove from collection"),
callback = function()
ReadCollection:removeItem(item.file, self._manager.coll_menu.collection)
self._manager:updateItemTable()
UIManager:close(self.collfile_dialog)
end,
},
},
local function close_dialog_callback()
UIManager:close(self.collfile_dialog)
end
local function close_dialog_menu_callback()
UIManager:close(self.collfile_dialog)
self._manager.coll_menu.close_callback()
end
local function close_dialog_update_callback()
UIManager:close(self.collfile_dialog)
self._manager:updateItemTable()
self._manager.files_updated = true
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
table.insert(buttons, filemanagerutil.genStatusButtonsRow(doc_settings_or_file, close_dialog_update_callback))
table.insert(buttons, {}) -- separator
table.insert(buttons, {
filemanagerutil.genResetSettingsButton(doc_settings_or_file, close_dialog_update_callback, is_currently_opened),
{
{
text = _("Book information"),
enabled = FileManagerBookInfo:isSupported(item.file),
callback = function()
FileManagerBookInfo:show(item.file)
UIManager:close(self.collfile_dialog)
end,
},
text = _("Remove from collection"),
callback = function()
UIManager:close(self.collfile_dialog)
ReadCollection:removeItem(file, self.collection_name)
self._manager:updateItemTable()
self._manager.files_updated = true
end,
},
}
-- NOTE: Duplicated from frontend/apps/filemanager/filemanager.lua
if Device:canExecuteScript(item.file) then
})
table.insert(buttons, {
filemanagerutil.genShowFolderButton(file, close_dialog_menu_callback),
filemanagerutil.genBookInformationButton(file, self.book_props, close_dialog_callback),
})
table.insert(buttons, {
filemanagerutil.genBookCoverButton(file, self.book_props, close_dialog_callback),
filemanagerutil.genBookDescriptionButton(file, self.book_props, close_dialog_callback),
})
if Device:canExecuteScript(file) then
table.insert(buttons, {
{
-- @translators This is the script's programming language (e.g., shell or python)
text = T(_("Execute %1 script"), util.getScriptType(item.file)),
enabled = true,
callback = function()
UIManager:close(self.collfile_dialog)
local script_is_running_msg = InfoMessage:new{
-- @translators %1 is the script's programming language (e.g., shell or python), %2 is the filename
text = T(_("Running %1 script %2…"), util.getScriptType(item.file), BD.filename(BaseUtil.basename(item.file))),
}
UIManager:show(script_is_running_msg)
UIManager:scheduleIn(0.5, function()
local rv = os.execute(BaseUtil.realpath(item.file))
UIManager:close(script_is_running_msg)
if rv == 0 then
UIManager:show(InfoMessage:new{
text = _("The script exited successfully."),
})
else
UIManager:show(InfoMessage:new{
text = T(_("The script returned a non-zero status code: %1!"), bit.rshift(rv, 8)),
icon = "notice-warning",
})
end
end)
end,
}
filemanagerutil.genExecuteScriptButton(file, close_dialog_menu_callback)
})
end
self.collfile_dialog = ButtonDialogTitle:new{
title = item.text:match("([^/]+)$"),
self.collfile_dialog = ButtonDialog:new{
title = BD.filename(item.text),
title_align = "center",
buttons = buttons,
}
@ -135,6 +188,83 @@ function FileManagerCollection:onMenuHold(item)
return true
end
function FileManagerCollection:showCollDialog()
local coll_dialog
local buttons = {
{{
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 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,
onConfirm = function(file)
if not ReadCollection:isFileInCollection(file, self.coll_menu.collection_name) then
ReadCollection:addItem(file, self.coll_menu.collection_name)
self:updateItemTable(true) -- show added item
self.files_updated = true
end
end,
}
UIManager:show(path_chooser)
end,
}},
}
if self.ui.document then
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 is_in_collection and _("Remove current book from collection") or _("Add current book to collection")
end,
callback = function()
UIManager:close(coll_dialog)
if is_in_collection then
ReadCollection:removeItem(file, self.coll_menu.collection_name)
else
ReadCollection:addItem(file, self.coll_menu.collection_name)
end
self:updateItemTable(not is_in_collection)
self.files_updated = true
end,
}})
end
coll_dialog = ButtonDialog:new{
buttons = buttons,
}
UIManager:show(coll_dialog)
end
function FileManagerCollection:sortCollection()
local sort_widget
sort_widget = SortWidget:new{
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()
end
}
UIManager:show(sort_widget)
end
function FileManagerCollection:MenuSetRotationModeHandler(rotation)
if rotation ~= nil and rotation ~= Screen:getRotationMode() then
UIManager:close(self._manager.coll_menu)
@ -150,26 +280,328 @@ function FileManagerCollection:MenuSetRotationModeHandler(rotation)
return true
end
function FileManagerCollection:onShowColl(collection)
self.coll_menu = Menu:new{
ui = self.ui,
width = Screen:getWidth(),
height = Screen:getHeight(),
covers_fullscreen = true, -- hint for UIManager:_repaint()
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,
onMenuHold = self.onMenuHold,
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,
collection = collection,
}
self.coll_list.close_callback = function(force_close)
if force_close or self.selected_colections == nil then
self:refreshFileManager()
UIManager:close(self.coll_list)
self.coll_list = nil
end
end
self:updateCollListItemTable(true) -- init
UIManager:show(self.coll_list)
return true
end
self:updateItemTable()
self.coll_menu.close_callback = function()
UIManager:close(self.coll_menu)
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
UIManager:show(self.coll_menu)
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: %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()
self.files_updated = true
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,280 +1,444 @@
local ButtonDialogTitle = require("ui/widget/buttondialogtitle")
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")
local FileChooser = require("ui/widget/filechooser")
local InfoMessage = require("ui/widget/infomessage")
local InputDialog = require("ui/widget/inputdialog")
local Menu = require("ui/widget/menu")
local Size = require("ui/size")
local UIManager = require("ui/uimanager")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local BaseUtil = require("ffi/util")
local Utf8Proc = require("ffi/utf8proc")
local filemanagerutil = require("apps/filemanager/filemanagerutil")
local lfs = require("libs/libkoreader-lfs")
local util = require("util")
local _ = require("gettext")
local Screen = require("device").screen
local N_ = _.ngettext
local T = require("ffi/util").template
local FileSearcher = WidgetContainer:extend{
dirs = nil, -- table
files = nil, -- table
results = nil, -- table
case_sensitive = false,
include_subfolders = true,
include_metadata = false,
}
local sys_folders = { -- do not search in sys_folders
["/dev"] = true,
["/proc"] = true,
["/sys"] = true,
}
function FileSearcher:init()
self.dirs = {}
self.files = {}
self.results = {}
end
function FileSearcher:readDir()
local ReaderUI = require("apps/reader/readerui")
local show_unsupported = G_reader_settings:isTrue("show_unsupported")
self.dirs = {self.path}
self.files = {}
while #self.dirs ~= 0 do
local new_dirs = {}
-- handle each dir
for __, d in pairs(self.dirs) do
-- handle files in d
local ok, iter, dir_obj = pcall(lfs.dir, d)
if ok then
for f in iter, dir_obj do
local fullpath = "/" .. f
if d ~= "/" then
fullpath = d .. fullpath
end
local attributes = lfs.attributes(fullpath) or {}
-- Don't traverse hidden folders if we're not showing them
if attributes.mode == "directory" and f ~= "." and f ~= ".."
and (G_reader_settings:isTrue("show_hidden") or not util.stringStartsWith(f, "."))
and FileChooser:show_dir(f)
then
if self.include_subfolders and not sys_folders[fullpath] then
table.insert(new_dirs, fullpath)
end
table.insert(self.files, {
dir = d,
name = f,
text = f.."/",
attr = attributes,
callback = function()
self:showFolder(fullpath .. "/")
end,
})
-- Always ignore macOS resource forks, too.
elseif attributes.mode == "file" and not util.stringStartsWith(f, "._")
and (show_unsupported or DocumentRegistry:hasProvider(fullpath))
and FileChooser:show_file(f)
then
table.insert(self.files, {
dir = d,
name = f,
text = f,
mandatory = util.getFriendlySize(attributes.size or 0),
attr = attributes,
callback = function()
ReaderUI:showReader(fullpath)
end,
})
end
end
end
end
self.dirs = new_dirs
end
end
function FileSearcher:setSearchResults()
local keywords = self.search_value
self.results = {}
if keywords == "*" then -- one * to show all files
self.results = self.files
else
if not self.case_sensitive then
keywords = Utf8Proc.lowercase(util.fixUtf8(keywords, "?"))
end
-- replace '.' with '%.'
keywords = keywords:gsub("%.","%%%.")
-- replace '*' with '.*'
keywords = keywords:gsub("%*","%.%*")
-- replace '?' with '.'
keywords = keywords:gsub("%?","%.")
for __,f in pairs(self.files) do
if self.case_sensitive then
if string.find(f.name, keywords) then
table.insert(self.results, f)
end
else
if string.find(Utf8Proc.lowercase(util.fixUtf8(f.name, "?")), keywords) then
table.insert(self.results, f)
end
end
end
end
end
function FileSearcher:close()
UIManager:close(self.search_dialog)
self:readDir() --- @todo this probably doesn't need to be repeated once it's been done
self:setSearchResults() --- @todo doesn't have to be repeated if the search term is the same
if #self.results > 0 then
self:showSearchResults()
else
UIManager:show(
InfoMessage:new{
text = BaseUtil.template(_("No results for '%1'."),
self.search_value)
}
)
end
end
function FileSearcher:onShowFileSearch(search_string)
self.search_dialog = InputDialog:new{
title = _("Enter filename to search for"),
input = search_string or self.search_value,
local search_dialog
local check_button_case, check_button_subfolders, check_button_metadata
search_dialog = InputDialog:new{
title = _("Enter text to search for in filename"),
input = search_string or self.search_string,
buttons = {
{
{
text = _("Cancel"),
id = "close",
callback = function()
UIManager:close(self.search_dialog)
UIManager:close(search_dialog)
end,
},
{
text = _("Home folder"),
enabled = G_reader_settings:has("home_dir"),
callback = function()
self.search_value = self.search_dialog:getInputText()
if self.search_value == "" then return end
self.search_string = search_dialog:getInputText()
if self.search_string == "" then return end
UIManager:close(search_dialog)
self.path = G_reader_settings:readSetting("home_dir")
self:close()
self:doSearch()
end,
},
{
text = _("Current folder"),
text = self.ui.file_chooser and _("Current folder") or _("Book folder"),
is_enter_default = true,
callback = function()
self.search_value = self.search_dialog:getInputText()
if self.search_value == "" then return end
self.search_string = search_dialog:getInputText()
if self.search_string == "" then return end
UIManager:close(search_dialog)
self.path = self.ui.file_chooser and self.ui.file_chooser.path or self.ui:getLastDirFile()
self:close()
self:doSearch()
end,
},
},
},
}
self.check_button_case = CheckButton:new{
check_button_case = CheckButton:new{
text = _("Case sensitive"),
checked = self.case_sensitive,
parent = self.search_dialog,
parent = search_dialog,
callback = function()
self.case_sensitive = self.check_button_case.checked
self.case_sensitive = check_button_case.checked
end,
}
self.search_dialog:addWidget(self.check_button_case)
self.check_button_subfolders = CheckButton:new{
search_dialog:addWidget(check_button_case)
check_button_subfolders = CheckButton:new{
text = _("Include subfolders"),
checked = self.include_subfolders,
parent = self.search_dialog,
parent = search_dialog,
callback = function()
self.include_subfolders = self.check_button_subfolders.checked
self.include_subfolders = check_button_subfolders.checked
end,
}
self.search_dialog:addWidget(self.check_button_subfolders)
search_dialog:addWidget(check_button_subfolders)
if self.ui.coverbrowser then
check_button_metadata = CheckButton:new{
text = _("Also search in book metadata"),
checked = self.include_metadata,
parent = search_dialog,
callback = function()
self.include_metadata = check_button_metadata.checked
end,
}
search_dialog:addWidget(check_button_metadata)
end
UIManager:show(search_dialog)
search_dialog:onShowKeyboard()
end
UIManager:show(self.search_dialog)
self.search_dialog:onShowKeyboard()
function FileSearcher:doSearch()
local dirs, files = self:getList()
-- If we have a FileChooser instance, use it, to be able to make use of its natsort cache
local results = (self.ui.file_chooser or FileChooser):genItemTable(dirs, files)
if #results > 0 then
self:showSearchResults(results)
else
self:showSearchResultsMessage(true)
end
end
function FileSearcher:showSearchResults()
local menu_container = CenterContainer:new{
dimen = Screen:getSize(),
function FileSearcher:getList()
self.no_metadata_count = 0
local sys_folders = { -- do not search in sys_folders
["/dev"] = true,
["/proc"] = true,
["/sys"] = true,
}
local collate = FileChooser:getCollate()
local search_string = self.search_string
if search_string ~= "*" then -- one * to show all files
if not self.case_sensitive then
search_string = Utf8Proc.lowercase(util.fixUtf8(search_string, "?"))
end
-- replace '.' with '%.'
search_string = search_string:gsub("%.","%%%.")
-- replace '*' with '.*'
search_string = search_string:gsub("%*","%.%*")
-- replace '?' with '.'
search_string = search_string:gsub("%?","%.")
end
local dirs, files = {}, {}
local scan_dirs = {self.path}
while #scan_dirs ~= 0 do
local new_dirs = {}
-- handle each dir
for _, d in ipairs(scan_dirs) do
-- handle files in d
local ok, iter, dir_obj = pcall(lfs.dir, d)
if ok then
for f in iter, dir_obj do
local fullpath = "/" .. f
if d ~= "/" then
fullpath = d .. fullpath
end
local attributes = lfs.attributes(fullpath) or {}
-- Don't traverse hidden folders if we're not showing them
if attributes.mode == "directory" and f ~= "." and f ~= ".."
and (FileChooser.show_hidden or not util.stringStartsWith(f, "."))
and FileChooser:show_dir(f) then
if self.include_subfolders and not sys_folders[fullpath] then
table.insert(new_dirs, fullpath)
end
if self:isFileMatch(f, fullpath, search_string) then
table.insert(dirs, FileChooser:getListItem(nil, f, fullpath, attributes, collate))
end
-- Always ignore macOS resource forks, too.
elseif attributes.mode == "file" and not util.stringStartsWith(f, "._")
and (FileChooser.show_unsupported or DocumentRegistry:hasProvider(fullpath))
and FileChooser:show_file(f) then
if self:isFileMatch(f, fullpath, search_string, true) then
table.insert(files, FileChooser:getListItem(nil, f, fullpath, attributes, collate))
end
end
end
end
end
scan_dirs = new_dirs
end
return dirs, files
end
function FileSearcher:isFileMatch(filename, fullpath, search_string, is_file)
if search_string == "*" then
return true
end
if not self.case_sensitive then
filename = Utf8Proc.lowercase(util.fixUtf8(filename, "?"))
end
if string.find(filename, search_string) then
return true
end
if self.include_metadata and is_file and DocumentRegistry:hasProvider(fullpath) then
local book_props = self.ui.coverbrowser:getBookInfo(fullpath) or
self.ui.bookinfo.getDocProps(fullpath, nil, true) -- do not open the document
if next(book_props) ~= nil then
if self.ui.bookinfo:findInProps(book_props, search_string, self.case_sensitive) then
return true
end
else
self.no_metadata_count = self.no_metadata_count + 1
end
end
end
function FileSearcher:showSearchResultsMessage(no_results)
local text = no_results and T(_("No results for '%1'."), self.search_string)
if self.no_metadata_count == 0 then
UIManager:show(InfoMessage:new{ text = text })
else
local txt = T(N_("1 book has been skipped.", "%1 books have been skipped.",
self.no_metadata_count), self.no_metadata_count) .. "\n" ..
_("Not all books metadata extracted yet.\nExtract metadata now?")
text = no_results and text .. "\n\n" .. txt or txt
UIManager:show(ConfirmBox:new{
text = text,
ok_text = _("Extract"),
ok_callback = function()
if not no_results then
self.search_menu.close_callback()
end
self.ui.coverbrowser:extractBooksInDirectory(self.path)
end
})
end
end
function FileSearcher:showSearchResults(results)
self.search_menu = Menu:new{
width = Screen:getWidth() - (Size.margin.fullscreen_popout * 2),
height = Screen:getHeight() - (Size.margin.fullscreen_popout * 2),
show_parent = menu_container,
subtitle = T(_("Query: %1"), self.search_string),
covers_fullscreen = true, -- hint for UIManager:_repaint()
is_borderless = true,
is_popout = false,
title_bar_fm_style = true,
title_bar_left_icon = "appbar.menu",
onLeftButtonTap = function() self:setSelectMode() end,
onMenuSelect = self.onMenuSelect,
onMenuHold = self.onMenuHold,
handle_hold_on_hold_release = true,
ui = self.ui,
_manager = self,
}
table.insert(menu_container, self.search_menu)
self.search_menu.close_callback = function()
UIManager:close(menu_container)
self.selected_files = nil
UIManager:close(self.search_menu)
if self.ui.file_chooser then
self.ui.file_chooser:refreshPath()
end
end
self:updateMenu(results)
UIManager:show(self.search_menu)
if self.no_metadata_count ~= 0 then
self:showSearchResultsMessage()
end
end
local collate = G_reader_settings:readSetting("collate") or "strcoll"
local reverse_collate = G_reader_settings:isTrue("reverse_collate")
local sorting = FileChooser:getSortingFunction(collate, reverse_collate)
function FileSearcher:updateMenu(item_table)
item_table = item_table or self.search_menu.item_table
self.search_menu:switchItemTable(T(_("Search results (%1)"), #item_table), item_table, -1)
end
table.sort(self.results, sorting)
self.search_menu:switchItemTable(T(_("Search results (%1)"), #self.results), self.results)
UIManager:show(menu_container)
function FileSearcher:onMenuSelect(item)
if self._manager.selected_files then
if item.is_file then
item.dim = not item.dim and true or nil
self._manager.selected_files[item.path] = item.dim
self._manager:updateMenu()
end
else
self._manager:showFileDialog(item)
end
end
function FileSearcher:showFileDialog(item)
local file = item.path
local bookinfo, dialog
local function close_dialog_callback()
UIManager:close(dialog)
end
local function close_dialog_menu_callback()
UIManager:close(dialog)
self.search_menu.close_callback()
end
local function update_item_callback()
item.mandatory = FileChooser:getMenuItemMandatory(item, FileChooser:getCollate())
self:updateMenu()
end
local buttons = {}
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),
self.ui.collections:genAddToCollectionButton(file, close_dialog_callback, update_item_callback),
})
end
table.insert(buttons, {
{
text = _("Delete"),
enabled = not is_currently_opened,
callback = function()
local function post_delete_callback()
UIManager:close(dialog)
table.remove(self.search_menu.item_table, item.idx)
self:updateMenu()
end
local FileManager = require("apps/filemanager/filemanager")
FileManager:showDeleteFileDialog(file, post_delete_callback)
end,
},
filemanagerutil.genBookInformationButton(file, bookinfo, close_dialog_callback),
})
end
table.insert(buttons, {
filemanagerutil.genShowFolderButton(file, close_dialog_menu_callback),
{
text = _("Open"),
enabled = DocumentRegistry:hasProvider(file, nil, true), -- allow auxiliary providers
callback = function()
close_dialog_menu_callback()
local FileManager = require("apps/filemanager/filemanager")
FileManager.openFile(self.ui, file)
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 = title .. "\n",
buttons = buttons,
}
UIManager:show(dialog)
end
function FileSearcher:onMenuHold(item)
local ReaderUI = require("apps/reader/readerui")
local is_file = item.attr.mode == "file"
local fullpath = item.dir .. "/" .. item.name .. (is_file and "" or "/")
if self._manager.selected_files then return true end
if item.is_file then
if DocumentRegistry:hasProvider(item.path, nil, true) then
self.close_callback()
local FileManager = require("apps/filemanager/filemanager")
FileManager.openFile(self.ui, item.path)
end
else
self.close_callback()
if self.ui.file_chooser then
local pathname = util.splitFilePathName(item.path)
self.ui.file_chooser:changeToPath(pathname, item.path)
else -- called from Reader
self.ui:onClose()
self.ui:showFileManager(item.path)
end
end
return true
end
function FileSearcher:setSelectMode()
if self.selected_files then
self:showSelectModeDialog()
else
self.selected_files = {}
self.search_menu:setTitleBarLeftIcon("check")
end
end
function FileSearcher:showSelectModeDialog()
local item_table = self.search_menu.item_table
local select_count = util.tableSize(self.selected_files)
local actions_enabled = select_count > 0
local title = actions_enabled and T(N_("1 file selected", "%1 files selected", select_count), select_count)
or _("No files selected")
local select_dialog
local buttons = {
{
{
text = _("Cancel"),
text = _("Deselect all"),
enabled = actions_enabled,
callback = function()
UIManager:close(select_dialog)
for file in pairs (self.selected_files) do
self.selected_files[file] = nil
end
for _, item in ipairs(item_table) do
item.dim = nil
end
self:updateMenu()
end,
},
{
text = _("Select all"),
callback = function()
UIManager:close(select_dialog)
for _, item in ipairs(item_table) do
if item.is_file then
item.dim = true
self.selected_files[item.path] = true
end
end
self:updateMenu()
end,
},
},
{
{
text = _("Exit select mode"),
callback = function()
UIManager:close(self.results_dialog)
UIManager:close(select_dialog)
self.selected_files = nil
self.search_menu:setTitleBarLeftIcon("appbar.menu")
if actions_enabled then
for _, item in ipairs(item_table) do
item.dim = nil
end
end
self:updateMenu()
end,
},
{
text = _("Show folder"),
text = _("Select in file browser"),
enabled = actions_enabled,
callback = function()
UIManager:close(self.results_dialog)
self.close_callback()
self._manager:showFolder(fullpath)
UIManager:close(select_dialog)
local selected_files = self.selected_files
self.search_menu.close_callback()
if self.ui.file_chooser then
self.ui.selected_files = selected_files
self.ui.title_bar:setRightIcon("check")
self.ui.file_chooser:refreshPath()
else -- called from Reader
self.ui:onClose()
self.ui:showFileManager(self.path .. "/", selected_files)
end
end,
},
},
}
if is_file then
table.insert(buttons[1], {
text = _("Open"),
callback = function()
UIManager:close(self.results_dialog)
self.close_callback()
ReaderUI:showReader(fullpath)
end,
})
end
self.results_dialog = ButtonDialogTitle:new{
title = fullpath,
select_dialog = ButtonDialog:new{
title = title,
title_align = "center",
buttons = buttons,
}
UIManager:show(self.results_dialog)
return true
end
function FileSearcher:showFolder(path)
if self.ui.file_chooser then
local pathname = util.splitFilePathName(path)
self.ui.file_chooser:changeToPath(pathname, path)
else -- called from Reader
self.ui:onClose()
self.ui:showFileManager(path)
end
UIManager:show(select_dialog)
end
return FileSearcher

@ -1,30 +1,33 @@
local BD = require("ui/bidi")
local ButtonDialogTitle = require("ui/widget/buttondialogtitle")
local ButtonDialog = require("ui/widget/buttondialog")
local CheckButton = require("ui/widget/checkbutton")
local ConfirmBox = require("ui/widget/confirmbox")
local DocSettings = require("docsettings")
local FFIUtil = require("ffi/util")
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
local Utf8Proc = require("ffi/utf8proc")
local filemanagerutil = require("apps/filemanager/filemanagerutil")
local lfs = require("libs/libkoreader-lfs")
local util = require("util")
local _ = require("gettext")
local C_ = _.pgettext
local T = FFIUtil.template
local T = require("ffi/util").template
local FileManagerHistory = WidgetContainer:extend{
hist_menu_title = _("History"),
}
local status_text = {
all = C_("Book status filter", "All"),
reading = C_("Book status filter", "Reading"),
local filter_text = {
all = C_("Book status filter", "All"),
reading = C_("Book status filter", "Reading"),
abandoned = C_("Book status filter", "On hold"),
complete = C_("Book status filter", "Finished"),
deleted = C_("Book status filter", "Deleted"),
new = C_("Book status filter", "New"),
complete = C_("Book status filter", "Finished"),
deleted = C_("Book status filter", "Deleted"),
new = C_("Book status filter", "New"),
}
function FileManagerHistory:init()
@ -32,7 +35,6 @@ function FileManagerHistory:init()
end
function FileManagerHistory:addToMainMenu(menu_items)
-- insert table to main tab of filemanager menu
menu_items.history = {
text = self.hist_menu_title,
callback = function()
@ -41,97 +43,186 @@ function FileManagerHistory:addToMainMenu(menu_items)
}
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
function FileManagerHistory:fetchStatuses(count)
for _, v in ipairs(require("readhistory").hist) do
local status
if v.dim then -- deleted file
status = "deleted"
elseif v.file == (self.ui.document and self.ui.document.file) then -- currently opened file
status = self.ui.doc_settings:readSetting("summary").status
else
status = filemanagerutil.getStatus(v.file)
end
if not filter_text[status] then
status = "reading"
end
if count then
self.count[status] = self.count[status] + 1
end
v.status = status
end
self.statuses_fetched = true
end
function FileManagerHistory:updateItemTable()
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 not self.filter or v.status == self.filter then
table.insert(item_table, v)
if self:isItemMatch(v) then
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, item)
end
if self.statuses_fetched then
self.count[v.status] = self.count[v.status] + 1
end
end
local title = self.hist_menu_title
if self.filter then
title = title .. " (" .. status_text[self.filter] .. ")"
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(title, item_table, select_number)
self.hist_menu:switchItemTable(nil, item_table, -1, nil, subtitle)
end
function FileManagerHistory:isItemMatch(item)
if self.search_string then
local filename = self.case_sensitive and item.text or Utf8Proc.lowercase(util.fixUtf8(item.text, "?"))
if not filename:find(self.search_string) then
local book_props
if self.ui.coverbrowser then
book_props = self.ui.coverbrowser:getBookInfo(item.file)
end
if not book_props then
book_props = self.ui.bookinfo.getDocProps(item.file, nil, true) -- do not open the document
end
if not self.ui.bookinfo:findInProps(book_props, self.search_string, self.case_sensitive) then
return false
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
function FileManagerHistory:onSetDimensions(dimen)
self.dimen = dimen
end
function FileManagerHistory:onMenuChoice(item)
if self.ui.document then
if self.ui.document.file ~= item.file then
self.ui:switchDocument(item.file)
end
else
self.ui:openFile(item.file)
end
end
function FileManagerHistory:onMenuHold(item)
local readerui_instance = require("apps/reader/readerui"):_getRunningInstance()
local currently_opened_file = readerui_instance and readerui_instance.document and readerui_instance.document.file
local file = item.file
self.histfile_dialog = nil
local buttons = {
self.book_props = self.ui.coverbrowser and self.ui.coverbrowser:getBookInfo(file)
local function close_dialog_callback()
UIManager:close(self.histfile_dialog)
end
local function close_dialog_menu_callback()
UIManager:close(self.histfile_dialog)
self._manager.hist_menu.close_callback()
end
local function close_dialog_update_callback()
UIManager:close(self.histfile_dialog)
if self._manager.filter ~= "all" or self._manager.is_frozen then
self._manager:fetchStatuses(false)
else
self._manager.statuses_fetched = false
end
self._manager:updateItemTable()
self._manager.files_updated = true -- sidecar folder may be created/deleted
end
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
table.insert(buttons, filemanagerutil.genStatusButtonsRow(doc_settings_or_file, close_dialog_update_callback))
table.insert(buttons, {}) -- separator
end
table.insert(buttons, {
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, {
{
{
text = _("Reset settings"),
enabled = item.file ~= currently_opened_file and DocSettings:hasSidecarFile(FFIUtil.realpath(item.file)),
callback = function()
UIManager:show(ConfirmBox:new{
text = T(_("Reset settings for this document?\n\n%1\n\nAny highlights or bookmarks will be permanently lost."),
BD.filepath(item.file)),
ok_text = _("Reset"),
ok_callback = function()
filemanagerutil.purgeSettings(item.file)
require("readhistory"):fileSettingsPurged(item.file)
self._manager:updateItemTable()
UIManager:close(self.histfile_dialog)
end,
})
end,
},
{
text = _("Remove from history"),
callback = function()
require("readhistory"):removeItem(item)
self._manager:updateItemTable()
text = _("Delete"),
enabled = not (item.dim or is_currently_opened),
callback = function()
local function post_delete_callback()
UIManager:close(self.histfile_dialog)
end,
},
self._manager:updateItemTable()
self._manager.files_updated = true
end
local FileManager = require("apps/filemanager/filemanager")
FileManager:showDeleteFileDialog(file, post_delete_callback)
end,
},
{
{
text = _("Delete"),
enabled = (item.file ~= currently_opened_file and lfs.attributes(item.file, "mode")) and true or false,
callback = function()
UIManager:show(ConfirmBox:new{
text = T(_("Are you sure that you want to delete this document?\n\n%1\n\nIf you delete a file, it is permanently lost."),
BD.filepath(item.file)),
ok_text = _("Delete"),
ok_callback = function()
local FileManager = require("apps/filemanager/filemanager")
FileManager:deleteFile(item.file)
require("readhistory"):fileDeleted(item.file) -- (will update "lastfile" if needed)
self._manager:updateItemTable()
UIManager:close(self.histfile_dialog)
end,
})
end,
},
{
text = _("Book information"),
enabled = FileManagerBookInfo:isSupported(item.file),
callback = function()
FileManagerBookInfo:show(item.file)
UIManager:close(self.histfile_dialog)
end,
},
text = _("Remove from history"),
callback = function()
UIManager:close(self.histfile_dialog)
require("readhistory"):removeItem(item)
self._manager:updateItemTable()
end,
},
}
self.histfile_dialog = ButtonDialogTitle:new{
title = BD.filename(item.text:match("([^/]+)$")),
})
table.insert(buttons, {
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(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{
title = BD.filename(item.text),
title_align = "center",
buttons = buttons,
}
@ -156,87 +247,113 @@ function FileManagerHistory:MenuSetRotationModeHandler(rotation)
return true
end
function FileManagerHistory:onShowHist()
function FileManagerHistory:onShowHist(search_info)
self.hist_menu = Menu:new{
ui = self.ui,
width = Screen:getWidth(),
height = Screen:getHeight(),
covers_fullscreen = true, -- hint for UIManager:_repaint()
is_borderless = true,
is_popout = false,
title = self.hist_menu_title,
-- 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:showHistDialog() end,
onMenuChoice = self.onMenuChoice,
onMenuHold = self.onMenuHold,
onSetRotationMode = self.MenuSetRotationModeHandler,
_manager = self,
}
if search_info then
self.search_string = search_info.search_string
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")
if self.filter ~= "all" or self.is_frozen then
self:fetchStatuses(false)
end
self:updateItemTable()
self.hist_menu.close_callback = function()
if self.files_updated then -- refresh Filemanager list of files
if self.ui.file_chooser then
self.ui.file_chooser:refreshPath()
end
self.files_updated = nil
end
self.statuses_fetched = nil
self.filter = nil
UIManager:close(self.hist_menu)
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
function FileManagerHistory:showHistDialog()
if not self.statuses_fetched then
local status
for _, v in ipairs(require("readhistory").hist) do
if v.dim then
status = "deleted"
else
if DocSettings:hasSidecarFile(v.file) then
local docinfo = DocSettings:open(v.file) -- no io handles created, do not close
if docinfo.data.summary and docinfo.data.summary.status
and docinfo.data.summary.status ~= "" then
status = docinfo.data.summary.status
else
status = "reading"
end
else
status = "new"
end
end
v.status = status
self.count[status] = self.count[status] + 1
end
self.statuses_fetched = true
self:fetchStatuses(true)
end
local hist_dialog
local buttons = {}
local function genFilterButton(status)
local function genFilterButton(filter)
return {
text = T(_("%1 (%2)"), status_text[status], self.count[status]),
text = T(_("%1 (%2)"), filter_text[filter], self.count[filter]),
callback = function()
UIManager:close(hist_dialog)
self.filter = status ~= "all" and status
self.filter = filter
if filter == "all" then -- reset all filters
self.search_string = nil
self.selected_colections = nil
end
self:updateItemTable()
end,
}
end
table.insert(buttons, {
genFilterButton("all"),
genFilterButton("new"),
genFilterButton("deleted"),
})
table.insert(buttons, {
genFilterButton("reading"),
genFilterButton("abandoned"),
genFilterButton("complete"),
})
table.insert(buttons, {
genFilterButton("complete"),
genFilterButton("deleted"),
{
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, {
genFilterButton("all"),
genFilterButton("new"),
{
text = _("Search in filename and book metadata"),
callback = function()
UIManager:close(hist_dialog)
self:onSearchHistory()
end,
},
})
if self.count.deleted > 0 then
table.insert(buttons, {})
table.insert(buttons, {}) -- separator
table.insert(buttons, {
{
text = _("Clear history of deleted files"),
callback = function()
UIManager:show(ConfirmBox:new{
local confirmbox = ConfirmBox:new{
text = _("Clear history of deleted files?"),
ok_text = _("Clear"),
ok_callback = function()
@ -244,12 +361,13 @@ function FileManagerHistory:showHistDialog()
require("readhistory"):clearMissing()
self:updateItemTable()
end,
})
}
UIManager:show(confirmbox)
end,
},
},
})
end
hist_dialog = ButtonDialogTitle:new{
hist_dialog = ButtonDialog:new{
title = _("Filter by book status"),
title_align = "center",
buttons = buttons,
@ -257,4 +375,61 @@ function FileManagerHistory:showHistDialog()
UIManager:show(hist_dialog)
end
function FileManagerHistory:onSearchHistory()
local search_dialog, check_button_case
search_dialog = InputDialog:new{
title = _("Enter text to search history for"),
input = self.search_string,
buttons = {
{
{
text = _("Cancel"),
id = "close",
callback = function()
UIManager:close(search_dialog)
end,
},
{
text = _("Search"),
is_enter_default = true,
callback = function()
local search_string = search_dialog:getInputText()
if search_string ~= "" then
UIManager:close(search_dialog)
self.search_string = self.case_sensitive and search_string or search_string:lower()
if self.hist_menu then -- called from History
self:updateItemTable()
else -- called by Dispatcher
local search_info = {
search_string = self.search_string,
case_sensitive = self.case_sensitive,
}
self:onShowHist(search_info)
end
end
end,
},
},
},
}
check_button_case = CheckButton:new{
text = _("Case sensitive"),
checked = self.case_sensitive,
parent = search_dialog,
callback = function()
self.case_sensitive = check_button_case.checked
end,
}
search_dialog:addWidget(check_button_case)
UIManager:show(search_dialog)
search_dialog:onShowKeyboard()
return true
end
function FileManagerHistory:onBookMetadataChanged()
if self.hist_menu then
self.hist_menu:updateItems()
end
end
return FileManagerHistory

@ -2,14 +2,16 @@ 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")
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")
local dbg = require("dbg")
local lfs = require("libs/libkoreader-lfs")
local logger = require("logger")
@ -48,17 +50,26 @@ function FileManagerMenu:init()
self.registered_widgets = {}
if Device:hasKeys() then
self.key_events = {
ShowMenu = { { "Menu" }, doc = "show menu" },
}
end
self:registerKeyEvents()
self.activation_menu = G_reader_settings:readSetting("activate_menu")
if self.activation_menu == nil then
self.activation_menu = "swipe_tap"
end
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
@ -137,69 +148,77 @@ function FileManagerMenu:onOpenLastDoc()
end
function FileManagerMenu:setUpdateItemTable()
local FileChooser = self.ui.file_chooser
-- setting tab
self.menu_items.filebrowser_settings = {
text = _("Settings"),
sub_item_table = {
{
text = _("Show finished books"),
checked_func = function() return FileChooser.show_finished end,
callback = function() FileChooser:toggleShowFilesMode("show_finished") end,
},
{
text = _("Show hidden files"),
checked_func = function() return self.ui.file_chooser.show_hidden end,
callback = function() self.ui:toggleHiddenFiles() end,
checked_func = function() return FileChooser.show_hidden end,
callback = function() FileChooser:toggleShowFilesMode("show_hidden") end,
},
{
text = _("Show unsupported files"),
checked_func = function() return self.ui.file_chooser.show_unsupported end,
callback = function() self.ui:toggleUnsupportedFiles() end,
checked_func = function() return FileChooser.show_unsupported end,
callback = function() FileChooser:toggleShowFilesMode("show_unsupported") end,
separator = true,
},
{
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
@ -207,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,
},
{
@ -257,6 +277,26 @@ function FileManagerMenu:setUpdateItemTable()
{
text = _("History settings"),
sub_item_table = {
{
text = _("Shorten date/time"),
checked_func = function()
return G_reader_settings:isTrue("history_datetime_short")
end,
callback = function()
G_reader_settings:flipNilOrFalse("history_datetime_short")
require("readhistory"):updateDateTimeString()
end,
},
{
text = _("Freeze last read date of finished books"),
checked_func = function()
return G_reader_settings:isTrue("history_freeze_finished_books")
end,
callback = function()
G_reader_settings:flipNilOrFalse("history_freeze_finished_books")
end,
separator = true,
},
{
text = _("Clear history of deleted files"),
callback = function()
@ -296,29 +336,14 @@ function FileManagerMenu:setUpdateItemTable()
{
text = _("Set home folder"),
callback = function()
local text
local home_dir = G_reader_settings:readSetting("home_dir")
if home_dir then
text = T(_("Home folder is set to:\n%1"), home_dir)
else
text = _("Home folder is not set.")
home_dir = Device.home_dir
local title_header = _("Current home folder:")
local current_path = G_reader_settings:readSetting("home_dir")
local default_path = filemanagerutil.getDefaultDir()
local caller_callback = function(path)
G_reader_settings:saveSetting("home_dir", path)
self.ui:updateTitleBarPath()
end
UIManager:show(ConfirmBox:new{
text = text .. "\nChoose new folder to set as home?",
ok_text = _("Choose folder"),
ok_callback = function()
local path_chooser = require("ui/widget/pathchooser"):new{
select_file = false,
show_files = false,
path = home_dir,
onConfirm = function(new_path)
G_reader_settings:saveSetting("home_dir", new_path)
end
}
UIManager:show(path_chooser)
end,
})
filemanagerutil.showChooseDialog(title_header, caller_callback, current_path, default_path)
end,
},
{
@ -328,8 +353,7 @@ function FileManagerMenu:setUpdateItemTable()
end,
callback = function()
G_reader_settings:flipNilOrTrue("shorten_home_dir")
local FileManager = require("apps/filemanager/filemanager")
if FileManager.instance then FileManager.instance:reinit() end
self.ui:updateTitleBarPath()
end,
help_text = _([[
"Shorten home folder" will display the home folder itself as "Home" instead of its full path.
@ -358,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
@ -385,12 +411,13 @@ To:
else
G_reader_settings:saveSetting("keyvalues_per_page", spin.value)
end
touchmenu_instance:updateItems()
end
}
UIManager:show(items)
UIManager:show(widget)
end,
},
}
},
}
for _, widget in pairs(self.registered_widgets) do
@ -400,24 +427,60 @@ To:
end
end
self.menu_items.sort_by = self.ui:getSortingMenuTable()
self.menu_items.sort_by = self:getSortingMenuTable()
self.menu_items.reverse_sorting = {
text = _("Reverse sorting"),
checked_func = function() return self.ui.file_chooser.reverse_collate end,
callback = function() self.ui:toggleReverseCollate() end
checked_func = function()
return G_reader_settings:isTrue("reverse_collate")
end,
callback = function()
G_reader_settings:flipNilOrFalse("reverse_collate")
FileChooser:refreshPath()
end,
}
self.menu_items.sort_mixed = {
text = _("Folders and files mixed"),
enabled_func = function()
local collate = FileChooser:getCollate()
return collate.can_collate_mixed
end,
checked_func = function()
local collate = FileChooser:getCollate()
return collate.can_collate_mixed and G_reader_settings:isTrue("collate_mixed")
end,
callback = function()
G_reader_settings:flipNilOrFalse("collate_mixed")
FileChooser:refreshPath()
end,
}
self.menu_items.start_with = self.ui:getStartWithMenuTable()
self.menu_items.start_with = self:getStartWithMenuTable()
if Device:supportsScreensaver() then
self.menu_items.screensaver = {
text = _("Screensaver"),
text = _("Sleep screen"),
sub_item_table = require("ui/elements/screensaver_menu"),
}
end
-- insert common settings
for id, common_setting in pairs(dofile("frontend/ui/elements/common_settings_menu_table.lua")) do
self.menu_items[id] = common_setting
end
-- Settings > Navigation; this mostly concerns physical keys, and applies *everywhere*
if Device:hasKeys() then
self.menu_items.physical_buttons_setup = require("ui/elements/physical_buttons")
end
-- settings tab - Document submenu
self.menu_items.document_metadata_location_move = {
text = _("Move book metadata"),
keep_menu_open = true,
callback = function()
self.ui.bookinfo:moveBookMetadata()
end,
}
-- tools tab
self.menu_items.advanced_settings = {
text = _("Advanced settings"),
@ -448,10 +511,7 @@ To:
-- Also remove from the Cache objet references to the cache files we've just deleted
local Cache = require("cache")
Cache.cached = {}
local InfoMessage = require("ui/widget/infomessage")
UIManager:show(InfoMessage:new{
text = _("Caches cleared. Please restart KOReader."),
})
UIManager:askForRestart(_("Caches cleared. Please restart KOReader."))
end,
})
end,
@ -489,7 +549,7 @@ To:
end
end,
},
}
},
}
if Device:isKobo() and not Device:isSunxi() then
table.insert(self.menu_items.developer_options.sub_item_table, {
@ -499,10 +559,7 @@ To:
end,
callback = function()
G_reader_settings:flipNilOrFalse("dev_startup_no_fbdepth")
local InfoMessage = require("ui/widget/infomessage")
UIManager:show(InfoMessage:new{
text = _("This will take effect on next restart."),
})
UIManager:askForRestart()
end,
})
end
@ -515,10 +572,7 @@ To:
end,
callback = function()
G_reader_settings:flipNilOrFalse("dev_abort_on_crash")
local InfoMessage = require("ui/widget/infomessage")
UIManager:show(InfoMessage:new{
text = _("This will take effect on next restart."),
})
UIManager:askForRestart()
end,
})
end
@ -575,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, {
@ -597,10 +675,7 @@ To:
mxcfb_bypass_wait_for = not Device:hasReliableMxcWaitFor()
end
G_reader_settings:saveSetting("mxcfb_bypass_wait_for", not mxcfb_bypass_wait_for)
local InfoMessage = require("ui/widget/infomessage")
UIManager:show(InfoMessage:new{
text = _("This will take effect on next restart."),
})
UIManager:askForRestart()
end,
})
end
@ -617,10 +692,7 @@ To:
end,
callback = function()
G_reader_settings:flipNilOrFalse("pb_ignore_b288_quirks")
local InfoMessage = require("ui/widget/infomessage")
UIManager:show(InfoMessage:new{
text = _("This will take effect on next restart."),
})
UIManager:askForRestart()
end,
})
end
@ -640,10 +712,7 @@ To:
end,
callback = function()
G_reader_settings:flipNilOrTrue("use_xtext")
local InfoMessage = require("ui/widget/infomessage")
UIManager:show(InfoMessage:new{
text = _("This will take effect on next restart."),
})
UIManager:askForRestart()
end,
})
table.insert(self.menu_items.developer_options.sub_item_table, {
@ -656,10 +725,7 @@ To:
end,
callback = function()
G_reader_settings:flipNilOrFalse("dev_reverse_ui_layout_mirroring")
local InfoMessage = require("ui/widget/infomessage")
UIManager:show(InfoMessage:new{
text = _("This will take effect on next restart."),
})
UIManager:askForRestart()
end
},
{
@ -669,13 +735,10 @@ To:
end,
callback = function()
G_reader_settings:flipNilOrFalse("dev_reverse_ui_text_direction")
local InfoMessage = require("ui/widget/infomessage")
UIManager:show(InfoMessage:new{
text = _("This will take effect on next restart."),
})
UIManager:askForRestart()
end
}
}
},
},
})
table.insert(self.menu_items.developer_options.sub_item_table, {
text_func = function()
@ -707,6 +770,7 @@ To:
})
if Device:isKobo() and Device:canToggleChargingLED() then
table.insert(self.menu_items.developer_options.sub_item_table, {
-- @translators This is a debug option to help determine cases when standby failed to initiate properly. PM = power management.
text = _("Turn on the LED on PM entry failure"),
checked_func = function()
return G_reader_settings:isTrue("pm_debug_entry_failure")
@ -742,7 +806,7 @@ The sorting order is the same as in filemanager.
Tap a book in the search results to open it.]]),
callback = function()
self.ui:handleEvent(Event:new("ShowFileSearch"))
self.ui.filesearcher:onShowFileSearch()
end
}
@ -808,6 +872,66 @@ dbg:guard(FileManagerMenu, 'setUpdateItemTable',
end
end)
function FileManagerMenu:getSortingMenuTable()
local sub_item_table = {}
for k, v in pairs(self.ui.file_chooser.collates) do
table.insert(sub_item_table, {
text = v.text,
menu_order = v.menu_order,
checked_func = function()
local _, id = self.ui.file_chooser:getCollate()
return k == id
end,
callback = function()
G_reader_settings:saveSetting("collate", k)
self.ui.file_chooser:clearSortingCache()
self.ui.file_chooser:refreshPath()
end,
})
end
table.sort(sub_item_table, function(a, b) return a.menu_order < b.menu_order end)
return {
text_func = function()
local collate = self.ui.file_chooser:getCollate()
return T(_("Sort by: %1"), collate.text)
end,
sub_item_table = sub_item_table,
}
end
function FileManagerMenu:getStartWithMenuTable()
local start_withs = {
{ _("file browser"), "filemanager" },
{ _("history"), "history" },
{ _("favorites"), "favorites" },
{ _("folder shortcuts"), "folder_shortcuts" },
{ _("last file"), "last" },
}
local sub_item_table = {}
for i, v in ipairs(start_withs) do
table.insert(sub_item_table, {
text = v[1],
checked_func = function()
return v[2] == G_reader_settings:readSetting("start_with", "filemanager")
end,
callback = function()
G_reader_settings:saveSetting("start_with", v[2])
end,
})
end
return {
text_func = function()
local start_with = G_reader_settings:readSetting("start_with") or "filemanager"
for i, v in ipairs(start_withs) do
if v[2] == start_with then
return T(_("Start with: %1"), v[1])
end
end
end,
sub_item_table = sub_item_table,
}
end
function FileManagerMenu:exitOrRestart(callback, force)
UIManager:close(self.menu_container)
@ -874,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
@ -915,11 +1040,19 @@ 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()
self:onShowMenu()
self.menu_container[1]:onShowMenuSearch()
end
function FileManagerMenu:registerToMainMenu(widget)

@ -7,199 +7,184 @@ local Screen = require("device").screen
local UIManager = require("ui/uimanager")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local lfs = require("libs/libkoreader-lfs")
local util = require("ffi/util")
local _ = require("gettext")
local T = util.template
local FileManagerShortcuts = WidgetContainer:extend{
title = _("Folder shortcuts"),
folder_shortcuts = G_reader_settings:readSetting("folder_shortcuts", {}),
}
function FileManagerShortcuts:updateItemTable(select_callback)
function FileManagerShortcuts:updateItemTable()
local item_table = {}
for _, item in ipairs(self.folder_shortcuts) do
for folder, item in pairs(self.folder_shortcuts) do
table.insert(item_table, {
text = string.format("%s (%s)", item.text, item.folder),
folder = item.folder,
friendly_name = item.text,
deletable = true,
editable = true,
callback = function()
UIManager:close(self.fm_bookmark)
local folder = item.folder
if folder ~= nil and lfs.attributes(folder, "mode") == "directory" then
if select_callback then
select_callback(folder)
else
if self.ui.file_chooser then
self.ui.file_chooser:changeToPath(folder)
else -- called from Reader
self.ui:onClose()
self.ui:showFileManager(folder .. "/")
end
end
end
end,
text = string.format("%s (%s)", item.text, folder),
folder = folder,
name = item.text,
})
end
table.sort(item_table, function(l, r)
return l.text < r.text
end)
self.shortcuts_menu:switchItemTable(nil, item_table, -1)
end
-- try to stay on current page
local select_number
function FileManagerShortcuts:hasFolderShortcut(folder)
return self.folder_shortcuts[folder] and true or false
end
if self.fm_bookmark.page and self.fm_bookmark.perpage and self.fm_bookmark.page > 0 then
select_number = (self.fm_bookmark.page - 1) * self.fm_bookmark.perpage + 1
function FileManagerShortcuts:onMenuChoice(item)
local folder = item.folder
if lfs.attributes(folder, "mode") ~= "directory" then return end
if self.select_callback then
self.select_callback(folder)
else
if self._manager.ui.file_chooser then
self._manager.ui.file_chooser:changeToPath(folder)
else -- called from Reader
self._manager.ui:onClose()
self._manager.ui:showFileManager(folder .. "/")
end
end
self.fm_bookmark:switchItemTable(nil,
item_table, select_number)
end
function FileManagerShortcuts:addNewFolder()
local PathChooser = require("ui/widget/pathchooser")
local path_chooser = PathChooser:new{
select_directory = true,
select_file = false,
path = self.fm_bookmark.curr_path,
onConfirm = function(path)
local add_folder_input
local friendly_name = util.basename(path) or _("my folder")
add_folder_input = InputDialog:new{
title = _("Enter friendly name"),
input = friendly_name,
description = T(_("Title for selected folder:\n%1"), BD.dirpath(path)),
buttons = {
{
{
text = _("Cancel"),
id = "close",
callback = function()
UIManager:close(add_folder_input)
end,
},
{
text = _("Add"),
is_enter_default = true,
callback = function()
self:addFolderFromInput(add_folder_input:getInputValue(), path)
UIManager:close(add_folder_input)
end,
},
}
},
}
UIManager:show(add_folder_input)
add_folder_input:onShowKeyboard()
end
function FileManagerShortcuts:onMenuHold(item)
local dialog
local buttons = {
{
{
text = _("Remove shortcut"),
callback = function()
UIManager:close(dialog)
self._manager:removeShortcut(item.folder)
end
},
{
text = _("Rename shortcut"),
callback = function()
UIManager:close(dialog)
self._manager:editShortcut(item.folder)
end
},
},
self._manager.ui.file_chooser and self._manager.ui.clipboard and {
{
text = _("Paste to folder"),
callback = function()
UIManager:close(dialog)
self._manager.ui:pasteFileFromClipboard(item.folder)
end
},
},
}
UIManager:show(path_chooser)
dialog = ButtonDialog:new{
title = item.name .. "\n" .. BD.dirpath(item.folder),
title_align = "center",
buttons = buttons,
}
UIManager:show(dialog)
return true
end
function FileManagerShortcuts:addFolderFromInput(friendly_name, folder)
for __, item in ipairs(self.folder_shortcuts) do
if item.text == friendly_name and item.folder == folder then
UIManager:show(InfoMessage:new{
text = _("A shortcut to this folder already exists."),
})
return
end
function FileManagerShortcuts:removeShortcut(folder)
self.folder_shortcuts[folder] = nil
if self.shortcuts_menu then
self.fm_updated = true
self:updateItemTable()
end
table.insert(self.folder_shortcuts, {
text = friendly_name,
folder = folder,
})
self:updateItemTable()
end
function FileManagerShortcuts:onMenuHold(item)
if item.deletable or item.editable then
local folder_shortcuts_dialog
folder_shortcuts_dialog = ButtonDialog:new{
buttons = {
{
{
text = _("Paste file"),
enabled = (self._manager.ui.file_chooser and self._manager.ui.clipboard) and true or false,
callback = function()
UIManager:close(folder_shortcuts_dialog)
self._manager.ui:pasteHere(item.folder)
end
},
{
text = _("Edit"),
enabled = item.editable,
callback = function()
UIManager:close(folder_shortcuts_dialog)
self._manager:editFolderShortcut(item)
end
},
{
text = _("Delete"),
enabled = item.deletable,
callback = function()
UIManager:close(folder_shortcuts_dialog)
self._manager:deleteFolderShortcut(item)
function FileManagerShortcuts:editShortcut(folder, post_callback)
local item = self.folder_shortcuts[folder]
local name = item and item.text -- rename
local input_dialog
input_dialog = InputDialog:new {
title = _("Enter folder shortcut name"),
input = name,
description = BD.dirpath(folder),
buttons = {{
{
text = _("Cancel"),
id = "close",
callback = function()
UIManager:close(input_dialog)
end,
},
{
text = _("Save"),
is_enter_default = true,
callback = function()
local new_name = input_dialog:getInputText()
if new_name == "" or new_name == name then return end
UIManager:close(input_dialog)
if item then
item.text = new_name
else
self.folder_shortcuts[folder] = { text = new_name, time = os.time() }
if post_callback then
post_callback()
end
},
},
}
}
UIManager:show(folder_shortcuts_dialog)
return true
end
end
if self.shortcuts_menu then
self.fm_updated = true
self:updateItemTable()
end
end,
},
}},
}
UIManager:show(input_dialog)
input_dialog:onShowKeyboard()
end
function FileManagerShortcuts:editFolderShortcut(item)
local edit_folder_input
edit_folder_input = InputDialog:new {
title = _("Edit friendly name"),
input = item.friendly_name,
description = T(_("Rename title for selected folder:\n%1"), BD.dirpath(item.folder)),
buttons = {
{
{
text = _("Cancel"),
id = "close",
callback = function()
UIManager:close(edit_folder_input)
end,
},
{
text = _("Apply"),
is_enter_default = true,
callback = function()
self:renameFolderShortcut(item, edit_folder_input:getInputText())
UIManager:close(edit_folder_input)
end,
},
}
},
function FileManagerShortcuts:addShortcut()
local PathChooser = require("ui/widget/pathchooser")
local path_chooser = PathChooser:new{
select_directory = true,
select_file = false,
path = self.ui.file_chooser and self.ui.file_chooser.path or self.ui:getLastDirFile(),
onConfirm = function(path)
if self:hasFolderShortcut(path) then
UIManager:show(InfoMessage:new{
text = _("Shortcut already exists."),
})
else
self:editShortcut(path)
end
end,
}
UIManager:show(edit_folder_input)
edit_folder_input:onShowKeyboard()
UIManager:show(path_chooser)
end
function FileManagerShortcuts:renameFolderShortcut(item, new_name)
for _, element in ipairs(self.folder_shortcuts) do
if element.text == item.friendly_name and element.folder == item.folder then
element.text = new_name
end
end
self:updateItemTable()
function FileManagerShortcuts:genShowFolderShortcutsButton(pre_callback)
return {
text = self.title,
callback = function()
pre_callback()
self:onShowFolderShortcutsDialog()
end,
}
end
function FileManagerShortcuts:deleteFolderShortcut(item)
for i = #self.folder_shortcuts, 1, -1 do
local element = self.folder_shortcuts[i]
if element.text == item.friendly_name and element.folder == item.folder then
table.remove(self.folder_shortcuts, i)
end
function FileManagerShortcuts:genAddRemoveShortcutButton(folder, pre_callback, post_callback)
if self:hasFolderShortcut(folder) then
return {
text = _("Remove from folder shortcuts"),
callback = function()
pre_callback()
self:removeShortcut(folder)
post_callback()
end,
}
else
return {
text = _("Add to folder shortcuts"),
callback = function()
pre_callback()
self:editShortcut(folder, post_callback)
end,
}
end
self:updateItemTable()
end
function FileManagerShortcuts:onSetDimensions(dimen)
@ -208,7 +193,7 @@ end
function FileManagerShortcuts:MenuSetRotationModeHandler(rotation)
if rotation ~= nil and rotation ~= Screen:getRotationMode() then
UIManager:close(self._manager.fm_bookmark)
UIManager:close(self._manager.shortcuts_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
@ -222,26 +207,32 @@ function FileManagerShortcuts:MenuSetRotationModeHandler(rotation)
end
function FileManagerShortcuts:onShowFolderShortcutsDialog(select_callback)
self.fm_bookmark = Menu:new{
title = _("Folder shortcuts"),
show_parent = self.ui,
width = Screen:getWidth(),
height = Screen:getHeight(),
no_title = false,
parent = nil,
has_close_button = true,
is_popout = false,
self.shortcuts_menu = Menu:new{
title = self.title,
covers_fullscreen = true,
is_borderless = true,
curr_path = self.ui.file_chooser and self.ui.file_chooser.path or self.ui:getLastDirFile(),
is_popout = false,
select_callback = select_callback, -- called from PathChooser titlebar left button
title_bar_left_icon = not select_callback and "plus" or nil,
onLeftButtonTap = function() self:addShortcut() end,
onMenuChoice = self.onMenuChoice,
onMenuHold = not select_callback and self.onMenuHold or nil,
onSetRotationMode = self.MenuSetRotationModeHandler,
title_bar_left_icon = not select_callback and "plus" or nil,
onLeftButtonTap = function() self:addNewFolder() end,
_manager = self,
}
self:updateItemTable(select_callback)
UIManager:show(self.fm_bookmark)
self.shortcuts_menu.close_callback = function()
UIManager:close(self.shortcuts_menu)
if self.fm_updated then
if self.ui.file_chooser then
self.ui.file_chooser:refreshPath()
self.ui:updateTitleBarPath()
end
self.fm_updated = nil
end
self.shortcuts_menu = nil
end
self:updateItemTable()
UIManager:show(self.shortcuts_menu)
end
return FileManagerShortcuts

@ -2,10 +2,16 @@
This module contains miscellaneous helper functions for FileManager
]]
local BD = require("ui/bidi")
local Device = require("device")
local DocSettings = require("docsettings")
local util = require("ffi/util")
local Event = require("ui/event")
local UIManager = require("ui/uimanager")
local ffiutil = require("ffi/util")
local lfs = require("libs/libkoreader-lfs")
local util = require("util")
local _ = require("gettext")
local T = ffiutil.template
local filemanagerutil = {}
@ -17,7 +23,7 @@ function filemanagerutil.abbreviate(path)
if not path then return "" end
if G_reader_settings:nilOrTrue("shorten_home_dir") then
local home_dir = G_reader_settings:readSetting("home_dir") or filemanagerutil.getDefaultDir()
if path == home_dir then
if path == home_dir or path == home_dir .. "/" then
return _("Home")
end
local len = home_dir:len()
@ -29,28 +35,61 @@ function filemanagerutil.abbreviate(path)
return path
end
-- Purge doc settings in sidecar directory
function filemanagerutil.purgeSettings(file)
local file_abs_path = util.realpath(file)
if file_abs_path then
DocSettings:open(file_abs_path):purge()
function filemanagerutil.splitFileNameType(filepath)
local _, filename = util.splitFilePathName(filepath)
local filename_without_suffix, filetype = util.splitFileNameSuffix(filename)
filetype = filetype:lower()
if filetype == "zip" then
local filename_without_sub_suffix, sub_filetype = util.splitFileNameSuffix(filename_without_suffix)
sub_filetype = sub_filetype:lower()
local supported_sub_filetypes = { "fb2", "htm", "html", "log", "md", "rtf", "txt", }
if util.arrayContains(supported_sub_filetypes, sub_filetype) then
return filename_without_sub_suffix, sub_filetype .. ".zip"
end
end
return filename_without_suffix, filetype
end
function filemanagerutil.getRandomFile(dir, match_func)
if not dir:match("/$") then
dir = dir .. "/"
end
local files = {}
local ok, iter, dir_obj = pcall(lfs.dir, dir)
if ok then
for entry in iter, dir_obj do
local file = dir .. entry
if lfs.attributes(file, "mode") == "file" and match_func(file) then
table.insert(files, entry)
end
end
if #files > 0 then
math.randomseed(os.time())
return dir .. files[math.random(#files)]
end
end
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_sorted = 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,
}
local file_abs_path = util.realpath(file)
local file_abs_path = ffiutil.realpath(file)
if file_abs_path then
local doc_settings = DocSettings:open(file_abs_path)
for k in pairs(doc_settings.data) do
@ -59,8 +98,300 @@ function filemanagerutil.resetDocumentSettings(file)
end
end
doc_settings:makeTrue("docsettings_reset_done") -- for readertypeset block_rendering_mode
doc_settings:close()
doc_settings:flush()
end
end
-- Get a document status ("new", "reading", "complete", or "abandoned")
function filemanagerutil.getStatus(file)
if DocSettings:hasSidecarFile(file) then
local summary = DocSettings:open(file):readSetting("summary")
if summary and summary.status and summary.status ~= "" then
return summary.status
end
return "reading"
end
return "new"
end
-- Set a document status ("reading", "complete", or "abandoned")
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
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())
doc_settings:flush()
end
function filemanagerutil.statusToString(status)
local status_to_text = {
new = _("Unread"),
reading = _("Reading"),
abandoned = _("On hold"),
complete = _("Finished"),
}
return status_to_text[status]
end
-- Generate all book status file dialog buttons in a row
function filemanagerutil.genStatusButtonsRow(doc_settings_or_file, caller_callback)
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
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 ""),
enabled = status ~= to_status,
callback = function()
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,
}
end
return {
genStatusButton("reading"),
genStatusButton("abandoned"),
genStatusButton("complete"),
}
end
-- Generate "Reset" file dialog button
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"),
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")
local ConfirmBox = require("ui/widget/confirmbox")
local check_button_settings, check_button_cover, check_button_metadata
local confirmbox = ConfirmBox:new{
text = T(_("Reset this document?") .. "\n\n%1\n\n" ..
_("Information will be permanently lost."),
BD.filepath(file)),
ok_text = _("Reset"),
ok_callback = function()
local data_to_purge = {
doc_settings = check_button_settings.checked,
custom_cover_file = check_button_cover.checked and custom_cover_file,
custom_metadata_file = check_button_metadata.checked and custom_metadata_file,
}
(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()
end,
}
check_button_settings = CheckButton:new{
text = _("document settings, progress, bookmarks, highlights, notes"),
checked = has_sidecar_file,
enabled = has_sidecar_file,
parent = confirmbox,
}
confirmbox:addWidget(check_button_settings)
check_button_cover = CheckButton:new{
text = _("custom cover image"),
checked = has_custom_cover_file,
enabled = has_custom_cover_file,
parent = confirmbox,
}
confirmbox:addWidget(check_button_cover)
check_button_metadata = CheckButton:new{
text = _("custom book metadata"),
checked = has_custom_metadata_file,
enabled = has_custom_metadata_file,
parent = confirmbox,
}
confirmbox:addWidget(check_button_metadata)
UIManager:show(confirmbox)
end,
}
end
function filemanagerutil.genShowFolderButton(file, caller_callback, button_disabled)
return {
text = _("Show folder"),
enabled = not button_disabled,
callback = function()
caller_callback()
local ui = require("apps/filemanager/filemanager").instance
if ui then
local pathname = util.splitFilePathName(file)
ui.file_chooser:changeToPath(pathname, file)
else
ui = require("apps/reader/readerui").instance
ui:onClose()
ui:showFileManager(file)
end
end,
}
end
function filemanagerutil.genBookInformationButton(file, book_props, caller_callback, button_disabled)
return {
text = _("Book information"),
enabled = not button_disabled,
callback = function()
caller_callback()
local FileManagerBookInfo = require("apps/filemanager/filemanagerbookinfo")
FileManagerBookInfo:show(file, book_props and FileManagerBookInfo.extendProps(book_props))
end,
}
end
function filemanagerutil.genBookCoverButton(file, book_props, caller_callback, button_disabled)
local has_cover = book_props and book_props.has_cover
return {
text = _("Book cover"),
enabled = (not button_disabled and (not book_props or has_cover)) and true or false,
callback = function()
caller_callback()
local FileManagerBookInfo = require("apps/filemanager/filemanagerbookinfo")
FileManagerBookInfo:onShowBookCover(file)
end,
}
end
function filemanagerutil.genBookDescriptionButton(file, book_props, caller_callback, button_disabled)
local description = book_props and book_props.description
return {
text = _("Book description"),
-- 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()
local FileManagerBookInfo = require("apps/filemanager/filemanagerbookinfo")
FileManagerBookInfo:onShowBookDescription(description, file)
end,
}
end
-- Generate "Execute script" file dialog button
function filemanagerutil.genExecuteScriptButton(file, caller_callback)
local InfoMessage = require("ui/widget/infomessage")
return {
-- @translators This is the script's programming language (e.g., shell or python)
text = T(_("Execute %1 script"), util.getScriptType(file)),
callback = function()
caller_callback()
local script_is_running_msg = InfoMessage:new{
-- @translators %1 is the script's programming language (e.g., shell or python), %2 is the filename
text = T(_("Running %1 script %2…"), util.getScriptType(file), BD.filename(ffiutil.basename(file))),
}
UIManager:show(script_is_running_msg)
UIManager:scheduleIn(0.5, function()
local rv
if Device:isAndroid() then
Device:setIgnoreInput(true)
rv = os.execute("sh " .. ffiutil.realpath(file)) -- run by sh, because sdcard has no execute permissions
Device:setIgnoreInput(false)
else
rv = os.execute(ffiutil.realpath(file))
end
UIManager:close(script_is_running_msg)
if rv == 0 then
UIManager:show(InfoMessage:new{
text = _("The script exited successfully."),
})
else
--- @note: Lua 5.1 returns the raw return value from the os's system call. Counteract this madness.
UIManager:show(InfoMessage:new{
text = T(_("The script returned a non-zero status code: %1!"), bit.rshift(rv, 8)),
icon = "notice-warning",
})
end
end)
end,
}
end
function filemanagerutil.showChooseDialog(title_header, caller_callback, current_path, default_path, file_filter)
local is_file = file_filter and true or false
local path = current_path or default_path
local dialog
local buttons = {
{
{
text = is_file and _("Choose file") or _("Choose folder"),
callback = function()
UIManager:close(dialog)
if path then
if is_file then
path = path:match("(.*/)")
end
if lfs.attributes(path, "mode") ~= "directory" then
path = G_reader_settings:readSetting("home_dir") or filemanagerutil.getDefaultDir()
end
end
local PathChooser = require("ui/widget/pathchooser")
local path_chooser = PathChooser:new{
select_directory = not is_file,
select_file = is_file,
show_files = is_file,
file_filter = file_filter,
path = path,
onConfirm = function(new_path)
caller_callback(new_path)
end,
}
UIManager:show(path_chooser)
end,
},
}
}
if default_path then
table.insert(buttons, {
{
text = _("Use default"),
enabled = path ~= default_path,
callback = function()
UIManager:close(dialog)
caller_callback(default_path)
end,
},
})
end
local title_value = path and (is_file and BD.filepath(path) or BD.dirpath(path))
or _("not set")
local ButtonDialog = require("ui/widget/buttondialog")
dialog = ButtonDialog:new{
title = title_header .. "\n\n" .. title_value .. "\n",
buttons = buttons,
}
UIManager:show(dialog)
end
return filemanagerutil

@ -2,10 +2,8 @@
local ReaderActivityIndicator = {}
function ReaderActivityIndicator:isStub() return true end
function ReaderActivityIndicator:init() end
function ReaderActivityIndicator:onStartActivityIndicator() end
function ReaderActivityIndicator:onStopActivityIndicator() end
function ReaderActivityIndicator:coda() end
-- Now, if we're on Kindle, and we haven't actually murdered Pillow, see what we can do...
local Device = require("device")
@ -28,10 +26,10 @@ end
-- Okay, if we're here, it's basically because we're running on a Kindle on FW 5.x under KPV
local EventListener = require("ui/widget/eventlistener")
local util = require("ffi/util")
-- lipc
ReaderActivityIndicator = EventListener:new{}
ReaderActivityIndicator = EventListener:extend{
lipc_handle = nil,
}
function ReaderActivityIndicator:isStub() return false end
@ -69,15 +67,15 @@ function ReaderActivityIndicator:onStopActivityIndicator()
"clientId":"com.github.koreader.activityindicator", \
"priority":true}}')
self.indicator_started = false
util.usleep(1000000)
end
return true
end
function ReaderActivityIndicator:coda()
function ReaderActivityIndicator:onCloseWidget()
if self.lipc_handle then
self.lipc_handle:close()
end
self.lipc_handle = nil
end
return ReaderActivityIndicator

@ -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

@ -20,9 +20,7 @@ local ReaderBack = EventListener:extend{
}
function ReaderBack:init()
if Device:hasKeys() then
self.ui.key_events.Back = { {Device.input.group.Back}, doc = "Reader back" }
end
self:registerKeyEvents()
-- Regular function wrapping our method, to avoid re-creating
-- an anonymous function at each page turn
self._addPreviousLocationToStackCallback = function()
@ -30,6 +28,14 @@ function ReaderBack:init()
end
end
function ReaderBack:registerKeyEvents()
if Device:hasKeys() then
self.ui.key_events.Back = { { Device.input.group.Back } }
end
end
ReaderBack.onPhysicalKeyboardConnected = ReaderBack.registerKeyEvents
function ReaderBack:_getCurrentLocation()
if self.ui.document.info.has_pages then
return self.ui.paging:getBookLocation()

File diff suppressed because it is too large Load Diff

@ -19,19 +19,28 @@ function ReaderConfig:init()
end
self.configurable:loadDefaults(self.options)
if Device:hasKeys() then
self.key_events = {
ShowConfigMenu = { {{"Press","AA"}}, doc = "show config dialog" },
}
end
self:registerKeyEvents()
self:initGesListener()
if G_reader_settings:has("activate_menu") then
self.activation_menu = G_reader_settings:readSetting("activate_menu")
else
self.activation_menu = "swipe_tap"
end
-- delegate gesture listener to ReaderUI, NOP our own
self.ges_events = nil
end
function ReaderConfig:onGesture() end
function ReaderConfig:registerKeyEvents()
if Device:hasKeys() then
self.key_events.ShowConfigMenu = { { { "Press", "AA" } } }
end
end
ReaderConfig.onPhysicalKeyboardConnected = ReaderConfig.registerKeyEvents
function ReaderConfig:initGesListener()
if not Device:isTouchDevice() then return end
@ -123,6 +132,7 @@ function ReaderConfig:onShowConfigMenu()
configurable = self.configurable,
config_options = self.options,
is_always_active = true,
covers_footer = true,
close_callback = function() self:onCloseCallback() end,
}
self.ui:handleEvent(Event:new("DisableHinting"))
@ -148,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

@ -13,14 +13,11 @@ local ReaderCoptListener = EventListener:extend{}
local CRE_HEADER_DEFAULT_SIZE = 20
function ReaderCoptListener:onReadSettings(config)
local view_mode = config:readSetting("copt_view_mode")
or G_reader_settings:readSetting("copt_view_mode")
or 0 -- default to "page" mode
local view_mode_name = view_mode == 0 and "page" or "scroll"
local view_mode_name = self.document.configurable.view_mode == 0 and "page" or "scroll"
-- Let crengine know of the view mode before rendering, as it can
-- cause a rendering change (2-pages would become 1-page in
-- scroll mode).
self.ui.document:setViewMode(view_mode_name)
self.document:setViewMode(view_mode_name)
-- ReaderView is the holder of the view_mode state
self.view.view_mode = view_mode_name
@ -35,20 +32,24 @@ function ReaderCoptListener:onReadSettings(config)
self.battery_percent = G_reader_settings:readSetting("cre_header_battery_percent", 0)
self.chapter_marks = G_reader_settings:readSetting("cre_header_chapter_marks", 1)
self.ui.document._document:setIntProperty("window.status.title", self.title)
self.ui.document._document:setIntProperty("window.status.clock", self.clock)
self.ui.document._document:setIntProperty("window.status.pos.page.number", self.page_number)
self.ui.document._document:setIntProperty("window.status.pos.page.count", self.page_count)
self.ui.document._document:setIntProperty("crengine.page.header.chapter.marks", self.chapter_marks)
self.ui.document._document:setIntProperty("window.status.battery", self.battery)
self.ui.document._document:setIntProperty("window.status.battery.percent", self.battery_percent)
self.ui.document._document:setIntProperty("window.status.pos.percent", self.reading_percent)
self.document._document:setIntProperty("window.status.title", self.title)
self.document._document:setIntProperty("window.status.clock", self.clock)
self.document._document:setIntProperty("window.status.pos.page.number", self.page_number)
self.document._document:setIntProperty("window.status.pos.page.count", self.page_count)
self.document._document:setIntProperty("crengine.page.header.chapter.marks", self.chapter_marks)
self.document._document:setIntProperty("window.status.battery", self.battery)
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)
local status_line = config:readSetting("copt_status_line") or G_reader_settings:readSetting("copt_status_line", 1)
self.ui:handleEvent(Event:new("SetStatusLine", status_line))
self.ui:handleEvent(Event:new("SetStatusLine", self.document.configurable.status_line))
self.old_battery_level = self.ui.rolling:updateBatteryState()
@ -68,38 +69,177 @@ 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)
pageno = pageno or self.ui.view.footer.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 == 1 or self.page_count == 1 then
page_info = page_info .. page_pre
if self.page_number == 1 then
page_info = page_info .. page_number
if self.page_count == 1 then
page_info = page_info .. page_sep
end
end
if self.page_count == 1 then
page_info = page_info .. page_count
end
page_info = page_info .. page_post
if self.reading_percent == 1 then
page_info = page_info .. " " -- (double space as done by crengine's own drawing)
end
end
if self.reading_percent == 1 then
page_info = page_info .. percentage_pre .. percentage_fmt:format(percentage*100) .. percentage_post
end
if self.battery == 1 and self.battery_percent == 1 then -- append battery percentage
local batt_pre = "["
local batt_post = "]"
local batt_val = nil
if Device:hasBattery() then
local powerd = Device:getPowerDevice()
local batt_lvl = powerd:getCapacity()
if Device:hasAuxBattery() and powerd:isAuxBatteryConnected() then
local aux_batt_lvl = powerd:getAuxCapacity()
if powerd:isAuxCharging() then
batt_pre = "[\u{21AF}"
end
-- Sum both batteries for the actual text
batt_lvl = batt_lvl + aux_batt_lvl
else
if powerd:isCharging() then
batt_pre = "[\u{21AF}"
end
end
batt_val = string.format("%2d%%", batt_lvl)
end
if batt_val then
local battery_info = " " .. batt_pre .. batt_val .. batt_post
-- ^--- (double space as done by crengine's own drawing)
page_info = page_info .. battery_info
end
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
if (option_name == "font_size" or option_name == "line_spacing") and option_value < 5 then return end
self.document.configurable[option_name] = option_value
self.ui:handleEvent(Event:new("StartActivityIndicator"))
return true
end
function ReaderCoptListener:onSetFontSize(font_size)
self.document.configurable.font_size = font_size
end
function ReaderCoptListener:onCharging()
self:headerRefresh()
end
ReaderCoptListener.onNotCharging = ReaderCoptListener.onCharging
function ReaderCoptListener:onNotCharging()
self:headerRefresh()
function ReaderCoptListener:onTimeFormatChanged()
self.document._document:setIntProperty("window.status.clock.12hours", G_reader_settings:isTrue("twelve_hour_clock") and 1 or 0)
end
function ReaderCoptListener:onTimeFormatChanged()
self.ui.document._document:setIntProperty("window.status.clock.12hours", G_reader_settings:isTrue("twelve_hour_clock") and 1 or 0)
function ReaderCoptListener:shouldHeaderBeRepainted()
local top_wg = UIManager:getTopmostVisibleWidget() or {}
if top_wg.name == "ReaderUI" then
-- We're on display, go ahead
return true
elseif top_wg.covers_fullscreen or top_wg.covers_header then
-- We're hidden behind something that definitely covers us, don't do anything
return false
else
-- There's something on top of us, but we might still be visible, request a ReaderUI repaint,
-- UIManager will sort it out.
return true
end
end
function ReaderCoptListener:updateHeader()
-- Have crengine display accurate time and battery on its next drawing
self.ui.document:resetBufferCache() -- be sure next repaint is a redrawing
self.document:resetBufferCache() -- be sure next repaint is a redrawing
-- Force a refresh if we're not hidden behind another widget
if UIManager:getTopWidget() == "ReaderUI" then
if self:shouldHeaderBeRepainted() then
UIManager:setDirty(self.view.dialog, "ui",
Geom:new{
x = 0, y = 0,
w = Device.screen:getWidth(),
h = self.ui.document:getHeaderHeight(),
h = self.document:getHeaderHeight(),
}
)
end
@ -147,10 +287,6 @@ function ReaderCoptListener:onResume()
self:headerRefresh()
end
function ReaderCoptListener:onLeaveStandby()
self:headerRefresh()
end
function ReaderCoptListener:onOutOfScreenSaver()
if not self._delayed_screensaver then
return
@ -163,11 +299,16 @@ end
-- Unschedule on these events
ReaderCoptListener.onCloseDocument = ReaderCoptListener.unscheduleHeaderRefresh
ReaderCoptListener.onSuspend = ReaderCoptListener.unscheduleHeaderRefresh
ReaderCoptListener.onEnterStandby = ReaderCoptListener.unscheduleHeaderRefresh
function ReaderCoptListener:setAndSave(setting, property, value)
self.ui.document._document:setIntProperty(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
if self.page_info_override then
self:updatePageInfoOverride()
else
self.document:setPageInfoOverride("") -- Don't forget to restore CRE default behaviour.
end
-- Have crengine redraw it (even if hidden by the menu at this time)
self.ui.rolling:updateBatteryState()
self:updateHeader()
@ -188,7 +329,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{
@ -271,15 +412,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
@ -57,8 +55,6 @@ function ReaderCropping:onPageCrop(mode)
-- prepare bottom buttons so we know the size available for the page above it
local button_table = ButtonTable:new{
width = Screen:getWidth(),
button_font_face = "cfont",
button_font_size = 20,
buttons = {{
{
text = _("Cancel"),

@ -70,7 +70,8 @@ function ReaderDeviceStatus:init()
UIManager:close(self.memory_confirm_box)
end
if Device:canRestart() then
if UIManager:getTopWidget() == "ReaderUI"
local top_wg = UIManager:getTopmostVisibleWidget() or {}
if top_wg.name == "ReaderUI"
and G_reader_settings:isTrue("device_status_memory_auto_restart") then
UIManager:show(InfoMessage:new{
text = _("High memory usage!\n\nKOReader is restarting…"),

@ -102,6 +102,7 @@ function ReaderDictionary:init()
self.disable_lookup_history = G_reader_settings:isTrue("disable_lookup_history")
self.dicts_order = G_reader_settings:readSetting("dicts_order", {})
self.dicts_disabled = G_reader_settings:readSetting("dicts_disabled", {})
self.disable_fuzzy_search_fm = G_reader_settings:isTrue("disable_fuzzy_search")
if self.ui then
self.ui.menu:registerToMainMenu(self)
@ -136,6 +137,7 @@ function ReaderDictionary:init()
f:close()
local dictname = content:match("\nbookname=(.-)\r?\n")
local is_html = content:find("sametypesequence=h", 1, true) ~= nil
local lang_in, lang_out = content:match("lang=(%a+)-?(%a*)\r?\n?")
-- sdcv won't use dict that don't have a bookname=
if dictname then
table.insert(available_ifos, {
@ -144,6 +146,7 @@ function ReaderDictionary:init()
is_html = is_html,
css = readDictionaryCss(ifo_file:gsub("%.ifo$", ".css")),
fix_html_func = getDictionaryFixHtmlFunc(ifo_file:gsub("%.ifo$", ".lua")),
lang = lang_in and { lang_in = lang_in, lang_out = lang_out },
})
end
end
@ -204,6 +207,9 @@ function ReaderDictionary:updateSdcvDictNamesOptions()
end
function ReaderDictionary:addToMainMenu(menu_items)
menu_items.search_settings = { -- submenu with Dict, Wiki, Translation settings
text = _("Settings"),
}
menu_items.dictionary_lookup = {
text = _("Dictionary lookup"),
callback = function()
@ -265,18 +271,32 @@ function ReaderDictionary:addToMainMenu(menu_items)
},
{
text = _("Download dictionaries"),
sub_item_table = self:_genDownloadDictionariesMenu()
sub_item_table_func = function() return self:_genDownloadDictionariesMenu() end,
},
{
text = _("Enable fuzzy search"),
text_func = function()
local text = _("Enable fuzzy search")
if G_reader_settings:nilOrFalse("disable_fuzzy_search") then
text = text .. ""
end
return text
end,
checked_func = function()
return not self.disable_fuzzy_search == true
if self.ui.doc_settings then
return not self.disable_fuzzy_search
end
return not self.disable_fuzzy_search_fm
end,
callback = function()
self.disable_fuzzy_search = not self.disable_fuzzy_search
if self.ui.doc_settings then
self.disable_fuzzy_search = not self.disable_fuzzy_search
self.ui.doc_settings:saveSetting("disable_fuzzy_search", self.disable_fuzzy_search)
else
self.disable_fuzzy_search_fm = not self.disable_fuzzy_search_fm
end
end,
hold_callback = function()
self:toggleFuzzyDefault()
hold_callback = function(touchmenu_instance)
self:toggleFuzzyDefault(touchmenu_instance)
end,
separator = true,
},
@ -404,17 +424,23 @@ 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)
logger.dbg("dict stripped word:", word)
self.highlight = highlight
local disable_fuzzy_search
if self.ui.doc_settings then
disable_fuzzy_search = self.disable_fuzzy_search
else
disable_fuzzy_search = self.disable_fuzzy_search_fm
end
-- Wrapped through Trapper, as we may be using Trapper:dismissablePopen() in it
Trapper:wrap(function()
self:stardictLookup(word, self.enabled_dict_names, not self.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
@ -477,10 +503,19 @@ end
function ReaderDictionary:_genDownloadDictionariesMenu()
local downloadable_dicts = require("ui/data/dictionaries")
local IsoLanguage = require("ui/data/isolanguage")
local languages = {}
for i = 1, #downloadable_dicts do
local dict = downloadable_dicts[i]
if not dict.ifo_lang then
-- this only needs to happen the first time this function is called
local ifo_in = IsoLanguage:getBCPLanguageTag(dict.lang_in)
local ifo_out = IsoLanguage:getBCPLanguageTag(dict.lang_out)
dict.ifo_lang = ("%s-%s"):format(ifo_in, ifo_out)
dict.lang_in = IsoLanguage:getLocalizedLanguage(dict.lang_in)
dict.lang_out = IsoLanguage:getLocalizedLanguage(dict.lang_out)
end
local dict_lang_in = dict.lang_in
local dict_lang_out = dict.lang_out
if not languages[dict_lang_in] then
@ -593,6 +628,9 @@ local function tidyMarkup(results)
local format_escape = "&[29Ib%+]{(.-)}"
for _, result in ipairs(results) do
local ifo = getAvailableIfoByName(result.dict)
if ifo and ifo.lang then
result.ifo_lang = ifo.lang
end
if ifo and ifo.is_html then
result.is_html = ifo.is_html
result.css = ifo.css
@ -639,14 +677,14 @@ function ReaderDictionary:cleanSelection(text, is_sane)
-- (example: pdf selection "quautrefois," will be cleaned to "autrefois")
--
-- Replace no-break space with regular space
text = text:gsub("\xC2\xA0", ' ') -- U+00A0 no-break space
text = text:gsub("\u{00A0}", ' ')
-- Trim any space at start or end
text = text:gsub("^%s+", "")
text = text:gsub("%s+$", "")
if not is_sane then
-- Replace extended quote (included in the general puncturation range)
-- with plain ascii quote (for french words like "aujourdhui")
text = text:gsub("\xE2\x80\x99", "'") -- U+2019 (right single quotation mark)
text = text:gsub("\u{2019}", "'") -- Right single quotation mark
-- Strip punctuation characters around selection
text = util.stripPunctuation(text)
-- Strip some common english grammatical construct
@ -894,18 +932,12 @@ 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
local book_title = self.ui.doc_settings and self.ui.doc_settings:readSetting("doc_props").title or _("Dictionary lookup")
if book_title == "" then -- no or empty metadata title
if self.ui.document and self.ui.document.file then
local directory, filename = util.splitFilePathName(self.ui.document.file) -- luacheck: no unused
book_title = util.splitFileNameSuffix(filename)
end
end
local book_title = self.ui.doc_props and self.ui.doc_props.display_title or _("Dictionary lookup")
-- Event for plugin to catch lookup with book title
self.ui:handleEvent(Event:new("WordLookedUp", word, book_title))
@ -960,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,
@ -1082,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{
@ -1118,9 +1158,15 @@ function ReaderDictionary:downloadDictionary(dict, download_location, continue)
return false
end
local ok, error = Device:unpackArchive(download_location, self.data_dir)
-- stable target directory is needed so we can look through the folder later
local dict_path = self.data_dir .. "/" .. dict.name
util.makePath(dict_path)
local ok, error = Device:unpackArchive(download_location, dict_path, true)
if ok then
if dict.ifo_lang then
self:extendIfoWithLanguage(dict_path, dict.ifo_lang)
end
available_ifos = false
self:init()
UIManager:show(InfoMessage:new{
@ -1135,6 +1181,24 @@ function ReaderDictionary:downloadDictionary(dict, download_location, continue)
end
end
function ReaderDictionary:extendIfoWithLanguage(dictionary_location, ifo_lang)
local function cb(path, filename)
if util.getFileNameSuffix(filename) == "ifo" then
local fmt_string = "lang=%s"
local f = io.open(path, "a+")
if f then
local ifo = f:read("a*")
if ifo[#ifo] ~= "\n" then
fmt_string = "\n" .. fmt_string
end
f:write(fmt_string:format(ifo_lang))
f:close()
end
end
end
util.findFiles(dictionary_location, cb)
end
function ReaderDictionary:onReadSettings(config)
self.preferred_dictionaries = config:readSetting("preferred_dictionaries") or {}
if #self.preferred_dictionaries == 0 then
@ -1158,7 +1222,6 @@ end
function ReaderDictionary:onSaveSettings()
if self.ui.doc_settings then
self.ui.doc_settings:saveSetting("preferred_dictionaries", self.preferred_dictionaries)
self.ui.doc_settings:saveSetting("disable_fuzzy_search", self.disable_fuzzy_search)
end
end
@ -1187,7 +1250,7 @@ function ReaderDictionary:onTogglePreferredDict(dict)
return true
end
function ReaderDictionary:toggleFuzzyDefault()
function ReaderDictionary:toggleFuzzyDefault(touchmenu_instance)
local disable_fuzzy_search = G_reader_settings:isTrue("disable_fuzzy_search")
UIManager:show(MultiConfirmBox:new{
text = T(
@ -1210,12 +1273,14 @@ The current default (★) is enabled.]])
end,
choice1_callback = function()
G_reader_settings:makeTrue("disable_fuzzy_search")
touchmenu_instance:updateItems()
end,
choice2_text_func = function()
return disable_fuzzy_search and _("Enable") or _("Enable (★)")
end,
choice2_callback = function()
G_reader_settings:makeFalse("disable_fuzzy_search")
touchmenu_instance:updateItems()
end,
})
end

@ -54,24 +54,17 @@ function ReaderDogear:setupDogear(new_dogear_size)
end
function ReaderDogear:onReadSettings(config)
if not self.ui.document.info.has_pages then
if self.ui.rolling then
-- Adjust to CreDocument margins (as done in ReaderTypeset)
local h_margins = config:readSetting("copt_h_page_margins")
or G_reader_settings:readSetting("copt_h_page_margins")
or G_defaults:readSetting("DCREREADER_CONFIG_H_MARGIN_SIZES_MEDIUM")
local t_margin = config:readSetting("copt_t_page_margin")
or G_reader_settings:readSetting("copt_t_page_margin")
or G_defaults:readSetting("DCREREADER_CONFIG_T_MARGIN_SIZES_LARGE")
local b_margin = config:readSetting("copt_b_page_margin")
or G_reader_settings:readSetting("copt_b_page_margin")
or G_defaults:readSetting("DCREREADER_CONFIG_B_MARGIN_SIZES_LARGE")
local margins = { h_margins[1], t_margin, h_margins[2], b_margin }
local configurable = self.ui.document.configurable
local margins = { configurable.h_page_margins[1], configurable.t_page_margin,
configurable.h_page_margins[2], configurable.b_page_margin }
self:onSetPageMargins(margins)
end
end
function ReaderDogear:onSetPageMargins(margins)
if self.ui.document.info.has_pages then
if not self.ui.rolling then
-- we may get called by readerfooter (when hiding the footer)
-- on pdf documents and get margins=nil
return
@ -86,7 +79,7 @@ function ReaderDogear:onSetPageMargins(margins)
end
function ReaderDogear:updateDogearOffset()
if self.ui.document.info.has_pages then
if not self.ui.rolling then
return
end
self.dogear_y_offset = 0
@ -101,10 +94,14 @@ function ReaderDogear:updateDogearOffset()
end
end
function ReaderDogear:onUpdatePos()
function ReaderDogear:onReaderReady()
self:updateDogearOffset()
end
function ReaderDogear:onDocumentRerendered()
-- Catching the top status bar toggling with :onSetStatusLine()
-- would be too early. But "UpdatePos" is sent after it has
-- been applied
-- would be too early. But "DocumentRerendered" is sent after
-- it has been applied
self:updateDogearOffset()
end

@ -5,19 +5,43 @@ local WidgetContainer = require("ui/widget/container/widgetcontainer")
local Screen = require("device").screen
local ReaderFlipping = WidgetContainer:extend{
orig_reflow_mode = 0,
-- Icons to show during crengine partial rerendering automation
rolling_rendering_state_icons = {
PARTIALLY_RERENDERED = "cre.render.partial",
FULL_RENDERING_IN_BACKGROUND = "cre.render.working",
FULL_RENDERING_READY = "cre.render.ready",
RELOADING_DOCUMENT = "cre.render.reload",
},
}
function ReaderFlipping:init()
local icon_size = Screen:scaleBySize(32)
local widget = IconWidget:new{
self.flipping_widget = IconWidget:new{
icon = "book.opened",
width = icon_size,
height = icon_size,
}
self.bookmark_flipping_widget = IconWidget:new{
icon = "bookmark",
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",
width = icon_size,
height = icon_size,
alpha = true,
}
self[1] = LeftContainer:new{
dimen = Geom:new{w = Screen:getWidth(), h = widget:getSize().h},
widget,
dimen = Geom:new{w = Screen:getWidth(), h = self.flipping_widget:getSize().h},
self.flipping_widget,
}
self:resetLayout()
end
@ -30,4 +54,63 @@ function ReaderFlipping:resetLayout()
self[1].dimen.w = new_screen_width
end
function ReaderFlipping:getRollingRenderingStateIconWidget()
if not self.rolling_rendering_state_widgets then
self.rolling_rendering_state_widgets = {}
end
local widget = self.rolling_rendering_state_widgets[self.ui.rolling.rendering_state]
if widget == nil then -- not met yet
local icon_size = Screen:scaleBySize(32)
for k, v in pairs(self.ui.rolling.RENDERING_STATE) do -- known states
if v == self.ui.rolling.rendering_state then -- current state
local icon = self.rolling_rendering_state_icons[k] -- our icon (or none) for this state
if icon then
self.rolling_rendering_state_widgets[v] = IconWidget:new{
icon = icon,
width = icon_size,
height = icon_size,
alpha = not self.ui.rolling.cre_top_bar_enabled,
-- if top status bar enabled, have them opaque, as they
-- will be displayed over the bar
-- otherwise, keep their alpha so some bits of text is
-- visible if displayed over the text when small margins
}
else
self.rolling_rendering_state_widgets[v] = false
end
break
end
end
widget = self.rolling_rendering_state_widgets[self.ui.rolling.rendering_state]
end
return widget or nil -- return nil if cached widget is false
end
function ReaderFlipping:onSetStatusLine()
-- Reset these widgets: we want new ones with proper alpha/opaque
self.rolling_rendering_state_widgets = nil
end
function ReaderFlipping:paintTo(bb, x, y)
local widget
if self.ui.paging and self.view.flipping_visible then
-- pdf page flipping or bookmark browsing mode
widget = self.ui.paging.bookmark_flipping_mode and self.bookmark_flipping_widget or self.flipping_widget
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()
end
if widget then
if self[1][1] ~= widget then
self[1][1] = widget
end
WidgetContainer.paintTo(self, bb, x, y)
end
end
return ReaderFlipping

@ -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,43 +7,38 @@ 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
local UIManager = require("ui/uimanager")
local cre -- Delayed loading
local T = require("ffi/util").template
local logger = require("logger")
local util = require("util")
local _ = require("gettext")
local T = require("ffi/util").template
local C_ = _.pgettext
local optionsutil = require("ui/data/optionsutil")
local ReaderFont = InputContainer:extend{
font_face = nil,
font_size = nil,
line_space_percent = nil,
font_menu_title = _("Font"),
face_table = nil,
-- default gamma from crengine's lvfntman.cpp
gamma_index = nil,
steps = {0,1,1,1,1,1,2,2,2,3,3,3,4,4,5},
}
-- Keep a list of the new fonts seen at launch
local newly_added_fonts = nil -- not yet filled
function ReaderFont:init()
if Device:hasKeyboard() then
-- add shortcut for keyboard
self.key_events = {
ShowFontMenu = { {"F"}, doc = "show font menu" },
IncreaseSize = {
{ "Shift", Input.group.PgFwd },
doc = "increase font size",
event = "ChangeSize", args = 0.5 },
DecreaseSize = {
{ "Shift", Input.group.PgBack },
doc = "decrease font size",
event = "ChangeSize", args = -0.5 },
}
end
self:registerKeyEvents()
self:setupFaceMenuTable()
self.ui.menu:registerToMainMenu(self)
-- NOP our own gesture handling
self.ges_events = nil
end
function ReaderFont:setupFaceMenuTable()
logger.dbg("building font face menu table")
-- Build face_table for menu
self.face_table = {}
-- Font settings
@ -75,6 +69,7 @@ function ReaderFont:init()
-- Font list
cre = require("document/credocument"):engineInit()
local face_list = cre.getFontFaces()
face_list = self:sortFaceList(face_list)
for k, v in ipairs(face_list) do
local font_filename, font_faceindex, is_monospace = cre.getFontFaceFilenameAndFaceIndex(v)
table.insert(self.face_table, {
@ -99,6 +94,9 @@ function ReaderFont:init()
if v == fallback_font then
text = text .. " <20>"
end
if newly_added_fonts[v] then
text = text .. " \u{EA93}" -- "NEW" in a black square, from nerdfont
end
return text
end,
font_func = function(size)
@ -110,6 +108,10 @@ function ReaderFont:init()
end,
callback = function()
self:onSetFont(v)
-- We add it to the recently selected list only for tap on the
-- menu item (and not when :onSetFont() would be triggered by
-- a gesture/profile), which may be convenient for some users.
self:addToRecentlySelectedList(v)
end,
hold_callback = function(touchmenu_instance)
self:makeDefault(v, is_monospace, touchmenu_instance)
@ -120,12 +122,42 @@ function ReaderFont:init()
menu_item_id = v,
})
end
self.face_table.refresh_func = function()
self:setupFaceMenuTable()
-- This might be used by TouchMenu to refresh its font list menu,
-- so return the newly created menu table.
return self.face_table
end
self.face_table.open_on_menu_item_id_func = function()
return self.font_face
end
self.ui.menu:registerToMainMenu(self)
-- Have TouchMenu show half of the usual nb of items, so we
-- have more room to see how the text looks with that font
self.face_table.max_per_page = 5
end
function ReaderFont:onGesture() end
function ReaderFont:registerKeyEvents()
if Device:hasKeyboard() then
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
}
self.key_events.DecreaseSize = {
{ "Shift", Input.group.PgBack },
event = "ChangeSize",
args = -0.5
}
end
end
end
ReaderFont.onPhysicalKeyboardConnected = ReaderFont.registerKeyEvents
function ReaderFont:onSetDimensions(dimen)
self.dimen = dimen
end
@ -136,56 +168,18 @@ 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)
self.font_size = config:readSetting("font_size")
or G_reader_settings:readSetting("copt_font_size")
or G_defaults:readSetting("DCREREADER_CONFIG_DEFAULT_FONT_SIZE")
or 22
self.ui.document:setFontSize(Screen:scaleBySize(self.font_size))
self.font_base_weight = config:readSetting("font_base_weight")
or G_reader_settings:readSetting("copt_font_base_weight")
or 0
self.ui.document:setFontBaseWeight(self.font_base_weight)
self.font_hinting = config:readSetting("font_hinting")
or G_reader_settings:readSetting("copt_font_hinting")
or 2 -- auto (default in cre.cpp)
self.ui.document:setFontHinting(self.font_hinting)
self.font_kerning = config:readSetting("font_kerning")
or G_reader_settings:readSetting("copt_font_kerning")
or 3 -- harfbuzz (slower, but needed for proper arabic)
self.ui.document:setFontKerning(self.font_kerning)
self.word_spacing = config:readSetting("word_spacing")
or G_reader_settings:readSetting("copt_word_spacing")
or {95, 75}
self.ui.document:setWordSpacing(self.word_spacing)
self.word_expansion = config:readSetting("word_expansion")
or G_reader_settings:readSetting("copt_word_expansion")
or 0
self.ui.document:setWordExpansion(self.word_expansion)
self.cjk_width_scaling = config:readSetting("cjk_width_scaling")
or G_reader_settings:readSetting("copt_cjk_width_scaling")
or 100
self.ui.document:setCJKWidthScaling(self.cjk_width_scaling)
self.line_space_percent = config:readSetting("line_space_percent")
or G_reader_settings:readSetting("copt_line_spacing")
or G_defaults:readSetting("DCREREADER_CONFIG_LINE_SPACE_PERCENT_MEDIUM")
self.ui.document:setInterlineSpacePercent(self.line_space_percent)
self.gamma_index = config:readSetting("gamma_index")
or G_reader_settings:readSetting("copt_font_gamma")
or 15 -- gamma = 1.0
self.ui.document:setGammaIndex(self.gamma_index)
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)
self.ui.document:setFontHinting(self.configurable.font_hinting)
self.ui.document:setFontKerning(self.configurable.font_kerning)
self.ui.document:setWordSpacing(self.configurable.word_spacing)
self.ui.document:setWordExpansion(self.configurable.word_expansion)
self.ui.document:setCJKWidthScaling(self.configurable.cjk_width_scaling)
self.ui.document:setInterlineSpacePercent(self.configurable.line_spacing)
self.ui.document:setGammaIndex(self.configurable.font_gamma)
self.font_family_fonts = config:readSetting("font_family_fonts") or {}
self:updateFontFamilyFonts()
@ -199,64 +193,34 @@ 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.
--]]
function ReaderFont:onChangeSize(delta)
self.font_size = self.font_size + delta
self.ui:handleEvent(Event:new("SetFontSize", self.font_size))
self:onSetFontSize(self.configurable.font_size + delta)
return true
end
function ReaderFont:onSetFontSize(new_size)
if new_size > 255 then new_size = 255 end
if new_size < 12 then new_size = 12 end
self.font_size = new_size
self.ui.document:setFontSize(Screen:scaleBySize(new_size))
function ReaderFont:onSetFontSize(size)
size = math.max(12, math.min(size, 255))
self.configurable.font_size = size
self.ui.document:setFontSize(Screen:scaleBySize(size))
self.ui:handleEvent(Event:new("UpdatePos"))
Notification:notify(T(_("Font size set to: %1."), self.font_size))
Notification:notify(T(_("Font size set to: %1."), size))
return true
end
function ReaderFont:onSetLineSpace(space)
self.line_space_percent = math.min(200, math.max(50, space))
self.ui.document:setInterlineSpacePercent(self.line_space_percent)
space = math.max(50, math.min(space, 200))
self.configurable.line_spacing = space
self.ui.document:setInterlineSpacePercent(space)
self.ui:handleEvent(Event:new("UpdatePos"))
Notification:notify(T(_("Line spacing set to: %1%."), self.line_space_percent))
Notification:notify(T(_("Line spacing set to: %1%."), space))
return true
end
function ReaderFont:onSetFontBaseWeight(weight)
self.font_base_weight = weight
self.configurable.font_base_weight = weight
self.ui.document:setFontBaseWeight(weight)
self.ui:handleEvent(Event:new("UpdatePos"))
Notification:notify(T(_("Font weight set to: %1."), optionsutil:getOptionText("SetFontBaseWeight", weight)))
@ -264,7 +228,7 @@ function ReaderFont:onSetFontBaseWeight(weight)
end
function ReaderFont:onSetFontHinting(mode)
self.font_hinting = mode
self.configurable.font_hinting = mode
self.ui.document:setFontHinting(mode)
self.ui:handleEvent(Event:new("UpdatePos"))
Notification:notify(T(_("Font hinting set to: %1"), optionsutil:getOptionText("SetFontHinting", mode)))
@ -272,7 +236,7 @@ function ReaderFont:onSetFontHinting(mode)
end
function ReaderFont:onSetFontKerning(mode)
self.font_kerning = mode
self.configurable.font_kerning = mode
self.ui.document:setFontKerning(mode)
self.ui:handleEvent(Event:new("UpdatePos"))
Notification:notify(T(_("Font kerning set to: %1"), optionsutil:getOptionText("SetFontKerning", mode)))
@ -280,7 +244,7 @@ function ReaderFont:onSetFontKerning(mode)
end
function ReaderFont:onSetWordSpacing(values)
self.word_spacing = values
self.configurable.word_spacing = values
self.ui.document:setWordSpacing(values)
self.ui:handleEvent(Event:new("UpdatePos"))
Notification:notify(T(_("Word spacing set to: %1%, %2%"), values[1], values[2]))
@ -288,7 +252,7 @@ function ReaderFont:onSetWordSpacing(values)
end
function ReaderFont:onSetWordExpansion(value)
self.word_expansion = value
self.configurable.word_expansion = value
self.ui.document:setWordExpansion(value)
self.ui:handleEvent(Event:new("UpdatePos"))
Notification:notify(T(_("Word expansion set to: %1%."), value))
@ -296,7 +260,7 @@ function ReaderFont:onSetWordExpansion(value)
end
function ReaderFont:onSetCJKWidthScaling(value)
self.cjk_width_scaling = value
self.configurable.cjk_width_scaling = value
self.ui.document:setCJKWidthScaling(value)
self.ui:handleEvent(Event:new("UpdatePos"))
Notification:notify(T(_("CJK width scaling set to: %1%."), value))
@ -304,8 +268,8 @@ function ReaderFont:onSetCJKWidthScaling(value)
end
function ReaderFont:onSetFontGamma(gamma)
self.gamma_index = gamma
self.ui.document:setGammaIndex(self.gamma_index)
self.configurable.font_gamma = gamma
self.ui.document:setGammaIndex(gamma)
local gamma_level = self.ui.document:getGammaLevel()
self.ui:handleEvent(Event:new("RedrawCurrentView"))
Notification:notify(T(_("Font gamma set to: %1."), gamma_level))
@ -314,16 +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_size", self.font_size)
self.ui.doc_settings:saveSetting("font_base_weight", self.font_base_weight)
self.ui.doc_settings:saveSetting("font_hinting", self.font_hinting)
self.ui.doc_settings:saveSetting("font_kerning", self.font_kerning)
self.ui.doc_settings:saveSetting("word_spacing", self.word_spacing)
self.ui.doc_settings:saveSetting("word_expansion", self.word_expansion)
self.ui.doc_settings:saveSetting("cjk_width_scaling", self.cjk_width_scaling)
self.ui.doc_settings:saveSetting("line_space_percent", self.line_space_percent)
self.ui.doc_settings:saveSetting("gamma_index", self.gamma_index)
self.ui.doc_settings:saveSetting("font_family_fonts", self.font_family_fonts)
end
@ -343,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)
@ -380,15 +334,17 @@ function ReaderFont:makeDefault(face, is_monospace, touchmenu_instance)
end
function ReaderFont:addToMainMenu(menu_items)
-- Have TouchMenu show half of the usual nb of items, so we
-- have more room to see how the text looks with that font
self.face_table.max_per_page = 5
-- insert table to main reader menu
menu_items.change_font = {
text_func = function()
return T(_("Font: %1"), BD.wrap(self.font_face))
end,
sub_item_table = self.face_table,
sub_item_table_func = function()
if self.face_table.needs_refresh and self.face_table.refresh_func then
self.face_table.refresh_func()
end
return self.face_table
end
}
end
@ -417,14 +373,14 @@ end
function ReaderFont:onIncreaseFontSize(ges)
local delta_int = self:gesToFontSize(ges)
Notification:notify(_("Increasing font size…"), true)
Notification:notify(_("Increasing font size…"), nil, true)
self:onChangeSize(delta_int)
return true
end
function ReaderFont:onDecreaseFontSize(ges)
local delta_int = self:gesToFontSize(ges)
Notification:notify(_("Decreasing font size…"), true)
Notification:notify(_("Decreasing font size…"), nil, true)
self:onChangeSize(-delta_int)
return true
end
@ -446,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") },
}
@ -580,6 +536,7 @@ Enabling this will ignore such font names and make sure your preferred family fo
self:updateFontFamilyFonts()
end,
sub_item_table = {
ignored_by_menu_search = true, -- those would be duplicated
{
text = T(_("Font for %1"), BD.wrap(T("'font-family: %1'", family_tag))),
separator = true,
@ -709,6 +666,35 @@ function ReaderFont:getFontSettingsTable()
G_reader_settings:flipNilOrTrue("font_menu_use_font_face")
end,
help_text = _([[In the font menu, display each font name with its own font face.]]),
})
table.insert(settings_table, {
text = _("Sort fonts by recently selected"),
checked_func = function()
return G_reader_settings:isTrue("font_menu_sort_by_recently_selected")
end,
callback = function()
G_reader_settings:flipTrue("font_menu_sort_by_recently_selected")
self.face_table.needs_refresh = true
end,
hold_callback = function()
UIManager:show(ConfirmBox:new{
text = _([[
The font list menu can show fonts sorted by name or by most recently selected.
New fonts discovered at KOReader startup will be shown first.
Do you want to clear the history of selected fonts?]]),
ok_text = _("Clear"),
ok_callback = function()
G_reader_settings:delSetting("cre_fonts_recently_selected")
-- Recreate it now, sorted alphabetically (we may not go visit
-- and refresh the font menu until quit, but we want to be able
-- to notice newly added fonts at next startup).
self:sortFaceList(cre.getFontFaces())
self.face_table.needs_refresh = true
end,
})
end,
separator = true,
})
@ -794,6 +780,65 @@ This setting allows scaling all monospace fonts by this percentage so they can f
return settings_table
end
function ReaderFont:addToRecentlySelectedList(face)
local idx = util.arrayContains(self.fonts_recently_selected, face)
if idx then
table.remove(self.fonts_recently_selected, idx)
end
table.insert(self.fonts_recently_selected, 1, face)
if G_reader_settings:isTrue("font_menu_sort_by_recently_selected") then
self.face_table.needs_refresh = true
end
end
function ReaderFont:sortFaceList(face_list)
self.fonts_recently_selected = G_reader_settings:readSetting("cre_fonts_recently_selected")
if not self.fonts_recently_selected then
-- Init this list with the alphabetical list we got
self.fonts_recently_selected = face_list
G_reader_settings:saveSetting("cre_fonts_recently_selected", self.fonts_recently_selected)
-- We got no list of previously known fonts, so we can't say which are new.
newly_added_fonts = {}
return face_list
end
if not newly_added_fonts then
-- First call after launch: check for fonts not yet known
newly_added_fonts = {}
local seen_fonts = {}
for _, face in ipairs(self.fonts_recently_selected) do
seen_fonts[face] = false -- was there last time, might no longer be now
end
for _, face in ipairs(face_list) do
if seen_fonts[face] == nil then -- not known
newly_added_fonts[face] = true
-- Add newly seen fonts at start of the recently set list,
-- so the user can see and test them more easily.
table.insert(self.fonts_recently_selected, 1, face)
end
seen_fonts[face] = true
end
-- Remove no-longer-there fonts from our list
util.arrayRemove(self.fonts_recently_selected, function(t, i, j)
return seen_fonts[t[i]]
end)
end
if G_reader_settings:isTrue("font_menu_sort_by_recently_selected") then
return self.fonts_recently_selected
end
-- Otherwise, return face_list as we got it, alphabetically (as sorted by crengine),
-- but still with newly added fonts first
if next(newly_added_fonts) then
local move_idx = 1
for i=1, #face_list do
if newly_added_fonts[face_list[i]] then
table.insert(face_list, move_idx, table.remove(face_list, i))
move_idx = move_idx + 1
end
end
end
return face_list
end
-- Default sample file
local FONT_TEST_DEFAULT_SAMPLE_PATH = "frontend/ui/elements/font-test-sample-default.template"
-- Users can set their own sample file, that will be used if found

File diff suppressed because it is too large Load Diff

@ -28,12 +28,7 @@ function ReaderGoto:addToMainMenu(menu_items)
end
function ReaderGoto:onShowGotoDialog()
local curr_page
if self.document.info.has_pages then
curr_page = self.ui.paging.current_page
else
curr_page = self.document:getCurrentPage()
end
local curr_page = self.ui:getCurrentPage()
local input_hint
if self.ui.pagemap and self.ui.pagemap:wantsPageLabels() then
input_hint = T("@%1 (%2 - %3)", self.ui.pagemap:getCurrentPageLabel(true),
@ -58,16 +53,7 @@ x for an absolute page number
text = _("Skim"),
callback = function()
self:close()
self.skimto = SkimToWidget:new{
document = self.document,
ui = self.ui,
callback_switch_to_goto = function()
UIManager:close(self.skimto)
self:onShowGotoDialog()
end,
}
UIManager:show(self.skimto)
self:onShowSkimtoDialog()
end,
},
{
@ -195,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

@ -0,0 +1,719 @@
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")
local UIManager = require("ui/uimanager")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local util = require("util")
local T = require("ffi/util").template
local _ = require("gettext")
local ReaderHandMade = WidgetContainer:extend{
custom_toc_symbol = "\u{EAEC}", -- used in a few places
}
function ReaderHandMade:init()
self.ui.menu:registerToMainMenu(self)
end
function ReaderHandMade:onReadSettings(config)
self.toc_enabled = config:isTrue("handmade_toc_enabled")
self.toc_edit_enabled = config:nilOrTrue("handmade_toc_edit_enabled")
self.toc = config:readSetting("handmade_toc") or {}
self.flows_enabled = config:isTrue("handmade_flows_enabled")
self.flows_edit_enabled = config:nilOrTrue("handmade_flows_edit_enabled")
self.flow_points = config:readSetting("handmade_flow_points") or {}
self.inactive_flow_points = {}
-- Don't mess toc and flow_points made on that document if saved when
-- we were using a different engine - backup them if that's the case.
if #self.toc > 0 then
local has_xpointers = self.toc[1].xpointer ~= nil
if self.ui.rolling and not has_xpointers then
config:saveSetting("handmade_toc_paging", self.toc)
self.toc = config:readSetting("handmade_toc_rolling") or {}
config:delSetting("handmade_toc_rolling")
elseif self.ui.paging and has_xpointers then
config:saveSetting("handmade_toc_rolling", self.toc)
self.toc = config:readSetting("handmade_toc_paging") or {}
config:delSetting("handmade_toc_paging")
end
else
if self.ui.rolling and config:has("handmade_toc_rolling") then
self.toc = config:readSetting("handmade_toc_rolling")
config:delSetting("handmade_toc_rolling")
elseif self.ui.paging and config:has("handmade_toc_paging") then
self.toc = config:readSetting("handmade_toc_paging")
config:delSetting("handmade_toc_paging")
end
end
if #self.flow_points > 0 then
local has_xpointers = self.flow_points[1].xpointer ~= nil
if self.ui.rolling and not has_xpointers then
config:saveSetting("handmade_flow_points_paging", self.flow_points)
self.flow_points = config:readSetting("handmade_flow_points_rolling") or {}
config:delSetting("handmade_flow_points_rolling")
elseif self.ui.paging and has_xpointers then
config:saveSetting("handmade_flow_points_rolling", self.flow_points)
self.flow_points = config:readSetting("handmade_flow_points_paging") or {}
config:delSetting("handmade_flow_points_paging")
end
else
if self.ui.rolling and config:has("handmade_flow_points_rolling") then
self.flow_points = config:readSetting("handmade_flow_points_rolling")
config:delSetting("handmade_flow_points_rolling")
elseif self.ui.paging and config:has("handmade_flow_points_paging") then
self.flow_points = config:readSetting("handmade_flow_points_paging")
config:delSetting("handmade_flow_points_paging")
end
end
end
function ReaderHandMade:onSaveSettings()
self.ui.doc_settings:saveSetting("handmade_toc_enabled", self.toc_enabled)
self.ui.doc_settings:saveSetting("handmade_toc_edit_enabled", self.toc_edit_enabled)
if #self.toc > 0 then
self.ui.doc_settings:saveSetting("handmade_toc", self.toc)
else
self.ui.doc_settings:delSetting("handmade_toc")
end
self.ui.doc_settings:saveSetting("handmade_flows_enabled", self.flows_enabled)
self.ui.doc_settings:saveSetting("handmade_flows_edit_enabled", self.flows_edit_enabled)
if #self.flow_points > 0 then
self.ui.doc_settings:saveSetting("handmade_flow_points", self.flow_points)
else
self.ui.doc_settings:delSetting("handmade_flow_points")
end
end
function ReaderHandMade:isHandmadeTocEnabled()
return self.toc_enabled
end
function ReaderHandMade:isHandmadeTocEditEnabled()
return self.toc_edit_enabled
end
function ReaderHandMade:isHandmadeHiddenFlowsEnabled()
-- Even if currently empty, we return true, which allows showing '//' in
-- the footer and let know hidden flows are enabled.
return self.flows_enabled
end
function ReaderHandMade:isHandmadeHiddenFlowsEditEnabled()
return self.flows_edit_enabled
end
function ReaderHandMade:onToggleHandmadeToc()
self.toc_enabled = not self.toc_enabled
self:setupToc()
-- Have footer updated, so we may see this took effect
self.view.footer:onUpdateFooter(self.view.footer_visible)
end
function ReaderHandMade:onToggleHandmadeFlows()
self.flows_enabled = not self.flows_enabled
self:setupFlows()
-- Have footer updated, so we may see this took effect
self.view.footer:onUpdateFooter(self.view.footer_visible)
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,
callback = function()
self:onToggleHandmadeToc()
end,
}
menu_items.handmade_hidden_flows = {
text = _("Custom hidden flows"),
checked_func = function() return self.flows_enabled end,
callback = function()
self:onToggleHandmadeFlows()
end,
}
--[[ Not yet implemented
menu_items.handmade_page_numbers = {
text = _("Custom page numbers"),
checked_func = function() return false end,
callback = function()
end,
}
]]--
menu_items.handmade_settings = {
text = _("Custom layout features"),
sub_item_table_func = function()
return {
{
text = _("About custom table of contents") .. " " .. self.custom_toc_symbol,
callback = function()
UIManager:show(InfoMessage:new{
text = _([[
If the book has no table of contents or you would like to substitute it with your own, you can create a custom TOC. The original TOC (if available) will not be altered.
You can create, edit and remove chapters:
- in Page browser, by long-pressing on a thumbnail;
- on a book page, by selecting some text to be used as the chapter title.
(Once you're done building it and don't want to see the buttons anymore, you can disable Edit mode.)
This custom table of contents is currently limited to a single level and can't have sub-chapters.]])
})
end,
keep_menu_open = true,
},
{
text = _("Edit mode"),
enabled_func = function()
return self:isHandmadeTocEnabled()
end,
checked_func = function()
return self:isHandmadeTocEditEnabled()
end,
callback = function()
self.toc_edit_enabled = not self.toc_edit_enabled
self:updateHighlightDialog()
end,
},
--[[ Not yet implemented
{
text = _("Add multiple chapter start page numbers"),
},
]]--
{
text = _("Clear custom table of contents"),
enabled_func = function()
return #self.toc > 0
end,
callback = function(touchmenu_instance)
UIManager:show(ConfirmBox:new{
text = _("Are you sure you want to clear your custom table of contents?"),
ok_callback = function()
self.toc = {}
self.ui:handleEvent(Event:new("UpdateToc"))
-- The footer may be visible, so have it update its chapter related items
self.view.footer:onUpdateFooter(self.view.footer_visible)
if touchmenu_instance then
touchmenu_instance:updateItems()
end
end,
})
end,
keep_menu_open = true,
separator = true,
},
{
text = _("About custom hidden flows"),
callback = function()
UIManager:show(InfoMessage:new{
text = _([[
Custom hidden flows can be created to exclude sections of the book from your normal reading flow:
- hidden flows will automatically be skipped when turning pages within the regular flow;
- pages part of hidden flows are assigned distinct page numbers and won't be considered in the various book & chapter progress and time to read features;
- following direct links to pages in hidden flows will still work, including from the TOC or Book map.
This can be useful to exclude long footnotes or bibliography sections.
It can also be handy when interested in reading only a subset of a book.
In Page browser, you can long-press on a thumbnail to start a hidden flow or restart the regular flow on this page.
(Once you're done building it and don't want to see the button anymore, you can disable Edit mode.)
Hidden flows are shown with gray or hatched background in Book map and Page browser.]])
})
end,
keep_menu_open = true,
},
{
text = _("Edit mode"),
enabled_func = function()
return self:isHandmadeHiddenFlowsEnabled()
end,
checked_func = function()
return self:isHandmadeHiddenFlowsEditEnabled()
end,
callback = function()
self.flows_edit_enabled = not self.flows_edit_enabled
end,
},
{
text_func = function()
return T(_("Clear inactive marked pages (%1)"), #self.inactive_flow_points)
end,
enabled_func = function()
return #self.inactive_flow_points > 0
end,
callback = function(touchmenu_instance)
UIManager:show(ConfirmBox:new{
text = _("Inactive marked pages are pages that you tagged as start hidden flow or restart regular flow, but that other marked pages made them have no effect.\nAre you sure you want to clear them?"),
ok_callback = function()
for i=#self.inactive_flow_points, 1, -1 do
table.remove(self.flow_points, self.inactive_flow_points[i])
end
self:updateDocFlows()
self.ui:handleEvent(Event:new("UpdateToc"))
self.ui:handleEvent(Event:new("InitScrollPageStates"))
-- The footer may be visible, so have it update its dependant items
self.view.footer:onUpdateFooter(self.view.footer_visible)
if touchmenu_instance then
touchmenu_instance:updateItems()
end
end,
})
end,
keep_menu_open = true,
},
{
text = _("Clear all marked pages"),
enabled_func = function()
return #self.flow_points > 0
end,
callback = function(touchmenu_instance)
UIManager:show(ConfirmBox:new{
text = _("Are you sure you want to clear all your custom hidden flows?"),
ok_callback = function()
self.flow_points = {}
self:updateDocFlows()
self.ui:handleEvent(Event:new("UpdateToc"))
self.ui:handleEvent(Event:new("InitScrollPageStates"))
-- The footer may be visible, so have it update its dependant items
self.view.footer:onUpdateFooter(self.view.footer_visible)
if touchmenu_instance then
touchmenu_instance:updateItems()
end
end,
})
end,
keep_menu_open = true,
separator = true,
},
--[[ Not yet implemented
{
text = _("About custom page numbers"),
},
{
text = _("Clear custom page numbers"),
},
]]--
}
end,
}
end
function ReaderHandMade:updateHandmagePages()
if not self.ui.rolling then
return
end
for _, item in ipairs(self.toc) do
item.page = self.document:getPageFromXPointer(item.xpointer)
end
for _, item in ipairs(self.flow_points) do
item.page = self.document:getPageFromXPointer(item.xpointer)
end
end
function ReaderHandMade:onReaderReady()
-- Called on load, and with a CRE document when reloading after partial rerendering.
-- Notes:
-- - ReaderFooter (from ReaderView) will have its onReaderReady() called before ours,
-- and it may fillToc(). So, it may happen that the expensive validateAndFixToc()
-- is called twice (first with the original ToC, then with ours).
-- - ReaderRolling will have its onReaderReady() called after ours, and if we
-- have set up hidden flows, we'll have overriden some documents methods so
-- its cacheFlows() is a no-op.
self:updateHandmagePages()
-- Don't have each of these send their own events: we'll send them once afterwards
self:setupFlows(true)
self:setupToc(true)
-- Now send the events
if self.toc_enabled or self.flows_enabled then
self.ui:handleEvent(Event:new("UpdateToc"))
end
if self.flows_enabled then
-- Needed to skip hidden flows if PDF in scroll mode
self.ui:handleEvent(Event:new("InitScrollPageStates"))
end
end
function ReaderHandMade:onDocumentRerendered()
-- Called with CRE document when partial rerendering not enabled
self:updateHandmagePages()
-- Don't have these send events their own events
self:setupFlows(true)
self:setupToc(true)
-- ReaderToc will process this event just after us, and will
-- call its onUpdateToc: we don't need to send it.
-- (Also, no need for InitScrollPageStates with CRE.)
end
function ReaderHandMade:setupToc(no_event)
if self.toc_enabled then
-- If enabled, plug one method into the document object,
-- so it is used instead of the method from its class.
self.document.getToc = function(this)
-- ReaderToc may add fieds to ToC items: return a copy,
-- so the one we will save doesn't get polluted.
return util.tableDeepCopy(self.toc)
end
else
-- If disabled, remove our plug so the method from the
-- class gets used again.
self.document.getToc = nil
end
self:updateHighlightDialog()
if not no_event then
self.ui:handleEvent(Event:new("UpdateToc"))
end
end
function ReaderHandMade:updateHighlightDialog()
if self.toc_enabled and self.toc_edit_enabled then
-- We don't want this button to be the last wide one, and rather
-- keep having the Search button being that one: so plug this one
-- just before 12_search.
self.ui.highlight:addToHighlightDialog("12_0_make_handmade_toc_item", function(this)
return {
text_func = function()
local selected_text = this.selected_text
local pageno, xpointer
if self.ui.rolling then
xpointer = selected_text.pos0
else
pageno = selected_text.pos0.page
end
local text
if self:hasPageTocItem(pageno, xpointer) then
text = _("Edit TOC chapter")
else
text = _("Start TOC chapter")
end
text = text .. " " .. self.custom_toc_symbol
return text
end,
callback = function()
local selected_text = this.selected_text
this:onClose()
self:addOrEditPageTocItem(nil, nil, selected_text)
end,
}
end)
else
self.ui.highlight:removeFromHighlightDialog("12_0_make_handmade_toc_item")
end
end
function ReaderHandMade:_getItemIndex(tab, pageno, xpointer)
if not pageno and xpointer then
pageno = self.document:getPageFromXPointer(xpointer)
end
-- (No need to use a binary search, our user made tables should
-- not be too large)
local matching_idx
local insertion_idx = #tab + 1
for i, item in ipairs(tab) do
if item.page >= pageno then
if item.page > pageno then
insertion_idx = i
break
end
-- Same page numbers.
-- (We can trust page numbers, and only compare xpointers when both
-- resolve to the same page.)
if xpointer and item.xpointer then
local order = self.document:compareXPointers(xpointer, item.xpointer)
if order > 0 then -- item.xpointer after xpointer
insertion_idx = i
break
elseif order == 0 then
matching_idx = i
break
end
else
matching_idx = i
break
end
end
end
-- We always return an index, and a boolean stating if this index is a match or not
-- (if not, the index is the insertion index if we ever want to insert an item with
-- the asked pageno/xpointer)
return matching_idx or insertion_idx, matching_idx and true or false
end
function ReaderHandMade:hasPageTocItem(pageno, xpointer)
local _, is_match = self:_getItemIndex(self.toc, pageno, xpointer)
return is_match
end
function ReaderHandMade:addOrEditPageTocItem(pageno, when_updated_callback, selected_text)
local xpointer, title
if selected_text then
-- If we get selected_text, it's from the highlight dialog after text selection
title = selected_text.text
if self.ui.rolling then
xpointer = selected_text.pos0
pageno = self.document:getPageFromXPointer(xpointer)
else
pageno = selected_text.pos0.page
end
end
local idx, item_found = self:_getItemIndex(self.toc, pageno, xpointer)
local item
if item_found then
-- Chapter found: it's an update (edit text or remove item)
item = self.toc[idx]
else
-- No chapter starting on this page or at this xpointer:
-- we'll add a new item
if not xpointer and self.ui.rolling and type(pageno) == "number" then
xpointer = self.document:getPageXPointer(pageno)
end
item = {
title = title or "",
page = pageno,
xpointer = xpointer,
depth = 1, -- we only support 1-level chapters to keep the UX simple
}
end
local dialog
dialog = InputDialog:new{
title = item_found and _("Edit custom TOC chapter") or _("Create new custom ToC chapter"),
input = item.title,
input_hint = _("TOC chapter title"),
description = T(_([[On page %1.]]), pageno),
buttons = {
{
{
text = _("Cancel"),
id = "close",
callback = function()
UIManager:close(dialog)
end,
},
{
text = item_found and _("Save") or _("Create"),
is_enter_default = true,
callback = function()
item.title = dialog:getInputText()
UIManager:close(dialog)
if not item_found then
table.insert(self.toc, idx, item)
end
self.ui:handleEvent(Event:new("UpdateToc"))
if when_updated_callback then
when_updated_callback()
end
end,
},
},
item_found and {
{
text = _("Remove"),
callback = function()
UIManager:close(dialog)
table.remove(self.toc, idx)
self.ui:handleEvent(Event:new("UpdateToc"))
if when_updated_callback then
when_updated_callback()
end
end,
},
selected_text and
{
text = _("Use selected text"),
callback = function()
-- Just replace the text without saving, to allow editing/fixing it
dialog:setInputText(selected_text.text, nil, false)
end,
} or nil,
} or nil,
},
}
UIManager:show(dialog)
dialog:onShowKeyboard()
return true
end
function ReaderHandMade:isInHiddenFlow(pageno)
local idx, is_match = self:_getItemIndex(self.flow_points, pageno)
if is_match then
return self.flow_points[idx].hidden
else
if idx > 1 then
return self.flow_points[idx-1].hidden
end
end
-- Before any first flow_point: not hidden
return false
end
function ReaderHandMade:toggleHiddenFlow(pageno)
local idx, is_match = self:_getItemIndex(self.flow_points, pageno)
if is_match then
-- Just remove the item (it feels we can, and that we don't
-- have to just toggle its hidden value)
table.remove(self.flow_points, idx)
self:updateDocFlows()
return
end
local hidden
if idx > 1 then
local previous_item = self.flow_points[idx-1]
hidden = not previous_item.hidden
else
-- First item, can only start an hidden flow
hidden = true
end
local xpointer
if self.ui.rolling and type(pageno) == "number" then
xpointer = self.document:getPageXPointer(pageno)
end
local item = {
hidden = hidden,
page = pageno,
xpointer = xpointer,
}
table.insert(self.flow_points, idx, item)
-- We could remove any followup item(s) with the same hidden state, but by keeping them,
-- we allow users to adjust the start of a flow without killing its end. One can clean
-- all the unnefective ones via the "Clear inactive marked pages" menu item.
self:updateDocFlows()
end
function ReaderHandMade:updateDocFlows()
local flows = {}
local inactive_flow_points = {}
-- (getPageCount(), needing the document to be fully loaded, is not available
-- until ReaderReady, so be sure this is called only after ReaderReady.)
local nb_pages = self.document:getPageCount()
local nb_hidden_pages = 0
local cur_hidden_flow
for i, point in ipairs(self.flow_points) do
if point.hidden and not cur_hidden_flow then
cur_hidden_flow = {point.page, 0}
elseif not point.hidden and cur_hidden_flow then
local cur_hidden_pages = point.page - cur_hidden_flow[1]
if cur_hidden_pages > 0 then
cur_hidden_flow[2] = cur_hidden_pages
nb_hidden_pages = nb_hidden_pages + cur_hidden_pages
table.insert(flows, cur_hidden_flow)
end
cur_hidden_flow = nil
else
table.insert(inactive_flow_points, i)
end
end
if cur_hidden_flow then
local cur_hidden_pages = nb_pages + 1 - cur_hidden_flow[1]
if cur_hidden_pages > 0 then
cur_hidden_flow[2] = cur_hidden_pages
nb_hidden_pages = nb_hidden_pages + cur_hidden_pages
table.insert(flows, cur_hidden_flow)
end
end
local first_linear_page
local last_linear_page
local prev_flow
for i, flow in ipairs(flows) do
if not prev_flow or prev_flow[1] + prev_flow[2] < flow[1] then
if not first_linear_page and flow[1] > 1 then
first_linear_page = prev_flow and prev_flow[1] + prev_flow[2] or 1
end
last_linear_page = flow[1] - 1
end
prev_flow = flow
end
if not prev_flow or prev_flow[1] + prev_flow[2] < nb_pages then
last_linear_page = nb_pages
end
if not first_linear_page then -- no flow met
first_linear_page = 1
end
-- CreDocument adds and item with key [0] with info about the main flow
flows[0] = {first_linear_page, nb_pages - nb_hidden_pages}
self.last_linear_page = last_linear_page
self.flows = flows
self.inactive_flow_points = inactive_flow_points
-- We plug our flows table into the document, as some code peeks into it
self.document.flows = self.flows
end
function ReaderHandMade:setupFlows(no_event)
if self.flows_enabled then
self:updateDocFlows()
-- If enabled, plug some methods into the document object,
-- so they are used instead of the methods from its class.
self.document.hasHiddenFlows = function(this)
return true
end
self.document.cacheFlows = function(this)
return
end
self.document.getPageFlow = function(this, page)
for i, flow in ipairs(self.flows) do
if page < flow[1] then
return 0 -- page is not in a hidden flow
end
if page < flow[1] + flow[2] then
return i
end
end
return 0
end
self.document.getFirstPageInFlow = function(this, flow)
return self.flows[flow][1]
end
self.document.getTotalPagesInFlow = function(this, flow)
return self.flows[flow][2]
end
self.document.getPageNumberInFlow = function(this, page)
local nb_hidden_pages = 0
for i, flow in ipairs(self.flows) do
if page < flow[1] then
break -- page is not in a hidden flow
end
if page < flow[1] + flow[2] then
return page - flow[1] + 1
end
nb_hidden_pages = nb_hidden_pages + flow[2]
end
return page - nb_hidden_pages
end
self.document.getLastLinearPage = function(this)
return self.last_linear_page
end
-- We can reuse as-is these ones from CreDocument, which uses the ones defined above.
-- Note: these could probably be rewritten and simplified.
local CreDocument = require("document/credocument")
self.document.getTotalPagesLeft = CreDocument.getTotalPagesLeft
self.document.getNextPage = CreDocument.getNextPage
self.document.getPrevPage = CreDocument.getPrevPage
else
-- Remove all our overrides, so the class methods can be used again
self.document.hasHiddenFlows = nil
self.document.cacheFlows = nil
self.document.getPageFlow = nil
self.document.getFirstPageInFlow = nil
self.document.getTotalPagesInFlow = nil
self.document.getPageNumberInFlow = nil
self.document.getLastLinearPage = nil
self.document.getTotalPagesLeft = nil
self.document.getNextPage = nil
self.document.getPrevPage = nil
self.document.flows = nil
if self.document.cacheFlows then
self.document:cacheFlows()
end
end
if not no_event then
self.ui:handleEvent(Event:new("UpdateToc"))
-- Needed to skip hidden flows if PDF in scroll mode
self.ui:handleEvent(Event:new("InitScrollPageStates"))
end
end
return ReaderHandMade

File diff suppressed because it is too large Load Diff

@ -2,7 +2,6 @@ local EventListener = require("ui/widget/eventlistener")
local Event = require("ui/event")
local ReaderZooming = require("apps/reader/modules/readerzooming")
local UIManager = require("ui/uimanager")
local util = require("util")
local ReaderKoptListener = EventListener:extend{}
@ -19,15 +18,10 @@ function ReaderKoptListener:onReadSettings(config)
-- normal zoom mode is zoom mode used in non-reflow mode.
local normal_zoom_mode = config:readSetting("normal_zoom_mode")
or ReaderZooming:combo_to_mode(G_reader_settings:readSetting("kopt_zoom_mode_genus"), G_reader_settings:readSetting("kopt_zoom_mode_type"))
normal_zoom_mode = util.arrayContains(ReaderZooming.available_zoom_modes, normal_zoom_mode)
and normal_zoom_mode
or ReaderZooming.DEFAULT_ZOOM_MODE
normal_zoom_mode = ReaderZooming.zoom_mode_label[normal_zoom_mode] and normal_zoom_mode or ReaderZooming.DEFAULT_ZOOM_MODE
self.normal_zoom_mode = normal_zoom_mode
self:setZoomMode(normal_zoom_mode)
self.document.configurable.contrast = config:readSetting("kopt_contrast")
or G_reader_settings:readSetting("kopt_contrast")
or 1.0
self.ui:handleEvent(Event:new("GammaUpdate", 1/self.document.configurable.contrast))
self.ui:handleEvent(Event:new("GammaUpdate", self.document.configurable.contrast))
-- since K2pdfopt v2.21 negative value of word spacing is also used, for config
-- compatability we should manually change previous -1 to a more reasonable -0.2
if self.document.configurable.word_spacing == -1 then
@ -75,6 +69,9 @@ function ReaderKoptListener:onDocLangUpdate(lang)
end
function ReaderKoptListener: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
if (option_name == "font_size" or option_name == "line_spacing") and option_value > 5 then return end
self.document.configurable[option_name] = option_value
self.ui:handleEvent(Event:new("StartActivityIndicator"))
UIManager:setDirty("all", "partial")

File diff suppressed because it is too large Load Diff

@ -56,36 +56,46 @@ function ReaderMenu:init()
self.registered_widgets = {}
self:registerKeyEvents()
if G_reader_settings:has("activate_menu") then
self.activation_menu = G_reader_settings:readSetting("activate_menu")
else
self.activation_menu = "swipe_tap"
end
-- delegate gesture listener to readerui, NOP our own
self.ges_events = nil
end
function ReaderMenu:onGesture() end
function ReaderMenu:registerKeyEvents()
if Device:hasKeys() then
if Device:isTouchDevice() then
self.key_events.TapShowMenu = { { "Menu" }, doc = "show menu", }
self.key_events.PressMenu = { { "Menu" } }
if Device:hasFewKeys() then
self.key_events.TapShowMenu = { { { "Menu", "Right" } }, doc = "show menu", }
self.key_events.PressMenu = { { { "Menu", "Right" } } }
end
else
-- map menu key to only top menu because bottom menu is only
-- designed for touch devices
self.key_events.ShowMenu = { { "Menu" }, doc = "show menu", }
-- Map Menu key to top menu only, because the bottom menu is only designed for touch devices.
--- @fixme: Is this still the case?
--- (Swapping between top and bottom might not be implemented, though, so it might still be a good idea).
self.key_events.ShowMenu = { { "Menu" } }
if Device:hasFewKeys() then
self.key_events.ShowMenu = { { { "Menu", "Right" } }, doc = "show menu", }
self.key_events.ShowMenu = { { { "Menu", "Right" } } }
end
end
end
if G_reader_settings:has("activate_menu") then
self.activation_menu = G_reader_settings:readSetting("activate_menu")
else
self.activation_menu = "swipe_tap"
end
end
ReaderMenu.onPhysicalKeyboardConnected = ReaderMenu.registerKeyEvents
function ReaderMenu:getPreviousFile()
return require("readhistory"):getPreviousFile(self.ui.document.file)
end
function ReaderMenu:onReaderReady()
-- deligate gesture listener to readerui
self.ges_events = {}
self.onGesture = nil
function ReaderMenu:initGesListener()
if not Device:isTouchDevice() then return end
local DTAP_ZONE_MENU = G_defaults:readSetting("DTAP_ZONE_MENU")
@ -169,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)
@ -178,23 +190,46 @@ function ReaderMenu:setUpdateItemTable()
end
-- typeset tab
self.menu_items.reset_document_settings = {
text = _("Reset document settings to default"),
keep_menu_open = true,
callback = function()
UIManager:show(ConfirmBox:new{
text = _("Reset current document settings to their default values?\n\nReading position, highlights and bookmarks will be kept.\nThe document will be reloaded."),
ok_text = _("Reset"),
ok_callback = function()
local current_file = self.ui.document.file
self:onTapCloseMenu()
self.ui:onClose()
require("apps/filemanager/filemanagerutil").resetDocumentSettings(current_file)
require("apps/reader/readerui"):showReader(current_file)
self.menu_items.document_settings = {
text = _("Document settings"),
sub_item_table = {
{
text = _("Reset document settings to default"),
keep_menu_open = true,
callback = function()
UIManager:show(ConfirmBox:new{
text = _("Reset current document settings to their default values?\n\nReading position, highlights and bookmarks will be kept.\nThe document will be reloaded."),
ok_text = _("Reset"),
ok_callback = function()
local current_file = self.ui.document.file
self:onTapCloseMenu()
self.ui:onClose()
require("apps/filemanager/filemanagerutil").resetDocumentSettings(current_file)
require("apps/reader/readerui"):showReader(current_file)
end,
})
end,
})
end,
},
{
text = _("Save document settings as default"),
keep_menu_open = true,
callback = function()
UIManager:show(ConfirmBox:new{
text = _("Save current document settings as default values?"),
ok_text = _("Save"),
ok_callback = function()
self:onTapCloseMenu()
self:saveDocumentSettingsAsDefault()
UIManager:show(require("ui/widget/notification"):new{
text = _("Default settings updated"),
})
end,
})
end,
},
},
}
self.menu_items.page_overlap = require("ui/elements/page_overlap")
-- settings tab
@ -204,10 +239,12 @@ function ReaderMenu:setUpdateItemTable()
end
if Device:isTouchDevice() then
-- Settings > Taps & Gestures; mostly concerns touch related page turn stuff, and only applies to Reader
self.menu_items.page_turns = require("ui/elements/page_turns")
else
-- Placed elsewhere than in Taps and gestures, with only a subset of menu items.
self.menu_items.page_turns_non_touch = require("ui/elements/page_turns")
end
-- Settings > Navigation; while also related to page turns, this mostly concerns physical keys, and applies *everywhere*
if Device:hasKeys() then
self.menu_items.physical_buttons_setup = require("ui/elements/physical_buttons")
end
-- insert DjVu render mode submenu just before the last entry (show advanced)
-- this is a bit of a hack
@ -217,10 +254,14 @@ function ReaderMenu:setUpdateItemTable()
if Device:supportsScreensaver() then
local ss_book_settings = {
text = _("Exclude this book's cover from screensaver"),
text = _("Do not show this book cover on sleep screen"),
enabled_func = function()
return not (self.ui == nil or self.ui.document == nil)
and G_reader_settings:readSetting("screensaver_type") == "cover"
if self.ui and self.ui.document then
local screensaverType = G_reader_settings:readSetting("screensaver_type")
return screensaverType == "cover" or screensaverType == "disable"
else
return false
end
end,
checked_func = function()
return self.ui and self.ui.doc_settings and self.ui.doc_settings:isTrue("exclude_screensaver")
@ -244,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
@ -305,6 +346,21 @@ dbg:guard(ReaderMenu, 'setUpdateItemTable',
end
end)
function ReaderMenu:saveDocumentSettingsAsDefault()
local prefix
if self.ui.rolling then
G_reader_settings:saveSetting("cre_font", self.ui.font.font_face)
G_reader_settings:saveSetting("copt_css", self.ui.document.default_css)
G_reader_settings:saveSetting("style_tweaks", self.ui.styletweak.global_tweaks)
prefix = "copt_"
else
prefix = "kopt_"
end
for k, v in pairs(self.ui.document.configurable) do
G_reader_settings:saveSetting(prefix .. k, v)
end
end
function ReaderMenu:exitOrRestart(callback, force)
if self.menu_container then self:onTapCloseMenu() end
@ -362,6 +418,7 @@ function ReaderMenu:onShowMenu(tab_index)
end
local menu_container = CenterContainer:new{
covers_header = true,
ignore = "height",
dimen = Screen:getSize(),
}
@ -386,7 +443,7 @@ function ReaderMenu:onShowMenu(tab_index)
end
main_menu.close_callback = function()
self.ui:handleEvent(Event:new("CloseReaderMenu"))
self:onCloseReaderMenu()
end
main_menu.touch_menu_callback = function ()
@ -401,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()
@ -449,7 +514,7 @@ function ReaderMenu:onSwipeShowMenu(ges)
if G_reader_settings:nilOrTrue("show_bottom_menu") then
self.ui:handleEvent(Event:new("ShowConfigMenu"))
end
self.ui:handleEvent(Event:new("ShowMenu", self:_getTabIndexFromLocation(ges)))
self:onShowMenu(self:_getTabIndexFromLocation(ges))
self.ui:handleEvent(Event:new("HandledAsSwipe")) -- cancel any pan scroll made
return true
end
@ -460,13 +525,21 @@ function ReaderMenu:onTapShowMenu(ges)
if G_reader_settings:nilOrTrue("show_bottom_menu") then
self.ui:handleEvent(Event:new("ShowConfigMenu"))
end
self.ui:handleEvent(Event:new("ShowMenu", self:_getTabIndexFromLocation(ges)))
self:onShowMenu(self:_getTabIndexFromLocation(ges))
return true
end
end
function ReaderMenu:onPressMenu()
if G_reader_settings:nilOrTrue("show_bottom_menu") then
self.ui:handleEvent(Event:new("ShowConfigMenu"))
end
self:onShowMenu()
return true
end
function ReaderMenu:onTapCloseMenu()
self.ui:handleEvent(Event:new("CloseReaderMenu"))
self:onCloseReaderMenu()
self.ui:handleEvent(Event:new("CloseConfigMenu"))
end
@ -478,6 +551,11 @@ function ReaderMenu:onSaveSettings()
self.ui.doc_settings:saveSetting("readermenu_tab_index", self.last_tab_index)
end
function ReaderMenu:onMenuSearch()
self:onShowMenu()
self.menu_container[1]:onShowMenuSearch()
end
function ReaderMenu:registerToMainMenu(widget)
table.insert(self.registered_widgets, widget)
end

@ -87,9 +87,7 @@ function ReaderPageMap:resetLayout()
end
function ReaderPageMap:onReadSettings(config)
local h_margins = config:readSetting("copt_h_page_margins")
or G_reader_settings:readSetting("copt_h_page_margins")
or G_defaults:readSetting("DCREREADER_CONFIG_H_MARGIN_SIZES_MEDIUM")
local h_margins = self.ui.document.configurable.h_page_margins
self.max_left_label_width = Screen:scaleBySize(h_margins[1])
self.max_right_label_width = Screen:scaleBySize(h_margins[2])
@ -282,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)
@ -408,7 +409,7 @@ function ReaderPageMap:addToMainMenu(menu_items)
},
{
text_func = function()
return T(_("Page labels font size (%1)"), self.label_font_size)
return T(_("Page labels font size: %1"), self.label_font_size)
end,
enabled_func = function() return self.show_page_labels end,
callback = function(touchmenu_instance)

@ -4,7 +4,6 @@ local Event = require("ui/event")
local Geom = require("ui/geometry")
local InputContainer = require("ui/widget/container/inputcontainer")
local Math = require("optmath")
local ReaderZooming = require("apps/reader/modules/readerzooming")
local UIManager = require("ui/uimanager")
local bit = require("bit")
local logger = require("logger")
@ -41,73 +40,121 @@ local ReaderPaging = InputContainer:extend{
}
function ReaderPaging:init()
self.key_events = {}
if Device:hasKeys() then
self:registerKeyEvents()
self.pan_interval = time.s(1 / self.pan_rate)
self.number_of_pages = self.ui.document.info.number_of_pages
-- delegate gesture listener to readerui, NOP our own
self.ges_events = nil
end
function ReaderPaging:onGesture() end
function ReaderPaging:registerKeyEvents()
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", "Right" } }, doc = "go to next page",
event = "GotoViewRel", args = 1,
{ { "RPgFwd", "LPgFwd", not Device:hasFewKeys() and "Right" } },
event = "GotoViewRel",
args = 1,
}
self.key_events.GotoPrevPage = {
{ { "RPgBack", "LPgBack", "Left" } }, doc = "go to previous page",
event = "GotoViewRel", args = -1,
{ { "RPgBack", "LPgBack", not Device:hasFewKeys() and "Left" } },
event = "GotoViewRel",
args = -1,
}
if Device:hasFewKeys() then
table.remove(self.key_events.GotoNextPage[1][1], 3) -- right
table.remove(self.key_events.GotoPrevPage[1][1], 3) -- left
end
self.key_events.GotoNextPos = {
{ {"Down" } }, doc = "go to next position",
event = "GotoPosRel", args = 1,
{ "Down" },
event = "GotoPosRel",
args = 1,
}
self.key_events.GotoPrevPos = {
{ { "Up" } }, doc = "go to previous position",
event = "GotoPosRel", args = -1,
{ "Up" },
event = "GotoPosRel",
args = -1,
}
end
if Device:hasKeyboard() then
self.key_events.GotoFirst = {
{"1"}, doc = "go to start", event = "GotoPercent", args = 0,
{ "1" },
event = "GotoPercent",
args = 0,
}
self.key_events.Goto11 = {
{"2"}, doc = "go to 11%", event = "GotoPercent", args = 11,
{ "2" },
event = "GotoPercent",
args = 11,
}
self.key_events.Goto22 = {
{"3"}, doc = "go to 22%", event = "GotoPercent", args = 22,
{ "3" },
event = "GotoPercent",
args = 22,
}
self.key_events.Goto33 = {
{"4"}, doc = "go to 33%", event = "GotoPercent", args = 33,
{ "4" },
event = "GotoPercent",
args = 33,
}
self.key_events.Goto44 = {
{"5"}, doc = "go to 44%", event = "GotoPercent", args = 44,
{ "5" },
event = "GotoPercent",
args = 44,
}
self.key_events.Goto55 = {
{"6"}, doc = "go to 55%", event = "GotoPercent", args = 55,
{ "6" },
event = "GotoPercent",
args = 55,
}
self.key_events.Goto66 = {
{"7"}, doc = "go to 66%", event = "GotoPercent", args = 66,
{ "7" },
event = "GotoPercent",
args = 66,
}
self.key_events.Goto77 = {
{"8"}, doc = "go to 77%", event = "GotoPercent", args = 77,
{ "8" },
event = "GotoPercent",
args = 77,
}
self.key_events.Goto88 = {
{"9"}, doc = "go to 88%", event = "GotoPercent", args = 88,
{ "9" },
event = "GotoPercent",
args = 88,
}
self.key_events.GotoLast = {
{"0"}, doc = "go to end", event = "GotoPercent", args = 100,
{ "0" },
event = "GotoPercent",
args = 100,
}
end
self.pan_interval = time.s(1 / self.pan_rate)
self.number_of_pages = self.ui.document.info.number_of_pages
end
ReaderPaging.onPhysicalKeyboardConnected = ReaderPaging.registerKeyEvents
function ReaderPaging:onReaderReady()
self:setupTouchZones()
end
function ReaderPaging:setupTouchZones()
self.ges_events = {}
self.onGesture = nil
if not Device:isTouchDevice() then return end
local forward_zone, backward_zone = self.view:getTapZones()
@ -165,10 +212,6 @@ function ReaderPaging:onReadSettings(config)
self:_gotoPage(config:readSetting("last_page") or 1)
self.flipping_zoom_mode = config:readSetting("flipping_zoom_mode") or "page"
self.flipping_scroll_mode = config:isTrue("flipping_scroll_mode")
self.is_reflowed = config:has("kopt_text_wrap") and config:readSetting("kopt_text_wrap") == 1
for _, v in ipairs(ReaderZooming.zoom_pan_settings) do
self[v] = config:readSetting(v) or G_reader_settings:readSetting(v) or ReaderZooming[v]
end
end
function ReaderPaging:onSaveSettings()
@ -204,7 +247,7 @@ book, the page view will be roughly the same.
--]]
function ReaderPaging:setPagePosition(page, pos)
logger.dbg("set page position", pos)
self.page_positions[page] = pos
self.page_positions[page] = pos ~= 0 and pos or nil
self.ui:handleEvent(Event:new("PagePositionUpdated"))
end
@ -246,15 +289,11 @@ function ReaderPaging:onToggleBookmarkFlipping()
if self.bookmark_flipping_mode then
self.orig_flipping_mode = self.view.flipping_visible
self.orig_dogear_mode = self.view.dogear_visible
self.view.flipping_visible = true
self.view.dogear_visible = true
self.bm_flipping_orig_page = self.current_page
self:enterFlippingMode()
else
self.view.flipping_visible = self.orig_flipping_mode
self.view.dogear_visible = self.orig_dogear_mode
self:exitFlippingMode()
self:_gotoPage(self.bm_flipping_orig_page)
end
@ -655,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(
@ -673,7 +712,9 @@ function ReaderPaging:onInitScrollPageStates(orig_mode)
blank_area.h = blank_area.h - self.view.page_gap.height
end
if blank_area.h > 0 then
self:_gotoPage(self.current_page + 1, "scrolling")
local next_page = self.ui.document:getNextPage(self.current_page)
if next_page == 0 then break end -- end of document reached
self:_gotoPage(next_page, "scrolling")
end
end
self:_gotoPage(self.orig_page, "scrolling")
@ -695,7 +736,7 @@ function ReaderPaging:onUpdateScrollPageGamma(gamma)
return true
end
function ReaderPaging:getNextPageState(blank_area, offset)
function ReaderPaging:getNextPageState(blank_area, image_offset)
local page_area = self.view:getPageArea(
self.view.state.page,
self.view.state.zoom,
@ -703,21 +744,25 @@ function ReaderPaging:getNextPageState(blank_area, offset)
local visible_area = Geom:new{x = 0, y = 0}
visible_area.w, visible_area.h = blank_area.w, blank_area.h
visible_area.x, visible_area.y = page_area.x, page_area.y
visible_area = visible_area:shrinkInside(page_area, offset.x, offset.y)
visible_area = visible_area:shrinkInside(page_area, image_offset.x, image_offset.y)
-- shrink blank area by the height of visible area
blank_area.h = blank_area.h - visible_area.h
local page_offset = Geom:new{x = self.view.state.offset.x, y = 0}
if blank_area.w > page_area.w then
page_offset:offsetBy((blank_area.w - page_area.w) / 2, 0)
end
return {
page = self.view.state.page,
zoom = self.view.state.zoom,
rotation = self.view.state.rotation,
gamma = self.view.state.gamma,
offset = Geom:new{ x = self.view.state.offset.x, y = 0},
offset = page_offset,
visible_area = visible_area,
page_area = page_area,
}
end
function ReaderPaging:getPrevPageState(blank_area, offset)
function ReaderPaging:getPrevPageState(blank_area, image_offset)
local page_area = self.view:getPageArea(
self.view.state.page,
self.view.state.zoom,
@ -726,15 +771,19 @@ function ReaderPaging:getPrevPageState(blank_area, offset)
visible_area.w, visible_area.h = blank_area.w, blank_area.h
visible_area.x = page_area.x
visible_area.y = page_area.y + page_area.h - visible_area.h
visible_area = visible_area:shrinkInside(page_area, offset.x, offset.y)
visible_area = visible_area:shrinkInside(page_area, image_offset.x, image_offset.y)
-- shrink blank area by the height of visible area
blank_area.h = blank_area.h - visible_area.h
local page_offset = Geom:new{x = self.view.state.offset.x, y = 0}
if blank_area.w > page_area.w then
page_offset:offsetBy((blank_area.w - page_area.w) / 2, 0)
end
return {
page = self.view.state.page,
zoom = self.view.state.zoom,
rotation = self.view.state.rotation,
gamma = self.view.state.gamma,
offset = Geom:new{ x = self.view.state.offset.x, y = 0},
offset = page_offset,
visible_area = visible_area,
page_area = page_area,
}
@ -747,7 +796,7 @@ function ReaderPaging:updateTopPageState(state, blank_area, offset)
w = blank_area.w,
h = blank_area.h,
}
if state.page == self.number_of_pages then
if self.ui.document:getNextPage(state.page) == 0 then -- last page
visible_area:offsetWithin(state.page_area, offset.x, offset.y)
else
visible_area = visible_area:shrinkInside(state.page_area, offset.x, offset.y)
@ -764,7 +813,7 @@ function ReaderPaging:updateBottomPageState(state, blank_area, offset)
w = blank_area.w,
h = blank_area.h,
}
if state.page == 1 then
if self.ui.document:getPrevPage(state.page) == 0 then -- first page
visible_area:offsetWithin(state.page_area, offset.x, offset.y)
else
visible_area = visible_area:shrinkInside(state.page_area, offset.x, offset.y)
@ -792,10 +841,10 @@ function ReaderPaging:genPageStatesFromTop(top_page_state, blank_area, offset)
while blank_area.h > 0 do
blank_area.h = blank_area.h - self.view.page_gap.height
if blank_area.h > 0 then
if current_page == self.number_of_pages then break end
self:_gotoPage(current_page + 1, "scrolling")
current_page = current_page + 1
state = self:getNextPageState(blank_area, Geom:new{})
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())
table.insert(page_states, state)
end
end
@ -817,13 +866,23 @@ function ReaderPaging:genPageStatesFromBottom(bottom_page_state, blank_area, off
while blank_area.h > 0 do
blank_area.h = blank_area.h - self.view.page_gap.height
if blank_area.h > 0 then
if current_page == 1 then break end
self:_gotoPage(current_page - 1, "scrolling")
current_page = current_page - 1
state = self:getPrevPageState(blank_area, Geom:new{})
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())
table.insert(page_states, 1, state)
end
end
if current_page == 0 then
-- We reached the start of document: we may have truncated too much
-- of the bottom page while scrolling up.
-- Re-generate everything with first page starting at top
offset = Geom:new{x = 0, y = 0}
blank_area:setSizeTo(self.view.visible_area)
local first_page_state = page_states[1]
first_page_state.visible_area.y = 0 -- anchor first page at top
return self:genPageStatesFromTop(first_page_state, blank_area, offset)
end
return page_states
end
@ -831,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
@ -868,14 +927,14 @@ function ReaderPaging:onScrollPageRel(page_diff)
-- page down, last page should be moved to top
local last_page_state = table.remove(self.view.page_states)
local last_visible_area = last_page_state.visible_area
if last_page_state.page == self.number_of_pages and
if self.ui.document:getNextPage(last_page_state.page) == 0 and
last_visible_area.y + last_visible_area.h >= last_page_state.page_area.h then
table.insert(self.view.page_states, last_page_state)
self.ui:handleEvent(Event:new("EndOfBook"))
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{
@ -885,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)
@ -965,14 +1024,41 @@ function ReaderPaging:onGotoPageRel(diff)
goto_end(x, -x_diff)
end
local function goto_next_page()
local new_page = self.current_page + diff
local new_page
if self.ui.document:hasHiddenFlows() then
local forward = diff > 0
local pdiff = forward and math.ceil(diff) or math.ceil(-diff)
new_page = self.current_page
for i=1, pdiff do
local test_page = forward and self.ui.document:getNextPage(new_page)
or self.ui.document:getPrevPage(new_page)
if test_page == 0 then -- start or end of document reached
if forward then
new_page = self.number_of_pages + 1 -- to trigger EndOfBook below
else
new_page = 0
end
break
end
new_page = test_page
end
else
new_page = self.current_page + diff
end
if new_page > self.number_of_pages then
self.ui:handleEvent(Event:new("EndOfBook"))
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
@ -1081,7 +1167,18 @@ function ReaderPaging:onGotoPage(number, pos)
end
function ReaderPaging:onGotoRelativePage(number)
self:_gotoPage(self.current_page + number)
local new_page = self.current_page
local test_page = new_page
local forward = number > 0
for i=1, math.abs(number) do
test_page = forward and self.ui.document:getNextPage(test_page)
or self.ui.document:getPrevPage(test_page)
if test_page == 0 then -- start or end of document reached
break
end
new_page = test_page
end
self:_gotoPage(new_page)
return true
end

@ -13,35 +13,49 @@ local ReaderPanning = InputContainer:extend{
}
function ReaderPanning:init()
self:registerKeyEvents()
-- NOP our own gesture handling
self.ges_events = nil
end
function ReaderPanning:onGesture() end
function ReaderPanning:registerKeyEvents()
if Device:hasKeyboard() then
self.key_events = {
-- these will all generate the same event, just with different arguments
MoveUp = {
{ "Up" }, doc = "move visible area up",
event = "Panning", args = {0, -1} },
{ "Up" },
event = "Panning",
args = {0, -1}
},
MoveDown = {
{ "Down" }, doc = "move visible area down",
event = "Panning", args = {0, 1} },
{ "Down" },
event = "Panning",
args = {0, 1}
},
MoveLeft = {
{ "Left" }, doc = "move visible area left",
event = "Panning", args = {-1, 0} },
{ "Left" },
event = "Panning",
args = {-1, 0}
},
MoveRight = {
{ "Right" }, doc = "move visible area right",
event = "Panning", args = {1, 0} },
{ "Right" },
event = "Panning",
args = {1, 0}
},
}
end
end
function ReaderPanning:onSetDimensions(dimensions)
self.dimen = dimensions
end
ReaderPanning.onPhysicalKeyboardConnected = ReaderPanning.registerKeyEvents
function ReaderPanning:onPanning(args, _)
local dx, dy = unpack(args)
-- for now, bounds checking/calculation is done in the view
self.view:PanningUpdate(
dx * self.panning_steps.normal * self.dimen.w * (1/100),
dy * self.panning_steps.normal * self.dimen.h * (1/100))
dx * self.panning_steps.normal * self.view.visible_area.w * (1/100),
dy * self.panning_steps.normal * self.view.visible_area.h * (1/100))
return true
end

File diff suppressed because it is too large Load Diff

@ -8,21 +8,33 @@ local ReaderRotation = InputContainer:extend{
}
function ReaderRotation:init()
self:registerKeyEvents()
-- NOP our own gesture handling
self.ges_events = nil
end
function ReaderRotation:onGesture() end
function ReaderRotation:registerKeyEvents()
if Device:hasKeyboard() then
self.key_events = {
-- these will all generate the same event, just with different arguments
RotateLeft = {
{"J"},
doc = "rotate left by 90 degrees",
event = "Rotate", args = -90 },
{ "J" },
event = "Rotate",
args = -90
},
RotateRight = {
{"K"},
doc = "rotate right by 90 degrees",
event = "Rotate", args = 90 },
{ "K" },
event = "Rotate",
args = 90
},
}
end
end
ReaderRotation.onPhysicalKeyboardConnected = ReaderRotation.registerKeyEvents
--- @todo Reset rotation on new document, maybe on new page?
function ReaderRotation:onRotate(rotate_by)

@ -4,11 +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
@ -26,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
@ -71,22 +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
-- 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
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)
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
@ -98,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
@ -111,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 = {
{
{
@ -121,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()
@ -178,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
@ -247,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
@ -282,12 +411,12 @@ function ReaderSearch:onShowSearchDialog(text, direction, regex, case_insensitiv
self.wait_button.movable:setMovedOffset(self.search_dialog.movable:getMovedOffset())
UIManager:show(self.wait_button)
UIManager:tickAfterNext(function()
do_search(func, pattern, param, regex, case_insensitive)()
do_search(func, pattern, param)()
UIManager:close(self.wait_button)
end)
end
else
return do_search(func, pattern, param, regex, case_insensitive)
return do_search(func, pattern, param)
end
end
self.search_dialog = ButtonDialog:new{
@ -310,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,
},
@ -337,14 +465,14 @@ function ReaderSearch:onShowSearchDialog(text, direction, regex, case_insensitiv
-- initial position: center of the screen
UIManager:show(self.wait_button)
UIManager:tickAfterNext(function()
do_search(self.searchFromCurrent, text, direction, regex, case_insensitive)()
do_search(self.searchFromCurrent, text, direction)()
UIManager:close(self.wait_button)
UIManager:show(self.search_dialog)
--- @todo regional
UIManager:setDirty(self.dialog, "partial")
end)
else
do_search(self.searchFromCurrent, text, direction, regex, case_insensitive)()
do_search(self.searchFromCurrent, text, direction)()
UIManager:show(self.search_dialog)
--- @todo regional
UIManager:setDirty(self.dialog, "partial")
@ -365,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
@ -377,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)
@ -411,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

@ -1,14 +1,13 @@
local BD = require("ui/bidi")
local BookStatusWidget = require("ui/widget/bookstatuswidget")
local ButtonDialogTitle = require("ui/widget/buttondialogtitle")
local Device = require("device")
local Event = require("ui/event")
local FileManagerBookInfo = require("apps/filemanager/filemanagerbookinfo")
local InfoMessage = require("ui/widget/infomessage")
local UIManager = require("ui/uimanager")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local util = require("util")
local _ = require("gettext")
local T = require("ffi/util").template
local ReaderStatus = WidgetContainer:extend{
document = nil,
@ -36,45 +35,42 @@ end
function ReaderStatus:onEndOfBook()
Device:performHapticFeedback("CONTEXT_CLICK")
local settings = G_reader_settings:readSetting("end_document_action")
local choose_action
local collate = true
local QuickStart = require("ui/quickstart")
local last_file = G_reader_settings:readSetting("lastfile")
if last_file and last_file == QuickStart.quickstart_filename then
self:openFileBrowser()
if last_file == QuickStart.quickstart_filename then
-- Like onOpenNextDocumentInFolder, delay this so as not to break instance lifecycle
UIManager:nextTick(function()
self:openFileBrowser()
end)
return
end
if G_reader_settings:readSetting("collate") == "access" then
collate = false
end
-- Should we start by marking the book as read?
-- Should we start by marking the book as finished?
if G_reader_settings:isTrue("end_document_auto_mark") then
self:onMarkBook(true)
end
if (settings == "pop-up" or settings == nil) and UIManager:getTopWidget() ~= "end_document" then
local next_file_enabled = G_reader_settings:readSetting("collate") ~= "access"
local settings = G_reader_settings:readSetting("end_document_action")
local top_widget = UIManager:getTopmostVisibleWidget() or {}
if (settings == "pop-up" or settings == nil) and top_widget.name ~= "end_document" then
local button_dialog
local buttons = {
{
{
text_func = function()
if self.settings.data.summary and self.settings.data.summary.status == "complete" then
return _("Mark as reading")
else
return _("Mark as read")
end
return self.summary.status == "complete" and _("Mark as reading") or _("Mark as finished")
end,
callback = function()
UIManager:close(button_dialog)
self:onMarkBook()
UIManager:close(choose_action)
end,
},
{
text = _("Book status"),
callback = function()
UIManager:close(button_dialog)
self:onShowBookStatus()
UIManager:close(choose_action)
end,
},
@ -83,16 +79,16 @@ function ReaderStatus:onEndOfBook()
{
text = _("Go to beginning"),
callback = function()
UIManager:close(button_dialog)
self.ui:handleEvent(Event:new("GoToBeginning"))
UIManager:close(choose_action)
end,
},
{
text = _("Open next file"),
enabled = collate,
enabled = next_file_enabled,
callback = function()
self:openNextFile(self.document.file)
UIManager:close(choose_action)
UIManager:close(button_dialog)
self:onOpenNextDocumentInFolder()
end,
},
},
@ -100,49 +96,40 @@ function ReaderStatus:onEndOfBook()
{
text = _("Delete file"),
callback = function()
self:deleteFile(self.document.file, false)
UIManager:close(choose_action)
UIManager:close(button_dialog)
self:deleteFile()
end,
},
{
text = _("File browser"),
callback = function()
self:openFileBrowser()
UIManager:close(choose_action)
end,
},
},
{
{
text = _("Cancel"),
callback = function()
UIManager:close(choose_action)
UIManager:close(button_dialog)
-- Ditto
UIManager:nextTick(function()
self:openFileBrowser()
end)
end,
},
},
}
choose_action = ButtonDialogTitle:new{
button_dialog = ButtonDialogTitle:new{
name = "end_document",
title = _("You've reached the end of the document.\nWhat would you like to do?"),
title_align = "center",
buttons = buttons,
}
UIManager:show(choose_action)
UIManager:show(button_dialog)
elseif settings == "book_status" then
self:onShowBookStatus()
elseif settings == "next_file" then
if G_reader_settings:readSetting("collate") ~= "access" then
if next_file_enabled then
local info = InfoMessage:new{
text = _("Searching next file…"),
}
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:openNextFile(self.document.file)
end)
self:onOpenNextDocumentInFolder()
else
UIManager:show(InfoMessage:new{
text = _("Could not open next file. Sort by last read date does not support this feature."),
@ -158,7 +145,7 @@ function ReaderStatus:onEndOfBook()
elseif settings == "mark_read" then
self:onMarkBook(true)
UIManager:show(InfoMessage:new{
text = _("You've reached the end of the document.\nThe current book is marked as read."),
text = _("You've reached the end of the document.\nThe current book is marked as finished."),
timeout = 3
})
elseif settings == "book_status_file_browser" then
@ -170,7 +157,7 @@ function ReaderStatus:onEndOfBook()
elseif settings == "delete_file" then
-- Ditto
UIManager:nextTick(function()
self:deleteFile(self.document.file, true)
self:deleteFile()
end)
end
end
@ -183,15 +170,15 @@ function ReaderStatus:openFileBrowser()
end
end
function ReaderStatus:openNextFile(next_file)
local FileManager = require("apps/filemanager/filemanager")
if not FileManager.instance then
self.ui:showFileManager()
end
next_file = FileManager.instance.file_chooser:getNextFile(next_file)
FileManager.instance:onClose()
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."),
@ -199,34 +186,23 @@ function ReaderStatus:openNextFile(next_file)
end
end
function ReaderStatus:deleteFile(file, text_end_book)
local ConfirmBox = require("ui/widget/confirmbox")
local message_end_book = ""
if text_end_book then
message_end_book = "You've reached the end of the document.\n"
function ReaderStatus:deleteFile()
self.settings:flush() -- enable additional warning text for newly opened file
local FileManager = require("apps/filemanager/filemanager")
local function pre_delete_callback()
self.ui:onClose()
end
UIManager:show(ConfirmBox:new{
text = T(_("%1Are you sure that you want to delete this file?\n%2\nIf you delete a file, it is permanently lost."), message_end_book, BD.filepath(file)),
ok_text = _("Delete"),
ok_callback = function()
local FileManager = require("apps/filemanager/filemanager")
self.ui:onClose()
FileManager:deleteFile(file)
require("readhistory"):fileDeleted(file) -- (will update "lastfile")
if FileManager.instance then
FileManager.instance.file_chooser:refreshPath()
else
local path = util.splitFilePathName(file)
FileManager:showFiles(path)
end
end,
})
local function post_delete_callback()
local path = util.splitFilePathName(self.document.file)
FileManager:showFiles(path)
end
FileManager:showDeleteFileDialog(self.document.file, post_delete_callback, pre_delete_callback)
end
function ReaderStatus:onShowBookStatus(before_show_callback)
local status_page = BookStatusWidget:new {
thumbnail = self.document:getCoverPageImage(),
props = self.document:getProps(),
thumbnail = FileManagerBookInfo:getCoverImage(self.document),
props = self.ui.doc_props,
document = self.document,
settings = self.settings,
ui = self.ui,
@ -239,31 +215,19 @@ function ReaderStatus:onShowBookStatus(before_show_callback)
return true
end
-- If mark_read is true then we change status only from reading/abandoned to read (complete).
-- Otherwise we change status from reading/abandoned to read or from read to reading.
-- If mark_read is true then we change status only from reading/abandoned to complete.
-- Otherwise we change status from reading/abandoned to complete or from complete to reading.
function ReaderStatus:onMarkBook(mark_read)
if self.settings.data.summary then
if self.settings.data.summary.status and self.settings.data.summary.status == "complete" then
if mark_read then
-- Keep mark as read.
self.settings.data.summary.status = "complete"
else
-- Change current status from read (complete) to reading
self.settings.data.summary.status = "reading"
end
else
self.settings.data.summary.status = "complete"
end
else
self.settings.data.summary = {status = "complete"}
end
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.settings.data.summary)
self.settings:saveSetting("summary", self.summary)
self.settings:flush()
end
function ReaderStatus:onReadSettings(config)
self.settings = config
self.summary = config:readSetting("summary") or {}
end
return ReaderStatus

@ -1,10 +1,12 @@
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")
local DataStorage = require("datastorage")
local Device = require("device")
local Dispatcher = require("dispatcher")
local Event = require("ui/event")
local Font = require("ui/font")
local FrameContainer = require("ui/widget/container/framecontainer")
@ -24,6 +26,7 @@ local lfs = require("libs/libkoreader-lfs")
local logger = require("logger")
local util = require("util")
local _ = require("gettext")
local C_ = _.pgettext
local Screen = Device.screen
local T = require("ffi/util").template
@ -50,11 +53,7 @@ function TweakInfoWidget:init()
}
}
end
if Device:hasKeys() then
self.key_events = {
Close = { {Device.input.group.Back}, doc = "cancel" }
}
end
self:registerKeyEvents()
local content = VerticalGroup:new{
TextBoxWidget:new{
@ -127,25 +126,34 @@ function TweakInfoWidget:init()
local buttons = {
{
text = _("Close"),
callback = function()
UIManager:close(self)
end,
{
text = self.is_tweak_in_dispatcher and _("Don't show in action list") or _("Show in action list"),
callback = function()
self.toggle_tweak_in_dispatcher_callback()
UIManager:close(self)
end,
},
},
{
text = self.is_global_default and _("Don't use on all books") or _("Use on all books"),
callback = function()
self.toggle_global_default_callback()
UIManager:close(self)
end,
{
text = _("Close"),
callback = function()
UIManager:close(self)
end,
},
{
text = self.is_global_default and _("Don't use on all books") or _("Use on all books"),
callback = function()
self.toggle_global_default_callback()
UIManager:close(self)
end,
},
},
}
local button_table = ButtonTable:new{
width = content:getSize().w,
button_font_face = "cfont",
button_font_size = 20,
buttons = { buttons },
buttons = buttons,
zero_sep = true,
show_parent = self,
}
@ -154,7 +162,6 @@ function TweakInfoWidget:init()
FrameContainer:new{
background = Blitbuffer.COLOR_WHITE,
radius = Size.radius.window,
margin = Size.margin.default,
padding = Size.padding.default,
padding_bottom = 0, -- no padding below buttontable
VerticalGroup:new{
@ -170,6 +177,14 @@ function TweakInfoWidget:init()
}
end
function TweakInfoWidget:registerKeyEvents()
if Device:hasKeys() then
self.key_events.Close = { { Device.input.group.Back } }
end
end
TweakInfoWidget.onPhysicalKeyboardConnected = TweakInfoWidget.registerKeyEvents
function TweakInfoWidget:onShow()
UIManager:setDirty(self, function()
return "ui", self.movable.dimen
@ -233,6 +248,7 @@ local ReaderStyleTweak = WidgetContainer:extend{
nb_enabled_tweaks = 0, -- for use by main menu item
css_text = nil, -- aggregated css text from tweaks individual css snippets
enabled = true, -- allows for toggling between selected tweaks / none
dispatcher_prefix = "style_tweak_",
}
function ReaderStyleTweak:isTweakEnabled(tweak_id)
@ -423,12 +439,23 @@ function ReaderStyleTweak:onSaveSettings()
end
self.ui.doc_settings:saveSetting("style_tweaks", util.tableSize(self.doc_tweaks) > 0 and self.doc_tweaks or nil)
G_reader_settings:saveSetting("style_tweaks", self.global_tweaks)
G_reader_settings:saveSetting("style_tweaks_in_dispatcher", self.tweaks_in_dispatcher)
self.ui.doc_settings:saveSetting("book_style_tweak", self.book_style_tweak)
self.ui.doc_settings:saveSetting("book_style_tweak_enabled", self.book_style_tweak_enabled)
self.ui.doc_settings:saveSetting("book_style_tweak_last_edit_pos", self.book_style_tweak_last_edit_pos)
end
local function dispatcherRegisterStyleTweak(tweak_id, tweak_title)
Dispatcher:registerAction(ReaderStyleTweak.dispatcher_prefix..tweak_id,
{category="none", event="ToggleStyleTweak", arg=tweak_id, title=T(_("Toggle style tweak: %1"), tweak_title), rolling=true})
end
local function dispatcherUnregisterStyleTweak(tweak_id)
Dispatcher:removeAction(ReaderStyleTweak.dispatcher_prefix..tweak_id)
end
function ReaderStyleTweak:init()
self.tweaks_in_dispatcher = G_reader_settings:readSetting("style_tweaks_in_dispatcher") or {}
self.tweaks_by_id = {}
self.tweaks_table = {}
@ -499,6 +526,9 @@ You can enable individual tweaks on this book with a tap, or view more details a
if self.global_tweaks[item.id] then
title = title .. ""
end
if self.tweaks_in_dispatcher[item.id] then
title = title .. " \u{F144}"
end
return title
end,
hold_callback = function(touchmenu_instance)
@ -530,27 +560,26 @@ You can enable individual tweaks on this book with a tap, or view more details a
end
touchmenu_instance:updateItems()
self:updateCssText(true) -- apply it immediately
end
end,
is_tweak_in_dispatcher = self.tweaks_in_dispatcher[item.id],
toggle_tweak_in_dispatcher_callback = function()
if self.tweaks_in_dispatcher[item.id] then
self.tweaks_in_dispatcher[item.id] = nil
dispatcherUnregisterStyleTweak(item.id)
if self.ui.profiles then
self.ui.profiles:updateProfiles(self.dispatcher_prefix..item.id)
end
else
self.tweaks_in_dispatcher[item.id] = item.title
dispatcherRegisterStyleTweak(item.id, item.title)
end
touchmenu_instance:updateItems()
end,
})
end,
callback = function()
-- enable/disable only for this book
local enabled, g_enabled = self:isTweakEnabled(item.id)
if enabled then
if g_enabled then
-- if globaly enabled, mark it as disabled
-- for this document only
self.doc_tweaks[item.id] = false
else
self.doc_tweaks[item.id] = nil
end
else
if item.conflicts_with then
self:resolveConflictsBeforeEnabling(item.id, item.conflicts_with)
end
self.doc_tweaks[item.id] = true
end
self:updateCssText(true) -- apply it immediately
self:onToggleStyleTweak(item.id, item, true) -- no notification
end,
separator = item.separator,
})
@ -602,7 +631,7 @@ You can enable individual tweaks on this book with a tap, or view more details a
local mode = lfs.attributes(dir.."/"..f, "mode")
if mode == "directory" then
table.insert(dir_list, f)
elseif mode == "file" and string.match(f, "%.css$") then
elseif mode == "file" and string.match(f, "%.css$") and not util.stringStartsWith(f, "._") then
table.insert(file_list, f)
end
end
@ -667,6 +696,7 @@ You can enable individual tweaks on this book with a tap, or view more details a
table.insert(self.tweaks_table, book_tweak_item)
self.ui.menu:registerToMainMenu(self)
self:onDispatcherRegisterActions()
end
function ReaderStyleTweak:addToMainMenu(menu_items)
@ -683,14 +713,67 @@ function ReaderStyleTweak:addToMainMenu(menu_items)
}
end
local BOOK_TWEAK_SAMPLE_CSS = [[
p.someTitleClassName { text-indent: 0; }
function ReaderStyleTweak:onToggleStyleTweak(tweak_id, item, no_notification)
local text
local enabled, g_enabled = self:isTweakEnabled(tweak_id)
if enabled then
if g_enabled then
-- if globaly enabled, mark it as disabled
-- for this document only
self.doc_tweaks[tweak_id] = false
else
self.doc_tweaks[tweak_id] = nil
end
text = T(C_("Style tweak", "Off: %1"), self.tweaks_in_dispatcher[tweak_id])
else
local conflicts_with
if item then
conflicts_with = item.conflicts_with
else -- called from Dispatcher
for _, v in ipairs(CssTweaks) do
if v.id == tweak_id then
conflicts_with = v.conflicts_with
break
end
end
end
if conflicts_with then
self:resolveConflictsBeforeEnabling(tweak_id, conflicts_with)
end
self.doc_tweaks[tweak_id] = true
text = T(C_("Style tweak", "On: %1"), self.tweaks_in_dispatcher[tweak_id])
end
self:updateCssText(true) -- apply it immediately
if not no_notification then
UIManager:show(Notification:new{
text = text,
})
end
end
DIV.advertisement { display: none !important; }
function ReaderStyleTweak:onDispatcherRegisterActions()
for tweak_id, tweak_title in pairs(self.tweaks_in_dispatcher) do
dispatcherRegisterStyleTweak(tweak_id, tweak_title)
end
end
local BOOK_TWEAK_SAMPLE_CSS = [[
/* Remove indent from some P used as titles */
p.someTitleClassName {
text-indent: 0;
}
/* Get in-page footnotes when no tweak works */
.footnoteContainerClassName {
-cr-hint: footnote-inpage;
margin: 0 !important;
}
/* Help getting some alternative ToC when no headings */
.someSeparatorClassName {
-cr-hint: toc-level1;
break-before: always;
}
/* Hide annoying content */
DIV.someAdvertisement {
display: none !important;
}
]]
@ -699,6 +782,126 @@ 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 = {
{ _("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)
local InputDialog = require("ui/widget/inputdialog")
local editor -- our InputDialog instance
@ -751,7 +954,7 @@ function ReaderStyleTweak:editBookTweak(touchmenu_instance)
add_nav_bar = true,
scroll_by_pan = true,
buttons = {{
-- First button on first row (row will be completed with Reset|Save|Close)
-- First buttons on first row (row will be completed with Reset|Save|Close)
{
id = tweak_button_id,
text_func = function()
@ -769,6 +972,110 @@ function ReaderStyleTweak:editBookTweak(touchmenu_instance)
end
end,
},
{
id = "css_suggestions_button_id",
text = "CSS \u{2261}",
callback = function()
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 = text,
id = title,
align = "left",
callback = function()
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 = description and function()
UIManager:show(InfoMessage:new{ text = description })
end or nil
}})
end
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 = buttons,
anchor = function()
-- we return prefers_pop_down=true so it pops over the keyboard
-- instead of the text if it can
return editor.button_table:getButtonById("css_suggestions_button_id").dimen, true
end,
}
UIManager:show(suggestions_popup_widget)
end,
},
}},
edited_callback = function()
if not editor then
@ -789,10 +1096,10 @@ function ReaderStyleTweak:editBookTweak(touchmenu_instance)
end
end
end,
-- Set/save view and cursor position callback
-- Store/retrieve view and cursor position callback
view_pos_callback = function(top_line_num, charpos)
-- This same callback is called with no argument to get initial position,
-- and with arguments to give back final position when closed.
-- This same callback is called with no arguments on init to retrieve the stored initial position,
-- and with arguments to store the final position on close.
if top_line_num and charpos then
self.book_style_tweak_last_edit_pos = {top_line_num, charpos}
else

@ -2,11 +2,11 @@ local Blitbuffer = require("ffi/blitbuffer")
local Cache = require("cache")
local Device = require("device")
local Geom = require("ui/geometry")
local InputContainer = require("ui/widget/container/inputcontainer")
local Persist = require("persist")
local RenderImage = require("ui/renderimage")
local TileCacheItem = require("document/tilecacheitem")
local UIManager = require("ui/uimanager")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local Screen = Device.screen
local ffiutil = require("ffi/util")
local logger = require("logger")
@ -18,13 +18,15 @@ local _ = require("gettext")
-- It handles launching via the menu or Dispatcher/Gestures two fullscreen
-- widgets related to showing pages and thumbnails that will make use of
-- its services: BookMap and PageBrowser.
local ReaderThumbnail = WidgetContainer:extend{}
local ReaderThumbnail = InputContainer:extend{}
function ReaderThumbnail:init()
if not Device:isTouchDevice() then
self:registerKeyEvents()
if not Device:isTouchDevice() and not Device:useDPadAsActionKeys() then
-- The BookMap and PageBrowser widgets depend too much on gestures,
-- making them work with keys would be hard and very limited, so
-- making them work with not enough keys on Non-Touch would be hard and very limited, so
-- just don't make them available.
-- We will only let BookMap run on useDPadAsActionKeys devices.
return
end
@ -62,13 +64,31 @@ function ReaderThumbnail:init()
end
end
function ReaderThumbnail:registerKeyEvents()
if Device:hasDPad() and Device:useDPadAsActionKeys() then
if Device:hasKeyboard() then
self.key_events.ShowBookMap = { { "Shift", "Down" } }
else
self.key_events.ShowBookMap = { { "ScreenKB", "Down" } }
end
end
end
function ReaderThumbnail:addToMainMenu(menu_items)
menu_items.book_map = {
text = _("Book map"),
callback = function()
self:onShowBookMap()
end,
-- Show the alternative overview mode (which is just a restricted
-- variation of the main book map) with long-press (let's avoid
-- adding another item in the crowded first menu).
hold_callback = function()
self:onShowBookMap(true)
end,
}
-- PageBrowser still needs some work before we can let it run on non-touch devices with useDPadAsActionKeys
if Device:hasDPad() and Device:useDPadAsActionKeys() then return end
menu_items.page_browser = {
text = _("Page browser"),
callback = function()
@ -77,10 +97,11 @@ function ReaderThumbnail:addToMainMenu(menu_items)
}
end
function ReaderThumbnail:onShowBookMap()
function ReaderThumbnail:onShowBookMap(overview_mode)
local BookMapWidget = require("ui/widget/bookmapwidget")
UIManager:show(BookMapWidget:new{
ui = self.ui,
overview_mode = overview_mode,
})
return true
end
@ -179,11 +200,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
@ -199,12 +220,15 @@ function ReaderThumbnail:resetCachedPagesForBookmarks(...)
end
else
if bm.page and type(bm.page) == "number" then
local p = bm.page
if not start_page or p < start_page then
start_page = p
end
if not end_page or p > end_page then
end_page = p
local bm_page0 = (bm.pos0 and bm.pos0.page) or bm.page
local bm_page1 = (bm.pos1 and bm.pos1.page) or bm.page
for p = bm_page0, bm_page1 do
if not start_page or p < start_page then
start_page = p
end
if not end_page or p > end_page then
end_page = p
end
end
end
end
@ -240,6 +264,10 @@ end
function ReaderThumbnail:getPageThumbnail(page, width, height, batch_id, when_generated_callback)
self:setupCache()
self.current_target_size_tag = string.format("w%d_h%d", width, height)
if self.ui.rolling and Screen.night_mode and self.ui.document.configurable.nightmode_images == 1 then
-- We'll get a different bb in this case: it needs its own cache hash
self.current_target_size_tag = self.current_target_size_tag .. "_nm"
end
local hash = string.format("p%d-%s", page, self.current_target_size_tag)
local tile = self.tile_cache and self.tile_cache:check(hash)
if tile then
@ -264,6 +292,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
@ -304,6 +336,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
@ -404,16 +441,24 @@ function ReaderThumbnail:_getPageImage(page)
if self.ui.view.highlight.lighten_factor < 0.3 then
self.ui.view.highlight.lighten_factor = 0.3 -- make lighten highlight a bit darker
end
self.ui.highlight.select_mode = false -- Remove any select mode icon
if self.ui.rolling then
-- CRE documents: pages all have the aspect ratio of our screen (alt top status bar
-- will be croped out after drawing), we will show them just as rendered.
self.ui.view:onSetViewMode("page") -- Get out of scroll mode
if self.ui.font.gamma_index < 30 then -- Increase font gamma (if not already increased),
self.ui.rolling.rendering_state = nil -- Remove any partial rerendering icon
if self.ui.view.view_mode == "scroll" then
-- Get out of scroll mode, and be sure we'll be in one-page mode as that
-- is what is shown in scroll mode (needs to do the following in that
-- order to avoid rendering hash change)
self.ui.rolling:onSetVisiblePages(1)
self.ui.view:onSetViewMode("page")
end
if self.ui.document.configurable.font_gamma < 30 then -- Increase font gamma (if not already increased),
self.ui.document:setGammaIndex(30) -- as downscaling will make text grayer
end
self.ui.document:setImageScaling(false) -- No need for smooth scaling as all will be downscaled
self.ui.document:setNightmodeImages(false) -- We don't invert page images even if nightmode set: keep images as-is
-- (We keep "nighmode_images" as it was set: we may get and cache a different bb whether nightmode is on or off)
self.ui.view.state.page = page -- Be on requested page
self.ui.document:gotoPage(page) -- Current xpointer needs to be updated for some of what follows
self.ui.bookmark:onPageUpdate(page) -- Update dogear state for this page
@ -491,6 +536,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()
@ -499,10 +548,9 @@ function ReaderThumbnail:onColorRenderingUpdate()
end
-- CRE: emitted after a re-rendering
ReaderThumbnail.onTocReset = ReaderThumbnail.resetCache
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.onAnnotationsModified = ReaderThumbnail.resetCachedPagesForBookmarks
return ReaderThumbnail

@ -29,19 +29,12 @@ local ReaderToc = InputContainer:extend{
collapsed_toc = nil, -- table
collapse_depth = 2,
expanded_nodes = nil, -- table
toc_menu_title = _("Table of contents"),
alt_toc_menu_title = _("Table of contents *"),
toc_items_per_page_default = 14,
alt_toc_symbol = "\u{E298}" -- two-folders in circle (custom toc uses a pen in square symbol)
}
function ReaderToc:init()
if Device:hasKeyboard() then
self.key_events = {
ShowToc = {
{ "T" },
doc = "show Table of Content menu" },
}
end
self:registerKeyEvents()
if G_reader_settings:hasNot("toc_items_per_page") then
-- The TOC items per page and items' font size can now be
@ -59,8 +52,34 @@ function ReaderToc:init()
self:resetToc()
self.ui.menu:registerToMainMenu(self)
-- NOP our own gesture handling
self.ges_events = nil
end
function ReaderToc:onGesture() end
function ReaderToc:registerKeyEvents()
if Device:hasScreenKB() then
self.key_events.ShowToc = { { "ScreenKB", "Up" } }
elseif Device:hasKeyboard() then
self.key_events.ShowToc = { { "T" } }
end
end
function ReaderToc:getTitle()
local title = _("Table of contents")
-- Handmade ToC has precedence over alternative ToC
if self.ui.handmade:isHandmadeTocEnabled() then
title = title .. " " .. self.ui.handmade.custom_toc_symbol
elseif self.ui.document:isTocAlternativeToc() then
title = title .. " " .. self.alt_toc_symbol
end
return title
end
ReaderToc.onPhysicalKeyboardConnected = ReaderToc.registerKeyEvents
function ReaderToc:onReadSettings(config)
self.toc_ticks_ignored_levels = config:readSetting("toc_ticks_ignored_levels") or {}
self.toc_chapter_navigation_bind_to_ticks = config:readSetting("toc_chapter_navigation_bind_to_ticks")
@ -76,7 +95,7 @@ end
function ReaderToc:cleanUpTocTitle(title, replace_empty)
title = title:gsub("\13", "")
if replace_empty and title:match("^%s*$") then
title = "\xE2\x80\x93" -- U+2013 En-Dash
title = "\u{2013}" -- En-Dash
end
return title
end
@ -87,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
@ -99,9 +119,12 @@ end
function ReaderToc:onUpdateToc()
self:resetToc()
self.ui:handleEvent(Event:new("TocReset"))
return true
end
--- @note: Let this propagate, plugins/statistics uses it to react to changes in document pagination
--return true
-- Be sure to update the ToC after a CRE rerendering
function ReaderToc:onDocumentRerendered()
self:onUpdateToc()
end
function ReaderToc:onPageUpdate(pageno)
@ -170,8 +193,10 @@ function ReaderToc:validateAndFixToc()
local has_bogus
local cur_page = 0
local max_depth = 0
local cur_seq_by_level = {}
for i = first, last do
local page = toc[i].page
local item = toc[i]
local page = item.page
if page < cur_page then
has_bogus = true
break
@ -179,9 +204,15 @@ function ReaderToc:validateAndFixToc()
cur_page = page
-- Use this loop to compute max_depth here (if has_bogus,
-- we will recompute it in the loop below)
if toc[i].depth > max_depth then
max_depth = toc[i].depth
local depth = item.depth
if depth > max_depth then
max_depth = depth
end
-- Also use this loop to compute seq_in_level for each
-- item, needed by BookMap with alternative theme
local seq = (cur_seq_by_level[depth] or 0) + 1
item.seq_in_level = seq
cur_seq_by_level[depth] = seq
end
if not has_bogus then -- no TOC items, or all are valid
logger.dbg("validateAndFixToc(): TOC is fine")
@ -193,6 +224,7 @@ function ReaderToc:validateAndFixToc()
-- Bad ordering previously noticed: try to fix the wrong items' page
-- by setting it to the previous or next good item page.
max_depth = 0 -- recompute this
cur_seq_by_level = {}
local nb_bogus = 0
local nb_fixed_pages = 0
-- We fix only one bogus item per loop, taking the option that
@ -205,10 +237,17 @@ function ReaderToc:validateAndFixToc()
-- (These cases are met in the following code with cur_page=57 and page=6)
cur_page = 0
for i = first, last do
if toc[i].depth > max_depth then
max_depth = toc[i].depth
local item = toc[i]
-- Recompute max_depth and item's seq_in_level
local depth = item.depth
if depth > max_depth then
max_depth = depth
end
local page = toc[i].fixed_page or toc[i].page
local seq = (cur_seq_by_level[depth] or 0) + 1
item.seq_in_level = seq
cur_seq_by_level[depth] = seq
-- Look for bogus page
local page = item.fixed_page or item.page
if page >= cur_page then
cur_page = page
else
@ -262,7 +301,7 @@ function ReaderToc:validateAndFixToc()
logger.dbg(" fix next", j, toc[j].page, "=>", fixed_page)
end
end
cur_page = toc[i].fixed_page or toc[i].page
cur_page = item.fixed_page or item.page
end
end
if nb_bogus > 0 then
@ -278,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
@ -285,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
@ -300,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)
@ -533,78 +603,54 @@ function ReaderToc:isChapterEnd(cur_pageno)
end
function ReaderToc:getChapterPageCount(pageno)
if self.ui.document:hasHiddenFlows() then
-- Count pages until new chapter, starting by going backwards to the beginning of the current chapter if necessary
local page_count = 1
if not self:isChapterStart(pageno) then
local test_page = self.ui.document:getPrevPage(pageno)
while test_page > 0 do
page_count = page_count + 1
if self:isChapterStart(test_page) then
break
end
test_page = self.ui.document:getPrevPage(test_page)
local next_chapter = self:getNextChapter(pageno) or self.ui.document:getPageCount() + 1
local previous_chapter = self:isChapterStart(pageno) and pageno or self:getPreviousChapter(pageno) or 1
local page_count = next_chapter - previous_chapter
if self.ui.document:hasHiddenFlows() and self.ui.document:getPageFlow(pageno) == 0 then
-- If current page in a hidden flow, return the full amount of pages in this chapter.
-- Otherwise, count only pages in the main flow
for page = previous_chapter, next_chapter - 1 do
if self.ui.document:getPageFlow(page) ~= 0 then
page_count = page_count - 1
end
end
-- Then forward
local test_page = self.ui.document:getNextPage(pageno)
while test_page > 0 do
page_count = page_count + 1
if self:isChapterStart(test_page) then
return page_count - 1
end
test_page = self.ui.document:getNextPage(test_page)
end
else
local next_chapter = self:getNextChapter(pageno) or self.ui.document:getPageCount() + 1
local previous_chapter = self:isChapterStart(pageno) and pageno or self:getPreviousChapter(pageno) or 1
local page_count = next_chapter - previous_chapter
return page_count
end
return page_count
end
function ReaderToc:getChapterPagesLeft(pageno)
if self.ui.document:hasHiddenFlows() then
-- Count pages until new chapter
local pages_left = 0
local test_page = self.ui.document:getNextPage(pageno)
while test_page > 0 do
pages_left = pages_left + 1
if self:isChapterStart(test_page) then
return pages_left - 1
local next_chapter = self:getNextChapter(pageno)
if not next_chapter then
-- (ReaderFooter deals itself with nil and pageno in last chapter)
return
end
local pages_left = next_chapter - pageno - 1
if self.ui.document:hasHiddenFlows() and self.ui.document:getPageFlow(pageno) == 0 then
for page = pageno, next_chapter - 1 do
if self.ui.document:getPageFlow(page) ~= 0 then
pages_left = pages_left - 1
end
test_page = self.ui.document:getNextPage(test_page)
end
else
local next_chapter = self:getNextChapter(pageno)
if next_chapter then
next_chapter = next_chapter - pageno - 1
end
return next_chapter
end
return pages_left
end
function ReaderToc:getChapterPagesDone(pageno)
if self:isChapterStart(pageno) then return 0 end
if self.ui.document:hasHiddenFlows() then
-- Count pages until chapter start
local pages_done = 0
local test_page = self.ui.document:getPrevPage(pageno)
while test_page > 0 do
pages_done = pages_done + 1
if self:isChapterStart(test_page) then
return pages_done
local previous_chapter = self:getPreviousChapter(pageno)
if not previous_chapter then
-- (ReaderFooter deals itself with nil and pageno not yet in first chapter)
return
end
local pages_done = pageno - previous_chapter
if self.ui.document:hasHiddenFlows() and self.ui.document:getPageFlow(pageno) == 0 then
for page = previous_chapter, pageno - 1 do
if self.ui.document:getPageFlow(page) ~= 0 then
pages_done = pages_done - 1
end
test_page = self.ui.document:getPrevPage(test_page)
end
else
local previous_chapter = self:getPreviousChapter(pageno)
if previous_chapter then
previous_chapter = pageno - previous_chapter
end
return previous_chapter
end
return pages_done
end
function ReaderToc:updateCurrentNode()
@ -649,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 = " ",
@ -667,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
@ -754,7 +810,7 @@ function ReaderToc:onShowToc()
-- This yields *slightly* better alignment between state & mandatory (in terms of effective margins).
local button_size = self.expand_button:getSize()
local toc_menu = Menu:new{
title = _("Table of Contents"),
title = self:getTitle(),
item_table = self.collapsed_toc,
state_w = can_collapse and button_size.w or 0,
ui = self.ui,
@ -936,7 +992,7 @@ function ReaderToc:addToMainMenu(menu_items)
-- insert table to main reader menu
menu_items.table_of_contents = {
text_func = function()
return self.ui.document:isTocAlternativeToc() and self.alt_toc_menu_title or self.toc_menu_title
return self:getTitle()
end,
callback = function()
self:onShowToc()
@ -949,13 +1005,22 @@ function ReaderToc:addToMainMenu(menu_items)
-- Alternative ToC (only available with CRE documents)
if self.ui.document:canHaveAlternativeToc() then
menu_items.toc_alt_toc = {
text = _("Alternative table of contents"),
help_text = _([[
text = _("Alternative table of contents") .. " " .. self.alt_toc_symbol,
help_text_func = function()
local help_text = _([[
An alternative table of contents can be built from document headings <H1> to <H6>.
If the document contains no headings, or all are ignored, the alternative ToC will be built from document fragments and will point to the start of each individual HTML file in the EPUB.
Some of the headings can be ignored, and hints can be set to other non-heading elements in a user style tweak, so they can be used as ToC items.
See Style tweaks Miscellaneous Alternative ToC hints.]]),
See Style tweaks Miscellaneous Alternative ToC hints.]])
if self.ui.handmade:isHandmadeTocEnabled() then
help_text = _([[To use the alternative ToC, disable your custom table of contents first.]]) .. "\n\n" .. help_text
end
return help_text
end,
enabled_func = function()
return not self.ui.handmade:isHandmadeTocEnabled()
end,
checked_func = function()
return self.ui.document:isTocAlternativeToc()
end,
@ -1126,8 +1191,19 @@ 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"),
text = _("Dot leaders"),
keep_menu_open = true,
checked_func = function()
return G_reader_settings:nilOrTrue("toc_items_with_dots")

@ -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,38 +26,24 @@ 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)
if config:has("embedded_fonts") then
self.embedded_fonts = config:isTrue("embedded_fonts")
else
-- default to enable embedded fonts
-- note that it's a bit confusing here:
-- global settins store 0/1, while document settings store false/true
-- we leave it that way for now to maintain backwards compatibility
local global = G_reader_settings:readSetting("copt_embedded_fonts")
self.embedded_fonts = (global == nil or global == 1) and true or false
end
-- As this is new, call it only when embedded_fonts are explicitely disabled
-- self.ui.document:setEmbeddedFonts(self.embedded_fonts and 1 or 0)
if not self.embedded_fonts then
self.ui.document:setEmbeddedFonts(0)
end
-- default to enable embedded fonts
self.ui.document:setEmbeddedFonts(self.configurable.embedded_fonts)
if config:has("embedded_css") then
self.embedded_css = config:isTrue("embedded_css")
else
-- default to enable embedded CSS
-- note that it's a bit confusing here:
-- global settings store 0/1, while document settings store false/true
-- we leave it that way for now to maintain backwards compatibility
local global = G_reader_settings:readSetting("copt_embedded_css")
self.embedded_css = (global == nil or global == 1) and true or false
end
self.ui.document:setEmbeddedStyleSheet(self.embedded_css and 1 or 0)
-- default to enable embedded CSS
self.ui.document:setEmbeddedStyleSheet(self.configurable.embedded_css)
-- Block rendering mode: stay with legacy rendering for books
-- previously opened so bookmarks and highlights stay valid.
@ -74,104 +59,98 @@ function ReaderTypeset:onReadSettings(config)
or 3 -- default to 'web' mode
end
-- Let ConfigDialog know so it can update it on screen and have it saved on quit
self.ui.document.configurable.block_rendering_mode = self.block_rendering_mode
self.configurable.block_rendering_mode = self.block_rendering_mode
end
self:setBlockRenderingMode(self.block_rendering_mode)
-- set render DPI
self.render_dpi = config:readSetting("render_dpi")
or G_reader_settings:readSetting("copt_render_dpi")
or 96
self:setRenderDPI(self.render_dpi)
-- default to 96 dpi
self.ui.document:setRenderDPI(self.configurable.render_dpi)
-- uncomment if we want font size to follow DPI changes
-- self.ui.document:setRenderScaleFontWithDPI(1)
-- set page margins
local h_margins = config:readSetting("copt_h_page_margins")
or G_reader_settings:readSetting("copt_h_page_margins")
or G_defaults:readSetting("DCREREADER_CONFIG_H_MARGIN_SIZES_MEDIUM")
local t_margin = config:readSetting("copt_t_page_margin")
or G_reader_settings:readSetting("copt_t_page_margin")
or G_defaults:readSetting("DCREREADER_CONFIG_T_MARGIN_SIZES_LARGE")
local b_margin = config:readSetting("copt_b_page_margin")
or G_reader_settings:readSetting("copt_b_page_margin")
or G_defaults:readSetting("DCREREADER_CONFIG_B_MARGIN_SIZES_LARGE")
self.unscaled_margins = { h_margins[1], t_margin, h_margins[2], b_margin }
self.unscaled_margins = { self.configurable.h_page_margins[1], self.configurable.t_page_margin,
self.configurable.h_page_margins[2], self.configurable.b_page_margin }
self:onSetPageMargins(self.unscaled_margins)
self.sync_t_b_page_margins = config:readSetting("copt_sync_t_b_page_margins")
or G_reader_settings:readSetting("copt_sync_t_b_page_margins")
or 0
self.sync_t_b_page_margins = self.sync_t_b_page_margins == 1 and true or false
self.sync_t_b_page_margins = self.configurable.sync_t_b_page_margins == 1 and true or false
-- default to disable TXT formatting as it does more harm than good
-- default to disable TXT formatting as it does more harm than good (the setting is not in UI)
self.txt_preformatted = config:readSetting("txt_preformatted")
or G_reader_settings:readSetting("txt_preformatted")
or 1
self:toggleTxtPreFormatted(self.txt_preformatted)
self.ui.document:setTxtPreFormatted(self.txt_preformatted)
-- default to disable smooth scaling for now.
if config:has("smooth_scaling") then
self.smooth_scaling = config:isTrue("smooth_scaling")
else
local global = G_reader_settings:readSetting("copt_smooth_scaling")
self.smooth_scaling = global == 1 and true or false
end
self:toggleImageScaling(self.smooth_scaling)
-- default to disable smooth scaling
self.ui.document:setImageScaling(self.configurable.smooth_scaling == 1)
-- default to automagic nightmode-friendly handling of images
if config:has("nightmode_images") then
self.nightmode_images = config:isTrue("nightmode_images")
else
local global = G_reader_settings:readSetting("copt_nightmode_images")
self.nightmode_images = (global == nil or global == 1) and true or false
end
self:toggleNightmodeImages(self.nightmode_images)
self.ui.document:setNightmodeImages(self.configurable.nightmode_images == 1)
end
function ReaderTypeset:onSaveSettings()
self.ui.doc_settings:saveSetting("css", self.css)
self.ui.doc_settings:saveSetting("embedded_css", self.embedded_css)
self.ui.doc_settings:saveSetting("embedded_fonts", self.embedded_fonts)
self.ui.doc_settings:saveSetting("render_dpi", self.render_dpi)
self.ui.doc_settings:saveSetting("smooth_scaling", self.smooth_scaling)
self.ui.doc_settings:saveSetting("nightmode_images", self.nightmode_images)
end
function ReaderTypeset:onToggleEmbeddedStyleSheet(toggle)
self:toggleEmbeddedStyleSheet(toggle)
local text
if toggle then
Notification:notify(_("Enabled embedded styles."))
self.configurable.embedded_css = 1
text = _("Enabled embedded styles.")
else
Notification:notify(_("Disabled embedded styles."))
self.configurable.embedded_css = 0
text = _("Disabled embedded styles.")
end
self.ui.document:setEmbeddedStyleSheet(self.configurable.embedded_css)
self.ui:handleEvent(Event:new("UpdatePos"))
Notification:notify(text)
return true
end
function ReaderTypeset:onToggleEmbeddedFonts(toggle)
self:toggleEmbeddedFonts(toggle)
local text
if toggle then
Notification:notify(_("Enabled embedded fonts."))
self.configurable.embedded_fonts = 1
text = _("Enabled embedded fonts.")
else
Notification:notify(_("Disabled embedded fonts."))
self.configurable.embedded_fonts = 0
text = _("Disabled embedded fonts.")
end
self.ui.document:setEmbeddedFonts(self.configurable.embedded_fonts)
self.ui:handleEvent(Event:new("UpdatePos"))
Notification:notify(text)
return true
end
function ReaderTypeset:onToggleImageScaling(toggle)
self:toggleImageScaling(toggle)
Notification:notify(T( _("Image scaling set to: %1"), optionsutil:getOptionText("ToggleImageScaling", toggle)))
self.configurable.smooth_scaling = toggle and 1 or 0
self.ui.document:setImageScaling(toggle)
self.ui:handleEvent(Event:new("UpdatePos"))
local text = T(_("Image scaling set to: %1"), optionsutil:getOptionText("ToggleImageScaling", toggle))
Notification:notify(text)
return true
end
function ReaderTypeset:onToggleNightmodeImages(toggle)
self:toggleNightmodeImages(toggle)
self.configurable.nightmode_images = toggle and 1 or 0
self.ui.document:setNightmodeImages(toggle)
self.ui:handleEvent(Event:new("UpdatePos"))
return true
end
function ReaderTypeset:onSetBlockRenderingMode(mode)
self:setBlockRenderingMode(mode)
Notification:notify(T( _("Render mode set to: %1"), optionsutil:getOptionText("SetBlockRenderingMode", mode)))
local text = T(_("Render mode set to: %1"), optionsutil:getOptionText("SetBlockRenderingMode", mode))
Notification:notify(text)
return true
end
function ReaderTypeset:onSetRenderDPI(dpi)
self.configurable.render_dpi = dpi
self.ui.document:setRenderDPI(dpi)
self.ui:handleEvent(Event:new("UpdatePos"))
local text = T(_("Zoom set to: %1"), optionsutil:getOptionText("SetRenderDPI", dpi))
Notification:notify(text)
return true
end
@ -191,23 +170,18 @@ local OBSOLETED_CSS = {
"txt.css",
}
function ReaderTypeset:onSetRenderDPI(dpi)
self:setRenderDPI(dpi)
Notification:notify(T( _("Zoom set to: %1"), optionsutil:getOptionText("SetRenderDPI", dpi)))
return true
end
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"
@ -215,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
@ -222,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
@ -233,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
@ -249,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
@ -260,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
@ -299,6 +317,7 @@ function ReaderTypeset:setStyleSheet(new_css)
end
end
-- Not used
function ReaderTypeset:setEmbededStyleSheetOnly()
if self.css ~= nil then
-- clear applied css
@ -309,30 +328,6 @@ function ReaderTypeset:setEmbededStyleSheetOnly()
end
end
function ReaderTypeset:toggleEmbeddedStyleSheet(toggle)
if not toggle then
self.embedded_css = false
self:setStyleSheet(self.ui.document.default_css)
self.ui.document:setEmbeddedStyleSheet(0)
else
self.embedded_css = true
--self:setStyleSheet(self.ui.document.default_css)
self.ui.document:setEmbeddedStyleSheet(1)
end
self.ui:handleEvent(Event:new("UpdatePos"))
end
function ReaderTypeset:toggleEmbeddedFonts(toggle)
if not toggle then
self.embedded_fonts = false
self.ui.document:setEmbeddedFonts(0)
else
self.embedded_fonts = true
self.ui.document:setEmbeddedFonts(1)
end
self.ui:handleEvent(Event:new("UpdatePos"))
end
-- crengine enhanced block rendering feature/flags (see crengine/include/lvrend.h):
-- legacy flat book web
-- ENHANCED 0x00000001 x x x
@ -404,39 +399,6 @@ function ReaderTypeset:ensureSanerBlockRenderingFlags(mode)
self:setBlockRenderingMode(self.block_rendering_mode)
end
function ReaderTypeset:toggleImageScaling(toggle)
if toggle and (toggle == true or toggle == 1) then
self.smooth_scaling = true
self.ui.document:setImageScaling(true)
else
self.smooth_scaling = false
self.ui.document:setImageScaling(false)
end
self.ui:handleEvent(Event:new("UpdatePos"))
end
function ReaderTypeset:toggleNightmodeImages(toggle)
if toggle and (toggle == true or toggle == 1) then
self.nightmode_images = true
self.ui.document:setNightmodeImages(true)
else
self.nightmode_images = false
self.ui.document:setNightmodeImages(false)
end
self.ui:handleEvent(Event:new("UpdatePos"))
end
function ReaderTypeset:toggleTxtPreFormatted(toggle)
self.ui.document:setTxtPreFormatted(toggle)
self.ui:handleEvent(Event:new("UpdatePos"))
end
function ReaderTypeset:setRenderDPI(dpi)
self.render_dpi = dpi
self.ui.document:setRenderDPI(dpi)
self.ui:handleEvent(Event:new("UpdatePos"))
end
function ReaderTypeset:addToMainMenu(menu_items)
-- insert table to main reader menu
menu_items.set_render_style = {
@ -445,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,
})
@ -465,7 +436,7 @@ function ReaderTypeset:onSetPageTopMargin(t_margin, when_applied_callback)
if self.sync_t_b_page_margins then
self.unscaled_margins[4] = t_margin
-- Let ConfigDialog know so it can update it on screen and have it saved on quit
self.ui.document.configurable.b_page_margin = t_margin
self.configurable.b_page_margin = t_margin
end
self.ui:handleEvent(Event:new("SetPageMargins", self.unscaled_margins, when_applied_callback))
end
@ -475,7 +446,7 @@ function ReaderTypeset:onSetPageBottomMargin(b_margin, when_applied_callback)
if self.sync_t_b_page_margins then
self.unscaled_margins[2] = b_margin
-- Let ConfigDialog know so it can update it on screen and have it saved on quit
self.ui.document.configurable.t_page_margin = b_margin
self.configurable.t_page_margin = b_margin
end
self.ui:handleEvent(Event:new("SetPageMargins", self.unscaled_margins, when_applied_callback))
end
@ -486,7 +457,7 @@ function ReaderTypeset:onSetPageTopAndBottomMargin(t_b_margins, when_applied_cal
if t_margin ~= b_margin then
-- Set Sync T/B Margins toggle to off, as user explicitly made them differ
self.sync_t_b_page_margins = false
self.ui.document.configurable.sync_t_b_page_margins = 0
self.configurable.sync_t_b_page_margins = 0
end
self.ui:handleEvent(Event:new("SetPageMargins", self.unscaled_margins, when_applied_callback))
end
@ -503,8 +474,8 @@ function ReaderTypeset:onSyncPageTopBottomMargins(toggle, when_applied_callback)
-- and later scaled, the end result could still be different.
-- So just take the mean and make them equal.
local mean_margin = Math.round((self.unscaled_margins[2] + self.unscaled_margins[4]) / 2)
self.ui.document.configurable.t_page_margin = mean_margin
self.ui.document.configurable.b_page_margin = mean_margin
self.configurable.t_page_margin = mean_margin
self.configurable.b_page_margin = mean_margin
self.unscaled_margins = { self.unscaled_margins[1], mean_margin, self.unscaled_margins[3], mean_margin }
self.ui:handleEvent(Event:new("SetPageMargins", self.unscaled_margins, when_applied_callback))
when_applied_callback = nil

@ -1,6 +1,7 @@
local BD = require("ui/bidi")
local Device = require("device")
local Event = require("ui/event")
local FileManagerBookInfo = require("apps/filemanager/filemanagerbookinfo")
local InfoMessage = require("ui/widget/infomessage")
local MultiConfirmBox = require("ui/widget/multiconfirmbox")
local UIManager = require("ui/uimanager")
@ -83,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" },
@ -196,11 +197,10 @@ When the book's language tag is not among our presets, no specific features will
-- Text might be too long for InfoMessage
local status_text = table.concat(lang_infos, "\n")
local TextViewer = require("ui/widget/textviewer")
local Font = require("ui/font")
UIManager:show(TextViewer:new{
title = _("Language tags (and hyphenation dictionaries) used since start up"),
text = status_text,
text_face = Font:getFace("smallinfont"),
text_type = "code",
height = math.floor(Screen:getHeight() * 0.8),
})
end,
@ -776,7 +776,8 @@ end
function ReaderTypography:onPreRenderDocument(config)
-- This is called after the document has been loaded,
-- when we know and can access the document language.
local doc_language = self.ui.document:getProps().language
local doc_language = FileManagerBookInfo.getCustomProp("language", self.ui.document.file)
or self.ui.document:getProps().language
self.book_lang_tag = self:fixLangTag(doc_language)
local is_known_lang_tag = self.book_lang_tag and LANG_TAG_TO_LANG_NAME[self.book_lang_tag] ~= nil

@ -22,6 +22,7 @@ local logger = require("logger")
local optionsutil = require("ui/data/optionsutil")
local Size = require("ui/size")
local time = require("ui/time")
local util = require("util")
local _ = require("gettext")
local Screen = Device.screen
local T = require("ffi/util").template
@ -47,10 +48,11 @@ local ReaderView = OverlapGroup:extend{
-- properties of the gap drawn between each page in scroll mode:
page_gap = nil, -- table
-- DjVu page rendering mode (used in djvu.c:drawPage())
render_mode = G_defaults:readSetting("DRENDER_MODE"), -- default to COLOR
render_mode = nil, -- default to COLOR, will be set in onReadSettings()
-- Crengine view mode
view_mode = G_defaults:readSetting("DCREREADER_VIEW_MODE"), -- default to page mode
hinting = true,
emitHintPageEvent = nil,
-- visible area within current viewing page
visible_area = nil,
@ -69,6 +71,10 @@ local ReaderView = OverlapGroup:extend{
-- might be directly updated by readerpaging/readerrolling when
-- they handle some panning/scrolling, to request "fast" refreshes
currently_scrolling = false,
-- image content stats of the current page, if supported by the Document engine
img_count = nil,
img_coverage = nil,
}
function ReaderView:init()
@ -89,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 = {}
@ -101,6 +106,10 @@ function ReaderView:init()
self.page_area = Geom:new{x = 0, y = 0, w = 0, h = 0}
self.dim_area = Geom:new{x = 0, y = 0, w = 0, h = 0}
-- Zero-init for sanity
self.img_count = 0
self.img_coverage = 0
self:addWidgets()
self.emitHintPageEvent = function()
self.ui:handleEvent(Event:new("HintPage", self.hinting))
@ -190,21 +199,36 @@ function ReaderView:paintTo(bb, x, y)
elseif self.view_mode == "scroll" then
self:drawScrollView(bb, x, y)
end
local should_repaint = self.ui.rolling:handlePartialRerendering()
if should_repaint then
-- ReaderRolling may have repositionned on another page containing
-- the xpointer of the top of the original page: recalling this is
-- all there is to do.
self:paintTo(bb, x, y)
return
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)
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)
@ -225,10 +249,9 @@ function ReaderView:paintTo(bb, x, y)
if self.footer_visible then
self.footer:paintTo(bb, x, y)
end
-- paint flipping
if self.flipping_visible then
self.flipping:paintTo(bb, x, y)
end
-- paint top left corner indicator
self.flipping:paintTo(bb, x, y)
-- paint view modules
for _, m in pairs(self.view_modules) do
m:paintTo(bb, x, y)
end
@ -241,18 +264,29 @@ 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,
-- If we're attempting to show a large enough amount of image data, request dithering (without triggering another repaint ;)).
local img_count, img_coverage = self.document:getDrawnImagesStatistics()
-- With some nil guards because this may not be implemented in every engine ;).
if img_count and img_count > 0 and img_coverage and img_coverage >= 0.075 then
-- We also want to deal with paging *away* from image content, which would have adverse effect on ghosting.
local coverage_diff = math.abs(img_coverage - self.img_coverage)
-- Which is why we remember the stats of the *previous* page.
self.img_count, self.img_coverage = img_count, img_coverage
if img_coverage >= 0.075 or coverage_diff >= 0.075 then
self.dialog.dithered = true
-- Request a flashing update while we're at it, but only if it's the first time we're painting it
if self.state.drawn == false then
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
@ -395,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)
@ -440,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
@ -499,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
@ -508,30 +543,24 @@ end
function ReaderView:drawPageSavedHighlight(bb, x, y)
local pages = self:getCurrentPageList()
for _, page in pairs(pages) do
local items = self.highlight.saved[page]
if items then
for i = 1, #items do
local item = items[i]
local pos0, pos1 = item.pos0, item.pos1
local boxes = self.document:getPageBoxesFromPositions(page, pos0, 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({ page = page, datetime = item.datetime, })
for _, box in pairs(boxes) do
local rect = self:pageToScreenTransform(page, box)
if rect then
self:drawHighlightRect(bb, x, y, rect, 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
for _, page in ipairs(pages) do
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 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, 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 for each highlight
end
end
end
end
end -- end for each page
end
end
function ReaderView:drawXPointerSavedHighlight(bb, x, y)
@ -539,46 +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({ page = item.pos0, datetime = item.datetime, })
for _, box in pairs(boxes) do
self:drawHighlightRect(bb, x, y, box, drawer, draw_note_mark)
-- 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)
@ -586,23 +607,23 @@ 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, 2, 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
line_y = line_y + 2
end
bb:paintRect(x, line_y, w, 2, Blitbuffer.COLOR_BLACK)
bb:paintRect(x, line_y, w, Size.line.medium, Blitbuffer.COLOR_BLACK)
elseif drawer == "invert" then
bb:invertRect(x, y, w, h)
end
if draw_note_mark then
if self.highlight.note_mark == "underline" then
bb:paintRect(x, y + h - 1, w, 2, Blitbuffer.COLOR_BLACK)
bb:paintRect(x, y + h - 1, w, Size.line.medium, Blitbuffer.COLOR_BLACK)
else
local note_mark_pos_x
if self.ui.paging or
(self.ui.document:getVisiblePageCount() == 1) or -- one-page mode
(self.document:getVisiblePageCount() == 1) or -- one-page mode
(x < Screen:getWidth() / 2) then -- page 1 in two-page mode
note_mark_pos_x = self.note_mark_pos_x1
else
@ -649,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
@ -769,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)
@ -824,28 +866,22 @@ In combination with zoom to fit page, page height, content height, content or co
end
function ReaderView:onReadSettings(config)
self.document:setTileCacheValidity(config:readSetting("tile_cache_validity_ts"))
self.render_mode = config:readSetting("render_mode") or 0
local rotation_mode = nil
local locked = G_reader_settings:isTrue("lock_rotation")
-- Keep current rotation by doing nothing when sticky rota is enabled.
if not locked then
-- Honor docsettings's rotation
if config:has("rotation_mode") then
rotation_mode = config:readSetting("rotation_mode") -- Doc's
else
-- No doc specific rotation, pickup global defaults for the doc type
if self.ui.paging then
rotation_mode = G_reader_settings:readSetting("kopt_rotation_mode") or Screen.ORIENTATION_PORTRAIT
else
rotation_mode = G_reader_settings:readSetting("copt_rotation_mode") or Screen.ORIENTATION_PORTRAIT
end
if self.ui.paging then
self.document:setTileCacheValidity(config:readSetting("tile_cache_validity_ts"))
self.render_mode = config:readSetting("render_mode") or G_defaults:readSetting("DRENDER_MODE")
if config:has("gamma") then -- old doc contrast setting
config:saveSetting("kopt_contrast", config:readSetting("gamma"))
config:delSetting("gamma")
end
end
if rotation_mode then
if G_reader_settings:nilOrFalse("lock_rotation") then
local setting_name = self.ui.paging and "kopt_rotation_mode" or "copt_rotation_mode"
-- document.configurable.rotation_mode is not ready yet
local rotation_mode = config:readSetting(setting_name)
or G_reader_settings:readSetting(setting_name)
or Screen.DEVICE_ROTATED_UPRIGHT
self:onSetRotationMode(rotation_mode)
end
self.state.gamma = config:readSetting("gamma") or 1.0
local full_screen = config:readSetting("kopt_full_screen") or self.document.configurable.full_screen
if full_screen == 0 then
self.footer_visible = false
@ -853,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"
@ -945,19 +947,15 @@ end
function ReaderView:onReaderFooterVisibilityChange()
-- Don't bother ReaderRolling with this nonsense, the footer's height is NOT handled via visible_area there ;)
if self.ui.paging and self.state.page then
-- NOTE: Simply relying on recalculate would be a wee bit too much: it'd reset the in-page offsets,
-- which would be wrong, and is also not necessary, since the footer is at the bottom of the screen ;).
-- So, simply mangle visible_area's height ourselves...
-- We don't need to do anything if reclaim is enabled ;).
if not self.footer.settings.reclaim_height then
-- NOTE: Yes, this means that toggling reclaim_height requires a page switch (for a proper recalculate).
-- Thankfully, most of the time, the quirks are barely noticeable ;).
if self.footer_visible then
self.visible_area.h = self.visible_area.h - self.footer:getHeight()
else
self.visible_area.h = self.visible_area.h + self.footer:getHeight()
end
-- NOTE: Mimic what onSetFullScreen does, since, without reclaim, toggling the footer affects the available area,
-- so we need to recompute the full layout.
self.ui:handleEvent(Event:new("SetDimensions", Screen:getSize()))
-- NOTE: Scroll mode's behavior after this might be suboptimal (until next page),
-- but I'm not familiar enough with it to make it behave...
-- (e.g., RedrawCurrentPage & co will snap to the top of the "current" page).
end
self.ui:handleEvent(Event:new("ViewRecalculate", self.visible_area, self.page_area))
end
end
@ -966,7 +964,7 @@ function ReaderView:onGammaUpdate(gamma)
if self.page_scroll then
self.ui:handleEvent(Event:new("UpdateScrollPageGamma", gamma))
end
Notification:notify(T(_("Font gamma set to: %1."), gamma))
Notification:notify(T(_("Contrast set to: %1."), gamma))
end
-- For ReaderKOptListener
@ -997,8 +995,10 @@ function ReaderView:onSWDitheringUpdate(toggle)
end
function ReaderView:onFontSizeUpdate(font_size)
self.ui:handleEvent(Event:new("ReZoom", font_size))
Notification:notify(T(_("Font zoom set to: %1."), font_size))
if self.ui.paging then
self.ui:handleEvent(Event:new("ReZoom", font_size))
Notification:notify(T(_("Font zoom set to: %1."), font_size))
end
end
function ReaderView:onDefectSizeUpdate()
@ -1032,21 +1032,22 @@ function ReaderView:onPageGapUpdate(page_gap)
end
function ReaderView:onSaveSettings()
if self.document:isEdited() and G_reader_settings:readSetting("save_document") ~= "always" then
-- Either "disable" (and the current tiles will be wrong) or "prompt" (but the
-- prompt will happen later, too late to catch "Don't save"), so force cached
-- tiles to be ignored on next opening.
self.document:resetTileCacheValidity()
end
self.ui.doc_settings:saveSetting("tile_cache_validity_ts", self.document:getTileCacheValidity())
self.ui.doc_settings:saveSetting("render_mode", self.render_mode)
if self.ui.paging then
if self.document:isEdited() and G_reader_settings:readSetting("save_document") ~= "always" then
-- Either "disable" (and the current tiles will be wrong) or "prompt" (but the
-- prompt will happen later, too late to catch "Don't save"), so force cached
-- tiles to be ignored on next opening.
self.document:resetTileCacheValidity()
end
self.ui.doc_settings:saveSetting("tile_cache_validity_ts", self.document:getTileCacheValidity())
if self.document.is_djvu then
self.ui.doc_settings:saveSetting("render_mode", self.render_mode)
end
end
-- Don't etch the current rotation in stone when sticky rotation is enabled
local locked = G_reader_settings:isTrue("lock_rotation")
if not locked then
self.ui.doc_settings:saveSetting("rotation_mode", Screen:getRotationMode())
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("gamma", self.state.gamma)
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)
@ -1075,9 +1076,13 @@ function ReaderView:getRenderModeMenuTable()
}
end
function ReaderView:onCloseDocument()
-- stop any pending HintPage event
function ReaderView:onCloseWidget()
-- Stop any pending HintPage event
UIManager:unschedule(self.emitHintPageEvent)
--- @fixme: The awful readerhighlight_spec test *relies* on this pointer being left dangling...
if not self.ui._testsuite then
self.emitHintPageEvent = nil
end
end
function ReaderView:onReaderReady()
@ -1116,7 +1121,7 @@ function ReaderView:isOverlapAllowed()
if self.ui.paging then
return not self.page_scroll
and (self.ui.paging.zoom_mode ~= "page"
or (self.ui.paging.zoom_mode == "page" and self.ui.paging.is_reflowed))
or (self.ui.paging.zoom_mode == "page" and self.document.configurable.text_wrap == 1))
and not self.ui.paging.zoom_mode:find("height")
else
return self.view_mode ~= "page"
@ -1124,20 +1129,14 @@ function ReaderView:isOverlapAllowed()
end
function ReaderView:setupTouchZones()
if self.ui.rolling then
self.ui.rolling:setupTouchZones()
else
self.ui.paging:setupTouchZones()
end
(self.ui.rolling or self.ui.paging):setupTouchZones()
end
function ReaderView:onToggleReadingOrder()
self.inverse_reading_order = not self.inverse_reading_order
self:setupTouchZones()
local is_rtl = self.inverse_reading_order ~= BD.mirroredUILayout() -- mirrored reading
UIManager:show(Notification:new{
text = is_rtl and _("RTL page turning.") or _("LTR page turning."),
})
Notification:notify(is_rtl and _("RTL page turning.") or _("LTR page turning."))
return true
end
@ -1157,10 +1156,10 @@ function ReaderView:getTapZones()
}
else -- user defined page turns tap zones
local tap_zone_forward_w = G_reader_settings:readSetting("page_turns_tap_zone_forward_size_ratio", G_defaults:readSetting("DTAP_ZONE_FORWARD").w)
local tap_zone_backward_w = 1 - tap_zone_forward_w
local tap_zone_backward_w = G_reader_settings:readSetting("page_turns_tap_zone_backward_size_ratio", G_defaults:readSetting("DTAP_ZONE_BACKWARD").w)
if tap_zones_type == "left_right" then
forward_zone = {
ratio_x = tap_zone_backward_w, ratio_y = 0,
ratio_x = 1 - tap_zone_forward_w, ratio_y = 0,
ratio_w = tap_zone_forward_w, ratio_h = 1,
}
backward_zone = {
@ -1169,7 +1168,7 @@ function ReaderView:getTapZones()
}
else
forward_zone = {
ratio_x = 0, ratio_y = tap_zone_backward_w,
ratio_x = 0, ratio_y = 1 - tap_zone_forward_w,
ratio_w = 1, ratio_h = tap_zone_forward_w,
}
backward_zone = {
@ -1216,17 +1215,17 @@ function ReaderView:setupNoteMarkPosition()
self.note_mark_pos_x1 = screen_w - sign_gap - sign_w
end
else
local doc_margins = self.ui.document:getPageMargins()
local doc_margins = self.document:getPageMargins()
local pos_x_r = screen_w - doc_margins["right"] + sign_gap -- mark in the right margin
local pos_x_l = doc_margins["left"] - sign_gap - sign_w -- mark in the left margin
if self.ui.document:getVisiblePageCount() == 1 then
if self.document:getVisiblePageCount() == 1 then
if BD.mirroredUILayout() then
self.note_mark_pos_x1 = pos_x_l
else
self.note_mark_pos_x1 = pos_x_r
end
else -- two-page mode
local page2_x = self.ui.document:getPageOffsetX(self.ui.document:getCurrentPage(true)+1)
local page2_x = self.document:getPageOffsetX(self.document:getCurrentPage(true)+1)
if BD.mirroredUILayout() then
self.note_mark_pos_x1 = pos_x_l
self.note_mark_pos_x2 = pos_x_l + page2_x
@ -1239,4 +1238,41 @@ function ReaderView:setupNoteMarkPosition()
end
end
function ReaderView:getCurrentPageLineWordCounts()
local lines_nb, words_nb = 0, 0
if self.ui.rolling then
local res = self.document:getTextFromPositions({x = 0, y = 0},
{x = Screen:getWidth(), y = Screen:getHeight()}, true) -- do not highlight
if res then
lines_nb = #self.document:getScreenBoxesFromPositions(res.pos0, res.pos1, true)
for word in util.gsplit(res.text, "[%s%p]+", false) do
if util.hasCJKChar(word) then
for char in util.gsplit(word, "[\192-\255][\128-\191]+", true) do
words_nb = words_nb + 1
end
else
words_nb = words_nb + 1
end
end
end
else
local page_boxes = self.document:getTextBoxes(self.ui:getCurrentPage())
if page_boxes and page_boxes[1][1].word then
lines_nb = #page_boxes
for _, line in ipairs(page_boxes) do
if #line == 1 and line[1].word == "" then -- empty line
lines_nb = lines_nb - 1
else
words_nb = words_nb + #line
local last_word = line[#line].word
if last_word:sub(-1) == "-" and last_word ~= "-" then -- hyphenated
words_nb = words_nb - 1
end
end
end
end
end
return lines_nb, words_nb
end
return ReaderView

@ -1,5 +1,3 @@
local BD = require("ui/bidi")
local ButtonDialogTitle = require("ui/widget/buttondialogtitle")
local ConfirmBox = require("ui/widget/confirmbox")
local DataStorage = require("datastorage")
local DictQuickLookup = require("ui/widget/dictquicklookup")
@ -13,6 +11,7 @@ local Trapper = require("ui/trapper")
local Translator = require("ui/translator")
local UIManager = require("ui/uimanager")
local Wikipedia = require("ui/wikipedia")
local filemanagerutil = require("apps/filemanager/filemanagerutil")
local lfs = require("libs/libkoreader-lfs")
local logger = require("logger")
local util = require("util")
@ -114,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 = {
@ -171,113 +181,26 @@ 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"),
keep_menu_open = true,
callback = function()
local choose_directory = function()
-- Default directory as chosen by DictQuickLookup
local default_dir = G_reader_settings:readSetting("wikipedia_save_dir")
or G_reader_settings:readSetting("home_dir")
or require("apps/filemanager/filemanagerutil").getDefaultDir()
local dialog
dialog = ButtonDialogTitle:new{
title = T(_("Current Wikipedia 'Save as EPUB' folder:\n\n%1\n"), BD.dirpath(default_dir)),
buttons = {
{
{
text = _("Keep this folder"),
callback = function()
UIManager:close(dialog)
end,
},
},
{
{
text = _("Choose other folder"),
callback = function()
UIManager:close(dialog)
-- Use currently read book's directory as starting point,
-- so a user reading a wikipedia article can quickly select
-- it to save related new articles in the same directory
local dir = G_reader_settings:readSetting("wikipedia_save_dir")
or G_reader_settings:readSetting("home_dir")
or require("apps/filemanager/filemanagerutil").getDefaultDir()
or "/"
-- If this directory has no subdirectory, we would be displaying
-- a single "..", so use parent directory in that case.
local has_subdirectory = false
for f in lfs.dir(dir) do
local attributes = lfs.attributes(dir.."/"..f)
if attributes and attributes.mode == "directory" then
if f ~= "." and f ~= ".." and f:sub(-4) ~= ".sdr"then
has_subdirectory = true
break
end
end
end
if not has_subdirectory then
dir = dir:match("(.*)/")
end
local PathChooser = require("ui/widget/pathchooser")
local path_chooser = PathChooser:new{
select_directory = true,
select_file = false,
path = dir,
onConfirm = function(path)
G_reader_settings:saveSetting("wikipedia_save_dir", path)
UIManager:show(InfoMessage:new{
text = T(_("Wikipedia 'Save as EPUB' folder set to:\n%1"), BD.dirpath(path)),
})
end
}
UIManager:show(path_chooser)
end,
},
},
},
}
UIManager:show(dialog)
end
-- If wikipedia_save_dir has not yet been set, propose to use
-- home_dir/Wikipedia/
if not G_reader_settings:readSetting("wikipedia_save_dir") then
local home_dir = G_reader_settings:readSetting("home_dir")
if not home_dir or not lfs.attributes(home_dir, "mode") == "directory" then
home_dir = require("apps/filemanager/filemanagerutil").getDefaultDir()
end
home_dir = home_dir:gsub("^(.-)/*$", "%1") -- remove trailing slash
if home_dir and lfs.attributes(home_dir, "mode") == "directory" then
local wikipedia_dir = home_dir.."/Wikipedia"
local text = _([[
help_text = _([[
Wikipedia articles can be saved as an EPUB for more comfortable reading.
You can choose an existing folder, or use a default folder named "Wikipedia" in your reader's home folder.
Where do you want them saved?]])
UIManager:show(ConfirmBox:new{
text = text,
ok_text = _("Use ~/Wikipedia/"),
ok_callback = function()
if not util.pathExists(wikipedia_dir) then
lfs.mkdir(wikipedia_dir)
end
G_reader_settings:saveSetting("wikipedia_save_dir", wikipedia_dir)
UIManager:show(InfoMessage:new{
text = T(_("Wikipedia 'Save as EPUB' folder set to:\n%1"), BD.dirpath(wikipedia_dir)),
})
end,
cancel_text = _("Choose folder"),
cancel_callback = function()
choose_directory()
end,
})
return
You can choose an existing folder, or use a default folder named "Wikipedia" in your reader's home folder.]]),
callback = function()
local title_header = _("Current Wikipedia 'Save as EPUB' folder:")
local current_path = G_reader_settings:readSetting("wikipedia_save_dir")
local default_path = DictQuickLookup.getWikiSaveEpubDefaultDir()
local caller_callback = function(path)
G_reader_settings:saveSetting("wikipedia_save_dir", path)
if not util.pathExists(path) then
lfs.mkdir(path)
end
end
-- If setting exists, or no home_dir found, let user choose directory
choose_directory()
filemanagerutil.showChooseDialog(title_header, caller_callback, current_path, default_path)
end,
},
{ -- setting used by dictquicklookup
@ -288,6 +211,41 @@ Where do you want them saved?]])
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,
},
{
@ -371,7 +329,7 @@ function ReaderWikipedia:initLanguages(word)
end
-- use book and UI languages
if self.view then
addLanguage(self.view.document:getProps().language)
addLanguage(self.ui.doc_props.language)
end
addLanguage(G_reader_settings:readSetting("language"))
if #self.wiki_languages == 0 and word then
@ -427,13 +385,7 @@ function ReaderWikipedia:lookupWikipedia(word, is_sane, box, get_fullpage, force
local display_word = word:gsub("_", " ")
if not self.disable_history then
local book_title = self.ui.doc_settings and self.ui.doc_settings:readSetting("doc_props").title or _("Wikipedia lookup")
if book_title == "" then -- no or empty metadata title
if self.ui.document and self.ui.document.file then
local directory, filename = util.splitFilePathName(self.ui.document.file) -- luacheck: no unused
book_title = util.splitFileNameSuffix(filename)
end
end
local book_title = self.ui.doc_props and self.ui.doc_props.display_title or _("Wikipedia lookup")
wikipedia_history:addTableItem("wikipedia_history", {
book_title = book_title,
time = os.time(),
@ -488,7 +440,7 @@ function ReaderWikipedia:lookupWikipedia(word, is_sane, box, get_fullpage, force
pages = sorted_pages
end
for pageid, page in pairs(pages) do
local definition = page.extract or no_result_text
local definition = page.extract or (page.length and _("No introduction.")) or no_result_text
if page.length then
-- we get 'length' only for intro results
-- let's append it to definition so we know

@ -8,7 +8,6 @@ local InputContainer = require("ui/widget/container/inputcontainer")
local SpinWidget = require("ui/widget/spinwidget")
local UIManager = require("ui/uimanager")
local logger = require("logger")
local util = require("util")
local _ = require("gettext")
local Input = Device.input
local Screen = Device.screen
@ -27,6 +26,17 @@ local ReaderZooming = InputContainer:extend{
"rows",
"manual",
},
zoom_mode_label = { -- const
page = _("page") .. " - " .. _("full"),
pagewidth = _("page") .. " - " .. _("width"),
pageheight = _("page") .. " - " .. _("height"),
content = _("content") .. " - " .. _("full"),
contentwidth = _("content") .. " - " .. _("width"),
contentheight = _("content") .. " - " .. _("height"),
columns = _("columns"),
rows = _("rows"),
manual = _("manual"),
},
zoom_genus_to_mode = { -- const
[4] = "page",
[3] = "content",
@ -58,17 +68,20 @@ local ReaderZooming = InputContainer:extend{
-- with overlap of zoom_overlap_h % (horizontally)
-- and zoom_overlap_v % (vertically).
kopt_zoom_factor = 1.5,
zoom_pan_settings = { -- const
"kopt_zoom_factor",
"zoom_overlap_h",
"zoom_overlap_v",
"zoom_bottom_to_top",
"zoom_direction_vertical",
},
zoom_overlap_h = 40,
zoom_overlap_v = 40,
zoom_bottom_to_top = nil, -- true for bottom-to-top
zoom_direction_vertical = nil, -- true for column mode
zoom_direction_settings = { -- const
[7] = {right_to_left = false, zoom_bottom_to_top = false, zoom_direction_vertical = false},
[6] = {right_to_left = false, zoom_bottom_to_top = false, zoom_direction_vertical = true },
[5] = {right_to_left = false, zoom_bottom_to_top = true, zoom_direction_vertical = false},
[4] = {right_to_left = false, zoom_bottom_to_top = true, zoom_direction_vertical = true },
[3] = {right_to_left = true, zoom_bottom_to_top = true, zoom_direction_vertical = true },
[2] = {right_to_left = true, zoom_bottom_to_top = true, zoom_direction_vertical = false},
[1] = {right_to_left = true, zoom_bottom_to_top = false, zoom_direction_vertical = true },
[0] = {right_to_left = true, zoom_bottom_to_top = false, zoom_direction_vertical = false},
},
current_page = 1,
rotation = 0,
paged_modes = { -- const
@ -81,57 +94,63 @@ local ReaderZooming = InputContainer:extend{
}
function ReaderZooming:init()
self:registerKeyEvents()
end
function ReaderZooming:registerKeyEvents()
if Device:hasKeyboard() then
self.key_events = {
ZoomIn = {
{ "Shift", Input.group.PgFwd },
doc = "zoom in",
event = "Zoom", args = "in"
event = "Zoom",
args = "in",
},
ZoomOut = {
{ "Shift", Input.group.PgBack },
doc = "zoom out",
event = "Zoom", args = "out"
event = "Zoom",
args = "out",
},
ZoomToFitPage = {
{ "A" },
doc = "zoom to fit page",
event = "SetZoomMode", args = "page"
event = "SetZoomMode",
args = "page",
},
ZoomToFitContent = {
{ "Shift", "A" },
doc = "zoom to fit content",
event = "SetZoomMode", args = "content"
event = "SetZoomMode",
args = "content",
},
ZoomToFitPageWidth = {
{ "S" },
doc = "zoom to fit page width",
event = "SetZoomMode", args = "pagewidth"
event = "SetZoomMode",
args = "pagewidth",
},
ZoomToFitContentWidth = {
{ "Shift", "S" },
doc = "zoom to fit content width",
event = "SetZoomMode", args = "contentwidth"
event = "SetZoomMode",
args = "contentwidth",
},
ZoomToFitPageHeight = {
{ "D" },
doc = "zoom to fit page height",
event = "SetZoomMode", args = "pageheight"
event = "SetZoomMode",
args = "pageheight",
},
ZoomToFitContentHeight = {
{ "Shift", "D" },
doc = "zoom to fit content height",
event = "SetZoomMode", args = "contentheight"
event = "SetZoomMode",
args = "contentheight",
},
ZoomManual = {
{ "Shift", "M" },
doc = "manual zoom mode",
event = "SetZoomMode", args = "manual"
event = "SetZoomMode",
args = "manual",
},
}
end
end
ReaderZooming.onPhysicalKeyboardConnected = ReaderZooming.registerKeyEvents
-- Conversions between genus/type combos and zoom_mode...
function ReaderZooming:mode_to_combo(zoom_mode)
if not zoom_mode then
@ -201,9 +220,7 @@ function ReaderZooming:onReadSettings(config)
local zoom_mode = config:readSetting("zoom_mode")
if zoom_mode then
-- Validate it first
zoom_mode = util.arrayContains(self.available_zoom_modes, zoom_mode)
and zoom_mode
or self.DEFAULT_ZOOM_MODE
zoom_mode = self.zoom_mode_label[zoom_mode] and zoom_mode or self.DEFAULT_ZOOM_MODE
-- Make sure the split genus & type match, to have an up-to-date ConfigDialog...
local zoom_mode_genus, zoom_mode_type = self:_updateConfigurable(zoom_mode)
@ -213,16 +230,13 @@ 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 = util.arrayContains(self.available_zoom_modes, zoom_mode)
and zoom_mode
or self.DEFAULT_ZOOM_MODE
zoom_mode = self.zoom_mode_label[zoom_mode] and zoom_mode or self.DEFAULT_ZOOM_MODE
end
-- Import legacy zoom_factor settings
@ -238,18 +252,23 @@ function ReaderZooming:onReadSettings(config)
local is_reflowed = config:has("kopt_text_wrap") and config:readSetting("kopt_text_wrap") == 1
self:setZoomMode(zoom_mode, true, is_reflowed) -- avoid informative message on load
for _, setting in ipairs(self.zoom_pan_settings) do
self[setting] = config:readSetting(setting)
or G_reader_settings:readSetting(setting)
or self[setting]
end
self.kopt_zoom_factor = config:readSetting("kopt_zoom_factor")
or G_reader_settings:readSetting("kopt_zoom_factor") or self.kopt_zoom_factor
self.zoom_overlap_h = config:readSetting("kopt_zoom_overlap_h")
or G_reader_settings:readSetting("kopt_zoom_overlap_h") or self.zoom_overlap_h
self.zoom_overlap_v = config:readSetting("kopt_zoom_overlap_v")
or G_reader_settings:readSetting("kopt_zoom_overlap_v") or self.zoom_overlap_v
-- update zoom direction parameters
local zoom_direction_setting = self.zoom_direction_settings[self.document.configurable.zoom_direction
or G_reader_settings:readSetting("kopt_zoom_direction") or 7]
self.zoom_bottom_to_top = zoom_direction_setting.zoom_bottom_to_top
self.zoom_direction_vertical = zoom_direction_setting.zoom_direction_vertical
end
function ReaderZooming:onSaveSettings()
self.ui.doc_settings:saveSetting("zoom_mode", self.orig_zoom_mode or self.zoom_mode)
for _, setting in ipairs(self.zoom_pan_settings) do
self.ui.doc_settings:saveSetting(setting, self[setting])
end
end
function ReaderZooming:onSpread(arg, ges)
@ -323,16 +342,12 @@ end
function ReaderZooming:onDefineZoom(btn, when_applied_callback)
local config = self.ui.document.configurable
local settings = ({
[7] = {right_to_left = false, zoom_bottom_to_top = false, zoom_direction_vertical = false},
[6] = {right_to_left = false, zoom_bottom_to_top = false, zoom_direction_vertical = true },
[5] = {right_to_left = false, zoom_bottom_to_top = true, zoom_direction_vertical = false},
[4] = {right_to_left = false, zoom_bottom_to_top = true, zoom_direction_vertical = true },
[3] = {right_to_left = true, zoom_bottom_to_top = true, zoom_direction_vertical = true },
[2] = {right_to_left = true, zoom_bottom_to_top = true, zoom_direction_vertical = false},
[1] = {right_to_left = true, zoom_bottom_to_top = false, zoom_direction_vertical = true },
[0] = {right_to_left = true, zoom_bottom_to_top = false, zoom_direction_vertical = false},
})[config.zoom_direction]
local zoom_direction_setting = self.zoom_direction_settings[config.zoom_direction]
local settings = { -- unpack the table, work on a local copy
right_to_left = zoom_direction_setting.right_to_left,
zoom_bottom_to_top = zoom_direction_setting.zoom_bottom_to_top,
zoom_direction_vertical = zoom_direction_setting.zoom_direction_vertical,
}
local zoom_range_number = config.zoom_range_number
local zoom_factor = config.zoom_factor
local zoom_mode_genus = self.zoom_genus_to_mode[config.zoom_mode_genus]
@ -354,7 +369,7 @@ function ReaderZooming:onDefineZoom(btn, when_applied_callback)
zoom_mode = zoom_mode_genus
self.ui:handleEvent(Event:new("SetScrollMode", false))
end
zoom_mode = util.arrayContains(self.available_zoom_modes, zoom_mode) and zoom_mode or self.DEFAULT_ZOOM_MODE
zoom_mode = self.zoom_mode_label[zoom_mode] and zoom_mode or self.DEFAULT_ZOOM_MODE
settings.zoom_mode = zoom_mode
if settings.right_to_left then
@ -409,7 +424,7 @@ function ReaderZooming:onDefineZoom(btn, when_applied_callback)
horizontal overlap: %3 %
vertical overlap: %5 %
zoom factor: %6]]),
zoom_mode,
self.zoom_mode_label[zoom_mode],
("%.2f"):format(self:getNumberOf("columns", settings.zoom_overlap_h)),
settings.zoom_overlap_h,
("%.2f"):format(self:getNumberOf("rows", settings.zoom_overlap_v)),
@ -700,9 +715,10 @@ function ReaderZooming:onZoomFactorChange()
end
function ReaderZooming:onSetZoomPan(settings, no_redraw)
self.ui.doc_settings:saveSetting("kopt_zoom_factor", settings.kopt_zoom_factor)
self.ui.doc_settings:saveSetting("zoom_mode", settings.zoom_mode)
for k, v in pairs(settings) do
self[k] = v
self.ui.doc_settings:saveSetting(k, v)
-- Configurable keys aren't prefixed...
local configurable_key = k:gsub("^kopt_", "")
if self.ui.document.configurable[configurable_key] then
@ -718,4 +734,12 @@ function ReaderZooming:onBBoxUpdate()
self:onDefineZoom()
end
function ReaderZooming:getZoomModeActions() -- for Dispatcher
local action_toggles = {}
for _, v in ipairs(ReaderZooming.available_zoom_modes) do
table.insert(action_toggles, ReaderZooming.zoom_mode_label[v])
end
return ReaderZooming.available_zoom_modes, action_toggles
end
return ReaderZooming

@ -21,8 +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")
@ -32,6 +35,7 @@ local ReaderDeviceStatus = require("apps/reader/modules/readerdevicestatus")
local ReaderDictionary = require("apps/reader/modules/readerdictionary")
local ReaderFont = require("apps/reader/modules/readerfont")
local ReaderGoto = require("apps/reader/modules/readergoto")
local ReaderHandMade = require("apps/reader/modules/readerhandmade")
local ReaderHinting = require("apps/reader/modules/readerhinting")
local ReaderHighlight = require("apps/reader/modules/readerhighlight")
local ReaderScrolling = require("apps/reader/modules/readerscrolling")
@ -58,12 +62,14 @@ 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")
local util = require("util")
local _ = require("gettext")
local Screen = require("device").screen
local Input = Device.input
local Screen = Device.screen
local T = ffiUtil.template
local ReaderUI = InputContainer:extend{
@ -80,7 +86,7 @@ local ReaderUI = InputContainer:extend{
password = nil,
postInitCallback = nil,
postReaderCallback = nil,
postReaderReadyCallback = nil,
}
function ReaderUI:registerModule(name, ui_module, always_active)
@ -99,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()
@ -109,10 +115,11 @@ function ReaderUI:init()
-- cap screen refresh on pan to 2 refreshes per second
local pan_rate = Screen.low_pan_rate and 2.0 or 30.0
Input:inhibitInput(true) -- Inhibit any past and upcoming input events.
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
@ -122,10 +129,7 @@ function ReaderUI:init()
-- Handle local settings migration
SettingsMigration:migrateSettings(self.doc_settings)
if Device:hasKeys() then
self.key_events.Home = { {"Home"}, doc = "open file browser" }
self.key_events.Reload = { {"F5"}, doc = "reload document" }
end
self:registerKeyEvents()
-- a view container (so it must be child #1!)
-- all paintable widgets need to be a child of reader view
@ -162,6 +166,13 @@ function ReaderUI:init()
view = self.view,
ui = self
})
-- Handmade/custom ToC and hidden flows
self:registerModule("handmade", ReaderHandMade:new{
dialog = self.dialog,
view = self.view,
ui = self,
document = self.document,
})
-- Table of content controller
self:registerModule("toc", ReaderToc:new{
dialog = self.dialog,
@ -174,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{
@ -318,12 +335,14 @@ function ReaderUI:init()
})
-- typeset controller
self:registerModule("typeset", ReaderTypeset:new{
configurable = self.document.configurable,
dialog = self.dialog,
view = self.view,
ui = self
})
-- font menu
self:registerModule("font", ReaderFont:new{
configurable = self.document.configurable,
dialog = self.dialog,
view = self.view,
ui = self
@ -342,6 +361,7 @@ function ReaderUI:init()
})
-- rolling controller
self:registerModule("rolling", ReaderRolling:new{
configurable = self.document.configurable,
pan_rate = pan_rate,
dialog = self.dialog,
view = self.view,
@ -416,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(
@ -433,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))
@ -453,22 +470,40 @@ function ReaderUI:init()
end
self.postInitCallback = nil
-- Now that document is loaded, store book metadata in settings
-- (so that filemanager can use it from sideCar file to display
-- Book information).
self.doc_settings:saveSetting("doc_props", self.document:getProps())
-- Now that document is loaded, store book metadata in settings.
local props = self.document:getProps()
self.doc_settings:saveSetting("doc_props", props)
-- And have an extended and customized copy in memory for quick access.
self.doc_props = FileManagerBookInfo.extendProps(props, self.document.file)
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
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
-- 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)
-- print("Ordered registered gestures:")
-- for _, tzone in ipairs(self._ordered_touch_zones) do
@ -484,6 +519,27 @@ function ReaderUI:init()
ReaderUI.instance = self
end
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
ReaderUI.onPhysicalKeyboardConnected = ReaderUI.registerKeyEvents
function ReaderUI:setLastDirForFileBrowser(dir)
if dir and #dir > 1 and dir:sub(-1) == "/" then
dir = dir:sub(1, -2)
@ -507,13 +563,12 @@ function ReaderUI:getLastDirFile(to_file_browser)
return last_dir, last_file
end
function ReaderUI:showFileManager(file)
function ReaderUI:showFileManager(file, selected_files)
local FileManager = require("apps/filemanager/filemanager")
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)
@ -521,7 +576,7 @@ function ReaderUI:showFileManager(file)
if FileManager.instance then
FileManager.instance:reinit(last_dir, last_file)
else
FileManager:showFiles(last_dir, last_file)
FileManager:showFiles(last_dir, last_file, selected_files)
end
end
@ -543,19 +598,20 @@ end
--- @note: Will sanely close existing FileManager/ReaderUI instance for you!
--- This is the *only* safe way to instantiate a new ReaderUI instance!
--- (i.e., don't look at the testsuite, which resorts to all kinds of nasty hacks).
function ReaderUI:showReader(file, provider)
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
@ -565,26 +621,30 @@ function ReaderUI:showReader(file, provider)
UIManager:broadcastEvent(Event:new("ShowingReader"))
provider = provider or DocumentRegistry:getProvider(file)
if provider.provider then
self:showReaderCoroutine(file, provider)
self:showReaderCoroutine(file, provider, seamless)
end
end
function ReaderUI:showReaderCoroutine(file, provider)
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,
})
-- doShowReader might block for a long time, so force repaint here
UIManager:forceRePaint()
UIManager:nextTick(function()
logger.dbg("creating coroutine for showing reader")
local co = coroutine.create(function()
self:doShowReader(file, provider)
self:doShowReader(file, provider, seamless)
end)
local ok, err = coroutine.resume(co)
if err ~= nil or ok == false then
io.stderr:write('[!] doShowReader coroutine crashed:\n')
io.stderr:write(debug.traceback(co, err, 1))
-- Restore input if we crashed before ReaderUI has restored it
Device:setIgnoreInput(false)
Input:inhibitInputUntil(0.2)
UIManager:show(InfoMessage:new{
text = _("No reader engine for this file or invalid file.")
})
@ -593,7 +653,10 @@ function ReaderUI:showReaderCoroutine(file, provider)
end)
end
function ReaderUI:doShowReader(file, provider)
function ReaderUI:doShowReader(file, provider, seamless)
if seamless then
UIManager:avoidFlashOnNextRepaint()
end
logger.info("opening file", file)
-- Only keep a single instance running
if ReaderUI.instance then
@ -620,22 +683,14 @@ function ReaderUI:doShowReader(file, provider)
end
end
end
require("readhistory"):addItem(file) -- (will update "lastfile")
local reader = ReaderUI:new{
dimen = Screen:getSize(),
covers_fullscreen = true, -- hint for UIManager:_repaint()
document = document,
}
local title = reader.document:getProps().title
if title ~= "" then
Screen:setWindowTitle(title)
else
local _, filename = util.splitFilePathName(file)
Screen:setWindowTitle(filename)
end
Device:notifyBookState(title, document)
Screen:setWindowTitle(reader.doc_props.display_title)
Device:notifyBookState(reader.doc_props.display_title, document)
-- This is mostly for the few callers that bypass the coroutine shenanigans and call doShowReader directly,
-- instead of showReader...
@ -646,14 +701,7 @@ function ReaderUI:doShowReader(file, provider)
FileManager.instance:onClose()
end
UIManager:show(reader, "full")
end
-- NOTE: The instance reference used to be stored in a private module variable, hence the getter method.
-- We've since aligned behavior with FileManager, which uses a class member instead,
-- but kept the function to avoid changing existing code.
function ReaderUI:_getRunningInstance()
return ReaderUI.instance
UIManager:show(reader, seamless and "ui" or "full")
end
function ReaderUI:unlockDocumentWithPassword(document, try_again)
@ -666,7 +714,6 @@ function ReaderUI:unlockDocumentWithPassword(document, try_again)
{
text = _("Cancel"),
id = "close",
enabled = true,
callback = function()
self:closeDialog()
coroutine.resume(self._coroutine)
@ -674,7 +721,6 @@ function ReaderUI:unlockDocumentWithPassword(document, try_again)
},
{
text = _("OK"),
enabled = true,
callback = function()
local success = self:onVerifyPassword(document)
self:closeDialog()
@ -714,8 +760,12 @@ function ReaderUI:saveSettings()
G_reader_settings:flush()
end
function ReaderUI:onFlushSettings()
function ReaderUI:onFlushSettings(show_notification)
self:saveSettings()
if show_notification then
-- Invoked from dispatcher to explicitely flush settings
Notification:notify(_("Book metadata saved."))
end
end
function ReaderUI:closeDocument()
@ -734,9 +784,9 @@ function ReaderUI:notifyCloseDocument()
self:closeDocument()
else
UIManager:show(ConfirmBox:new{
text = _("Do you want to save this document?"),
ok_text = _("Save"),
cancel_text = _("Don't save"),
text = _("Write highlights into this PDF?"),
ok_text = _("Write"),
dismissable = false,
ok_callback = function()
self:closeDocument()
end,
@ -764,6 +814,7 @@ function ReaderUI:onClose(full_refresh)
self:saveSettings()
end
if self.document ~= nil then
require("readhistory"):updateLastBookTime(self.tearing_down)
-- Serialize the most recently displayed page for later launch
DocCache:serialize(self.document.file)
logger.dbg("closing document")
@ -788,10 +839,6 @@ function ReaderUI:dealWithLoadDocumentFailure()
-- We can't do much more than crash properly here (still better than
-- going on and segfaulting when calling other methods on unitiliazed
-- _document)
-- We must still remove it from lastfile and history (as it has
-- already been added there) so that koreader don't crash again
-- at next launch...
require("readhistory"):removeItemByPath(self.document.file) -- (will update "lastfile")
-- As we are in a coroutine, we can pause and show an InfoMessage before exiting
local _coroutine = coroutine.running()
if coroutine then
@ -802,6 +849,9 @@ function ReaderUI:dealWithLoadDocumentFailure()
coroutine.resume(_coroutine, false)
end,
})
-- Restore input, so can catch the InfoMessage dismiss and exit
Device:setIgnoreInput(false)
Input:inhibitInputUntil(0.2)
coroutine.yield() -- pause till InfoMessage is dismissed
end
-- We have to error and exit the coroutine anyway to avoid any segfault
@ -818,7 +868,7 @@ function ReaderUI:onReload()
self:reloadDocument()
end
function ReaderUI:reloadDocument(after_close_callback)
function ReaderUI:reloadDocument(after_close_callback, seamless)
local file = self.document.file
local provider = getmetatable(self.document).__index
@ -828,6 +878,7 @@ function ReaderUI:reloadDocument(after_close_callback)
self:handleEvent(Event:new("CloseReaderMenu"))
self:handleEvent(Event:new("CloseConfigMenu"))
self:handleEvent(Event:new("PreserveCurrentSession")) -- don't reset statistics' start_current_period
self.highlight:onClose() -- close highlight dialog if any
self:onClose(false)
if after_close_callback then
@ -835,7 +886,7 @@ function ReaderUI:reloadDocument(after_close_callback)
after_close_callback(file, provider)
end
self:showReader(file, provider)
self:showReader(file, provider, seamless)
end
function ReaderUI:switchDocument(new_file)
@ -858,11 +909,7 @@ function ReaderUI:onOpenLastDoc()
end
function ReaderUI:getCurrentPage()
if self.document.info.has_pages then
return self.paging.current_page
else
return self.document:getCurrentPage()
end
return self.paging and self.paging.current_page or self.document:getCurrentPage()
end
return ReaderUI

@ -38,9 +38,6 @@ function Configurable:loadDefaults(config_options)
local settings_key = prefix..key
local default = G_reader_settings:readSetting(settings_key)
self[key] = default or options[j].default_value
if not self[key] then
self[key] = options[j].default_arg
end
end
end
end

@ -0,0 +1,319 @@
--[[--
This module contains date translations and helper functions for the KOReader frontend.
]]
local BaseUtil = require("ffi/util")
local _ = require("gettext")
local C_ = _.pgettext
local T = BaseUtil.template
local datetime = {}
datetime.weekDays = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" } -- in Lua wday order
datetime.shortMonthTranslation = {
["Jan"] = _("Jan"),
["Feb"] = _("Feb"),
["Mar"] = _("Mar"),
["Apr"] = _("Apr"),
["May"] = _("May"),
["Jun"] = _("Jun"),
["Jul"] = _("Jul"),
["Aug"] = _("Aug"),
["Sep"] = _("Sep"),
["Oct"] = _("Oct"),
["Nov"] = _("Nov"),
["Dec"] = _("Dec"),
}
datetime.longMonthTranslation = {
["January"] = _("January"),
["February"] = _("February"),
["March"] = _("March"),
["April"] = _("April"),
["May"] = _("May"),
["June"] = _("June"),
["July"] = _("July"),
["August"] = _("August"),
["September"] = _("September"),
["October"] = _("October"),
["November"] = _("November"),
["December"] = _("December"),
}
datetime.shortDayOfWeekTranslation = {
["Mon"] = _("Mon"),
["Tue"] = _("Tue"),
["Wed"] = _("Wed"),
["Thu"] = _("Thu"),
["Fri"] = _("Fri"),
["Sat"] = _("Sat"),
["Sun"] = _("Sun"),
}
datetime.shortDayOfWeekToLongTranslation = {
["Mon"] = _("Monday"),
["Tue"] = _("Tuesday"),
["Wed"] = _("Wednesday"),
["Thu"] = _("Thursday"),
["Fri"] = _("Friday"),
["Sat"] = _("Saturday"),
["Sun"] = _("Sunday"),
}
--[[--
Converts seconds to a clock string.
Source: <a href="https://gist.github.com/jesseadams/791673">https://gist.github.com/jesseadams/791673</a>
]]
---- @int seconds number of seconds
---- @bool withoutSeconds if true 00:00, if false 00:00:00
---- @treturn string clock string in the form of 00:00 or 00:00:00
function datetime.secondsToClock(seconds, withoutSeconds, withDays)
seconds = tonumber(seconds)
if not seconds then
if withoutSeconds then
return "--:--"
else
return "--:--:--"
end
elseif seconds == 0 or seconds ~= seconds then
if withoutSeconds then
return "00:00"
else
return "00:00:00"
end
else
local round = withoutSeconds and require("optmath").round or function(n) return n end
local days = "0"
local hours
if withDays then
days = string.format("%d", seconds * (1/(24*3600))) -- implicit math.floor for string.format
hours = string.format("%02d", (seconds * (1/3600)) % 24)
else
hours = string.format("%02d", seconds * (1/3600))
end
local mins = string.format("%02d", round(seconds % 3600 * (1/60)))
if withoutSeconds then
if mins == "60" then
-- Can only happen because of rounding, which only happens if withoutSeconds...
mins = string.format("%02d", 0)
hours = string.format("%02d", hours + 1)
end
return (days ~= "0" and (days .. C_("Time", "d")) or "") .. hours .. ":" .. mins
else
local secs = string.format("%02d", seconds % 60)
return (days ~= "0" and (days .. C_("Time", "d")) or "") .. hours .. ":" .. mins .. ":" .. secs
end
end
end
--- Converts seconds to a period of time string.
---- @int seconds number of seconds
---- @bool withoutSeconds if true 1h30', if false 1h30'10"
---- @bool hmsFormat, if true format 1h30m10s
---- @bool withDays, if true format 1d12h30'10" or 1d12h30m10s
---- @bool compact, if set removes all leading zeros (incl. units if necessary) and turns thinspaces into hairspaces (if present)
---- @treturn string clock string in the form of 1h30'10" or 1h30m10s
function datetime.secondsToHClock(seconds, withoutSeconds, hmsFormat, withDays, compact)
local SECONDS_SYMBOL = "\""
seconds = tonumber(seconds)
if seconds == 0 then
if withoutSeconds then
if hmsFormat then
return T(_("%1m"), "0")
else
return "0'"
end
else
if hmsFormat then
return T(C_("Time", "%1s"), "0")
else
return "0" .. SECONDS_SYMBOL
end
end
elseif seconds < 60 then
if withoutSeconds and seconds < 30 then
if hmsFormat then
return T(C_("Time", "%1m"), "0")
else
return "0'"
end
elseif withoutSeconds and seconds >= 30 then
if hmsFormat then
return T(C_("Time", "%1m"), "1")
else
return "1'"
end
else
if hmsFormat then
if compact then
return T(C_("Time", "%1s"), string.format("%d", seconds))
else
return T(C_("Time", "%1m\xE2\x80\x89%2s"), "0", string.format("%d", seconds)) -- use a thin space
end
else
if compact then
return string.format("%d", seconds) .. SECONDS_SYMBOL
else
return "0'" .. string.format("%02d", seconds) .. SECONDS_SYMBOL
end
end
end
else
local time_string = datetime.secondsToClock(seconds, withoutSeconds, withDays)
if withoutSeconds then
time_string = time_string .. ":"
end
time_string = time_string:gsub(":", C_("Time", "h"), 1)
time_string = time_string:gsub(":", C_("Time", "m"), 1)
time_string = time_string:gsub("^00" .. C_("Time", "h"), "") -- delete leading "00h"
time_string = time_string:gsub("^00" .. C_("Time", "m"), "") -- delete leading "00m"
if time_string:find("^0%d") then
time_string = time_string:gsub("^0", "") -- delete leading "0"
end
if withoutSeconds and time_string == "" then
time_string = "0" .. C_("Time", "m")
end
if hmsFormat then
time_string = time_string:gsub("0(%d)", "%1") -- delete all leading "0"s
time_string = time_string:gsub(C_("Time", "d"), C_("Time", "d") .. "\u{2009}") -- add thin space after "d"
time_string = time_string:gsub(C_("Time", "h"), C_("Time", "h") .. "\u{2009}") -- add thin space after "h"
if not withoutSeconds then
time_string = time_string:gsub(C_("Time", "m"), C_("Time", "m") .. "\u{2009}") .. C_("Time", "s") -- add thin space after "m"
end
if compact then
time_string = time_string:gsub("\u{2009}", "\u{200A}") -- replace thin space with hair space
end
return time_string
else
time_string = time_string:gsub(C_("Time", "m"), "'") -- replace m with '
return withoutSeconds and time_string or (time_string .. SECONDS_SYMBOL)
end
end
end
--- Converts seconds to a clock type (classic or modern), based on the given format preference
--- "Classic" format calls secondsToClock, "Modern" and "Letters" formats call secondsToHClock
---- @string Either "modern" for 1h30'10", "letters" for 1h30m10s, or "classic" for 1:30:10
---- @bool withoutSeconds if true 1h30' or 1h30m, if false 1h30'10" or 1h30m10s
---- @bool withDays, if hours>=24 include days in clock string 1d12h10'10" or 1d12h10m10s
---- @bool compact, if set removes all leading zeros (incl. units if necessary) and turns thinspaces into hairspaces (if present)
---- @treturn string clock string in the specific format of 1h30', 1h30'10" resp. 1h30m, 1h30m10s
function datetime.secondsToClockDuration(format, seconds, withoutSeconds, withDays, compact)
if format == "modern" then
return datetime.secondsToHClock(seconds, withoutSeconds, false, withDays, compact)
elseif format == "letters" then
return datetime.secondsToHClock(seconds, withoutSeconds, true, withDays, compact)
else
-- Assume "classic" to give safe default
return datetime.secondsToClock(seconds, withoutSeconds, withDays)
end
end
if jit.os == "Windows" then
--- Converts timestamp to an hour string
---- @int seconds number of seconds
---- @bool twelve_hour_clock
---- @treturn string hour string
---- @note: The MS CRT doesn't support either %l & %k, or the - format modifier (as they're not technically C99 or POSIX).
---- They are otherwise supported on Linux, BSD & Bionic, so, just special-case Windows...
---- We *could* arguably feed the os.date output to gsub("^0(%d)(.*)$", "%1%2"), but, while unlikely,
---- it's conceivable that a translator would put something other that the hour at the front of the string ;).
function datetime.secondsToHour(seconds, twelve_hour_clock)
if twelve_hour_clock then
if os.date("%p", seconds) == "AM" then
-- @translators This is the time in the morning using a 12-hour clock (%I is the hour, %M the minute).
return os.date(_("%I:%M AM"), seconds)
else
-- @translators This is the time in the afternoon using a 12-hour clock (%I is the hour, %M the minute).
return os.date(_("%I:%M PM"), seconds)
end
else
-- @translators This is the time using a 24-hour clock (%H is the hour, %M the minute).
return os.date(_("%H:%M"), seconds)
end
end
else
function datetime.secondsToHour(seconds, twelve_hour_clock, pad_with_spaces)
if twelve_hour_clock then
if os.date("%p", seconds) == "AM" then
if pad_with_spaces then
-- @translators This is the time in the morning using a 12-hour clock (%_I is the hour, %M the minute).
return os.date(_("%_I:%M AM"), seconds)
else
-- @translators This is the time in the morning using a 12-hour clock (%-I is the hour, %M the minute).
return os.date(_("%-I:%M AM"), seconds)
end
else
if pad_with_spaces then
-- @translators This is the time in the afternoon using a 12-hour clock (%_I is the hour, %M the minute).
return os.date(_("%_I:%M PM"), seconds)
else
-- @translators This is the time in the afternoon using a 12-hour clock (%-I is the hour, %M the minute).
return os.date(_("%-I:%M PM"), seconds)
end
end
else
if pad_with_spaces then
-- @translators This is the time using a 24-hour clock (%_H is the hour, %M the minute).
return os.date(_("%_H:%M"), seconds)
else
-- @translators This is the time using a 24-hour clock (%-H is the hour, %M the minute).
return os.date(_("%-H:%M"), seconds)
end
end
end
end
--- Converts timestamp to a date string
---- @int seconds number of seconds
---- @use_locale if true allows to translate the date-time string, if false return "%Y-%m-%d time"
---- @treturn string date string
function datetime.secondsToDate(seconds, use_locale)
seconds = seconds or os.time()
if use_locale then
local wday = os.date("%a", seconds)
local month = os.date("%b", seconds)
local day = os.date("%d", seconds)
local year = os.date("%Y", seconds)
-- @translators Use the following placeholders in the desired order: %1 name of day, %2 name of month, %3 day, %4 year
return T(C_("Date string", "%1 %2 %3 %4"),
datetime.shortDayOfWeekTranslation[wday], datetime.shortMonthTranslation[month], day, year)
else
-- @translators This is the date (%Y is the year, %m the month, %d the day)
return os.date(C_("Date string", "%Y-%m-%d"), seconds)
end
end
--- Converts timestamp to a date+time string
---- @int seconds number of seconds
---- @bool twelve_hour_clock
---- @use_locale if true allows to translate the date-time string, if false return "%Y-%m-%d time"
---- @treturn string date+time
function datetime.secondsToDateTime(seconds, twelve_hour_clock, use_locale)
seconds = seconds or os.time()
if twelve_hour_clock == nil then
twelve_hour_clock = G_reader_settings:isTrue("twelve_hour_clock")
end
local BD = require("ui/bidi")
local date_string = datetime.secondsToDate(seconds, use_locale)
local time_string = datetime.secondsToHour(seconds, twelve_hour_clock, not use_locale)
-- @translators Use the following placeholders in the desired order: %1 date, %2 time
local message_text = T(C_("Date string", "%1 %2"), BD.wrap(date_string), BD.wrap(time_string))
return message_text
end
--- Converts a date+time string to seconds
---- @string "YYYY-MM-DD HH:MM:SS", time may be absent
---- @treturn seconds
function datetime.stringToSeconds(datetime_string)
local year, month, day = datetime_string:match("(%d+)-(%d+)-(%d+)")
local hour, min, sec = datetime_string:match("(%d+):(%d+):(%d+)")
return os.time({ year = year, month = month, day = day, hour = hour or 0, min = min or 0, sec = sec or 0 })
end
return datetime

@ -49,7 +49,7 @@ function Dbg:turnOn()
if post_guard then
post_guard(...)
end
return unpack(values)
return unpack(values, 1, values.n)
end
end
--- Use this instead of a regular Lua @{assert}().
@ -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())

@ -1,9 +1,11 @@
local FFIUtil = require("ffi/util")
local Generic = require("device/generic/device")
local A, android = pcall(require, "android") -- luacheck: ignore
local Event = require("ui/event")
local Geom = require("ui/geometry")
local Generic = require("device/generic/device")
local UIManager
local ffi = require("ffi")
local C = ffi.C
local FFIUtil = require("ffi/util")
local lfs = require("libs/libkoreader-lfs")
local logger = require("logger")
local util = require("util")
@ -72,6 +74,7 @@ local Device = Generic:extend{
model = android.prop.product,
hasKeys = yes,
hasDPad = no,
hasSeamlessWifiToggle = no, -- Requires losing focus to the sytem's network settings and user interaction
hasExitOptions = no,
hasEinkScreen = function() return android.isEink() end,
hasColorScreen = android.isColorScreen,
@ -129,8 +132,6 @@ function Device:init()
device = self,
event_map = event_map,
handleMiscEv = function(this, ev)
local Event = require("ui/event")
local UIManager = require("ui/uimanager")
logger.dbg("Android application event", ev.code)
if ev.code == C.APP_CMD_SAVE_STATE then
UIManager:broadcastEvent(Event:new("FlushSettings"))
@ -280,6 +281,10 @@ function Device:init()
Generic.init(self)
end
function Device:UIManagerReady(uimgr)
UIManager = uimgr
end
function Device:initNetworkManager(NetworkMgr)
function NetworkMgr:turnOnWifi(complete_callback)
android.openWifiSettings()
@ -292,12 +297,13 @@ function Device:initNetworkManager(NetworkMgr)
android.openWifiSettings()
end
function NetworkMgr:isWifiOn()
function NetworkMgr:isConnected()
local ok = android.getNetworkInfo()
ok = tonumber(ok)
if not ok then return false end
return ok == 1
end
NetworkMgr.isWifiOn = NetworkMgr.isConnected
end
function Device:performHapticFeedback(type)
@ -330,8 +336,8 @@ function Device:retrieveNetworkInfo()
end
end
function Device:setViewport(x,y,w,h)
logger.info(string.format("Switching viewport to new geometry [x=%d,y=%d,w=%d,h=%d]",x, y, w, h))
function Device:setViewport(x, y, w, h)
logger.info(string.format("Switching viewport to new geometry [x=%d,y=%d,w=%d,h=%d]", x, y, w, h))
local viewport = Geom:new{x=x, y=y, w=w, h=h}
self.screen:setViewport(viewport)
end
@ -379,12 +385,13 @@ function Device:_toggleStatusBarVisibility()
local statusbar_height = android.getStatusBarHeight()
local new_height = height - statusbar_height
local Input = require("device/input")
if not is_fullscreen and self.viewport then
statusbar_height = 0
-- reset touchTranslate to normal
self.input:registerEventAdjustHook(
self.input.adjustTouchTranslate,
{x = 0 + self.viewport.x, y = 0 + self.viewport.y})
-- (since we don't setup any hooks besides the viewport one,
-- (we can just reset the hook to the default NOP instead of piling on +/- translations...)
self.input.eventAdjustHook = Input.eventAdjustHook
end
local viewport = Geom:new{x=0, y=statusbar_height, w=width, h=new_height}
@ -392,10 +399,11 @@ function Device:_toggleStatusBarVisibility()
0, statusbar_height, width, new_height))
self.screen:setViewport(viewport)
if is_fullscreen and self.viewport then
if is_fullscreen and self.viewport and self.viewport.y ~= 0 then
self.input:registerEventAdjustHook(
self.input.adjustTouchTranslate,
{x = 0 - self.viewport.x, y = 0 - self.viewport.y})
{x = 0 - self.viewport.x, y = 0 - self.viewport.y}
)
end
self.fullscreen = is_fullscreen
@ -422,8 +430,8 @@ function Device:info()
local is_eink, eink_platform = android.isEink()
local product_type = android.getPlatformName()
local common_text = T(_("%1\n\nOS: Android %2, api %3\nBuild flavor: %4\n"),
android.prop.product, getCodename(), Device.firmware_rev, android.prop.flavor)
local common_text = T(_("%1\n\nOS: Android %2, api %3 on %4\nBuild flavor: %5\n"),
android.prop.product, getCodename(), Device.firmware_rev, jit.arch, android.prop.flavor)
local platform_text = ""
if product_type ~= "android" then
@ -443,11 +451,17 @@ function Device:info()
return common_text..platform_text..eink_text..wakelocks_text
end
function Device:isDeprecated()
return self.firmware_rev < 18
end
function Device:test()
android.runTest()
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
@ -480,7 +494,6 @@ function Device:showLightDialog()
-- Delay it until next tick so that the event loop gets a chance to drain the input queue,
-- and consume the APP_CMD_LOST_FOCUS event.
-- This helps prevent ANRs on Tolino (c.f., #6583 & #7552).
local UIManager = require("ui/uimanager")
UIManager:nextTick(function() self:_showLightDialog() end)
end
@ -489,15 +502,18 @@ function Device:_showLightDialog()
android.lights.showDialog(title, _("Brightness"), _("Warmth"), _("OK"), _("Cancel"))
local action = android.lights.dialogState()
while action == C.ALIGHTS_DIALOG_OPENED do
FFIUtil.usleep(250) -- dont pin the CPU
action = android.lights.dialogState()
end
if action == C.ALIGHTS_DIALOG_OK then
self.powerd.fl_intensity = self.powerd:frontlightIntensityHW()
self.powerd:_decideFrontlightState()
logger.dbg("Dialog OK, brightness: " .. self.powerd.fl_intensity)
if android.isWarmthDevice() then
self.powerd.fl_warmth = self.powerd:frontlightWarmthHW()
logger.dbg("Dialog OK, warmth: " .. self.powerd.fl_warmth)
end
local Event = require("ui/event")
local UIManager = require("ui/uimanager")
UIManager:broadcastEvent(Event:new("FrontlightStateChanged"))
elseif action == C.ALIGHTS_DIALOG_CANCEL then
logger.dbg("Dialog Cancel, brightness: " .. self.powerd.fl_intensity)
@ -516,7 +532,6 @@ end
function Device:download(link, name, ok_text)
local ConfirmBox = require("ui/widget/confirmbox")
local InfoMessage = require("ui/widget/infomessage")
local UIManager = require("ui/uimanager")
local ok = android.download(link, name)
if ok == C.ADOWNLOAD_EXISTS then
self:install()
@ -537,8 +552,6 @@ end
function Device:install()
local ConfirmBox = require("ui/widget/confirmbox")
local Event = require("ui/event")
local UIManager = require("ui/uimanager")
UIManager:show(ConfirmBox:new{
text = _("Update is ready. Install it now?"),
ok_text = _("Install"),

@ -6,25 +6,17 @@ local AndroidPowerD = BasePowerD:new{
fl_max = 100,
}
-- Let the footer know of the change
local function broadcastLightChanges()
if package.loaded["ui/uimanager"] ~= nil then
local Event = require("ui/event")
local UIManager = require("ui/uimanager")
UIManager:broadcastEvent(Event:new("FrontlightStateChanged"))
end
end
function AndroidPowerD:frontlightIntensityHW()
return math.floor(android.getScreenBrightness() / self.bright_diff * self.fl_max)
end
function AndroidPowerD:setIntensityHW(intensity)
-- if frontlight switch was toggled of, turn it on
-- If the frontlight switch was off, turn it on.
android.enableFrontlightSwitch()
self.fl_intensity = intensity
android.setScreenBrightness(math.floor(intensity * self.bright_diff / self.fl_max))
self:_decideFrontlightState()
end
function AndroidPowerD:init()
@ -66,11 +58,13 @@ function AndroidPowerD:turnOffFrontlightHW()
return
end
android.setScreenBrightness(self.fl_min)
self.is_fl_on = false
broadcastLightChanges()
if android.hasStandaloneWarmth() then
android.setScreenWarmth(self.fl_warmth_min)
end
end
function AndroidPowerD:turnOnFrontlightHW()
function AndroidPowerD:turnOnFrontlightHW(done_callback)
if self:isFrontlightOn() and self:isFrontlightOnHW() then
return
end
@ -79,8 +73,10 @@ function AndroidPowerD:turnOnFrontlightHW()
android.setScreenBrightness(math.floor(self.fl_intensity * self.bright_diff / self.fl_max))
self.is_fl_on = true
broadcastLightChanges()
if android.hasStandaloneWarmth() then
android.setScreenWarmth(math.floor(self.fl_warmth / self.warm_diff))
end
return false
end
return AndroidPowerD

@ -12,20 +12,6 @@ local function getProductId()
return product_id
end
local function isConnected()
-- read carrier state from sysfs (for eth0)
local file = io.open("/sys/class/net/eth0/carrier", "rb")
-- file exists while Wi-Fi module is loaded.
if not file then return 0 end
-- 0 means not connected, 1 connected
local out = file:read("*number")
file:close()
return out or 0
end
local function isMassStorageSupported()
-- we rely on 3rd party package for that. It should be installed as part of KOReader prerequisites,
local safemode_version = io.open("/usr/share/safemode/version", "rb")
@ -46,9 +32,11 @@ local Cervantes = Generic:extend{
hasFastWifiStatusQuery = yes,
hasKeys = yes,
hasWifiManager = yes,
hasWifiRestore = yes,
canReboot = yes,
canPowerOff = yes,
canSuspend = yes,
supportsScreensaver = yes,
home_dir = "/mnt/public",
-- do we support usb mass storage?
@ -106,13 +94,9 @@ local Cervantes4 = Cervantes:extend{
-- input events
function Cervantes:initEventAdjustHooks()
if self.touch_switch_xy then
self.input:registerEventAdjustHook(self.input.adjustTouchSwitchXY)
end
if self.touch_mirrored_x then
if self.touch_switch_xy and self.touch_mirrored_x then
self.input:registerEventAdjustHook(
self.input.adjustTouchMirrorX,
self.input.adjustTouchSwitchAxesAndMirrorX,
(self.screen:getWidth() - 1)
)
end
@ -175,10 +159,10 @@ function Cervantes:initNetworkManager(NetworkMgr)
complete_callback()
end
end
function NetworkMgr:turnOnWifi(complete_callback)
function NetworkMgr:turnOnWifi(complete_callback, interactive)
logger.info("Cervantes: enabling Wi-Fi")
os.execute("./enable-wifi.sh")
self:reconnectOrShowNetworkMenu(complete_callback)
return self:reconnectOrShowNetworkMenu(complete_callback, interactive)
end
function NetworkMgr:getNetworkInterfaceName()
return "eth0"
@ -193,33 +177,8 @@ function Cervantes:initNetworkManager(NetworkMgr)
function NetworkMgr:restoreWifiAsync()
os.execute("./restore-wifi-async.sh")
end
function NetworkMgr:isWifiOn()
return 1 == isConnected()
end
end
-- screensaver
function Cervantes:supportsScreensaver()
return true
end
function Cervantes:intoScreenSaver()
local Screensaver = require("ui/screensaver")
if self.screen_saver_mode == false then
Screensaver:setup()
Screensaver:show()
end
self.powerd:beforeSuspend()
self.screen_saver_mode = true
end
function Cervantes:outofScreenSaver()
if self.screen_saver_mode == true then
local Screensaver = require("ui/screensaver")
Screensaver:close()
local UIManager = require("ui/uimanager")
UIManager:nextTick(function() UIManager:setDirty("all", "full") end)
end
self.powerd:afterResume()
self.screen_saver_mode = false
NetworkMgr.isWifiOn = NetworkMgr.sysfsWifiOn
NetworkMgr.isConnected = NetworkMgr.ifHasAnAddress
end
-- power functions: suspend, resume, reboot, poweroff
@ -242,16 +201,10 @@ function Cervantes:setEventHandlers(UIManager)
-- suspend. So let's unschedule it when suspending, and restart it after
-- resume. Done via the plugin's onSuspend/onResume handlers.
UIManager.event_handlers.Suspend = function()
self:_beforeSuspend()
self:onPowerEvent("Suspend")
end
UIManager.event_handlers.Resume = function()
-- MONOTONIC doesn't tick during suspend,
-- invalidate the last battery capacity pull time so that we get up to date data immediately.
self:getPowerDevice():invalidateCapacityCache()
self:onPowerEvent("Resume")
self:_afterResume()
end
UIManager.event_handlers.PowerPress = function()
-- Always schedule power off.
@ -263,7 +216,11 @@ function Cervantes:setEventHandlers(UIManager)
UIManager:unschedule(UIManager.poweroff_action)
-- resume if we were suspended
if self.screen_saver_mode then
UIManager.event_handlers.Resume()
if self.screen_saver_lock then
UIManager.event_handlers.Suspend()
else
UIManager.event_handlers.Resume()
end
else
UIManager.event_handlers.Suspend()
end
@ -276,7 +233,7 @@ function Cervantes:setEventHandlers(UIManager)
UIManager.event_handlers.Charging = function()
self:_beforeCharging()
-- NOTE: Plug/unplug events will wake the device up, which is why we put it back to sleep.
if self.screen_saver_mode then
if self.screen_saver_mode and not self.screen_saver_lock then
UIManager.event_handlers.Suspend()
end
end
@ -284,7 +241,7 @@ function Cervantes:setEventHandlers(UIManager)
-- We need to put the device into suspension, other things need to be done before it.
self:usbPlugOut()
self:_afterNotCharging()
if self.screen_saver_mode then
if self.screen_saver_mode and not self.screen_saver_lock then
UIManager.event_handlers.Suspend()
end
end
@ -292,9 +249,9 @@ function Cervantes:setEventHandlers(UIManager)
UIManager.event_handlers.UsbPlugIn = function()
self:_beforeCharging()
-- NOTE: Plug/unplug events will wake the device up, which is why we put it back to sleep.
if self.screen_saver_mode then
if self.screen_saver_mode and not self.screen_saver_lock then
UIManager.event_handlers.Suspend()
else
elseif not self.screen_saver_lock then
-- Potentially start an USBMS session
local MassStorage = require("ui/elements/mass_storage")
MassStorage:start()
@ -304,9 +261,9 @@ function Cervantes:setEventHandlers(UIManager)
-- We need to put the device into suspension, other things need to be done before it.
self:usbPlugOut()
self:_afterNotCharging()
if self.screen_saver_mode then
if self.screen_saver_mode and not self.screen_saver_lock then
UIManager.event_handlers.Suspend()
else
elseif not self.screen_saver_lock then
-- Potentially dismiss the USBMS ConfirmBox
local MassStorage = require("ui/elements/mass_storage")
MassStorage:dismiss()

@ -51,10 +51,13 @@ function CervantesPowerD:init()
if self.device:hasNaturalLight() then
local nl_config = G_reader_settings:readSetting("natural_light_config")
if nl_config then
for key,val in pairs(nl_config) do
for key, val in pairs(nl_config) do
self.device.frontlight_settings[key] = val
end
end
-- Does this device's NaturalLight use a custom scale?
self.fl_warmth_min = self.device.frontlight_settings.nl_min or self.fl_warmth_min
self.fl_warmth_max = self.device.frontlight_settings.nl_max or self.fl_warmth_max
-- If this device has a mixer, we can use the ioctl for brightness control, as it's much lower latency.
if self.device:hasNaturalLightMixer() then
local kobolight = require("ffi/kobolight")
@ -132,19 +135,29 @@ function CervantesPowerD:isChargingHW()
end
function CervantesPowerD:beforeSuspend()
if self.fl == nil then return end
-- just turn off frontlight without remembering its state
self.fl:setBrightness(0)
-- Inhibit user input and emit the Suspend event.
self.device:_beforeSuspend()
if self.fl then
-- just turn off frontlight without remembering its state
self.fl:setBrightness(0)
end
end
function CervantesPowerD:afterResume()
if self.fl == nil then return end
-- just re-set it to self.hw_intensity that we haven't change on Suspend
if not self.device:hasNaturalLight() then
self.fl:setBrightness(self.hw_intensity)
else
self.fl:setNaturalBrightness(self.hw_intensity, self.fl_warmth)
if self.fl then
-- just re-set it to self.hw_intensity that we haven't changed on Suspend
if not self.device:hasNaturalLight() then
self.fl:setBrightness(self.hw_intensity)
else
self.fl:setNaturalBrightness(self.hw_intensity, self.fl_warmth)
end
end
self:invalidateCapacityCache()
-- Restore user input and emit the Resume event.
self.device:_afterResume()
end
return CervantesPowerD

@ -10,18 +10,6 @@ local T = require("ffi/util").template
local DeviceListener = EventListener:extend{}
local function _setSetting(name)
G_reader_settings:makeTrue(name)
end
local function _unsetSetting(name)
G_reader_settings:delSetting(name)
end
local function _toggleSetting(name)
G_reader_settings:flipNilOrFalse(name)
end
function DeviceListener:onToggleNightMode()
local night_mode = G_reader_settings:isTrue("night_mode")
Screen:toggleNightMode()
@ -54,87 +42,68 @@ function DeviceListener:onShowIntensity()
return true
end
function DeviceListener:onShowWarmth(value)
function DeviceListener:onShowWarmth()
if not Device:hasNaturalLight() then return true end
-- Display it in the native scale, like FrontLightWidget
local powerd = Device:getPowerDevice()
if powerd.fl_warmth ~= nil then
-- powerd.fl_warmth holds the warmth-value in the internal koreader scale [0,100]
-- powerd.fl_warmth_max is the maximum value the hardware accepts
Notification:notify(T(_("Warmth set to %1%."), math.floor(powerd.fl_warmth)))
end
Notification:notify(T(_("Warmth set to %1."), powerd:toNativeWarmth(powerd:frontlightWarmth())))
return true
end
-- frontlight controller
if Device:hasFrontlight() then
local function calculateGestureDelta(ges, direction, min, max)
local delta_int
if type(ges) == "table" then
-- here we are using just two scales
-- big scale is for high dynamic ranges (e.g. brightness from 1..100)
-- original scale maybe tuned by hand
-- small scale is for lower dynamic ranges (e.g. warmth from 1..10)
-- scale entries are calculated by math.round(1*sqrt(2)^n)
local steps_fl_big_scale = { 0.1, 0.1, 0.2, 0.4, 0.7, 1.1, 1.6, 2.2, 2.9, 3.7, 4.6, 5.6, 6.7, 7.9, 9.2, 10.6, }
local steps_fl_small_scale = { 1.0, 1.0, 2.0, 3.0, 4.0, 6.0, 8.1, 11.3 }
local steps_fl = steps_fl_big_scale
if (max - min) < 50 then
steps_fl = steps_fl_small_scale
end
local gestureScale
local scale_multiplier
local gesture_multiplier
if ges.ges == "two_finger_swipe" or ges.ges == "swipe" then
scale_multiplier = 0.8
gesture_multiplier = 0.8
else
scale_multiplier = 1
gesture_multiplier = 1
end
local gestureScale
if ges.direction == "south" or ges.direction == "north" then
gestureScale = Screen:getHeight() * scale_multiplier
gestureScale = Screen:getHeight() * gesture_multiplier
elseif ges.direction == "west" or ges.direction == "east" then
gestureScale = Screen:getWidth() * scale_multiplier
gestureScale = Screen:getWidth() * gesture_multiplier
else
local width = Screen:getWidth()
local height = Screen:getHeight()
-- diagonal
gestureScale = math.sqrt(width * width + height * height) * scale_multiplier
end
local steps_tbl = {}
local scale = (max - min) / steps_fl[#steps_fl] / 2 -- full swipe gives half scale
for i = 1, #steps_fl, 1 do
steps_tbl[i] = math.ceil(steps_fl[i] * scale)
gestureScale = math.sqrt(width^2 + height^2) * gesture_multiplier
end
-- In case we're passed a gesture that doesn't imply movement (e.g., tap or hold)
if ges.distance == nil then
ges.distance = 1
end
local step = math.ceil(#steps_tbl * ges.distance / gestureScale)
delta_int = steps_tbl[step] or steps_tbl[#steps_tbl]
-- delta_int is calculated by a function f(x) = coeff * x^2
-- *) f(x) has the boundary condition: f(1) = max/2;
-- *) x is roughly the swipe distance as a fraction of the screen geometry,
-- clamped between 0 and 1
local x = math.min(1, ges.distance / gestureScale)
delta_int = math.ceil(1/2 * max * x^2)
else
-- received amount to change
-- The ges arg passed by our caller wasn't a gesture, but an absolute integer increment
delta_int = ges
end
if direction ~= -1 and direction ~= 1 then
-- set default value (increase frontlight)
-- If the caller didn't specify, opt to *increase* by default
direction = 1
end
return direction, delta_int
return direction * delta_int
end
-- direction +1 - increase frontlight
-- direction -1 - decrease frontlight
function DeviceListener:onChangeFlIntensity(ges, direction)
local powerd = Device:getPowerDevice()
local delta_int
--received gesture
direction, delta_int = calculateGestureDelta(ges, direction, powerd.fl_min, powerd.fl_max)
local delta = calculateGestureDelta(ges, direction, powerd.fl_min, powerd.fl_max)
local new_intensity = powerd.fl_intensity + direction * delta_int
if new_intensity == nil then return true end
-- when new_intensity <=0, toggle light off
local new_intensity = powerd:frontlightIntensity() + delta
-- when new_intensity <= 0, toggle light off
self:onSetFlIntensity(new_intensity)
self:onShowIntensity()
return true
@ -163,15 +132,17 @@ if Device:hasFrontlight() then
-- direction +1 - increase frontlight warmth
-- direction -1 - decrease frontlight warmth
function DeviceListener:onChangeFlWarmth(ges, direction)
local powerd = Device:getPowerDevice()
if powerd.fl_warmth == nil then return false end
if not Device:hasNaturalLight() then return true end
local delta_int
--received gesture
local powerd = Device:getPowerDevice()
local delta = calculateGestureDelta(ges, direction, powerd.fl_warmth_min, powerd.fl_warmth_max)
direction, delta_int = calculateGestureDelta(ges, direction, powerd.fl_warmth_min, powerd.fl_warmth_max)
-- Given that the native warmth ranges are usually pretty restrictive (e.g., [0, 10] or [0, 24]),
-- do the computations in the native scale, to ensure we always actually *change* something,
-- in case both the old and new value would round to the same native step,
-- despite being different in the API scale, which is stupidly fixed at [0, 100]...
local warmth = powerd:fromNativeWarmth(powerd:toNativeWarmth(powerd:frontlightWarmth()) + delta)
local warmth = math.floor(powerd.fl_warmth + direction * delta_int * 100 / powerd.fl_warmth_max)
self:onSetFlWarmth(warmth)
self:onShowWarmth()
return true
@ -198,15 +169,20 @@ if Device:hasFrontlight() then
function DeviceListener:onToggleFrontlight()
local powerd = Device:getPowerDevice()
powerd:toggleFrontlight()
local new_text
if powerd.is_fl_on then
new_text = _("Frontlight enabled.")
else
if powerd:isFrontlightOn() then
new_text = _("Frontlight disabled.")
else
new_text = _("Frontlight enabled.")
end
-- We defer displaying the Notification to PowerD, as the toggle may be a ramp, and we both want to make sure the refresh fencing won't affect it, and that we only display the Notification at the end...
local notif_source = Notification.notify_source
local notif_cb = function()
Notification:notify(new_text, notif_source)
end
if not powerd:toggleFrontlight(notif_cb) then
Notification:notify(_("Frontlight unchanged."), notif_source)
end
Notification:notify(new_text)
return true
end
function DeviceListener:onShowFlDialog()
@ -215,9 +191,9 @@ if Device:hasFrontlight() then
end
if Device:canToggleGSensor() then
if Device:hasGSensor() then
function DeviceListener:onToggleGSensor()
_toggleSetting("input_ignore_gsensor")
G_reader_settings:flipNilOrFalse("input_ignore_gsensor")
Device:toggleGSensor(not G_reader_settings:isTrue("input_ignore_gsensor"))
local new_text
if G_reader_settings:isTrue("input_ignore_gsensor") then
@ -225,9 +201,20 @@ if Device:canToggleGSensor() then
else
new_text = _("Accelerometer rotation events on.")
end
UIManager:show(Notification:new{
text = new_text,
})
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
@ -287,33 +274,64 @@ end
function DeviceListener:onSetFlashOnChapterBoundaries(toggle)
if toggle == true then
_setSetting("refresh_on_chapter_boundaries")
G_reader_settings:makeTrue("refresh_on_chapter_boundaries")
else
_unsetSetting("refresh_on_chapter_boundaries")
G_reader_settings:delSetting("refresh_on_chapter_boundaries")
end
end
function DeviceListener:onToggleFlashOnChapterBoundaries()
_toggleSetting("refresh_on_chapter_boundaries")
G_reader_settings:flipNilOrFalse("refresh_on_chapter_boundaries")
end
function DeviceListener:onSetNoFlashOnSecondChapterPage(toggle)
if toggle == true then
_setSetting("no_refresh_on_second_chapter_page")
G_reader_settings:makeTrue("no_refresh_on_second_chapter_page")
else
_unsetSetting("no_refresh_on_second_chapter_page")
G_reader_settings:delSetting("no_refresh_on_second_chapter_page")
end
end
function DeviceListener:onToggleNoFlashOnSecondChapterPage()
_toggleSetting("no_refresh_on_second_chapter_page")
G_reader_settings:flipNilOrFalse("no_refresh_on_second_chapter_page")
end
function DeviceListener:onSetFlashOnPagesWithImages(toggle)
if toggle == true then
G_reader_settings:delSetting("refresh_on_pages_with_images")
else
G_reader_settings:makeFalse("refresh_on_pages_with_images")
end
end
function DeviceListener:onToggleFlashOnPagesWithImages()
G_reader_settings:flipNilOrTrue("refresh_on_pages_with_images")
end
function DeviceListener:onSwapPageTurnButtons()
_toggleSetting("input_invert_page_turn_keys")
G_reader_settings:flipNilOrFalse("input_invert_page_turn_keys")
Device:invertButtons()
end
function DeviceListener:onToggleKeyRepeat(toggle)
if toggle == true then
G_reader_settings:makeFalse("input_no_key_repeat")
elseif toggle == false then
G_reader_settings:makeTrue("input_no_key_repeat")
else
G_reader_settings:flipNilOrFalse("input_no_key_repeat")
end
Device:toggleKeyRepeat(G_reader_settings:nilOrFalse("input_no_key_repeat"))
end
function DeviceListener:onRequestUSBMS()
local MassStorage = require("ui/elements/mass_storage")
-- It already takes care of the canToggleMassStorage cap check for us
-- NOTE: Never request confirmation, it's sorted right next to exit, restart & friends in Dispatcher,
-- and they don't either...
MassStorage:start(false)
end
function DeviceListener:onRestart()
self.ui.menu:exitOrRestart(function() UIManager:restartKOReader() end)
end
@ -323,11 +341,11 @@ function DeviceListener:onRequestSuspend()
end
function DeviceListener:onRequestReboot()
UIManager:reboot()
UIManager:askForReboot()
end
function DeviceListener:onRequestPowerOff()
UIManager:powerOff()
UIManager:askForPowerOff()
end
function DeviceListener:onExit(callback)
@ -341,4 +359,10 @@ function DeviceListener:onFullRefresh()
UIManager:setDirty(nil, "full")
end
-- On resume, make sure we restore Gestures handling in InputContainer, to avoid confusion for scatter-brained users ;).
-- It's also helpful when the IgnoreTouchInput event is emitted by Dispatcher through other means than Gestures.
function DeviceListener:onResume()
UIManager:setIgnoreTouchInput(false)
end
return DeviceListener

@ -5,25 +5,31 @@ This module defines stubs for common methods.
--]]
local DataStorage = require("datastorage")
local Event = require("ui/event")
local Geom = require("ui/geometry")
local UIManager -- Updated on UIManager init
local logger = require("logger")
local ffi = require("ffi")
local time = require("ui/time")
local util = require("util")
local _ = require("gettext")
local ffiUtil = require("ffi/util")
local C = ffi.C
local T = ffiUtil.template
-- We'll need a bunch of stuff for getifaddrs & co in Device:retrieveNetworkInfo
require("ffi/posix_h")
local function yes() return true end
local function no() return false end
local Device = {
screen_saver_mode = false,
charging_mode = false,
survive_screen_saver = false,
screen_saver_lock = false,
is_cover_closed = false,
model = nil,
powerd = nil,
screen = nil,
screen_dpi_override = nil,
input = nil,
home_dir = nil,
-- For Kobo, wait at least 15 seconds before calling suspend script. Otherwise, suspend might
@ -35,24 +41,31 @@ 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,
hasSeamlessWifiToggle = yes, -- Can toggle Wi-Fi without focus loss and extra user interaction (i.e., not Android)
hasWifiManager = no,
hasWifiRestore = no,
isDefaultFullscreen = yes,
isHapticFeedbackEnabled = no,
isDeprecated = no, -- device no longer receive OTA updates
isTouchDevice = no,
hasFrontlight = no,
hasNaturalLight = no, -- FL warmth implementation specific to NTX boards (Kobo, Cervantes)
hasNaturalLightMixer = no, -- Same, but only found on newer boards
hasNaturalLightApi = no,
needsTouchScreenProbe = no,
hasClipboard = yes, -- generic internal clipboard on all devices
hasEinkScreen = yes,
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
@ -61,10 +74,10 @@ local Device = {
canImportFiles = no,
canShareText = no,
hasGSensor = no,
canToggleGSensor = no,
isGSensorLocked = no,
canToggleMassStorage = no,
canToggleChargingLED = no,
_updateChargingLED = nil,
canUseWAL = yes, -- requires mmap'ed I/O on the target FS
canRestart = yes,
canSuspend = no,
@ -157,6 +170,30 @@ function Device:invertButtons()
end
end
function Device:invertButtonsLeft()
if self:hasKeys() and self.input and self.input.event_map then
for key, value in pairs(self.input.event_map) do
if value == "LPgFwd" then
self.input.event_map[key] = "LPgBack"
elseif value == "LPgBack" then
self.input.event_map[key] = "LPgFwd"
end
end
end
end
function Device:invertButtonsRight()
if self:hasKeys() and self.input and self.input.event_map then
for key, value in pairs(self.input.event_map) do
if value == "RPgFwd" then
self.input.event_map[key] = "RPgBack"
elseif value == "RPgBack" then
self.input.event_map[key] = "RPgFwd"
end
end
end
end
function Device:init()
if not self.screen then
error("screen/framebuffer must be implemented")
@ -201,12 +238,18 @@ function Device:init()
self.powerd = require("device/generic/powerd"):new{device = self}
end
-- NOTE: This needs to run *after* implementation-specific event hooks,
-- especially if those require swapping/mirroring...
-- (e.g., Device implementations should setup their own hooks *before* calling this via Generic.init(self)).
if self.viewport then
logger.dbg("setting a viewport:", self.viewport)
self.screen:setViewport(self.viewport)
self.input:registerEventAdjustHook(
self.input.adjustTouchTranslate,
{x = 0 - self.viewport.x, y = 0 - self.viewport.y})
if self.viewport.x ~= 0 or self.viewport.y ~= 0 then
self.input:registerEventAdjustHook(
self.input.adjustTouchTranslate,
{x = 0 - self.viewport.x, y = 0 - self.viewport.y}
)
end
end
-- Handle button mappings shenanigans
@ -214,10 +257,21 @@ function Device:init()
if G_reader_settings:isTrue("input_invert_page_turn_keys") then
self:invertButtons()
end
if G_reader_settings:isTrue("input_invert_left_page_turn_keys") then
self:invertButtonsLeft()
end
if G_reader_settings:isTrue("input_invert_right_page_turn_keys") then
self:invertButtonsRight()
end
end
-- Honor the gyro lock
if self:hasGSensor() then
-- Setup our standard gyro event handler (EV_MSC:MSC_GYRO)
if G_reader_settings:nilOrFalse("input_ignore_gsensor") then
self.input.handleGyroEv = self.input.handleMiscGyroEv
end
-- Honor the gyro lock
if G_reader_settings:isTrue("input_lock_gsensor") then
self:lockGSensor(true)
end
@ -229,6 +283,47 @@ function Device:init()
local rect = self.screen.getRawSize(self.screen)
return Geom:new{ x = rect.x, y = rect.y, w = rect.w, h = rect.h }
end
-- DPI
local dpi_override = G_reader_settings:readSetting("screen_dpi")
if dpi_override ~= nil then
self:setScreenDPI(dpi_override)
end
-- Night mode
self.orig_hw_nightmode = self.screen:getHWNightmode()
if G_reader_settings:isTrue("night_mode") then
self.screen:toggleNightMode()
end
-- Ensure the proper rotation on startup.
-- We default to the rotation KOReader closed with.
-- If the rotation is not locked it will be overridden by a book or the FM when opened.
local rotation_mode = G_reader_settings:readSetting("closed_rotation_mode")
if rotation_mode and rotation_mode ~= self.screen:getRotationMode() then
self.screen:setRotationMode(rotation_mode)
end
-- Dithering
if self:hasEinkScreen() then
self.screen:setupDithering()
if self.screen.hw_dithering and G_reader_settings:isTrue("dev_no_hw_dither") then
self.screen:toggleHWDithering(false)
end
if self.screen.sw_dithering and G_reader_settings:isTrue("dev_no_sw_dither") then
self.screen:toggleSWDithering(false)
end
-- NOTE: If device can HW dither (i.e., after setupDithering(), hw_dithering is true, but sw_dithering is false),
-- but HW dither is explicitly disabled, and SW dither enabled, don't leave SW dither disabled (i.e., re-enable sw_dithering)!
if self:canHWDither() and G_reader_settings:isTrue("dev_no_hw_dither") and G_reader_settings:nilOrFalse("dev_no_sw_dither") then
self.screen:toggleSWDithering(true)
end
end
-- Can't be seamless if you can't do it at all ;)
if not self:hasWifiToggle() then
self.hasSeamlessWifiToggle = no
end
end
function Device:setScreenDPI(dpi_override)
@ -246,7 +341,6 @@ function Device:getPowerDevice()
end
function Device:rescheduleSuspend()
local UIManager = require("ui/uimanager")
UIManager:unschedule(self.suspend)
UIManager:scheduleIn(self.suspend_wait_timeout, self.suspend, self)
end
@ -257,38 +351,47 @@ function Device:onPowerEvent(ev)
if self.screen_saver_mode then
if ev == "Power" or ev == "Resume" then
if self.is_cover_closed then
-- don't let power key press wake up device when the cover is in closed state
-- Don't let power key press wake up device when the cover is in closed state.
logger.dbg("Pressed power while asleep in screen saver mode with a closed sleepcover, going back to suspend...")
self:rescheduleSuspend()
else
logger.dbg("Resuming...")
local UIManager = require("ui/uimanager")
UIManager:unschedule(self.suspend)
if self:hasWifiManager() and not self:isEmulator() then
local network_manager = require("ui/network/manager")
if network_manager.wifi_was_on and G_reader_settings:isTrue("auto_restore_wifi") then
network_manager:restoreWifiAsync()
network_manager:scheduleConnectivityCheck()
end
end
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.screen_saver_mode = false
self.powerd:afterResume()
end
elseif ev == "Suspend" then
-- Already in screen saver mode, no need to update UI/state before
-- suspending the hardware. This usually happens when sleep cover
-- is closed after the device was sent to suspend state.
logger.dbg("Already in screen saver mode, going back to suspend...")
-- Already in screen saver mode, no need to update the UI (and state, usually) before suspending again.
-- This usually happens when the sleep cover is closed on an already sleeping device,
-- (e.g., it was previously suspended via the Power button).
if self.screen_saver_lock then
-- This can only happen when some sort of screensaver_delay is set,
-- and the user presses the Power button *after* already having woken up the device.
-- In this case, we want to go back to suspend *without* affecting the screensaver,
-- so we simply mimic our own behavior when *not* in screen_saver_mode ;).
logger.dbg("Pressed power while awake in screen saver mode, going back to suspend...")
-- Basically, this is the only difference.
-- We need it because we're actually in a sane post-Resume event state right now.
self.powerd:beforeSuspend()
else
logger.dbg("Already in screen saver mode, going back to suspend...")
end
-- Much like the real suspend codepath below, in case we got here via screen_saver_lock,
-- make sure we murder WiFi again (because restore WiFi on resume could have kicked in).
if self:hasWifiToggle() then
local network_manager = require("ui/network/manager")
if network_manager:isWifiOn() then
network_manager:disableWifi()
end
end
self:rescheduleSuspend()
end
-- else we were not in screensaver mode
elseif ev == "Power" or ev == "Suspend" then
self.powerd:beforeSuspend()
local UIManager = require("ui/uimanager")
logger.dbg("Suspending...")
-- Add the current state of the SleepCover flag...
logger.dbg("Sleep cover is", self.is_cover_closed and "closed" or "open")
@ -302,35 +405,31 @@ 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
self.screen_saver_mode = true
UIManager:scheduleIn(0.1, function()
-- NOTE: This side of the check needs to be laxer, some platforms can handle Wi-Fi without WifiManager ;).
if self:hasWifiToggle() then
local network_manager = require("ui/network/manager")
-- NOTE: wifi_was_on does not necessarily mean that Wi-Fi is *currently* on! It means *we* enabled it.
-- This is critical on Kobos (c.f., #3936), where it might still be on from KSM or Nickel,
-- without us being aware of it (i.e., wifi_was_on still unset or false),
-- because suspend will at best fail, and at worst deadlock the system if Wi-Fi is on,
-- regardless of who enabled it!
if network_manager:isWifiOn() then
network_manager:releaseIP()
network_manager:turnOffWifi()
end
end
-- Only actually schedule suspension if we're still supposed to go to sleep,
-- because the Wi-Fi stuff above may have blocked for a significant amount of time...
if self.screen_saver_mode then
self:rescheduleSuspend()
-- NOTE: In the same vein as above, make sure we update the screen *now*, before dealing with Wi-Fi.
UIManager:forceRePaint()
-- NOTE: This side of the check needs to be laxer, some platforms can handle Wi-Fi without WifiManager ;).
if self:hasWifiToggle() then
local network_manager = require("ui/network/manager")
-- NOTE: wifi_was_on does not necessarily mean that Wi-Fi is *currently* on! It means *we* enabled it.
-- This is critical on Kobos (c.f., #3936), where it might still be on from KSM or Nickel,
-- without us being aware of it (i.e., wifi_was_on still unset or false),
-- because suspend will at best fail, and at worst deadlock the system if Wi-Fi is on,
-- regardless of who enabled it!
if network_manager:isWifiOn() then
network_manager:disableWifi()
end
end)
end
-- Only turn off the frontlight *after* we've displayed the screensaver and dealt with Wi-Fi,
-- to prevent that from affecting the smoothness of the frontlight ramp down.
self.powerd:beforeSuspend()
self:rescheduleSuspend()
end
end
function Device:showLightDialog()
local FrontLightWidget = require("ui/widget/frontlightwidget")
local UIManager = require("ui/uimanager")
UIManager:show(FrontLightWidget:new{})
end
@ -339,8 +438,6 @@ function Device:info()
end
function Device:install()
local Event = require("ui/event")
local UIManager = require("ui/uimanager")
local ConfirmBox = require("ui/widget/confirmbox")
UIManager:show(ConfirmBox:new{
text = _("Update is ready. Install it now?"),
@ -352,6 +449,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
@ -420,8 +526,17 @@ function Device:performHapticFeedback(type) end
-- Device specific method for toggling input events
function Device:setIgnoreInput(enable) return true end
-- Device specific method for toggling the GSensor
function Device:toggleGSensor(toggle) end
-- Device agnostic method for toggling the GSensor
-- (can be reimplemented if need be, but you really, really should try not to. c.f., Kobo, Kindle & PocketBook)
function Device:toggleGSensor(toggle)
if not self:hasGSensor() then
return
end
if self.input then
self.input:toggleGyroEvents(toggle)
end
end
-- Whether or not the GSensor should be locked to the current orientation (i.e. Portrait <-> Inverted Portrait or Landscape <-> Inverted Landscape only)
function Device:lockGSensor(toggle)
@ -456,56 +571,400 @@ function Device:setupChargingLED() end
function Device:enableCPUCores(amount) end
-- NOTE: For this to work, all three must be implemented, and getKeyRepeat must be run on init (c.f., Kobo)!
-- Device specific method to get the current key repeat setup
-- Device specific method to get the current key repeat setup (and is responsible for setting the canKeyRepeat cap)
function Device:getKeyRepeat() end
-- Device specific method to disable key repeat
function Device:disableKeyRepeat() end
-- Device specific method to restore key repeat
-- Device specific method to restore the initial key repeat config
function Device:restoreKeyRepeat() end
-- NOTE: This one is for the user-facing toggle, it *ignores* the stock delay/period combo,
-- opting instead for a hard-coded one (as we can't guarantee that key repeat is actually setup properly or at all).
-- Device specific method to toggle key repeat
function Device:toggleKeyRepeat(toggle) end
--[[
prepare for application shutdown
--]]
function Device:exit()
-- Save any implementation-specific settings
self:saveSettings()
-- Save current rotation (or the original rotation if ScreenSaver temporarily modified it) to remember it for next startup
G_reader_settings:saveSetting("closed_rotation_mode", self.orig_rotation_mode or self.screen:getRotationMode())
-- Restore initial HW inversion state
self.screen:setHWNightmode(self.orig_hw_nightmode)
-- Tear down the fb backend
self.screen:close()
require("ffi/input"):closeAll()
-- Flush settings to disk
G_reader_settings:close()
-- I/O teardown
self.input.teardown()
end
function Device:retrieveNetworkInfo()
local std_out = io.popen("ifconfig | " ..
"sed -n " ..
"-e 's/ \\+$//g' " ..
"-e 's/ \\+/ /g' " ..
"-e 's/ \\?inet6\\? addr: \\?\\([^ ]\\+\\) .*$/IP: \\1/p' " ..
"-e 's/Link encap:Ethernet\\(.*\\)/\\1/p'",
"r")
if std_out then
local result = std_out:read("*all")
std_out:close()
std_out = io.popen('2>/dev/null iwconfig | grep ESSID | cut -d\\" -f2')
if std_out then
local ssid = std_out:read("*all")
result = result .. "SSID: " .. util.trim(ssid) .. "\n"
std_out:close()
end
if os.execute("ip r | grep -q default") == 0 then
-- NOTE: No -w flag available in the old busybox build used on Legacy Kindles (K4 included)...
local pingok
-- Lifted from busybox's libbb/inet_cksum.c
local function inet_cksum(ptr, nleft)
local addr = ffi.new("const uint16_t *", ptr)
local sum = ffi.new("unsigned int", 0)
while nleft > 1 do
sum = sum + addr[0]
addr = addr + 1
nleft = nleft - 2
end
if nleft == 1 then
local u8p = ffi.cast("uint8_t *", addr)
sum = sum + u8p[0]
end
sum = bit.rshift(sum, 16) + bit.band(sum, 0xFFFF)
sum = sum + bit.rshift(sum, 16)
sum = bit.bnot(sum)
return ffi.cast("uint16_t", sum)
end
function Device:ping4(ip)
-- Try an unprivileged ICMP socket first
-- NOTE: This is disabled by default, barring custom distro setup during init, c.f., sysctl net.ipv4.ping_group_range
-- It also requires Linux 3.0+ (https://github.com/torvalds/linux/commit/c319b4d76b9e583a5d88d6bf190e079c4e43213d)
local socket, socket_type
socket = C.socket(C.AF_INET, bit.bor(C.SOCK_DGRAM, C.SOCK_NONBLOCK, C.SOCK_CLOEXEC), C.IPPROTO_ICMP)
if socket == -1 then
local errno = ffi.errno()
logger.dbg("Device:ping4: unprivileged ICMP socket:", ffi.string(C.strerror(errno)))
-- Try a raw socket
socket = C.socket(C.AF_INET, bit.bor(C.SOCK_RAW, C.SOCK_NONBLOCK, C.SOCK_CLOEXEC), C.IPPROTO_ICMP)
if socket == -1 then
errno = ffi.errno()
if errno == C.EPERM then
logger.dbg("Device:ping4: opening a RAW ICMP socket requires CAP_NET_RAW capabilities!")
else
logger.dbg("Device:ping4: raw ICMP socket:", ffi.string(C.strerror(errno)))
end
--- Fall-back to the ping CLI tool, in the hope that it's setuid...
if self:isKindle() and self:hasDPad() then
pingok = os.execute("ping -q -c 2 `ip r | grep default | tail -n 1 | cut -d ' ' -f 3` > /dev/null")
-- NOTE: No -w flag available in the old busybox build used on Legacy Kindles (K4 included)...
return os.execute("ping -q -c1 " .. ip .. " > /dev/null") == 0
else
pingok = os.execute("ping -q -w 3 -c 2 `ip r | grep default | tail -n 1 | cut -d ' ' -f 3` > /dev/null")
return os.execute("ping -q -c1 -w2 " .. ip .. " > /dev/null") == 0
end
else
socket_type = C.SOCK_RAW
end
else
socket_type = C.SOCK_DGRAM
end
-- c.f., busybox's networking/ping.c
local DEFDATALEN = 56 -- 64 - 8
local MAXIPLEN = 60
local MAXICMPLEN = 76
-- Base the id on our PID (like busybox)
local myid = ffi.cast("uint16_t", C.getpid())
myid = C.htons(myid)
-- Setup the packet
local packet = ffi.new("char[?]", DEFDATALEN + MAXIPLEN + MAXICMPLEN)
local pkt = ffi.cast("struct icmphdr *", packet)
pkt.type = C.ICMP_ECHO
pkt.un.echo.id = myid
pkt.un.echo.sequence = C.htons(1)
pkt.checksum = inet_cksum(ffi.cast("const void *", pkt), ffi.sizeof(packet))
-- Set the destination address
local addr = ffi.new("struct sockaddr_in")
addr.sin_family = C.AF_INET
local in_addr = ffi.new("struct in_addr")
if C.inet_aton(ip, in_addr) == 0 then
logger.err("Device:ping4: Invalid address:", ip)
C.close(socket)
return false
end
addr.sin_addr = in_addr
addr.sin_port = 0
-- Send the ping
local start_time = time.now()
if C.sendto(socket, packet, DEFDATALEN + C.ICMP_MINLEN, 0, ffi.cast("struct sockaddr*", addr), ffi.sizeof(addr)) == - 1 then
local errno = ffi.errno()
logger.err("Device:ping4: sendto:", ffi.string(C.strerror(errno)))
C.close(socket)
return false
end
-- We'll poll to make timing out easier on us (busybox uses a SIGALRM :s)
local pfd = ffi.new("struct pollfd")
pfd.fd = socket
pfd.events = C.POLLIN
local timeout = 2000
-- Wait for a response
while true do
local poll_num = C.poll(pfd, 1, timeout)
-- Slice the timeout in two on every retry, ensuring we'll bail definitively after 4s...
timeout = bit.rshift(timeout, 1)
if poll_num == -1 then
local errno = ffi.errno()
if errno ~= C.EINTR then
logger.err("Device:ping4: poll:", ffi.string(C.strerror(errno)))
C.close(socket)
return false
end
if pingok == 0 then
result = result .. "Gateway ping successful"
elseif poll_num > 0 then
local c = C.recv(socket, packet, ffi.sizeof(packet), 0)
if c == -1 then
local errno = ffi.errno()
if errno ~= C.EINTR then
logger.err("Device:ping4: recv:", ffi.string(C.strerror(errno)))
C.close(socket)
return false
end
else
result = result .. "Gateway ping FAILED"
-- Do some minimal verification of the reply's validity.
-- This is mostly based on busybox's ping,
-- with some extra inspiration from iputils's ping, especially as far as SOCK_DGRAM is concerned.
local iphdr = ffi.cast("struct iphdr *", packet) -- ip + icmp
local hlen
if socket_type == C.SOCK_RAW then
hlen = bit.lshift(iphdr.ihl, 2)
if c < (hlen + 8) or iphdr.ihl < 5 then
-- Packet too short (we don't use recvfrom, so we can't log where it's from ;o))
logger.dbg("Device:ping4: received a short packet")
goto continue
end
else
hlen = 0
end
-- Skip ip hdr to get at the ICMP part
local icp = ffi.cast("struct icmphdr *", packet + hlen)
-- Check that we got a *reply* to *our* ping
-- NOTE: The reply's ident is defined by the kernel for SOCK_DGRAM, so we can't do anything with it!
if icp.type == C.ICMP_ECHOREPLY and
(socket_type == C.SOCK_DGRAM or icp.un.echo.id == myid) then
break
end
end
else
local end_time = time.now()
logger.info("Device:ping4: timed out waiting for a response from", ip)
C.close(socket)
return false, end_time - start_time
end
::continue::
end
local end_time = time.now()
-- If we got this far, we've got a reply to our ping in time!
C.close(socket)
return true, end_time - start_time
end
function Device:getDefaultRoute(interface)
local fd = io.open("/proc/net/route", "re")
if not fd then
return
end
local gateway
local l = 1
for line in fd:lines() do
-- Skip the first line (header)
if l > 1 then
local fields = {}
for field in line:gmatch("%S+") do
table.insert(fields, field)
end
-- Check the requested interface or anything that isn't lo
if (interface and fields[1] == interface) or (not interface and fields[1] ~= "lo") then
-- We're looking for something that's up & a gateway
if bit.band(fields[4], C.RTF_UP) ~= 0 and bit.band(fields[4], C.RTF_GATEWAY) ~= 0 then
-- Handle the conversion from network endianness hex string into a human-readable numeric form
local sockaddr_in = ffi.new("struct sockaddr_in")
sockaddr_in.sin_family = C.AF_INET
sockaddr_in.sin_addr.s_addr = tonumber(fields[3], 16)
local host = ffi.new("char[?]", C.NI_MAXHOST)
local s = C.getnameinfo(ffi.cast("struct sockaddr *", sockaddr_in),
ffi.sizeof("struct sockaddr_in"),
host, C.NI_MAXHOST,
nil, 0,
C.NI_NUMERICHOST)
if s ~= 0 then
logger.err("Device:getDefaultRoute: getnameinfo:", ffi.string(C.gai_strerror(s)))
break
else
gateway = ffi.string(host)
-- If we specified an interface, we're done.
-- If we didn't, we'll just keep the last gateway in the routing table...
if interface then
break
end
end
end
end
end
l = l + 1
end
fd:close()
return gateway
end
function Device:retrieveNetworkInfo()
-- We're going to need a random socket for the network & wireless ioctls...
local socket = C.socket(C.PF_INET, C.SOCK_DGRAM, C.IPPROTO_IP);
if socket == -1 then
local errno = ffi.errno()
logger.err("Device:retrieveNetworkInfo: socket:", ffi.string(C.strerror(errno)))
return
end
local ifaddr = ffi.new("struct ifaddrs *[1]")
if C.getifaddrs(ifaddr) == -1 then
local errno = ffi.errno()
logger.err("Device:retrieveNetworkInfo: getifaddrs:", ffi.string(C.strerror(errno)))
return false
end
-- Build a string rope to format the results
local results = {}
local interfaces = {}
local prev_ifname, default_gw
-- Loop over all the network interfaces
local ifa = ifaddr[0]
while ifa ~= nil do
-- Skip over loopback or downed interfaces
if ifa.ifa_addr ~= nil and
bit.band(ifa.ifa_flags, C.IFF_UP) ~= 0 and
bit.band(ifa.ifa_flags, C.IFF_LOOPBACK) == 0 then
local family = ifa.ifa_addr.sa_family
if family == C.AF_INET or family == C.AF_INET6 then
local host = ffi.new("char[?]", C.NI_MAXHOST)
local s = C.getnameinfo(ifa.ifa_addr,
family == C.AF_INET and ffi.sizeof("struct sockaddr_in") or ffi.sizeof("struct sockaddr_in6"),
host, C.NI_MAXHOST,
nil, 0,
C.NI_NUMERICHOST)
if s ~= 0 then
logger.err("Device:retrieveNetworkInfo: getnameinfo:", ffi.string(C.gai_strerror(s)))
else
-- Only print the ifname once
local ifname = ffi.string(ifa.ifa_name)
if not interfaces[ifname] then
if prev_ifname and ifname ~= prev_ifname then
-- Add a linebreak between interfaces
table.insert(results, "")
end
prev_ifname = ifname
table.insert(results, T(_("Interface: %1"), ifname))
interfaces[ifname] = true
-- Get its MAC address
local ifr = ffi.new("struct ifreq")
ffi.copy(ifr.ifr_ifrn.ifrn_name, ifa.ifa_name, C.IFNAMSIZ)
if C.ioctl(socket, C.SIOCGIFHWADDR, ifr) == -1 then
local errno = ffi.errno()
logger.err("Device:retrieveNetworkInfo: SIOCGIFHWADDR ioctl:", ffi.string(C.strerror(errno)))
else
local mac = string.format("%02X:%02X:%02X:%02X:%02X:%02X",
bit.band(ifr.ifr_ifru.ifru_hwaddr.sa_data[0], 0xFF),
bit.band(ifr.ifr_ifru.ifru_hwaddr.sa_data[1], 0xFF),
bit.band(ifr.ifr_ifru.ifru_hwaddr.sa_data[2], 0xFF),
bit.band(ifr.ifr_ifru.ifru_hwaddr.sa_data[3], 0xFF),
bit.band(ifr.ifr_ifru.ifru_hwaddr.sa_data[4], 0xFF),
bit.band(ifr.ifr_ifru.ifru_hwaddr.sa_data[5], 0xFF))
table.insert(results, T(_("MAC: %1"), mac))
end
-- Check if it's a wireless interface (c.f., wireless-tools)
local iwr = ffi.new("struct iwreq")
ffi.copy(iwr.ifr_ifrn.ifrn_name, ifa.ifa_name, C.IFNAMSIZ)
if C.ioctl(socket, C.SIOCGIWNAME, iwr) ~= -1 then
interfaces[ifname] = "wireless"
-- Get its ESSID
local essid = ffi.new("char[?]", C.IW_ESSID_MAX_SIZE + 1)
iwr.u.essid.pointer = ffi.cast("caddr_t", essid)
iwr.u.essid.length = C.IW_ESSID_MAX_SIZE + 1
iwr.u.essid.flags = 0
if C.ioctl(socket, C.SIOCGIWESSID, iwr) == -1 then
local errno = ffi.errno()
logger.err("Device:retrieveNetworkInfo: SIOCGIWESSID ioctl:", ffi.string(C.strerror(errno)))
else
local essid_on = iwr.u.data.flags
if essid_on ~= 0 then
-- Knowing the token index may be fun, bit it isn't in fact, super interesting...
--[[
local token_index = bit.band(essid_on, C.IW_ENCODE_INDEX)
if token_index > 1 then
table.insert(results, T(_("SSID: \"%1\" [%2]"), ffi.string(essid), token_index))
else
table.insert(results, T(_("SSID: \"%1\""), ffi.string(essid)))
end
--]]
table.insert(results, T(_("SSID: \"%1\""), ffi.string(essid)))
else
table.insert(results, _("SSID: off/any"))
end
end
end
end
if family == C.AF_INET then
table.insert(results, T(_("IP: %1"), ffi.string(host)))
local gw = self:getDefaultRoute(ifname)
if gw then
table.insert(results, T(_("Default gateway: %1"), gw))
-- If that's a wireless interface, use *that* one for the ping test
if interfaces[ifname] == "wireless" then
default_gw = gw
end
end
else
table.insert(results, T(_("IPv6: %1"), ffi.string(host)))
--- @todo: Build an IPv6 variant of getDefaultRoute that parses /proc/net/ipv6_route
end
end
end
end
ifa = ifa.ifa_next
end
C.freeifaddrs(ifaddr[0])
C.close(socket)
if prev_ifname then
table.insert(results, "")
end
-- Only ping a single gateway (if we found a wireless interface earlier, we've kept its gateway address around)
if not default_gw then
-- If not, we'll simply use the last one in the list...
default_gw = self:getDefaultRoute()
end
if default_gw then
local ok, rtt = self:ping4(default_gw)
if ok then
table.insert(results, _("Gateway ping successful"))
if rtt then
rtt = string.format("%.3f", rtt * 1/1000) -- i.e., time.to_ms w/o flooring
table.insert(results, T(_("RTT: %1 ms"), rtt))
end
else
result = result .. "No default gateway to ping"
table.insert(results, _("Gateway ping FAILED"))
if rtt then
rtt = string.format("%.1f", time.to_s(rtt))
table.insert(results, T(_("Timed out after %1 s"), rtt))
end
end
return result
else
table.insert(results, _("No default gateway to ping"))
end
return table.concat(results, "\n")
end
function Device:setTime(hour, min)
@ -552,13 +1011,14 @@ end
-- filename extension. Inspired by luarocks archive_unpack()
-- @param archive string: Filename of archive.
-- @param extract_to string: Destination directory.
-- @param with_stripped_root boolean: true if root directory in archive should be stripped
-- @return boolean or (boolean, string): true on success, false and an error message on failure.
function Device:unpackArchive(archive, extract_to)
function Device:unpackArchive(archive, extract_to, with_stripped_root)
require("dbg").dassert(type(archive) == "string")
local BD = require("ui/bidi")
local ok
if archive:match("%.tar%.bz2$") or archive:match("%.tar%.gz$") or archive:match("%.tar%.lz$") or archive:match("%.tgz$") then
ok = self:untar(archive, extract_to)
ok = self:untar(archive, extract_to, with_stripped_root)
else
return false, T(_("Couldn't extract archive:\n\n%1\n\nUnrecognized filename extension."), BD.filepath(archive))
end
@ -568,21 +1028,46 @@ function Device:unpackArchive(archive, extract_to)
return true
end
function Device:untar(archive, extract_to)
return os.execute(("./tar xf %q -C %q"):format(archive, extract_to))
function Device:untar(archive, extract_to, with_stripped_root)
local cmd = "./tar xf %q -C %q"
if with_stripped_root then
cmd = cmd .. " --strip-components=1"
end
return os.execute(cmd:format(archive, extract_to))
end
-- Update our UIManager reference once it's ready
function Device:_UIManagerReady(uimgr)
-- Our own ref
UIManager = uimgr
-- Let implementations do the same thing
self:UIManagerReady(uimgr)
-- Forward that to PowerD
self.powerd:UIManagerReady(uimgr)
-- And to Input
self.input:UIManagerReady(uimgr)
-- Setup PM event handlers
-- NOTE: We keep forwarding the uimgr reference because some implementations don't actually have a module-local UIManager ref to update
self:_setEventHandlers(uimgr)
-- Returns a self-debouncing scheduling call (~4s to give some leeway to the kernel, and debounce to deal with potential chattering)
self._updateChargingLED = UIManager:debounce(4, false, function() self:setupChargingLED() end)
end
-- In case implementations *also* need a reference to UIManager, *this* is the one to implement!
function Device:UIManagerReady(uimgr) end
-- Set device event handlers common to all devices
function Device:_setEventHandlers(UIManager)
function Device:_setEventHandlers(uimgr)
if self:canReboot() then
UIManager.event_handlers.Reboot = function()
UIManager.event_handlers.Reboot = function(message_text)
local ConfirmBox = require("ui/widget/confirmbox")
UIManager:show(ConfirmBox:new{
text = _("Are you sure you want to reboot the device?"),
text = message_text or _("Are you sure you want to reboot the device?"),
ok_text = _("Reboot"),
ok_callback = function()
local Event = require("ui/event")
UIManager:broadcastEvent(Event:new("Reboot"))
UIManager:nextTick(UIManager.reboot_action)
end,
})
@ -592,14 +1077,12 @@ function Device:_setEventHandlers(UIManager)
end
if self:canPowerOff() then
UIManager.event_handlers.PowerOff = function()
UIManager.event_handlers.PowerOff = function(message_text)
local ConfirmBox = require("ui/widget/confirmbox")
UIManager:show(ConfirmBox:new{
text = _("Are you sure you want to power off the device?"),
text = message_text or _("Are you sure you want to power off the device?"),
ok_text = _("Power off"),
ok_callback = function()
local Event = require("ui/event")
UIManager:broadcastEvent(Event:new("PowerOff"))
UIManager:nextTick(UIManager.poweroff_action)
end,
})
@ -608,24 +1091,44 @@ function Device:_setEventHandlers(UIManager)
UIManager.event_handlers.PowerOff = function() end
end
self:setEventHandlers(UIManager)
if self:canRestart() then
UIManager.event_handlers.Restart = function(message_text)
local ConfirmBox = require("ui/widget/confirmbox")
UIManager:show(ConfirmBox:new{
text = message_text or _("This will take effect on next restart."),
ok_text = _("Restart now"),
ok_callback = function()
UIManager:broadcastEvent(Event:new("Restart"))
end,
cancel_text = _("Restart later"),
})
end
else
UIManager.event_handlers.Restart = function(message_text)
local InfoMessage = require("ui/widget/infomessage")
UIManager:show(InfoMessage:new{
text = message_text or _("This will take effect on next restart."),
})
end
end
-- Let implementations expand on that
self:setEventHandlers(uimgr)
end
-- Devices can add additional event handlers by overwriting this method.
function Device:setEventHandlers(UIManager)
-- These will be most probably overwritten in the device specific `setEventHandlers`
-- Devices can add additional event handlers by implementing this method.
function Device:setEventHandlers(uimgr)
-- These will most probably be overwritten by device-specific `setEventHandlers` implementations
UIManager.event_handlers.Suspend = function()
self:_beforeSuspend(false)
self.powerd:beforeSuspend()
end
UIManager.event_handlers.Resume = function()
self:_afterResume(false)
self.powerd:afterResume()
end
end
-- The common operations that should be performed before suspending the device.
function Device:_beforeSuspend(inhibit)
local Event = require("ui/event")
local UIManager = require("ui/uimanager")
UIManager:flushSettings()
UIManager:broadcastEvent(Event:new("Suspend"))
@ -641,33 +1144,30 @@ end
-- The common operations that should be performed after resuming the device.
function Device:_afterResume(inhibit)
if inhibit ~= false then
-- Restore key repeat
self:restoreKeyRepeat()
-- Restore key repeat if it's not disabled
if G_reader_settings:nilOrFalse("input_no_key_repeat") then
self:restoreKeyRepeat()
end
-- Restore full input handling
self.input:inhibitInput(false)
end
local Event = require("ui/event")
local UIManager = require("ui/uimanager")
UIManager:broadcastEvent(Event:new("Resume"))
end
-- The common operations that should be performed when the device is plugged to a power source.
function Device:_beforeCharging()
-- Leave the kernel some time to figure it out ;o).
local Event = require("ui/event")
local UIManager = require("ui/uimanager")
UIManager:scheduleIn(1, function() self:setupChargingLED() end)
-- Invalidate the capacity cache to make sure we poll up-to-date values for the LED check
self.powerd:invalidateCapacityCache()
self:_updateChargingLED()
UIManager:broadcastEvent(Event:new("Charging"))
end
-- The common operations that should be performed when the device is unplugged from a power source.
function Device:_afterNotCharging()
-- Leave the kernel some time to figure it out ;o).
local Event = require("ui/event")
local UIManager = require("ui/uimanager")
UIManager:scheduleIn(1, function() self:setupChargingLED() end)
self.powerd:invalidateCapacityCache()
self:_updateChargingLED()
UIManager:broadcastEvent(Event:new("NotCharging"))
end

@ -1,5 +1,6 @@
local UIManager -- will be updated when available
local Event = require("ui/event")
local Math = require("optmath")
local UIManager
local logger = require("logger")
local time = require("ui/time")
local BasePowerD = {
@ -41,12 +42,11 @@ function BasePowerD:new(o)
return o
end
function BasePowerD:readyUI()
UIManager = require("ui/uimanager")
end
function BasePowerD:init() end
function BasePowerD:setIntensityHW(intensity) end
--- @note: This should *always* call self:_decideFrontlightState() in its coda (unless you have a custom isFrontlightOn implementation)!
function BasePowerD:setIntensityHW(intensity)
self:_decideFrontlightState()
end
--- @note: Unlike the "public" setWarmth, this one takes a value in the *native* scale!
function BasePowerD:setWarmthHW(warmth) end
function BasePowerD:getCapacityHW() return 0 end
@ -62,15 +62,46 @@ function BasePowerD:isAuxChargingHW() return false end
function BasePowerD:isAuxChargedHW() return false end
function BasePowerD:frontlightIntensityHW() return 0 end
function BasePowerD:isFrontlightOnHW() return self.fl_intensity > self.fl_min end
function BasePowerD:turnOffFrontlightHW() self:setIntensityHW(self.fl_min) end
function BasePowerD:turnOnFrontlightHW() self:setIntensityHW(self.fl_intensity) end --- @fixme: what if fl_intensity == fl_min (c.f., kindle)?
--- @note: done_callback is used to display Notifications,
--- some implementations *may* need to handle it themselves because of timing constraints,
--- in which case they should return *true* here, so that the public API knows not to consume the callback early.
function BasePowerD:turnOffFrontlightHW(done_callback)
self:setIntensityHW(self.fl_min)
-- Nothing fancy required, so we leave done_callback handling to the public API
return false
end
function BasePowerD:turnOnFrontlightHW(done_callback)
--- @fixme: what if fl_intensity == fl_min (c.f., kindle)?
self:setIntensityHW(self.fl_intensity)
return false
end
function BasePowerD:frontlightWarmthHW() return 0 end
-- Anything that needs to be done before doing a real hardware suspend.
-- (Such as turning the front light off).
function BasePowerD:beforeSuspend() end
-- Do *not* omit calling Device's _beforeSuspend method! This default implementation passes `false` so as *not* to disable input events during PM.
function BasePowerD:beforeSuspend() self.device:_beforeSuspend(false) end
-- Anything that needs to be done after doing a real hardware resume.
-- (Such as restoring front light state).
function BasePowerD:afterResume() end
-- Do *not* omit calling Device's _afterResume method!
function BasePowerD:afterResume()
-- MONOTONIC doesn't tick during suspend,
-- invalidate the last battery capacity pull time so that we get up to date data immediately.
self:invalidateCapacityCache()
self.device:_afterResume(false)
end
-- Update our UIManager reference once it's ready
function BasePowerD:UIManagerReady(uimgr)
-- Our own ref
UIManager = uimgr
-- Let implementations do the same thing, too
self:UIManagerReadyHW(uimgr)
end
-- Ditto, but for implementations
function BasePowerD:UIManagerReadyHW(uimgr) end
function BasePowerD:isFrontlightOn()
return self.is_fl_on
@ -94,31 +125,37 @@ function BasePowerD:frontlightIntensity()
return self.fl_intensity
end
function BasePowerD:toggleFrontlight()
function BasePowerD:toggleFrontlight(done_callback)
if not self.device:hasFrontlight() then return false end
if self:isFrontlightOn() then
return self:turnOffFrontlight()
return self:turnOffFrontlight(done_callback)
else
return self:turnOnFrontlight()
return self:turnOnFrontlight(done_callback)
end
end
function BasePowerD:turnOffFrontlight()
function BasePowerD:turnOffFrontlight(done_callback)
if not self.device:hasFrontlight() then return end
if self:isFrontlightOff() then return false end
self:turnOffFrontlightHW()
local cb_handled = self:turnOffFrontlightHW(done_callback)
self.is_fl_on = false
self:stateChanged()
if not cb_handled and done_callback then
done_callback()
end
return true
end
function BasePowerD:turnOnFrontlight()
function BasePowerD:turnOnFrontlight(done_callback)
if not self.device:hasFrontlight() then return end
if self:isFrontlightOn() then return false end
if self.fl_intensity == self.fl_min then return false end --- @fixme what the hell?
self:turnOnFrontlightHW()
local cb_handled = self:turnOnFrontlightHW(done_callback)
self.is_fl_on = true
self:stateChanged()
if not cb_handled and done_callback then
done_callback()
end
return true
end
@ -173,7 +210,6 @@ function BasePowerD:setIntensity(intensity)
if not self.device:hasFrontlight() then return false end
if intensity == self:frontlightIntensity() then return false end
self.fl_intensity = self:normalizeIntensity(intensity)
self:_decideFrontlightState()
logger.dbg("set light intensity", self.fl_intensity)
self:setIntensityHW(self.fl_intensity)
self:stateChanged()
@ -273,7 +309,6 @@ end
function BasePowerD:stateChanged()
-- BasePowerD is loaded before UIManager. So we cannot broadcast events before UIManager has been loaded.
if UIManager then
local Event = require("ui/event")
UIManager:broadcastEvent(Event:new("FrontlightStateChanged"))
end
end

@ -118,6 +118,16 @@ function GestureDetector:init()
self.MULTISWIPE_THRESHOLD = self.DOUBLE_TAP_DISTANCE
end
local function deepCopyEv(tev)
return {
x = tev.x,
y = tev.y,
id = tev.id,
slot = tev.slot,
timev = tev.timev, -- A ref is enough for this table, it's re-assigned to a new object on every SYN_REPORT
}
end
-- Contact object, it'll keep track of everything we need for a single contact across its lifetime
-- i.e., from this contact's down to up (or its *effective* up for double-taps, e.g., when the tap or double_tap is emitted).
-- We'll identify contacts by their slot numbers, and store 'em in GestureDetector's active_contacts table (hash).
@ -139,7 +149,7 @@ function GestureDetector:newContact(slot)
state = Contact.initialState, -- Current state function
slot = slot, -- Current ABS_MT_SLOT value (also its key in the active_contacts hash)
id = -1, -- Current ABS_MT_TRACKING_ID value
initial_tev = nil, -- Copy of the input event table at first contact (i.e., at contact down)
initial_tev = nil, -- Copy of the input event table at first contact (i.e., at contact down [iff the platform is sane, might be a copy of current_tev otherwise])
current_tev = nil, -- Pointer to the current input event table, ref is *stable*, c.f., NOTE in feedEvent below
down = false, -- Contact is down (as opposed to up, i.e., lifted). Only really happens for double-tap handling, in every other case the Contact object is destroyed on lift.
pending_double_tap_timer = false, -- Contact is pending a double_tap timer
@ -157,6 +167,12 @@ function GestureDetector:newContact(slot)
-- If we have a buddy contact, point its own buddy ref to us
if buddy_contact then
buddy_contact.buddy_contact = self.active_contacts[slot]
-- And make sure it has an initial_tev recorded, for misbehaving platforms...
if not buddy_contact.initial_tev then
buddy_contact.initial_tev = deepCopyEv(buddy_contact.current_tev)
logger.warn("GestureDetector:newContact recorded an initial_tev out of order for buddy slot", buddy_contact.slot)
end
end
return self.active_contacts[slot]
@ -230,16 +246,6 @@ function GestureDetector:feedEvent(tevs)
return gestures
end
local function deepCopyEv(tev)
return {
x = tev.x,
y = tev.y,
id = tev.id,
slot = tev.slot,
timev = tev.timev, -- A ref is enough for this table, it's re-assigned to a new object on every SYN_REPORT
}
end
--[[
tap2 is the later tap
--]]
@ -379,6 +385,18 @@ function Contact:switchState(state_func, func_arg)
return state_func(self, func_arg)
end
-- Unlike switchState, we don't *call* the new state, and we ensure that initial_tev is set,
-- in case initialState never ran on a contact down because the platform screwed up (e.g., PB with broken MT).
-- The rest of the code, in particular the buddy system, assumes initial_tev is always set (and supposedly sane).
function Contact:setState(state_func)
-- NOTE: Safety net for broken platforms that might screw up slot order...
if not self.initial_tev then
self.initial_tev = deepCopyEv(self.current_tev)
logger.warn("Contact:setState recorded an initial_tev out of order for slot", self.slot)
end
self.state = state_func
end
function Contact:initialState()
local tev = self.current_tev
@ -436,7 +454,7 @@ function GestureDetector:probeClockSource(timev)
-- Finally, BOOTTIME
local boottime = time.boottime()
-- NOTE: It was implemented in Linux 2.6.39, so, reject 0, which would mean it's unsupported...
if not boottime == 0 and timev >= boottime - threshold and timev <= boottime + threshold then
if boottime ~= 0 and timev >= boottime - threshold and timev <= boottime + threshold then
self.clock_id = C.CLOCK_BOOTTIME
logger.dbg("GestureDetector:probeClockSource: Touch event timestamps appear to use CLOCK_BOOTTIME")
return
@ -483,7 +501,7 @@ function Contact:tapState(new_tap)
-- Mark that slot
self.mt_gesture = "tap"
-- Neuter its buddy
buddy_contact.state = Contact.voidState
buddy_contact:setState(Contact.voidState)
buddy_contact.mt_gesture = "tap"
local pos0 = Geom:new{
@ -513,7 +531,7 @@ function Contact:tapState(new_tap)
logger.dbg("Contact:tapState: Two-contact tap failed to pass the two_finger_tap constraints -> single tap @", tev.x, tev.y)
-- We blew the gesture position/time constraints,
-- neuter buddy and send a single tap on this slot.
buddy_contact.state = Contact.voidState
buddy_contact:setState(Contact.voidState)
gesture_detector:dropContact(self)
return {
@ -714,7 +732,7 @@ function Contact:panState(keep_contact)
else
buddy_contact.mt_gesture = "swipe"
end
buddy_contact.state = Contact.voidState
buddy_contact:setState(Contact.voidState)
local ges_ev = self:handleTwoFingerPan(buddy_contact)
if ges_ev then
@ -722,6 +740,13 @@ function Contact:panState(keep_contact)
-- Only accept gestures that require both contacts to have been lifted
if ges_ev.ges == "two_finger_pan" then
ges_ev.ges = "two_finger_swipe"
-- Swap from pan semantics to swipe semantics
ges_ev.pos = ges_ev._start_pos
ges_ev._start_pos = nil
ges_ev.start_pos = nil
ges_ev.end_pos = ges_ev._end_pos
ges_ev._end_pos = nil
ges_ev.relative = nil
elseif ges_ev.ges == "inward_pan" then
ges_ev.ges = "pinch"
elseif ges_ev.ges == "outward_pan" then
@ -811,8 +836,8 @@ function Contact:voidState()
-- and if it lifts even later, we'd have to deal with spurious moves first, probably leading into a tap...
-- If the gesture *succeeds*, the buddy contact will be dropped whenever it's actually lifted,
-- thanks to the temporary tracking id switcheroo & voidState...
buddy_contact.state = Contact.voidState
buddy_contact.current_tev.id = buddy_tid
buddy_contact:setState(Contact.voidState)
end
-- Regardless of whether we detected a gesture, this is a contact lift, so it's curtains for us!
gesture_detector:dropContact(self)
@ -873,6 +898,12 @@ function Contact:handleSwipe()
w = 0,
h = 0,
}
local end_pos = Geom:new{
x = tev.x,
y = tev.y,
w = 0,
h = 0,
}
local ges = "swipe"
local multiswipe_directions
@ -893,8 +924,13 @@ function Contact:handleSwipe()
gesture_detector:dropContact(self)
return {
ges = ges,
-- use first pan tev coordination as swipe start point
-- NOTE: Unlike every other gesture, we use the *contact* point as the gesture's position,
-- instead of the *lift* point, mainly because that's what makes the most sense
-- from a hit-detection standpoint (c.f., `GestureRange:match` & `InputContainer:onGesture`),
-- and that's 99% of the use-cases where the position actually matters for a swipe.
pos = start_pos,
-- And for those rare cases that need it, we provide the lift point separately.
end_pos = end_pos,
direction = swipe_direction,
multiswipe_directions = multiswipe_directions,
distance = swipe_distance,
@ -924,7 +960,7 @@ function Contact:handlePan()
else
buddy_contact.mt_gesture = "pan"
end
buddy_contact.state = Contact.voidState
buddy_contact:setState(Contact.voidState)
return self:handleTwoFingerPan(buddy_contact)
elseif self.down then
@ -932,27 +968,26 @@ function Contact:handlePan()
local pan_ev = {
ges = "pan",
relative = {
-- default to pan 0
x = 0,
y = 0,
x = tev.x - self.initial_tev.x,
y = tev.y - self.initial_tev.y,
},
start_pos = Geom:new{
x = self.initial_tev.x,
y = self.initial_tev.y,
w = 0,
h = 0,
},
pos = Geom:new{
x = tev.x,
y = tev.y,
w = 0,
h = 0,
},
pos = nil,
direction = pan_direction,
distance = pan_distance,
time = tev.timev,
}
-- Regular pan
pan_ev.relative.x = tev.x - self.initial_tev.x
pan_ev.relative.y = tev.y - self.initial_tev.y
pan_ev.pos = Geom:new{
x = tev.x,
y = tev.y,
w = 0,
h = 0,
}
local msd_cnt = #self.multiswipe_directions
local msd_direction_prev = (msd_cnt > 0) and self.multiswipe_directions[msd_cnt][1] or ""
local prev_ms_ev, fake_initial_tev
@ -984,7 +1019,7 @@ function Contact:handlePan()
if msd_distance > gesture_detector.MULTISWIPE_THRESHOLD then
local pan_ev_multiswipe = pan_ev
-- store a copy of pan_ev without rotation adjustment for multiswipe calculations when rotated
if gesture_detector.screen:getTouchRotation() > gesture_detector.screen.ORIENTATION_PORTRAIT then
if gesture_detector.screen:getTouchRotation() > gesture_detector.screen.DEVICE_ROTATED_UPRIGHT then
pan_ev_multiswipe = util.tableDeepCopy(pan_ev)
end
if msd_direction ~= msd_direction_prev then
@ -1070,9 +1105,21 @@ function Contact:handleTwoFingerPan(buddy_contact)
-- We'll also want to remember the span between both contacts on start & end for some gestures
local start_distance = tstart_pos:distance(rstart_pos)
local end_distance = tend_pos:distance(rend_pos)
-- NOTE: "pan" and "hold_pan" use the current/end point as pos,
-- but swipe reports pos as the *starting* point (c.f., `Contact:handleSwipe`).
-- Since this table will be used for both pans and two_finger_swipe (via panState),
-- we stuff a bunch of extra info in there to swap it around as-needed...
local ges_ev = {
ges = "two_finger_pan",
pos = start_point,
relative = {
x = end_point.x - start_point.x,
y = end_point.y - start_point.y,
},
-- Default to the pan semantics, c.f., note above
pos = end_point,
start_pos = start_point,
_start_pos = start_point,
_end_pos = end_point,
distance = avg_distance,
direction = tpan_dir,
time = self.current_tev.timev,
@ -1080,15 +1127,27 @@ function Contact:handleTwoFingerPan(buddy_contact)
if tpan_dir ~= rpan_dir then
if start_distance > end_distance then
ges_ev.ges = "inward_pan"
-- Use the end pos (this is the default already)
ges_ev._start_pos = nil
ges_ev._end_pos = nil
else
ges_ev.ges = "outward_pan"
-- Use the start pos, it'll make more sense than the midpoint of the current contacts,
-- given the potentially wide span between the two...
ges_ev.pos = ges_ev._start_pos
ges_ev._start_pos = nil
ges_ev.start_pos = nil
ges_ev.end_pos = ges_ev._end_pos
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
ges_ev.start_span = start_distance
-- Drop unnecessary field
ges_ev.relative = nil
elseif self.state == Contact.holdState then
ges_ev.ges = "two_finger_hold_pan"
-- Flag 'em for holdState to discriminate with two_finger_hold_release
@ -1125,11 +1184,19 @@ function Contact:handlePanRelease(keep_contact)
-- Both main contacts are actives and we are down, mark that slot
self.mt_gesture = "pan_release"
-- Neuter its buddy
buddy_contact.state = Contact.voidState
buddy_contact:setState(Contact.voidState)
buddy_contact.mt_gesture = "pan_release"
logger.dbg("Contact:handlePanRelease: two_finger_pan_release detected")
pan_ev.ges = "two_finger_pan_release"
-- The pan itself used the midpoint between the two contacts, keep doing that.
local buddy_pos = Geom:new{
x = buddy_contact.current_tev.x,
y = buddy_contact.current_tev.y,
w = 0,
h = 0,
}
pan_ev.pos = release_pos:midpoint(buddy_pos)
-- Don't drop buddy, voidState will handle it
-- NOTE: This is yet another rotate hack, emanating from voidState into panState.
if not keep_contact then
@ -1163,7 +1230,7 @@ function Contact:holdState(new_hold)
-- Both main contacts are actives and we are down, mark that slot
self.mt_gesture = "hold"
-- Neuter its buddy
buddy_contact.state = Contact.voidState
buddy_contact:setState(Contact.voidState)
buddy_contact.mt_gesture = "hold"
local pos0 = Geom:new{
@ -1220,7 +1287,7 @@ function Contact:holdState(new_hold)
end
end
-- Regardless of whether this panned out (pun intended), this is a lift, so we'll defer to voidState next.
buddy_contact.state = Contact.voidState
buddy_contact:setState(Contact.voidState)
gesture_detector:dropContact(self)
return ges_ev
elseif self.mt_gesture == "hold_pan" or self.mt_gesture == "pan" then
@ -1231,7 +1298,7 @@ function Contact:holdState(new_hold)
buddy_contact.mt_gesture = "hold_release"
end
-- Neuter its buddy
buddy_contact.state = Contact.voidState
buddy_contact:setState(Contact.voidState)
-- Don't drop buddy, voidState will handle it
gesture_detector:dropContact(self)
@ -1335,7 +1402,7 @@ end
--]]
function GestureDetector:adjustGesCoordinate(ges)
local mode = self.screen:getTouchRotation()
if mode == self.screen.ORIENTATION_LANDSCAPE then
if mode == self.screen.DEVICE_ROTATED_CLOCKWISE then
-- in landscape mode rotated 90
if ges.pos then
ges.pos.x, ges.pos.y = (self.screen:getWidth() - ges.pos.y), (ges.pos.x)
@ -1350,6 +1417,9 @@ function GestureDetector:adjustGesCoordinate(ges)
ges.direction = translateGesDirCoordinate(ges.direction, ges_coordinate_translation_90)
if ges.ges == "multiswipe" then
ges.multiswipe_directions = translateMultiswipeGesDirCoordinate(ges.multiswipe_directions, ges_coordinate_translation_90)
logger.dbg("GestureDetector: Landscape translation for multiswipe:", ges.multiswipe_directions)
else
logger.dbg("GestureDetector: Landscape translation for ges:", ges.ges, ges.direction)
end
if ges.relative then
ges.relative.x, ges.relative.y = -ges.relative.y, ges.relative.x
@ -1362,8 +1432,9 @@ function GestureDetector:adjustGesCoordinate(ges)
elseif ges.direction == "vertical" then
ges.direction = "horizontal"
end
logger.dbg("GestureDetector: Landscape translation for ges:", ges.ges, ges.direction)
end
elseif mode == self.screen.ORIENTATION_LANDSCAPE_ROTATED then
elseif mode == self.screen.DEVICE_ROTATED_COUNTER_CLOCKWISE then
-- in landscape mode rotated 270
if ges.pos then
ges.pos.x, ges.pos.y = (ges.pos.y), (self.screen:getHeight() - ges.pos.x)
@ -1378,6 +1449,9 @@ function GestureDetector:adjustGesCoordinate(ges)
ges.direction = translateGesDirCoordinate(ges.direction, ges_coordinate_translation_270)
if ges.ges == "multiswipe" then
ges.multiswipe_directions = translateMultiswipeGesDirCoordinate(ges.multiswipe_directions, ges_coordinate_translation_270)
logger.dbg("GestureDetector: Inverted landscape translation for multiswipe:", ges.multiswipe_directions)
else
logger.dbg("GestureDetector: Inverted landscape translation for ges:", ges.ges, ges.direction)
end
if ges.relative then
ges.relative.x, ges.relative.y = ges.relative.y, -ges.relative.x
@ -1390,8 +1464,9 @@ function GestureDetector:adjustGesCoordinate(ges)
elseif ges.direction == "vertical" then
ges.direction = "horizontal"
end
logger.dbg("GestureDetector: Inverted landscape translation for ges:", ges.ges, ges.direction)
end
elseif mode == self.screen.ORIENTATION_PORTRAIT_ROTATED then
elseif mode == self.screen.DEVICE_ROTATED_UPSIDE_DOWN then
-- in portrait mode rotated 180
if ges.pos then
ges.pos.x, ges.pos.y = (self.screen:getWidth() - ges.pos.x), (self.screen:getHeight() - ges.pos.y)
@ -1406,21 +1481,16 @@ function GestureDetector:adjustGesCoordinate(ges)
ges.direction = translateGesDirCoordinate(ges.direction, ges_coordinate_translation_180)
if ges.ges == "multiswipe" then
ges.multiswipe_directions = translateMultiswipeGesDirCoordinate(ges.multiswipe_directions, ges_coordinate_translation_180)
logger.dbg("GestureDetector: Inverted portrait translation for multiswipe:", ges.multiswipe_directions)
else
logger.dbg("GestureDetector: Inverted portrait translation for ges:", ges.ges, ges.direction)
end
if ges.relative then
ges.relative.x, ges.relative.y = -ges.relative.x, -ges.relative.y
end
elseif ges.ges == "pinch" or ges.ges == "spread"
or ges.ges == "inward_pan"
or ges.ges == "outward_pan" then
if ges.direction == "horizontal" then
ges.direction = "horizontal"
elseif ges.direction == "vertical" then
ges.direction = "vertical"
end
end
-- pinch/spread are unaffected
end
logger.dbg("adjusted ges:", ges.ges, ges.multiswipe_directions or ges.direction)
return ges
end

File diff suppressed because it is too large Load Diff

@ -51,8 +51,10 @@ least one key in this table must match.
E.g.:
Key:match({ "Alt", "K" }) -- match Alt-K
Key:match({ "Alt", { "K", "L" }}) -- match Alt-K _or_ Alt-L
Key:match({ "K" }) -- match K
Key:match({ { "K", "L" } }) -- match K _or_ L
Key:match({ "Alt", "K" }) -- match Alt+K
Key:match({ "Alt", { "K", "L" }}) -- match Alt+K _or_ Alt+L
]]
function Key:match(sequence)
local mod_keys = {} -- a hash table for checked modifiers

File diff suppressed because it is too large Load Diff

@ -1,6 +1,8 @@
local BasePowerD = require("device/generic/powerd")
local UIManager
local WakeupMgr = require("device/wakeupmgr")
local logger = require("logger")
local ffiUtil = require("ffi/util")
-- liblipclua, see require below
local KindlePowerD = BasePowerD:new{
@ -27,17 +29,22 @@ end
-- If we start with the light off (fl_intensity is fl_min), ensure a toggle will set it to the lowest "on" step,
-- and that we update fl_intensity (by using setIntensity and not setIntensityHW).
function KindlePowerD:turnOnFrontlightHW()
function KindlePowerD:turnOnFrontlightHW(done_callback)
self:setIntensity(self.fl_intensity == self.fl_min and self.fl_min + 1 or self.fl_intensity)
return false
end
-- Which means we need to get rid of the insane fl_intensity == fl_min shortcut in turnOnFrontlight, too...
-- That dates back to #2941, and I have no idea what it's supposed to help with.
function BasePowerD:turnOnFrontlight()
function KindlePowerD:turnOnFrontlight(done_callback)
if not self.device:hasFrontlight() then return end
if self:isFrontlightOn() then return false end
self:turnOnFrontlightHW()
local cb_handled = self:turnOnFrontlightHW(done_callback)
self.is_fl_on = true
self:stateChanged()
if not cb_handled and done_callback then
done_callback()
end
return true
end
@ -77,6 +84,14 @@ function KindlePowerD:frontlightIntensityHW()
end
end
-- Make sure isFrontlightOn reflects the actual HW state,
-- as self.fl_intensity is kept as-is when toggling the light off,
-- in order to be able to toggle it back on at the right intensity.
function KindlePowerD:isFrontlightOnHW()
local hw_intensity = self:frontlightIntensityHW()
return hw_intensity > self.fl_min
end
function KindlePowerD:setIntensityHW(intensity)
-- Handle the synthetic step switcheroo on ! canTurnFrontlightOff devices...
local turn_it_off = false
@ -98,13 +113,17 @@ function KindlePowerD:setIntensityHW(intensity)
-- NOTE: when intensity is 0, we want to *really* kill the light, so do it manually
-- (asking lipc to set it to 0 would in fact set it to > 0 on ! canTurnFrontlightOff Kindles).
-- We do *both* to make the fl restore on resume less jarring on devices where lipc 0 != off.
os.execute("printf '%s' ".. intensity .." > " .. self.fl_intensity_file)
ffiUtil.writeToSysfs(intensity, self.fl_intensity_file)
-- And in case there are two LED groups...
-- This should never happen as all warmth devices so far canTurnFrontlightOff
if self.warmth_intensity_file then
os.execute("printf '%s' ".. intensity .." > " .. self.warmth_intensity_file)
ffiUtil.writeToSysfs(intensity, self.warmth_intensity_file)
end
end
-- The state might have changed, make sure we don't break isFrontlightOn
self:_decideFrontlightState()
end
function KindlePowerD:frontlightWarmthHW()
@ -161,25 +180,22 @@ function KindlePowerD:isChargedHW()
return false
end
function KindlePowerD:_readFLIntensity()
return self:read_int_file(self.fl_intensity_file)
function KindlePowerD:hasHallSensor()
return self.hall_file ~= nil
end
function KindlePowerD:afterResume()
if not self.device:hasFrontlight() then
return
end
local UIManager = require("ui/uimanager")
if self:isFrontlightOn() then
-- The Kindle framework should turn the front light back on automatically.
-- The following statement ensures consistency of intensity, but should basically always be redundant,
-- since we set intensity via lipc and not sysfs ;).
-- NOTE: This is race-y, and we want to *lose* the race, hence the use of the scheduler (c.f., #4392)
UIManager:tickAfterNext(function() self:turnOnFrontlightHW() end)
else
-- But in the off case, we *do* use sysfs, so this one actually matters.
UIManager:tickAfterNext(function() self:turnOffFrontlightHW() end)
end
function KindlePowerD:isHallSensorEnabled()
local int = self:read_int_file(self.hall_file)
return int == 1
end
function KindlePowerD:onToggleHallSensor()
local stat = self:isHallSensorEnabled()
ffiUtil.writeToSysfs(stat and 0 or 1, self.hall_file)
end
function KindlePowerD:_readFLIntensity()
return self:read_int_file(self.fl_intensity_file)
end
function KindlePowerD:toggleSuspend()
@ -206,15 +222,15 @@ end
function KindlePowerD:checkUnexpectedWakeup()
local state = self:getPowerdState()
logger.info("Powerd resume state:", state)
logger.dbg("Powerd resume state:", state)
-- If we moved on to the active state,
-- then we were woken by user input not our alarm.
if state ~= "screenSaver" and state ~= "suspended" then return end
if self.device.wakeup_mgr:isWakeupAlarmScheduled() and self.device.wakeup_mgr:wakeupAction() then
if self.device.wakeup_mgr:isWakeupAlarmScheduled() and self.device.wakeup_mgr:wakeupAction(90) then
logger.info("Kindle scheduled wakeup")
else
logger.info("Kindle unscheduled wakeup")
logger.warn("Kindle unscheduled wakeup")
end
end
@ -227,18 +243,15 @@ function KindlePowerD:initWakeupMgr()
if not self.device:supportsScreensaver() then return end
if self.lipc_handle == nil then return end
function KindlePowerD:wakeupFromSuspend()
logger.info("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
-- so the alarm may fire 10 seconds early
local UIManager = require("ui/uimanager")
UIManager:scheduleIn(15, self.checkUnexpectedWakeup, self)
end
function KindlePowerD:readyToSuspend()
logger.info("Kindle readyToSuspend")
function KindlePowerD:readyToSuspend(delay)
if self.device.wakeup_mgr:isWakeupAlarmScheduled() then
local now = os.time()
local alarm = self.device.wakeup_mgr:getWakeupAlarmEpoch()
@ -255,7 +268,50 @@ function KindlePowerD:initWakeupMgr()
self.device.wakeup_mgr = WakeupMgr:new{rtc = require("device/kindle/mockrtc")}
end
--- @fixme: This won't ever fire, as KindlePowerD is already a metatable on a plain table.
-- Ask powerd to reset the t1 timeout, so that AutoSuspend can do its thing properly
function KindlePowerD:resetT1Timeout()
-- NOTE: powerd will only send a t1TimerReset event every $(kdb get system/daemon/powerd/send_t1_reset_interval) (15s),
-- which is just fine, as we should only request it at most every 5 minutes ;).
-- NOTE: This will fail if the device is already showing the screensaver.
if self.lipc_handle then
-- AFAIK, the value is irrelevant
self.lipc_handle:set_int_property("com.lab126.powerd", "touchScreenSaverTimeout", 1)
else
os.execute("lipc-set-prop -i com.lab126.powerd touchScreenSaverTimeout 1")
end
end
function KindlePowerD:beforeSuspend()
-- Inhibit user input and emit the Suspend event.
self.device:_beforeSuspend()
end
function KindlePowerD:afterResume()
self:invalidateCapacityCache()
-- Restore user input and emit the Resume event.
self.device:_afterResume()
if not self.device:hasFrontlight() then
return
end
if self:isFrontlightOn() then
-- The Kindle framework should turn the front light back on automatically.
-- The following statement ensures consistency of intensity, but should basically always be redundant,
-- since we set intensity via lipc and not sysfs ;).
-- NOTE: This is race-y, and we want to *lose* the race, hence the use of the scheduler (c.f., #4392)
UIManager:tickAfterNext(function() self:turnOnFrontlightHW() end)
else
-- But in the off case, we *do* use sysfs, so this one actually matters.
UIManager:tickAfterNext(function() self:turnOffFrontlightHW() end)
end
end
function KindlePowerD:UIManagerReadyHW(uimgr)
UIManager = uimgr
end
--- @fixme: This won't ever fire on its own, as KindlePowerD is already a metatable on a plain table.
function KindlePowerD:__gc()
if self.lipc_handle then
self.lipc_handle:close()

File diff suppressed because it is too large Load Diff

@ -2,7 +2,7 @@ local BasePowerD = require("device/generic/powerd")
local Math = require("optmath")
local NickelConf = require("device/kobo/nickel_conf")
local SysfsLight = require ("device/sysfs_light")
local ffiUtil = require("ffi/util")
local UIManager
local RTC = require("ffi/rtc")
-- Here, we only deal with the real hw intensity.
@ -26,6 +26,7 @@ function KoboPowerD:_syncKoboLightOnStart()
local is_frontlight_on = nil
local new_warmth = nil
local kobo_light_on_start = tonumber(G_defaults:readSetting("KOBO_LIGHT_ON_START"))
if kobo_light_on_start then
if kobo_light_on_start > 0 then
new_intensity = math.min(kobo_light_on_start, 100)
@ -132,8 +133,17 @@ function KoboPowerD:init()
self.initial_is_fl_on = true
if self.device:hasFrontlight() then
-- If this device has natural light (currently only KA1 & Forma)
-- Use the SysFS interface, and ioctl otherwise.
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 (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,
-- but it does use sysfs for the NL...
if self.device:hasNaturalLight() then
@ -146,6 +156,8 @@ function KoboPowerD:init()
-- Does this device's NaturalLight use a custom scale?
self.fl_warmth_min = self.device.frontlight_settings.nl_min or self.fl_warmth_min
self.fl_warmth_max = self.device.frontlight_settings.nl_max or self.fl_warmth_max
-- Generic does it *after* init, but we're going to need it *now*...
self.warmth_scale = 100 / self.fl_warmth_max
-- If this device has a mixer, we can use the ioctl for brightness control, as it's much lower latency.
if self.device:hasNaturalLightMixer() then
local kobolight = require("ffi/kobolight")
@ -170,7 +182,7 @@ function KoboPowerD:init()
if self:isFrontlightOnHW() then
-- On devices with a mixer, setIntensity will *only* set the FL, so, ensure we honor the warmth, too.
if self.device:hasNaturalLightMixer() then
self:setWarmth(self.fl_warmth)
self:setWarmth(self.fl_warmth, true)
end
-- Use setIntensity to ensure it sets fl_intensity, and because we don't want the ramping behavior of turnOn
self:setIntensity(self:frontlightIntensityHW())
@ -238,10 +250,10 @@ function KoboPowerD:isFrontlightOnHW()
self.initial_is_fl_on = nil
return ret
end
return self.hw_intensity > 0
return self.hw_intensity > 0 and not self.fl_ramp_down_running
end
function KoboPowerD:setIntensityHW(intensity)
function KoboPowerD:_setIntensityHW(intensity)
if self.fl == nil then return end
if self.fl_warmth == nil or self.device:hasNaturalLightMixer() then
-- We either don't have NL, or we have a mixer: we only want to set the intensity (c.f., #5429)
@ -257,6 +269,11 @@ function KoboPowerD:setIntensityHW(intensity)
self:_decideFrontlightState()
end
function KoboPowerD:setIntensityHW(intensity)
self:_stopFrontlightRamp()
self:_setIntensityHW(intensity)
end
-- NOTE: We *can* actually read this from the system (as well as frontlight level, since Mk. 7),
-- but this is already a huge mess, so, keep ignoring it...
function KoboPowerD:frontlightWarmthHW()
@ -297,34 +314,109 @@ function KoboPowerD:isChargedHW()
return false
end
function KoboPowerD:turnOffFrontlightHW()
function KoboPowerD:_endRampDown(end_intensity, done_callback)
self:_setIntensityHW(end_intensity)
self.fl_ramp_down_running = false
if done_callback then
done_callback()
end
end
function KoboPowerD:_stopFrontlightRamp()
if self.fl_ramp_up_running or self.fl_ramp_down_running then
-- Make sure we have no other ramp running.
UIManager:unschedule(self.turnOffFrontlightRamp)
UIManager:unschedule(self.turnOnFrontlightRamp)
UIManager:unschedule(self._endRampDown)
UIManager:unschedule(self._endRampUp)
self.fl_ramp_up_running = false
self.fl_ramp_down_running = false
end
end
-- This will ramp down faster at high intensity values (i.e., start), and slower at lower intensity values (i.e., end).
-- That's an attempt at making the *perceived* effect appear as a more linear brightness change.
-- The whole function gets called at most log(100)/log(0.75) = 17 times,
-- leading to a 0.025*17 + 0.5 = 0.925s ramp down time (non blocking); can be aborted.
function KoboPowerD:turnOffFrontlightRamp(curr_ramp_intensity, end_intensity, done_callback)
curr_ramp_intensity = math.floor(math.max(curr_ramp_intensity * .75, self.fl_min))
if curr_ramp_intensity > end_intensity then
self:_setIntensityHW(curr_ramp_intensity)
UIManager:scheduleIn(self.device.frontlight_settings.ramp_delay, self.turnOffFrontlightRamp, self, curr_ramp_intensity, end_intensity, done_callback)
else
-- Some devices require delaying the final step, to prevent them from jumping straight to zero and messing up the ramp.
UIManager:scheduleIn(self.device.frontlight_settings.ramp_off_delay, self._endRampDown, self, end_intensity, done_callback)
-- no reschedule here, as we are done
end
end
function KoboPowerD:turnOffFrontlightHW(done_callback)
if not self:isFrontlightOnHW() then
return
end
ffiUtil.runInSubProcess(function()
for i = 1,5 do
self:setIntensityHW(math.floor(self.fl_intensity - ((self.fl_intensity * (1/5)) * i)))
--- @note: Newer devices appear to block slightly longer on FL ioctls/sysfs, so only sleep on older devices,
--- otherwise we get a jump and not a ramp ;).
if not self.device:hasNaturalLight() then
if (i < 5) then
ffiUtil.usleep(35 * 1000)
if UIManager then
-- We've got nothing to do if we're already ramping down
if not self.fl_ramp_down_running then
self:_stopFrontlightRamp()
-- NOTE: For devices with a ramp_off_delay, we only ramp if we start from > 2%,
-- otherwise you just see a single delayed step (1%) or two stuttery ones (2%) ;).
-- FWIW, modern devices with a different PWM controller (i.e., with no controller-specific ramp_off_delay workarounds)
-- deal with our 2% ramp without stuttering.
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
-- 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
end, false, true)
-- NOTE: This is essentially what setIntensityHW does, except we don't actually touch the FL,
-- we only sync the state of the main process with the final state of what we're doing in the forks.
-- And update hw_intensity in our actual process ;).
self.hw_intensity = self.fl_min
-- NOTE: And don't forget to update sysfs_light, too, as a real setIntensityHW would via setBrightness
if self.fl then
self.fl.current_brightness = self.fl_min
else
-- If UIManager is not initialized yet, just turn it off immediately
self:setIntensityHW(self.fl_min)
end
-- We consume done_callback ourselves, make sure Generic's PowerD gets the memo
return true
end
function KoboPowerD:_endRampUp(end_intensity, done_callback)
self:_setIntensityHW(end_intensity)
self.fl_ramp_up_running = false
if done_callback then
done_callback()
end
self:_decideFrontlightState()
end
function KoboPowerD:turnOnFrontlightHW()
-- Similar functionality as `Kobo:turnOnFrontlightHW`, but the other way around ;).
function KoboPowerD:turnOnFrontlightRamp(curr_ramp_intensity, end_intensity, done_callback)
if curr_ramp_intensity == 0 then
curr_ramp_intensity = 1
else
curr_ramp_intensity = math.ceil(math.min(curr_ramp_intensity * 1.5, self.fl_max))
end
if curr_ramp_intensity < end_intensity then
self:_setIntensityHW(curr_ramp_intensity)
UIManager:scheduleIn(self.device.frontlight_settings.ramp_delay, self.turnOnFrontlightRamp, self, curr_ramp_intensity, end_intensity, done_callback)
else
UIManager:scheduleIn(self.device.frontlight_settings.ramp_delay, self._endRampUp, self, end_intensity, done_callback)
-- no reschedule here, as we are done
end
end
function KoboPowerD:turnOnFrontlightHW(done_callback)
-- NOTE: Insane workaround for the first toggle after a startup with the FL off.
-- The light is actually off, but hw_intensity couldn't have been set to a sane value because of a number of interactions.
-- So, fix it now, so we pass the isFrontlightOnHW check (which checks if hw_intensity > fl_min).
@ -334,55 +426,77 @@ function KoboPowerD:turnOnFrontlightHW()
if self:isFrontlightOnHW() then
return
end
ffiUtil.runInSubProcess(function()
for i = 1,5 do
self:setIntensityHW(math.ceil(self.fl_min + ((self.fl_intensity * (1/5)) * i)))
--- @note: Newer devices appear to block slightly longer on FL ioctls/sysfs, so only sleep on older devices,
--- otherwise we get a jump and not a ramp ;).
if not self.device:hasNaturalLight() then
if (i < 5) then
ffiUtil.usleep(35 * 1000)
if UIManager then
-- We've got nothing to do if we're already ramping up
if not self.fl_ramp_up_running then
self:_stopFrontlightRamp()
if self.device.frontlight_settings.ramp_off_delay > 0.0 and self.fl_intensity <= 2 then
-- 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
-- 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
end, false, true)
-- NOTE: This is essentially what setIntensityHW does, except we don't actually touch the FL,
-- we only sync the state of the main process with the final state of what we're doing in the forks.
-- And update hw_intensity in our actual process ;).
self.hw_intensity = self.fl_intensity
-- NOTE: And don't forget to update sysfs_light, too, as a real setIntensityHW would via setBrightness
if self.fl then
self.fl.current_brightness = self.fl_intensity
else
-- If UIManager is not initialized yet, just turn it on immediately
self:setIntensityHW(self.fl_intensity)
end
self:_decideFrontlightState()
-- We consume done_callback ourselves, make sure Generic's PowerD gets the memo
return true
end
-- Turn off front light before suspend.
function KoboPowerD:beforeSuspend()
if self.fl == nil then return end
-- Remember the current frontlight state
self.fl_was_on = self.is_fl_on
-- Turn off the frontlight
self:turnOffFrontlight()
-- Inhibit user input and emit the Suspend event.
self.device:_beforeSuspend()
-- Handle the frontlight last,
-- to prevent as many things as we can from interfering with the smoothness of the ramp
if self.fl then
-- Remember the current frontlight state
self.fl_was_on = self.is_fl_on
-- Turn off the frontlight
-- NOTE: Funky delay mainly to yield to the EPDC's refresh on UP systems.
-- (Neither yieldToEPDC nor nextTick & friends quite cut it here)...
UIManager:scheduleIn(0.001, self.turnOffFrontlight, self)
end
end
-- Restore front light state after resume.
function KoboPowerD:afterResume()
if self.fl == nil then return end
-- Don't bother if the light was already off on suspend
if not self.fl_was_on then return end
-- Update warmth state
if self.fl_warmth ~= nil then
-- And we need an explicit setWarmth if the device has a mixer, because turnOn won't touch the warmth on those ;).
if self.device:hasNaturalLightMixer() then
self:setWarmth(self.fl_warmth)
-- Set the system clock to the hardware clock's time.
RTC:HCToSys()
self:invalidateCapacityCache()
-- Restore user input and emit the Resume event.
self.device:_afterResume()
-- There's a whole bunch of stuff happening before us in Generic:onPowerEvent,
-- so we'll delay this ever so slightly so as to appear as smooth as possible...
if self.fl then
-- Don't bother if the light was already off on suspend
if self.fl_was_on then
-- Turn the frontlight back on
-- NOTE: There's quite likely *more* resource contention than on suspend here :/.
UIManager:scheduleIn(0.001, self.turnOnFrontlight, self)
end
end
-- Turn the frontlight back on
self:turnOnFrontlight()
end
-- Set the system clock to the hardware clock's time.
RTC:HCToSys()
function KoboPowerD:UIManagerReadyHW(uimgr)
UIManager = uimgr
end
return KoboPowerD

@ -1,4 +1,6 @@
local Generic = require("device/generic/device") -- <= look at this file!
local Geom = require("ui/geometry")
local UIManager
local logger = require("logger")
local ffi = require("ffi")
local C = ffi.C
@ -11,10 +13,6 @@ require("ffi/posix_h")
require("ffi/linux_input_h")
require("ffi/inkview_h")
-- FIXME: Signal ffi/input.lua (brought in by device/input later on) that we want to use poll mode backend.
-- Remove this once backend becomes poll-only.
_G.POCKETBOOK_FFI = true
local function yes() return true end
local function no() return false end
@ -58,9 +56,11 @@ 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.
inkview_translates_buttons = false,
-- Will be set appropriately at init
isB288SoC = no,
@ -188,6 +188,7 @@ function PocketBook:init()
device = self,
raw_input = raw_input,
event_map = setmetatable({
[C.KEY_HOME] = "Home",
[C.KEY_MENU] = "Menu",
[C.KEY_PREV] = "LPgBack",
[C.KEY_NEXT] = "LPgFwd",
@ -212,40 +213,36 @@ function PocketBook:init()
quasiSuspended = false
return "Resume"
end
elseif ev.code == C.EVT_EXIT then
-- Auto shutdown event from inkview framework,
-- gracefully close everything and let the framework shutdown the device.
return "Exit"
elseif ev.code == C.MSC_GYRO then
return this:handleGyroEv(ev)
end
end,
}
-- in contrast to kobo/kindle, pocketbook-devices do not use linux/input
-- events directly. To be able to use input.lua nevertheless, we make
-- inkview-events look like linux/input events or handle them directly
-- here.
-- Unhandled events will leave Input:waitEvent() as "GenericInput"
self.input:registerEventAdjustHook(function(_input, ev)
if ev.type == C.EVT_KEYDOWN or ev.type == C.EVT_KEYUP then
ev.value = ev.type == C.EVT_KEYDOWN and 1 or 0
ev.type = C.EV_KEY
end
-- If InkView translates buttons for us, disable our own translation map
if self.inkview_translates_buttons then
self.input:disableRotationMap()
end
-- handle C.EVT_BACKGROUND and C.EVT_FOREGROUND as MiscEvent as this makes
-- it easy to return a string directly which can be used in
-- uimanager.lua as event_handler index.
if ev.type == C.EVT_BACKGROUND or ev.type == C.EVT_FOREGROUND
or ev.type == C.EVT_SHOW or ev.type == C.EVT_HIDE then
ev.code = ev.type
ev.type = C.EV_MSC -- handle as MiscEvent, see above
end
-- If InkView tells us this device has a gsensor enable the event based functionality
if inkview.QueryGSensor() ~= 0 then
self.hasGSensor = yes
end
-- auto shutdown event from inkview framework, gracefully close
-- everything and let the framework shutdown the device
if ev.type == C.EVT_EXIT then
require("ui/uimanager"):broadcastEvent(
require("ui/event"):new("Close"))
end
end)
-- In contrast to kobo/kindle, pocketbook-devices do not use linux/input events directly.
-- To be able to use input.lua nevertheless,
-- we make inkview-events look like linux/input events or handle them directly here.
-- Unhandled events will leave Input:waitEvent() as "GenericInput"
-- 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.
@ -260,6 +257,21 @@ function PocketBook:init()
Generic.init(self)
end
function PocketBook:exit()
-- Exit code can be shoddy on some devices due to broken library dtors calling _exit(0) from os.exit(N)
local ko_exit = os.getenv("KO_EXIT_CODE")
if ko_exit then
local f = io.open(ko_exit, "w+")
if f then
-- As returned by UIManager:run() in reader.lua
f:write(tostring(UIManager._exit_code))
f:close()
end
end
Generic.exit(self)
end
function PocketBook:notifyBookState(title, document)
local fn = document and document.file
logger.dbg("Notify book state", title, fn)
@ -338,8 +350,6 @@ function PocketBook:reboot()
end
function PocketBook:initNetworkManager(NetworkMgr)
local UIManager = require("ui/uimanager")
local function keepWifiAlive()
-- Make sure only one wifiKeepAlive is scheduled
UIManager:unschedule(keepWifiAlive)
@ -373,9 +383,18 @@ function PocketBook:initNetworkManager(NetworkMgr)
end
end
function NetworkMgr:isWifiOn()
function NetworkMgr:isConnected()
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()
@ -390,16 +409,44 @@ function PocketBook:getDefaultCoverPath()
return "/mnt/ext1/system/logo/offlogo/cover.bmp"
end
function PocketBook:setEventHandlers(UIManager)
function PocketBook:UIManagerReady(uimgr)
UIManager = uimgr
end
function PocketBook:setEventHandlers(uimgr)
-- Only fg/bg state plugin notifiers, not real power event.
UIManager.event_handlers.Suspend = function()
self:_beforeSuspend()
self.powerd:beforeSuspend()
end
UIManager.event_handlers.Resume = function()
self:_afterResume()
self.powerd:afterResume()
end
UIManager.event_handlers.Exit = function()
local Event = require("ui/event")
UIManager:broadcastEvent(Event:new("Close"))
UIManager:quit(0)
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
@ -444,6 +491,7 @@ local PocketBook613 = PocketBook:extend{
display_dpi = 167,
isTouchDevice = no,
hasWifiToggle = no,
hasSeamlessWifiToggle = no,
hasFrontlight = no,
hasDPad = yes,
hasFewKeys = yes,
@ -487,6 +535,12 @@ local PocketBook617 = PocketBook:extend{
hasNaturalLight = yes,
}
-- PocketBook Basic Lux 4 (618)
local PocketBook618 = PocketBook:extend{
model = "PBBLux4",
display_dpi = 212,
}
-- PocketBook Touch (622)
local PocketBook622 = PocketBook:extend{
model = "PBTouch",
@ -536,6 +590,14 @@ local PocketBook628 = PocketBook:extend{
hasNaturalLight = yes,
}
-- PocketBook Verse (629)
local PocketBook629 = PocketBook:extend{
model = "PB629",
display_dpi = 212,
isAlwaysPortrait = yes,
hasNaturalLight = yes,
}
-- PocketBook Sense / Sense 2 (630)
local PocketBook630 = PocketBook:extend{
model = "PBSense",
@ -564,11 +626,20 @@ local PocketBook633 = PocketBook:extend{
model = "PBColor",
display_dpi = 300,
hasColorScreen = yes,
canHWDither = yes, -- Adjust color saturation with inkview
canUseCBB = no, -- 24bpp
isAlwaysPortrait = yes,
usingForcedRotation = landscape_ccw,
}
-- PocketBook Verse Pro (634)
local PocketBook634 = PocketBook:extend{
model = "PB634",
display_dpi = 300,
isAlwaysPortrait = yes,
hasNaturalLight = yes,
}
-- PocketBook Aqua (640)
local PocketBook640 = PocketBook:extend{
model = "PBAqua",
@ -593,8 +664,28 @@ local PocketBook700 = PocketBook:extend{
display_dpi = 300,
isAlwaysPortrait = yes,
hasNaturalLight = yes,
-- c.f., https://github.com/koreader/koreader/issues/9556
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",
@ -626,6 +717,7 @@ local PocketBook741 = PocketBook:extend{
model = "PBInkPadColor",
display_dpi = 300,
hasColorScreen = yes,
canHWDither = yes, -- Adjust color saturation with inkview
canUseCBB = no, -- 24bpp
isAlwaysPortrait = yes,
usingForcedRotation = landscape_ccw,
@ -636,16 +728,61 @@ function PocketBook741._fb_init(fb, finfo, vinfo)
vinfo.bits_per_pixel = 24
end
-- PocketBook InkPad Color 2 (743C)
local PocketBook743C = PocketBook:extend{
model = "PBInkPadColor2",
display_dpi = 300,
hasColorScreen = yes,
canHWDither = yes, -- Adjust color saturation with inkview
canUseCBB = no, -- 24bpp
isAlwaysPortrait = yes,
usingForcedRotation = landscape_ccw,
hasNaturalLight = yes,
}
function PocketBook743C._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 Color 3 (743K3)
local PocketBook743K3 = PocketBook:extend{
model = "PBInkPadColor3",
display_dpi = 300,
viewport = Geom:new{x=3, y=2, w=1395, h=1864},
hasColorScreen = yes,
canHWDither = yes, -- Adjust color saturation with inkview
canUseCBB = no, -- 24bpp
isAlwaysPortrait = yes,
usingForcedRotation = landscape_ccw,
hasNaturalLight = yes,
}
function PocketBook743K3._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 4 (743G/743g)
local PocketBook743G = PocketBook:extend{
model = "PBInkPad4",
display_dpi = 300,
isAlwaysPortrait = yes,
usingForcedRotation = landscape_ccw,
hasNaturalLight = yes,
}
-- PocketBook Color Lux (801)
local PocketBookColorLux = PocketBook:extend{
model = "PBColorLux",
display_dpi = 125,
hasColorScreen = yes,
canHWDither = yes, -- Adjust color saturation with inkview
canUseCBB = no, -- 24bpp
}
function PocketBookColorLux:_model_init()
self.screen.blitbuffer_rotation_mode = self.screen.ORIENTATION_PORTRAIT
self.screen.native_rotation_mode = self.screen.ORIENTATION_PORTRAIT
self.screen.blitbuffer_rotation_mode = self.screen.DEVICE_ROTATED_UPRIGHT
self.screen.native_rotation_mode = self.screen.DEVICE_ROTATED_UPRIGHT
end
function PocketBookColorLux._fb_init(fb, finfo, vinfo)
-- Pocketbook Color Lux reports bits_per_pixel = 8, but actually uses an RGB24 framebuffer
@ -679,71 +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 == "PocketBook 622" then
elseif codename == "618" then
return PocketBook618
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 == "PocketBook 630" then
elseif codename == "629" then
return PocketBook629
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 == "PB640" or codename == "PocketBook 640" then
elseif codename == "634" then
return PocketBook634
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 == "PocketBook 840" then
elseif codename == "743C" then
return PocketBook743C
elseif codename == "743K3" then
return PocketBook743K3
elseif codename == "743G" or codename == "743g" then
return PocketBook743G
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

@ -41,6 +41,9 @@ function PocketBookPowerD:setIntensityHW(intensity)
else
inkview.SetFrontlightState(intensity)
end
-- We have a custom isFrontlightOn implementation, so this is redundant
self:_decideFrontlightState()
end
function PocketBookPowerD:isFrontlightOn()
@ -73,4 +76,16 @@ function PocketBookPowerD:isChargingHW()
end
end
function PocketBookPowerD:beforeSuspend()
-- Inhibit user input and emit the Suspend event.
self.device:_beforeSuspend()
end
function PocketBookPowerD:afterResume()
self:invalidateCapacityCache()
-- Restore user input and emit the Resume event.
self.device:_afterResume()
end
return PocketBookPowerD

@ -35,6 +35,8 @@ local Remarkable = Generic:extend{
hasKeys = yes,
needsScreenRefreshAfterResume = no,
hasOTAUpdates = yes,
hasFastWifiStatusQuery = yes,
hasWifiManager = yes,
canReboot = yes,
canPowerOff = yes,
canSuspend = yes,
@ -59,8 +61,7 @@ local Remarkable1 = Remarkable:extend{
function Remarkable1:adjustTouchEvent(ev, by)
if ev.type == C.EV_ABS then
-- Mirror X and Y and scale up both X & Y as touch input is different res from
-- display
-- Mirror X and Y and scale up both X & Y as touch input is different res from display
if ev.code == C.ABS_MT_POSITION_X then
ev.value = (Remarkable1.mt_width - ev.value) * by.mt_scale_x
end
@ -82,8 +83,7 @@ local Remarkable2 = Remarkable:extend{
function Remarkable2:adjustTouchEvent(ev, by)
if ev.type == C.EV_ABS then
-- Mirror Y and scale up both X & Y as touch input is different res from
-- display
-- Mirror Y and scale up both X & Y as touch input is different res from display
if ev.code == C.ABS_MT_POSITION_X then
ev.value = (ev.value) * by.mt_scale_x
end
@ -116,7 +116,6 @@ local adjustAbsEvt = function(self, ev)
end
end
function Remarkable:init()
local oxide_running = os.execute("systemctl is-active --quiet tarnish") == 0
logger.info(string.format("Oxide running?: %s", oxide_running))
@ -146,6 +145,27 @@ function Remarkable:init()
wacom_protocol = true,
}
-- Assume input stuff is saner on mainline kernels...
-- (c.f., https://github.com/koreader/koreader/issues/10012)
local is_mainline = false
--- @fixme: Find a better way to discriminate mainline from stock...
local std_out = io.popen("uname -r", "r")
if std_out then
local release = std_out:read("*line")
std_out:close()
release = release:match("^(%d+%.%d+)%.%d+.*$")
release = tonumber(release)
if release and release >= 6.2 then
is_mainline = true
end
end
if is_mainline then
self.input_wacom = "/dev/input/by-path/platform-30a20000.i2c-event-mouse"
self.input_buttons = "/dev/input/by-path/platform-30370000.snvs:snvs-powerkey-event"
self.input_ts = "/dev/input/touchscreen0"
end
self.input.open(self.input_wacom) -- Wacom
self.input.open(self.input_ts) -- Touchscreen
self.input.open(self.input_buttons) -- Buttons
@ -153,13 +173,37 @@ function Remarkable:init()
local scalex = screen_width / self.mt_width
local scaley = screen_height / self.mt_height
self.input:registerEventAdjustHook(adjustAbsEvt)
self.input:registerEventAdjustHook(self.adjustTouchEvent, {mt_scale_x=scalex, mt_scale_y=scaley})
if is_mainline then
-- NOTE: The panel sends *both* ABS_MT_ & ABS_ coordinates, while the pen only sends ABS_ coordinates.
-- Since we have to apply *different* mangling to each of them,
-- we use a custom input handler that'll ignore ABS_ coordinates from the panel...
self.input.handleTouchEv = self.input.handleMixedTouchEv
local mt_height = self.mt_height
local mainlineInputMangling = function(this, ev)
if ev.type == C.EV_ABS then
-- Mirror Y for the touch panel
if ev.code == C.ABS_MT_POSITION_Y then
ev.value = mt_height - ev.value
-- Handle the Wacom pen
elseif ev.code == C.ABS_X then
ev.code = C.ABS_Y
ev.value = (wacom_height - ev.value) * wacom_scale_y
elseif ev.code == C.ABS_Y then
ev.code = C.ABS_X
ev.value = ev.value * wacom_scale_x
end
end
end
self.input:registerEventAdjustHook(mainlineInputMangling)
else
self.input:registerEventAdjustHook(adjustAbsEvt)
self.input:registerEventAdjustHook(self.adjustTouchEvent, {mt_scale_x=scalex, mt_scale_y=scaley})
end
-- USB plug/unplug, battery charge/not charging are generated as fake events
self.input.open("fake_events")
local rotation_mode = self.screen.ORIENTATION_PORTRAIT
local rotation_mode = self.screen.DEVICE_ROTATED_UPRIGHT
self.screen.native_rotation_mode = rotation_mode
self.screen.cur_rotation_mode = rotation_mode
@ -174,11 +218,9 @@ end
function Remarkable:supportsScreensaver() return true end
function Remarkable:initNetworkManager(NetworkMgr)
function NetworkMgr:turnOnWifi(complete_callback)
function NetworkMgr:turnOnWifi(complete_callback, interactive)
os.execute("./enable-wifi.sh")
self:reconnectOrShowNetworkMenu(function()
self:connectivityCheck(1, complete_callback)
end)
return self:reconnectOrShowNetworkMenu(complete_callback, interactive)
end
function NetworkMgr:turnOffWifi(complete_callback)
@ -194,9 +236,8 @@ function Remarkable:initNetworkManager(NetworkMgr)
NetworkMgr:setWirelessBackend("wpa_supplicant", {ctrl_interface = "/var/run/wpa_supplicant/wlan0"})
NetworkMgr.isWifiOn = function()
return NetworkMgr:isConnected()
end
NetworkMgr.isWifiOn = NetworkMgr.sysfsWifiOn
NetworkMgr.isConnected = NetworkMgr.ifHasAnAddress
end
function Remarkable:setDateTime(year, month, day, hour, min, sec)
@ -234,12 +275,10 @@ end
function Remarkable:setEventHandlers(UIManager)
UIManager.event_handlers.Suspend = function()
self:_beforeSuspend()
self:onPowerEvent("Suspend")
end
UIManager.event_handlers.Resume = function()
self:onPowerEvent("Resume")
self:_afterResume()
end
UIManager.event_handlers.PowerPress = function()
UIManager:scheduleIn(2, UIManager.poweroff_action)
@ -249,7 +288,11 @@ function Remarkable:setEventHandlers(UIManager)
UIManager:unschedule(UIManager.poweroff_action)
-- resume if we were suspended
if self.screen_saver_mode then
UIManager.event_handlers.Resume()
if self.screen_saver_lock then
UIManager.event_handlers.Suspend()
else
UIManager.event_handlers.Resume()
end
else
UIManager.event_handlers.Suspend()
end

@ -22,4 +22,16 @@ function Remarkable_PowerD:isChargingHW()
return self:read_str_file(self.status_file) == "Charging"
end
function Remarkable_PowerD:beforeSuspend()
-- Inhibit user input and emit the Suspend event.
self.device:_beforeSuspend()
end
function Remarkable_PowerD:afterResume()
self:invalidateCapacityCache()
-- Restore user input and emit the Resume event.
self.device:_afterResume()
end
return Remarkable_PowerD

@ -1,5 +1,7 @@
local Event = require("ui/event")
local Geom = require("ui/geometry")
local Generic = require("device/generic/device")
local UIManager
local SDL = require("ffi/SDL2_0")
local ffi = require("ffi")
local logger = require("logger")
@ -21,12 +23,11 @@ local function isUrl(s)
end
local function isCommand(s)
return os.execute("which "..s.." >/dev/null 2>&1") == 0
return os.execute("command -v "..s.." >/dev/null") == 0
end
local function runCommand(command)
local env = jit.os ~= "OSX" and 'env -u LD_LIBRARY_PATH ' or ""
return os.execute(env..command) == 0
return os.execute(command) == 0
end
local function getDesktopDicts()
@ -67,8 +68,10 @@ 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,
isTouchDevice = yes,
isDefaultFullscreen = no,
needsScreenRefreshAfterResume = no,
@ -107,7 +110,6 @@ local Device = Generic:extend{
local AppImage = Device:extend{
model = "AppImage",
hasMultitouch = no,
hasOTAUpdates = yes,
isDesktop = yes,
}
@ -119,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,
@ -128,7 +136,6 @@ local Emulator = Device:extend{
hasNaturalLight = yes,
hasNaturalLightApi = yes,
hasWifiToggle = yes,
hasWifiManager = yes,
-- Not really, Device:reboot & Device:powerOff are not implemented, so we just exit ;).
canPowerOff = yes,
canReboot = yes,
@ -171,6 +178,9 @@ function Device:init()
y = self.window.top,
is_always_portrait = self.isAlwaysPortrait(),
}
-- Pickup the updated window sizes if they were enforced in S.open (we'll get the coordinates via the inital SDL_WINDOWEVENT_MOVED)...
self.window.width = self.screen.w
self.window.height = self.screen.h
self.powerd = require("device/sdl/powerd"):new{device = self}
local ok, re = pcall(self.screen.setWindowIcon, self.screen, "resources/koreader.png")
@ -181,8 +191,7 @@ function Device:init()
device = self,
event_map = require("device/sdl/event_map_sdl2"),
handleSdlEv = function(device_input, ev)
local Geom = require("ui/geometry")
local UIManager = require("ui/uimanager")
-- SDL events can remain cdata but are almost completely transparent
local SDL_TEXTINPUT = 771
@ -228,12 +237,12 @@ function Device:init()
local fake_release_ev = Event:new("Gesture", fake_ges_release)
if scrolled_y == down then
fake_ges.direction = "north"
UIManager:broadcastEvent(fake_pan_ev)
UIManager:broadcastEvent(fake_release_ev)
UIManager:sendEvent(fake_pan_ev)
UIManager:sendEvent(fake_release_ev)
elseif scrolled_y == up then
fake_ges.direction = "south"
UIManager:broadcastEvent(fake_pan_ev)
UIManager:broadcastEvent(fake_release_ev)
UIManager:sendEvent(fake_pan_ev)
UIManager:sendEvent(fake_release_ev)
end
elseif ev.code == SDL_MULTIGESTURE then
-- no-op for now
@ -245,13 +254,14 @@ function Device:init()
ReaderUI:doShowReader(dropped_file_path)
end
elseif ev.code == SDL_WINDOWEVENT_RESIZED then
device_input.device.screen.screen_size.w = ev.value.data1
device_input.device.screen.screen_size.h = ev.value.data2
device_input.device.screen.resize(device_input.device.screen, ev.value.data1, ev.value.data2)
self.window.width = ev.value.data1
self.window.height = ev.value.data2
local new_size = device_input.device.screen:getSize()
device_input.device.screen.screen_size.w = new_size.w
device_input.device.screen.screen_size.h = new_size.h
logger.dbg("Resizing screen to", new_size)
-- try to catch as many flies as we can
@ -269,6 +279,9 @@ function Device:init()
FileManager.instance:reinit(FileManager.instance.path,
FileManager.instance.focused_file)
end
-- make sure dialogs are displayed
UIManager:setDirty("all", "ui")
elseif ev.code == SDL_WINDOWEVENT_MOVED then
self.window.left = ev.value.data1
self.window.top = ev.value.data2
@ -301,9 +314,8 @@ function Device:init()
end
if portrait then
self.input:registerEventAdjustHook(self.input.adjustTouchSwitchXY)
self.input:registerEventAdjustHook(
self.input.adjustTouchMirrorX,
self.input.adjustTouchSwitchAxesAndMirrorX,
(self.screen:getScreenWidth() - 1)
)
end
@ -343,50 +355,68 @@ function Device:toggleFullscreen()
end
end
function Device:setEventHandlers(UIManager)
function Device:UIManagerReady(uimgr)
UIManager = uimgr
end
function Device:setEventHandlers(uimgr)
if not self:canSuspend() then
-- If we can't suspend, we have no business even trying to, as we may not have overloaded `Device:simulateResume`,
-- and since the empty Generic prototype doesn't flip `Device.screen_saver_mode`, we'd be stuck if we tried...
-- Instead, rely on the Generic Suspend/Resume handlers, which are sane ;).
-- If we can't suspend, we have no business even trying to, as we may not have overloaded `Device:simulateResume`.
-- Instead, rely on the Generic Suspend/Resume handlers.
return
end
UIManager.event_handlers.Suspend = function()
self:_beforeSuspend()
self:simulateSuspend()
end
UIManager.event_handlers.Resume = function()
self:simulateResume()
self:_afterResume()
end
UIManager.event_handlers.PowerRelease = function()
-- Resume if we were suspended
if self.screen_saver_mode then
UIManager.event_handlers.Resume()
if self.screen_saver_lock then
UIManager.event_handlers.Suspend()
else
UIManager.event_handlers.Resume()
end
else
UIManager.event_handlers.Suspend()
end
end
end
function Device:initNetworkManager(NetworkMgr)
function NetworkMgr:isWifiOn() return true end
function NetworkMgr:isConnected()
-- Pull the default gateway first, so we don't even try to ping anything if there isn't one...
local default_gw = Device:getDefaultRoute()
if not default_gw then
return false
end
return 0 == os.execute("ping -c1 -w2 " .. default_gw .. " > /dev/null")
end
end
function Emulator:supportsScreensaver() return true end
function Emulator:simulateSuspend()
local Screensaver = require("ui/screensaver")
Screensaver:setup()
Screensaver:show()
self.screen_saver_mode = true
self.powerd:beforeSuspend()
end
function Emulator:simulateResume()
local Screensaver = require("ui/screensaver")
Screensaver:close()
self.screen_saver_mode = false
self.powerd:afterResume()
end
-- fake network manager for the emulator
function Emulator:initNetworkManager(NetworkMgr)
local UIManager = require("ui/uimanager")
local connectionChangedEvent = function()
if G_reader_settings:nilOrTrue("emulator_fake_wifi_connected") then
UIManager:broadcastEvent(Event:new("NetworkConnected"))
@ -405,6 +435,7 @@ function Emulator:initNetworkManager(NetworkMgr)
function NetworkMgr:isWifiOn()
return G_reader_settings:nilOrTrue("emulator_fake_wifi_connected")
end
NetworkMgr.isConnected = NetworkMgr.isWifiOn
end
io.write("Starting SDL in " .. SDL.getBasePath() .. "\n")
@ -412,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

@ -18,6 +18,7 @@ end
function SDLPowerD:setIntensityHW(intensity)
require("logger").info("set brightness to", intensity)
self.hw_intensity = intensity or self.hw_intensity
self:_decideFrontlightState()
end
function SDLPowerD:frontlightWarmthHW()
@ -37,4 +38,16 @@ function SDLPowerD:isChargingHW()
return false
end
function SDLPowerD:beforeSuspend()
-- Inhibit user input and emit the Suspend event.
self.device:_beforeSuspend()
end
function SDLPowerD:afterResume()
self:invalidateCapacityCache()
-- Restore user input and emit the Resume event.
self.device:_afterResume()
end
return SDLPowerD

@ -1,5 +1,6 @@
local Generic = require("device/generic/device") -- <= look at this file!
local PluginShare = require("pluginshare")
local UIManager
local ffi = require("ffi")
local logger = require("logger")
@ -13,7 +14,7 @@ local SonyPRSTUX = Generic:extend{
model = "Sony PRSTUX",
isSonyPRSTUX = yes,
hasKeys = yes,
hasOTAUpdates = yes,
hasOTAUpdates = no,
hasWifiManager = yes,
canReboot = yes,
canPowerOff = yes,
@ -64,7 +65,7 @@ function SonyPRSTUX:init()
self.input.open("fake_events") -- usb plug-in/out and charging/not-charging
self.input:registerEventAdjustHook(adjustTouchEvt)
local rotation_mode = self.screen.ORIENTATION_LANDSCAPE_ROTATED
local rotation_mode = self.screen.DEVICE_ROTATED_COUNTER_CLOCKWISE
self.screen.native_rotation_mode = rotation_mode
self.screen.cur_rotation_mode = rotation_mode
@ -91,23 +92,20 @@ end
function SonyPRSTUX:intoScreenSaver()
local Screensaver = require("ui/screensaver")
if self.screen_saver_mode == false then
if not self.screen_saver_mode then
Screensaver:setup()
Screensaver:show()
end
self.powerd:beforeSuspend()
self.screen_saver_mode = true
end
function SonyPRSTUX:outofScreenSaver()
if self.screen_saver_mode == true then
if self.screen_saver_mode then
local Screensaver = require("ui/screensaver")
Screensaver:close()
local UIManager = require("ui/uimanager")
UIManager:nextTick(function() UIManager:setDirty("all", "full") end)
end
self.powerd:afterResume()
self.screen_saver_mode = false
end
function SonyPRSTUX:suspend()
@ -149,9 +147,9 @@ function SonyPRSTUX:initNetworkManager(NetworkMgr)
end
end
function NetworkMgr:turnOnWifi(complete_callback)
function NetworkMgr:turnOnWifi(complete_callback, interactive)
os.execute("./set-wifi.sh on")
self:reconnectOrShowNetworkMenu(complete_callback)
return self:reconnectOrShowNetworkMenu(complete_callback, interactive)
end
function NetworkMgr:getNetworkInterfaceName()
@ -170,13 +168,13 @@ function SonyPRSTUX:initNetworkManager(NetworkMgr)
os.execute("dhclient -x wlan0")
end
function NetworkMgr:restoreWifiAsync()
-- os.execute("./restore-wifi-async.sh")
end
--[[
function NetworkMgr:isWifiOn()
return 0 == os.execute("wmiconfig -i wlan0 --wlan query | grep -q enabled")
end
--]]
NetworkMgr.isWifiOn = NetworkMgr.sysfsWifiOn
NetworkMgr.isConnected = NetworkMgr.ifHasAnAddress
end
@ -188,16 +186,18 @@ function SonyPRSTUX:getDeviceModel()
return ffi.string("PRS-T2")
end
function SonyPRSTUX:setEventHandlers(UIManager)
function SonyPRSTUX:UIManagerReady(uimgr)
UIManager = uimgr
end
function SonyPRSTUX:setEventHandlers(uimgr)
UIManager.event_handlers.Suspend = function()
self:_beforeSuspend()
self:intoScreenSaver()
self:suspend()
end
UIManager.event_handlers.Resume = function()
self:resume()
self:outofScreenSaver()
self:_afterResume()
end
UIManager.event_handlers.PowerPress = function()
UIManager:scheduleIn(2, UIManager.poweroff_action)
@ -207,7 +207,11 @@ function SonyPRSTUX:setEventHandlers(UIManager)
UIManager:unschedule(UIManager.poweroff_action)
-- resume if we were suspended
if self.screen_saver_mode then
UIManager.event_handlers.Resume()
if self.screen_saver_lock then
UIManager.event_handlers.Suspend()
else
UIManager.event_handlers.Resume()
end
else
UIManager.event_handlers.Suspend()
end
@ -220,22 +224,15 @@ function SonyPRSTUX:setEventHandlers(UIManager)
self:_afterNotCharging()
end
UIManager.event_handlers.UsbPlugIn = function()
if self.screen_saver_mode then
if self.screen_saver_mode and not self.screen_saver_lock then
self:resume()
self:outofScreenSaver()
self:_afterResume()
end
self:usbPlugIn()
end
UIManager.event_handlers.UsbPlugOut = function()
self:usbPlugOut()
end
UIManager.event_handlers.__default__ = function(input_event)
-- Same as in Kobo: we want to ignore keys during suspension
if not self.screen_saver_mode then
UIManager:sendEvent(input_event)
end
end
end
-- For Sony PRS-T2

@ -28,4 +28,16 @@ function SonyPRSTUX_PowerD:isChargingHW()
return self:read_str_file(self.status_file) == "Charging"
end
function SonyPRSTUX_PowerD:beforeSuspend()
-- Inhibit user input and emit the Suspend event.
self.device:_beforeSuspend()
end
function SonyPRSTUX_PowerD:afterResume()
self:invalidateCapacityCache()
-- Restore user input and emit the Resume event.
self.device:_afterResume()
end
return SonyPRSTUX_PowerD

@ -2,12 +2,8 @@
-- This also supports the natural light, which consists of additional
-- red and green light LEDs.
local logger = require("logger")
local dbg = require("dbg")
local ffi = require("ffi")
local C = ffi.C
require("ffi/posix_h")
local ffiUtil = require("ffi/util")
local SysfsLight = {
frontlight_white = nil,
@ -77,15 +73,15 @@ function SysfsLight:setNaturalBrightness(brightness, warmth)
if self.frontlight_ioctl then
self.frontlight_ioctl:setBrightness(brightness)
else
self:_write_value(self.frontlight_white, brightness)
ffiUtil.writeToSysfs(brightness, self.frontlight_white)
end
end
-- The mixer might be using inverted values... (cold is nl_max, warm is nl_min)
if set_warmth then
if self.nl_inverted then
self:_write_value(self.frontlight_mixer, self.nl_max - warmth)
ffiUtil.writeToSysfs(self.nl_max - warmth, self.frontlight_mixer)
else
self:_write_value(self.frontlight_mixer, warmth)
ffiUtil.writeToSysfs(warmth, self.frontlight_mixer)
end
end
else
@ -128,30 +124,11 @@ function SysfsLight:_set_light_value(sysfs_directory, value)
if not sysfs_directory then return end
-- bl_power is '31' when the light is turned on, '0' otherwise.
if (value > 0) then
self:_write_value(sysfs_directory .. "/bl_power", 31)
ffiUtil.writeToSysfs(31, sysfs_directory .. "/bl_power")
else
self:_write_value(sysfs_directory .. "/bl_power", 0)
end
self:_write_value(sysfs_directory .. "/brightness", value)
end
function SysfsLight:_write_value(file, val)
local fd = C.open(file, bit.bor(C.O_WRONLY, C.O_CLOEXEC)) -- procfs/sysfs, we shouldn't need O_TRUNC
if fd == -1 then
logger.err("Cannot open file `" .. file .. "`:", ffi.string(C.strerror(ffi.errno())))
return false
end
val = tostring(val)
local bytes = #val
local nw = C.write(fd, val, bytes)
if nw == -1 then
logger.err("Cannot write `" .. val .. "` to file `" .. file .. "`:", ffi.string(C.strerror(ffi.errno())))
C.close(fd)
return false
ffiUtil.writeToSysfs(0, sysfs_directory .. "/bl_power")
end
C.close(fd)
-- NOTE: Allows the caller to possibly handle short writes (not that these should ever happen here).
return nw == bytes
ffiUtil.writeToSysfs(value, sysfs_directory .. "/brightness")
end
return SysfsLight

@ -47,6 +47,10 @@ end
function WakeupMgr:init()
self._task_queue = {}
-- Propagate rtc quirks to the RTC backend
self.rtc.dev_rtc = self.dev_rtc
self.rtc.dodgy_rtc = self.dodgy_rtc
end
-- This is a dummy task we use when working around i.MX5 RTC issues.
@ -216,6 +220,12 @@ Unset wakeup alarm.
Simple wrapper for @{ffi.rtc.unsetWakeupAlarm}.
--]]
function WakeupMgr:unsetWakeupAlarm()
-- Apparently, toggling the interrupt doesn't work on some RTCs,
-- and not necessarily the ones we've flagged as dodgy... (#10031).
-- Deal with this insanity by ensuring the alarm is not set in the future,
-- by overwriting the current alarm with an already expired disabled one.
logger.dbg("WakeupMgr:unsetWakeupAlarm will invalidate any future alarms")
self:setWakeupAlarm(0, false)
return self.rtc:unsetWakeupAlarm()
end

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

Loading…
Cancel
Save