Compare commits

..

No commits in common. 'master' and 'v2022.08' have entirely different histories.

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

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

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

@ -0,0 +1,22 @@
#!/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

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

@ -4,11 +4,9 @@ CI_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=/dev/null
source "${CI_DIR}/common.sh"
pushd install/koreader && {
pushd koreader-emulator-x86_64-linux-gnu/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,165 +1,112 @@
version: "2.1"
version: 2
# Parameters. {{{
parameters:
# Bump this to reset all caches.
cache_epoch:
type: integer
default: 0
# }}}
# Executors. {{{
executors:
workflows:
version: 2
build:
jobs:
- build
- docs:
context: koreader-vars
filters:
branches:
only: master
requires:
- build
base:
jobs:
build:
docker:
- image: koreader/kobase:0.3.2-20.04
- image: koreader/kobase:0.3.0
auth:
username: $DOCKER_USERNAME
password: $DOCKER_PASSWORD
# }}}
# 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"
environment:
EMULATE_READER: 1
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:
- &CACHE_KEY_BUILD_DIR '<< pipeline.parameters.cache_epoch >>-{{ .Environment.CIRCLE_JOB }}-build-{{ arch }}-{{ checksum "base/cache-key" }}'
# 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
- restore_cache:
name: Restore build cache
keys:
- &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: Setup build cache
command: |
set -x
which ccache
ccache --version
ccache --zero-stats
ccache --show-config
# }}}
# Build.
- run:
name: Build
command: .ci/build.sh
# Clean / save caches. {{{
# We want to save cache prior to testing so we don't have to clean it up.
- build-{{ arch }}-{{ checksum "git-rev-base" }}
- run: echo 'export PATH=${HOME}/bin:${PATH}' >> $BASH_ENV
# installs and caches testing tools
- 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
name: install
command: .ci/install.sh
- save_cache:
name: Save build cache
key: *CACHE_KEY_BUILD_CACHE
key: deps-{{ arch }}-{{ checksum ".ci/install.sh" }}-{{ checksum ".ci/helper_luarocks.sh" }}
paths:
- /home/ko/.ccache
- "/home/ko/bin"
- "/home/ko/.luarocks"
# compiled luarocks binaries
- "install"
# installs everything and caches base
- run:
name: fetch
command: .ci/fetch.sh
- run:
name: check
command: .ci/check.sh
- run:
name: build
command: .ci/build.sh
# we want to save cache prior to testing so we don't have to clean it up
- save_cache:
name: Save build directory
key: *CACHE_KEY_BUILD_DIR
key: build-{{ arch }}-{{ checksum "git-rev-base" }}
paths:
- base/build
# }}}
# Tests / coverage. {{{
# Our lovely unit tests.
- "/home/ko/.ccache"
- "base"
# 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: &TESTS_XML install/koreader/junit-test-results.xml
# CircleCI doesn't make the test results available as artifacts (October 2017).
path: koreader-emulator-x86_64-linux-gnu/koreader
# CircleCI doesn't make the test results available as artifacts (October 2017)
- store_artifacts:
path: *TESTS_XML
# }}}
# }}}
# Docs. {{{
path: koreader-emulator-x86_64-linux-gnu/koreader/junit-test-results.xml
docs:
executor: base
resource_class: small
environment:
BASH_ENV: "~/.bashrc"
docker:
- image: koreader/kobase:0.3.0
auth:
username: $DOCKER_USERNAME
password: $DOCKER_PASSWORD
environment:
EMULATE_READER: 1
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: fetch
command: .ci/fetch.sh
name: init-submodules
command: git submodule init && git submodule sync && git submodule update
# docs, coverage, and test timing (can we use two outputs at once?); master branch only
- run:
name: docs-and-translation
command: .ci/after_success_docs_translation.sh
# }}}
# }}}
# Workflows. {{{
workflows:
version: 2
build:
jobs:
- build
- docs:
context: koreader-vars
filters:
branches:
only: master
requires:
- build
# }}}
# vim: foldmethod=marker foldlevel=0

@ -1,6 +1,6 @@
---
name: Bug report
about: Create a bug report to help us improve the application
about: Create a bug report to help us improve
title: ''
labels: ''
assignees: ''
@ -15,21 +15,20 @@ assignees: ''
#### Steps to reproduce
##### `crash.log` (if applicable)
`crash.log` is a file that is automatically created when KOReader crashes. It can normally be found in the KOReader directory:
`crash.log` is a file that is automatically created when KOReader crashes. It can
normally be found in the KOReader directory:
* `/mnt/private/koreader` for Cervantes
* `koreader/` directory for Kindle
* `.adds/koreader/` directory for Kobo
* `applications/koreader/` directory for Pocketbook
Android logs are kept in memory. Please go to [Menu] → Help → Bug Report to save these logs to a file.
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.
Android logs are kept in memory. Please go to Help → Bug Report to save these logs to a file.
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`.
You'll need to restart KOReader after toggling these on.
Please try to include the relevant sections in your issue description.
You can upload the whole `crash.log` file on GitHub by dragging and
dropping it onto this textbox.
If you instead opt to inline it, please do so behind a spoiler tag:
<details>

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

1
.gitmodules vendored

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

@ -5,13 +5,124 @@ self = false
globals = {
"G_reader_settings",
"G_defaults",
"table.pack",
"table.unpack",
}
read_globals = {
"_ENV",
"KOBO_TOUCH_MIRRORED",
"KOBO_SYNC_BRIGHTNESS_WITH_NICKEL",
"DHINTCOUNT",
"DRENDER_MODE",
"DGLOBAL_CACHE_SIZE_MINIMUM",
"DGLOBAL_CACHE_FREE_PROPORTION",
"DGLOBAL_CACHE_SIZE_MAXIMUM",
"DBACKGROUND_COLOR",
"DOUTER_PAGE_COLOR",
"DGENERIC_ICON_SIZE",
"DCREREADER_VIEW_MODE",
"DSHOWOVERLAP",
"DSHOWHIDDENFILES",
"DLANDSCAPE_CLOCKWISE_ROTATION",
"DCREREADER_TWO_PAGE_THRESHOLD",
"DOVERLAPPIXELS",
"FOLLOW_LINK_TIMEOUT",
"DTAP_ZONE_MENU",
"DTAP_ZONE_MENU_EXT",
"DTAP_ZONE_CONFIG",
"DTAP_ZONE_CONFIG_EXT",
"DTAP_ZONE_MINIBAR",
"DTAP_ZONE_FORWARD",
"DTAP_ZONE_BACKWARD",
"DTAP_ZONE_BOOKMARK",
"DTAP_ZONE_FLIPPING",
"DTAP_ZONE_TOP_LEFT",
"DTAP_ZONE_TOP_RIGHT",
"DTAP_ZONE_BOTTOM_LEFT",
"DTAP_ZONE_BOTTOM_RIGHT",
"DDOUBLE_TAP_ZONE_NEXT_CHAPTER",
"DDOUBLE_TAP_ZONE_PREV_CHAPTER",
"DCHANGE_WEST_SWIPE_TO_EAST",
"DCHANGE_EAST_SWIPE_TO_WEST",
"DKOPTREADER_CONFIG_FONT_SIZE",
"DKOPTREADER_CONFIG_TEXT_WRAP",
"DKOPTREADER_CONFIG_TRIM_PAGE",
"DKOPTREADER_CONFIG_DETECT_INDENT",
"DKOPTREADER_CONFIG_DEFECT_SIZE",
"DKOPTREADER_CONFIG_PAGE_MARGIN",
"DKOPTREADER_CONFIG_LINE_SPACING",
"DKOPTREADER_CONFIG_RENDER_QUALITY",
"DKOPTREADER_CONFIG_AUTO_STRAIGHTEN",
"DKOPTREADER_CONFIG_JUSTIFICATION",
"DKOPTREADER_CONFIG_MAX_COLUMNS",
"DKOPTREADER_CONFIG_CONTRAST",
"DKOPTREADER_CONFIG_WORD_SPACINGS",
"DKOPTREADER_CONFIG_DEFAULT_WORD_SPACING",
"DKOPTREADER_CONFIG_DOC_LANGS_TEXT",
"DKOPTREADER_CONFIG_DOC_LANGS_CODE",
"DKOPTREADER_CONFIG_DOC_DEFAULT_LANG_CODE",
"DCREREADER_CONFIG_FONT_SIZES",
"DCREREADER_CONFIG_DEFAULT_FONT_SIZE",
"DCREREADER_CONFIG_H_MARGIN_SIZES_SMALL",
"DCREREADER_CONFIG_H_MARGIN_SIZES_MEDIUM",
"DCREREADER_CONFIG_H_MARGIN_SIZES_LARGE",
"DCREREADER_CONFIG_H_MARGIN_SIZES_X_LARGE",
"DCREREADER_CONFIG_H_MARGIN_SIZES_XX_LARGE",
"DCREREADER_CONFIG_H_MARGIN_SIZES_XXX_LARGE",
"DCREREADER_CONFIG_H_MARGIN_SIZES_HUGE",
"DCREREADER_CONFIG_H_MARGIN_SIZES_X_HUGE",
"DCREREADER_CONFIG_H_MARGIN_SIZES_XX_HUGE",
"DCREREADER_CONFIG_T_MARGIN_SIZES_SMALL",
"DCREREADER_CONFIG_T_MARGIN_SIZES_MEDIUM",
"DCREREADER_CONFIG_T_MARGIN_SIZES_LARGE",
"DCREREADER_CONFIG_T_MARGIN_SIZES_X_LARGE",
"DCREREADER_CONFIG_T_MARGIN_SIZES_XX_LARGE",
"DCREREADER_CONFIG_T_MARGIN_SIZES_XXX_LARGE",
"DCREREADER_CONFIG_T_MARGIN_SIZES_HUGE",
"DCREREADER_CONFIG_T_MARGIN_SIZES_X_HUGE",
"DCREREADER_CONFIG_T_MARGIN_SIZES_XX_HUGE",
"DCREREADER_CONFIG_B_MARGIN_SIZES_SMALL",
"DCREREADER_CONFIG_B_MARGIN_SIZES_MEDIUM",
"DCREREADER_CONFIG_B_MARGIN_SIZES_LARGE",
"DCREREADER_CONFIG_B_MARGIN_SIZES_X_LARGE",
"DCREREADER_CONFIG_B_MARGIN_SIZES_XX_LARGE",
"DCREREADER_CONFIG_B_MARGIN_SIZES_XXX_LARGE",
"DCREREADER_CONFIG_B_MARGIN_SIZES_HUGE",
"DCREREADER_CONFIG_B_MARGIN_SIZES_X_HUGE",
"DCREREADER_CONFIG_B_MARGIN_SIZES_XX_HUGE",
"DCREREADER_CONFIG_LIGHTER_FONT_GAMMA",
"DCREREADER_CONFIG_DEFAULT_FONT_GAMMA",
"DCREREADER_CONFIG_DARKER_FONT_GAMMA",
"DCREREADER_CONFIG_LINE_SPACE_PERCENT_X_TINY",
"DCREREADER_CONFIG_LINE_SPACE_PERCENT_TINY",
"DCREREADER_CONFIG_LINE_SPACE_PERCENT_XX_SMALL",
"DCREREADER_CONFIG_LINE_SPACE_PERCENT_X_SMALL",
"DCREREADER_CONFIG_LINE_SPACE_PERCENT_SMALL",
"DCREREADER_CONFIG_LINE_SPACE_PERCENT_L_SMALL",
"DCREREADER_CONFIG_LINE_SPACE_PERCENT_MEDIUM",
"DCREREADER_CONFIG_LINE_SPACE_PERCENT_L_MEDIUM",
"DCREREADER_CONFIG_LINE_SPACE_PERCENT_XL_MEDIUM",
"DCREREADER_CONFIG_LINE_SPACE_PERCENT_XXL_MEDIUM",
"DCREREADER_CONFIG_LINE_SPACE_PERCENT_LARGE",
"DCREREADER_CONFIG_LINE_SPACE_PERCENT_X_LARGE",
"DCREREADER_CONFIG_LINE_SPACE_PERCENT_XX_LARGE",
"DCREREADER_CONFIG_WORD_SPACING_SMALL",
"DCREREADER_CONFIG_WORD_SPACING_MEDIUM",
"DCREREADER_CONFIG_WORD_SPACING_LARGE",
"DCREREADER_CONFIG_WORD_EXPANSION_NONE",
"DCREREADER_CONFIG_WORD_EXPANSION_SOME",
"DCREREADER_CONFIG_WORD_EXPANSION_MORE",
"DMINIBAR_CONTAINER_HEIGHT",
"DGESDETECT_DISABLE_DOUBLE_TAP",
"FRONTLIGHT_SENSITIVITY_DECREASE",
"DALPHA_SORT_CASE_INSENSITIVE",
"KOBO_LIGHT_ON_START",
"NETWORK_PROXY",
"DUSE_TURBO_LIB",
"STARDICT_DATA_DIR",
"cre",
"lfs",
"lipc",
"xtext",
}
exclude_files = {

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

@ -0,0 +1,48 @@
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,49 +1,37 @@
PHONY = all android-ndk android-sdk base clean coverage doc fetchthirdparty po pot static-check test testfront
# koreader-base directory
KOR_BASE?=base
include $(KOR_BASE)/Makefile.defs
# the repository might not have been checked out yet, so make this
# able to fail:
-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)_$(RELEASE_DATE)
VERSION:=$(VERSION)_$(shell git describe HEAD | xargs git show -s --format=format:"%cd" --date=short)
endif
# releases do not contain tests and misc data
IS_RELEASE := $(if $(or $(EMULATE_READER),$(WIN32)),,1)
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
IS_RELEASE := $(if $(or $(IS_RELEASE),$(APPIMAGE),$(DEBIAN),$(MACOS)),1,)
ANDROID_ARCH?=arm
ifeq ($(ANDROID_ARCH), x86)
ANDROID_ABI:=$(ANDROID_ARCH)
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)
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
# set PATH to find CC in managed toolchains
ifeq ($(TARGET), android)
PATH:=$(ANDROID_TOOLCHAIN)/bin:$(PATH)
endif
LINUX_ARCH_NAME?=$(LINUX_ARCH)
MACHINE=$(TARGET_MACHINE)
MACHINE=$(shell PATH='$(PATH)' $(CC) -dumpmachine 2>/dev/null)
ifdef KODEBUG
MACHINE:=$(MACHINE)-debug
KODEDUG_SUFFIX:=-debug
@ -55,81 +43,104 @@ 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
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
all: $(if $(ANDROID),,$(KOR_BASE)/$(OUTPUT_DIR)/luajit)
$(MAKE) -C $(KOR_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)
bash -O extglob -c '$(RCP) -fL $(OUTPUT_DIR_ARTIFACTS) $(INSTALL_DIR)/koreader/'
$(RCP) -fL $(KOR_BASE)/$(OUTPUT_DIR)/. $(INSTALL_DIR)/koreader/.
else
cp -f $(KOR_BASE)/ev_replay.py $(INSTALL_DIR)/koreader/
@echo "[*] create symlink instead of copying files in development mode"
bash -O extglob -c '$(SYMLINK) $(OUTPUT_DIR_ARTIFACTS) $(INSTALL_DIR)/koreader/'
ifneq (,$(EMULATE_READER))
cd $(INSTALL_DIR)/koreader && \
bash -O extglob -c "ln -sf ../../$(KOR_BASE)/$(OUTPUT_DIR)/!(cache) ."
@echo "[*] install front spec only for the emulator"
$(SYMLINK) $(abspath spec) $(INSTALL_DIR)/koreader/spec/front
$(SYMLINK) $(abspath test) $(INSTALL_DIR)/koreader/spec/front/unit/data
endif
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
endif
$(SYMLINK) $(abspath $(INSTALL_FILES)) $(INSTALL_DIR)/koreader/
for f in $(INSTALL_FILES); do \
ln -sf ../../$$f $(INSTALL_DIR)/koreader/; \
done
ifdef ANDROID
$(SYMLINK) $(abspath $(ANDROID_DIR)/*.lua) $(INSTALL_DIR)/koreader/
cd $(INSTALL_DIR)/koreader && \
ln -sf ../../$(ANDROID_DIR)/*.lua .
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..."
$(SYMLINK) $(abspath $(WIN32_DIR)/*.dll) $(INSTALL_DIR)/koreader/
endif
ifdef SHIP_SHARED_STL
@echo "[*] Install C++ runtime..."
cp -fL $(SHARED_STL_LIB) $(INSTALL_DIR)/koreader/libs/
chmod 755 $(INSTALL_DIR)/koreader/libs/$(notdir $(SHARED_STL_LIB))
$(STRIP) --strip-unneeded $(INSTALL_DIR)/koreader/libs/$(notdir $(SHARED_STL_LIB))
cd $(INSTALL_DIR)/koreader && cp ../../$(WIN32_DIR)/*.dll .
endif
@echo "[*] Install plugins"
$(SYMLINK) $(abspath plugins) $(INSTALL_DIR)/koreader/
@# 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
@echo "[*] Install resources"
$(SYMLINK) $(abspath resources/fonts/*) $(INSTALL_DIR)/koreader/fonts/
$(RCP) -pL 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
base:
$(KOR_BASE)/$(OUTPUT_DIR)/luajit:
$(MAKE) -C $(KOR_BASE)
$(INSTALL_DIR)/koreader/.busted: .busted
$(SYMLINK) $(abspath .busted) $@
ln -sf ../../.busted $(INSTALL_DIR)/koreader
$(INSTALL_DIR)/koreader/.luacov:
$(SYMLINK) $(abspath .luacov) $@
test -e $(INSTALL_DIR)/koreader/.luacov || \
ln -sf ../../.luacov $(INSTALL_DIR)/koreader
testfront: $(INSTALL_DIR)/koreader/.busted
# sdr files may have unexpected impact on unit testing
-rm -rf spec/unit/data/*.sdr
cd $(INSTALL_DIR)/koreader && $(BUSTED_LUAJIT) $(BUSTED_OVERRIDES) $(BUSTED_SPEC_FILE)
cd $(INSTALL_DIR)/koreader && ./luajit $(shell which busted) \
--sort-files \
--output=gtest \
--exclude-tags=notest $(BUSTED_OVERRIDES) $(BUSTED_SPEC_FILE)
test: $(INSTALL_DIR)/koreader/.busted
$(MAKE) -C $(KOR_BASE) test
@ -146,21 +157,10 @@ coverage: $(INSTALL_DIR)/koreader/.luacov
+$$(($$(grep -nm1 -e "^Summary$$" luacov.report.out|cut -d: -f1)-1)) \
luacov.report.out
$(KOR_BASE)/Makefile.defs fetchthirdparty:
fetchthirdparty:
git submodule init
git submodule sync
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
git submodule update
$(MAKE) -C $(KOR_BASE) fetchthirdparty
VERBOSE ?= @
@ -178,16 +178,410 @@ dist-clean: clean
$(MAKE) -C $(KOR_BASE) dist-clean
$(MAKE) -C doc clean
# Include target specific rules.
ifneq (,$(wildcard make/$(TARGET).mk))
include make/$(TARGET).mk
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)
endif
android-ndk:
$(MAKE) -C $(KOR_BASE)/toolchain $(ANDROID_NDK_HOME)
androiddev: androidupdate
$(MAKE) -C $(ANDROID_LAUNCHER_DIR) dev
android-toolchain:
$(MAKE) -C $(KOR_BASE) android-toolchain
android-sdk:
$(MAKE) -C $(KOR_BASE)/toolchain $(ANDROID_HOME)
# for gettext
DOMAIN=koreader
@ -219,10 +613,4 @@ static-check:
doc:
make -C doc
.NOTPARALLEL:
.PHONY: $(PHONY)
LEFTOVERS = $(filter-out $(PHONY) $(INSTALL_DIR)/%,$(MAKECMDGOALS))
.PHONY: $(LEFTOVERS)
$(LEFTOVERS):
$(MAKE) -C $(KOR_BASE) $@
.PHONY: all clean doc test update

@ -11,7 +11,7 @@
[![Weblate Status][badge-weblate]][link-weblate]
[Download](https://github.com/koreader/koreader/releases) •
[User guide](http://koreader.rocks/user_guide/) •
[User guide](http://koreader.rocks/koreader-user-guide.pdf) •
[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, 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.
* **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.
* **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/user_guide/) and the [wiki][link-wiki] to discover more features and to help us document them.
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.
## 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.
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.
## Contributors
@ -101,4 +101,3 @@ 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 a6e112a5ee06e6939203accbd0736134af8939cd
Subproject commit 7fb47e46054ebf0499131db8f6fc006729795f3e

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

@ -1,150 +1,180 @@
-- To make configuration changes that persists between (nightly) releases,
-- copy defaults.lua to defaults.custom.lua and make the changes there,
-- or go to [Tools] > More tools > Advanced settings in the filemanager.
-- copy defaults.lua to defaults.persistent.lua and make the changes there.
-- number of page turns between full screen refresh
-- default to do a full refresh on every 6 page turns
-- no longer needed
--DRCOUNTMAX = 6
return {
-- number of pages for hinting
-- default to pre-rendering 1 page
DHINTCOUNT = 1,
DHINTCOUNT = 1
-- full screen mode, 1 for true, 0 for false
-- no longer needed
--DFULL_SCREEN = 1
-- scroll mode, 1 for true, 0 for false
-- no longer needed
--DSCROLL_MODE = 1
-- default gamma setting:
-- no longer needed
--DGLOBALGAMMA = 1.0
-- DjVu page rendering mode (used in djvu.c:drawPage())
-- See comments in djvureader.lua:DJVUReader:select_render_mode()
DRENDER_MODE = 0, -- 0 is COLOUR
DRENDER_MODE = 0 -- 0 is COLOUR
-- minimum cache size
DGLOBAL_CACHE_SIZE_MINIMUM = 1024*1024*16,
DGLOBAL_CACHE_SIZE_MINIMUM = 1024*1024*16
-- proportion of system free memory used as global cache
DGLOBAL_CACHE_FREE_PROPORTION = 0.4,
DGLOBAL_CACHE_FREE_PROPORTION = 0.4
-- maximum cache size
DGLOBAL_CACHE_SIZE_MAXIMUM = 1024*1024*512,
DGLOBAL_CACHE_SIZE_MAXIMUM = 1024*1024*512
-- background colour in non scroll mode: 8 = gray, 0 = white, 15 = black
DBACKGROUND_COLOR = 0,
DBACKGROUND_COLOR = 0
-- outer page colour in scroll mode: 8 = gray, 0 = white, 15 = black
DOUTER_PAGE_COLOR = 0,
DOUTER_PAGE_COLOR = 0
-- generic icon size
DGENERIC_ICON_SIZE = 40,
DGENERIC_ICON_SIZE = 40
-- supported view mode includes: "scroll" and "page"
DCREREADER_VIEW_MODE = "page",
DCREREADER_VIEW_MODE = "page"
-- show dimmed area to indicate page overlap in "page" view mode,
-- default to false
DSHOWOVERLAP = 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,
DCREREADER_TWO_PAGE_THRESHOLD = 7
-- page overlap pixels
DOVERLAPPIXELS = 30,
DOVERLAPPIXELS = 30
-- timeout to show link rectangle around links
-- default to 0.5 second
-- set to 0 to disable showing rectangle and follow link immediately
FOLLOW_LINK_TIMEOUT = 0.5,
FOLLOW_LINK_TIMEOUT = 0.5
-- customizable tap zones(rectangles)
-- x: x coordinate of top left corner in proportion to screen width
-- y: y coordinate of top left corner in proportion to screen height
-- w: tap zone width in proportion to screen width
-- h: tap zone height in proportion to screen height
DTAP_ZONE_MENU = {x = 0, y = 0, w = 1, h = 1/8},
DTAP_ZONE_MENU_EXT = {x = 1/4, y = 0, w = 2/4, h = 1/5}, -- taller, narrower extension
DTAP_ZONE_CONFIG = {x = 0, y = 7/8, w = 1, h = 1/8},
DTAP_ZONE_CONFIG_EXT = {x = 1/4, y = 4/5, w = 2/4, h = 1/5}, -- taller, narrower extension
DTAP_ZONE_MINIBAR = {x = 0, y = 12/13, w = 1, h = 1/13},
DTAP_ZONE_FORWARD = {x = 1/4, y = 0, w = 3/4, h = 1},
DTAP_ZONE_BACKWARD = {x = 0, y = 0, w = 1/4, h = 1},
DTAP_ZONE_TOP_LEFT = {x = 0, y = 0, w = 1/8, h = 1/8},
DTAP_ZONE_TOP_RIGHT = {x = 7/8, y = 0, w = 1/8, h = 1/8},
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},
DTAP_ZONE_MENU = {x = 0, y = 0, w = 1, h = 1/8}
DTAP_ZONE_MENU_EXT = {x = 1/4, y = 0, w = 2/4, h = 1/5} -- taller, narrower extension
DTAP_ZONE_CONFIG = {x = 0, y = 7/8, w = 1, h = 1/8}
DTAP_ZONE_CONFIG_EXT = {x = 1/4, y = 4/5, w = 2/4, h = 1/5} -- taller, narrower extension
DTAP_ZONE_MINIBAR = {x = 0, y = 12/13, w = 1, h = 1/13}
DTAP_ZONE_FORWARD = {x = 1/4, y = 0, w = 3/4, h = 1}
DTAP_ZONE_BACKWARD = {x = 0, y = 0, w = 1/4, h = 1}
-- DTAP_ZONE_BOOKMARK = {x = 7/8, y = 0, w = 1/8, h = 1/8} -- deprecated
-- DTAP_ZONE_FLIPPING = {x = 0, y = 0, w = 1/8, h = 1/8} -- deprecated
DTAP_ZONE_TOP_LEFT = {x = 0, y = 0, w = 1/8, h = 1/8}
DTAP_ZONE_TOP_RIGHT = {x = 7/8, y = 0, w = 1/8, h = 1/8}
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}
-- behaviour of swipes
DCHANGE_WEST_SWIPE_TO_EAST = false
DCHANGE_EAST_SWIPE_TO_WEST = false
-- koptreader config defaults
DKOPTREADER_CONFIG_FONT_SIZE = 1.0, -- range from 0.1 to 3.0
DKOPTREADER_CONFIG_TEXT_WRAP = 0, -- 1 = on, 0 = off
DKOPTREADER_CONFIG_TRIM_PAGE = 1, -- 1 = auto, 0 = manual
DKOPTREADER_CONFIG_DETECT_INDENT = 1, -- 1 = enable, 0 = disable
DKOPTREADER_CONFIG_DEFECT_SIZE = 1.0, -- range from 0.0 to 3.0
DKOPTREADER_CONFIG_PAGE_MARGIN = 0.10, -- range from 0.0 to 1.0
DKOPTREADER_CONFIG_LINE_SPACING = 1.2, -- range from 0.5 to 2.0
DKOPTREADER_CONFIG_RENDER_QUALITY = 1.0, -- range from 0.5 to 2.0
DKOPTREADER_CONFIG_AUTO_STRAIGHTEN = 0, -- range from 0 to 10
DKOPTREADER_CONFIG_JUSTIFICATION = 3, -- -1 = auto, 0 = left, 1 = center, 2 = right, 3 = full
DKOPTREADER_CONFIG_MAX_COLUMNS = 2, -- range from 1 to 4
DKOPTREADER_CONFIG_CONTRAST = 1.0, -- range from 0.2 to 2.0
DKOPTREADER_CONFIG_FONT_SIZE = 1.0 -- range from 0.1 to 3.0
DKOPTREADER_CONFIG_TEXT_WRAP = 0 -- 1 = on, 0 = off
DKOPTREADER_CONFIG_TRIM_PAGE = 1 -- 1 = auto, 0 = manual
DKOPTREADER_CONFIG_DETECT_INDENT = 1 -- 1 = enable, 0 = disable
DKOPTREADER_CONFIG_DEFECT_SIZE = 1.0 -- range from 0.0 to 3.0
DKOPTREADER_CONFIG_PAGE_MARGIN = 0.10 -- range from 0.0 to 1.0
DKOPTREADER_CONFIG_LINE_SPACING = 1.2 -- range from 0.5 to 2.0
DKOPTREADER_CONFIG_RENDER_QUALITY = 1.0 -- range from 0.5 to 2.0
DKOPTREADER_CONFIG_AUTO_STRAIGHTEN = 0 -- range from 0 to 10
DKOPTREADER_CONFIG_JUSTIFICATION = 3 -- -1 = auto, 0 = left, 1 = center, 2 = right, 3 = full
DKOPTREADER_CONFIG_MAX_COLUMNS = 2 -- range from 1 to 4
DKOPTREADER_CONFIG_CONTRAST = 1.0 -- range from 0.2 to 2.0
-- word spacing for reflow
DKOPTREADER_CONFIG_WORD_SPACINGS = {0.05, -0.2, 0.375}, -- range from (+/-)0.05 to (+/-)0.5
DKOPTREADER_CONFIG_DEFAULT_WORD_SPACING = -0.2, -- range from (+/-)0.05 to (+/-)0.5
DKOPTREADER_CONFIG_WORD_SPACINGS = {0.05, -0.2, 0.375} -- range from (+/-)0.05 to (+/-)0.5
DKOPTREADER_CONFIG_DEFAULT_WORD_SPACING = -0.2 -- range from (+/-)0.05 to (+/-)0.5
-- document languages for OCR
DKOPTREADER_CONFIG_DOC_LANGS_TEXT = {"English", "Chinese"},
DKOPTREADER_CONFIG_DOC_LANGS_CODE = {"eng", "chi_sim"}, -- language code, make sure you have corresponding training data
DKOPTREADER_CONFIG_DOC_DEFAULT_LANG_CODE = "eng", -- that have filenames starting with the language codes
DKOPTREADER_CONFIG_DOC_LANGS_TEXT = {"English", "Chinese"}
DKOPTREADER_CONFIG_DOC_LANGS_CODE = {"eng", "chi_sim"} -- language code, make sure you have corresponding training data
DKOPTREADER_CONFIG_DOC_DEFAULT_LANG_CODE = "eng" -- that have filenames starting with the language codes
-- crereader font sizes
-- feel free to add more entries in this list
DCREREADER_CONFIG_FONT_SIZES = {12, 16, 20, 22, 24, 26, 28, 30, 34, 38, 44}, -- option range from 12 to 44
DCREREADER_CONFIG_DEFAULT_FONT_SIZE = 22, -- default font size
DCREREADER_CONFIG_FONT_SIZES = {12, 16, 20, 22, 24, 26, 28, 30, 34, 38, 44} -- option range from 12 to 44
DCREREADER_CONFIG_DEFAULT_FONT_SIZE = 22 -- default font size
-- crereader margin sizes
-- horizontal margins {left, right} in (relative) pixels
DCREREADER_CONFIG_H_MARGIN_SIZES_SMALL = {5, 5},
DCREREADER_CONFIG_H_MARGIN_SIZES_MEDIUM = {10, 10},
DCREREADER_CONFIG_H_MARGIN_SIZES_LARGE = {15, 15},
DCREREADER_CONFIG_H_MARGIN_SIZES_X_LARGE = {20, 20},
DCREREADER_CONFIG_H_MARGIN_SIZES_XX_LARGE = {30, 30},
DCREREADER_CONFIG_H_MARGIN_SIZES_XXX_LARGE = {50, 50},
DCREREADER_CONFIG_H_MARGIN_SIZES_HUGE = {70, 70},
DCREREADER_CONFIG_H_MARGIN_SIZES_X_HUGE = {100, 100},
DCREREADER_CONFIG_H_MARGIN_SIZES_XX_HUGE = {140, 140},
DCREREADER_CONFIG_H_MARGIN_SIZES_SMALL = {5, 5}
DCREREADER_CONFIG_H_MARGIN_SIZES_MEDIUM = {10, 10}
DCREREADER_CONFIG_H_MARGIN_SIZES_LARGE = {15, 15}
DCREREADER_CONFIG_H_MARGIN_SIZES_X_LARGE = {20, 20}
DCREREADER_CONFIG_H_MARGIN_SIZES_XX_LARGE = {30, 30}
DCREREADER_CONFIG_H_MARGIN_SIZES_XXX_LARGE = {50, 50}
DCREREADER_CONFIG_H_MARGIN_SIZES_HUGE = {70, 70}
DCREREADER_CONFIG_H_MARGIN_SIZES_X_HUGE = {100, 100}
DCREREADER_CONFIG_H_MARGIN_SIZES_XX_HUGE = {140, 140}
-- top margin in (relative) pixels
DCREREADER_CONFIG_T_MARGIN_SIZES_SMALL = 5,
DCREREADER_CONFIG_T_MARGIN_SIZES_MEDIUM = 10,
DCREREADER_CONFIG_T_MARGIN_SIZES_LARGE = 15,
DCREREADER_CONFIG_T_MARGIN_SIZES_X_LARGE = 20,
DCREREADER_CONFIG_T_MARGIN_SIZES_XX_LARGE = 30,
DCREREADER_CONFIG_T_MARGIN_SIZES_XXX_LARGE = 50,
DCREREADER_CONFIG_T_MARGIN_SIZES_HUGE = 70,
DCREREADER_CONFIG_T_MARGIN_SIZES_X_HUGE = 100,
DCREREADER_CONFIG_T_MARGIN_SIZES_XX_HUGE = 140,
DCREREADER_CONFIG_T_MARGIN_SIZES_SMALL = 5
DCREREADER_CONFIG_T_MARGIN_SIZES_MEDIUM = 10
DCREREADER_CONFIG_T_MARGIN_SIZES_LARGE = 15
DCREREADER_CONFIG_T_MARGIN_SIZES_X_LARGE = 20
DCREREADER_CONFIG_T_MARGIN_SIZES_XX_LARGE = 30
DCREREADER_CONFIG_T_MARGIN_SIZES_XXX_LARGE = 50
DCREREADER_CONFIG_T_MARGIN_SIZES_HUGE = 70
DCREREADER_CONFIG_T_MARGIN_SIZES_X_HUGE = 100
DCREREADER_CONFIG_T_MARGIN_SIZES_XX_HUGE = 140
-- bottom margin in (relative) pixels
DCREREADER_CONFIG_B_MARGIN_SIZES_SMALL = 5,
DCREREADER_CONFIG_B_MARGIN_SIZES_MEDIUM = 10,
DCREREADER_CONFIG_B_MARGIN_SIZES_LARGE = 15,
DCREREADER_CONFIG_B_MARGIN_SIZES_X_LARGE = 20,
DCREREADER_CONFIG_B_MARGIN_SIZES_XX_LARGE = 30,
DCREREADER_CONFIG_B_MARGIN_SIZES_XXX_LARGE = 50,
DCREREADER_CONFIG_B_MARGIN_SIZES_HUGE = 70,
DCREREADER_CONFIG_B_MARGIN_SIZES_X_HUGE = 100,
DCREREADER_CONFIG_B_MARGIN_SIZES_XX_HUGE = 140,
DCREREADER_CONFIG_B_MARGIN_SIZES_SMALL = 5
DCREREADER_CONFIG_B_MARGIN_SIZES_MEDIUM = 10
DCREREADER_CONFIG_B_MARGIN_SIZES_LARGE = 15
DCREREADER_CONFIG_B_MARGIN_SIZES_X_LARGE = 20
DCREREADER_CONFIG_B_MARGIN_SIZES_XX_LARGE = 30
DCREREADER_CONFIG_B_MARGIN_SIZES_XXX_LARGE = 50
DCREREADER_CONFIG_B_MARGIN_SIZES_HUGE = 70
DCREREADER_CONFIG_B_MARGIN_SIZES_X_HUGE = 100
DCREREADER_CONFIG_B_MARGIN_SIZES_XX_HUGE = 140
-- crereader font gamma (no longer used)
-- DCREREADER_CONFIG_LIGHTER_FONT_GAMMA = 10
-- DCREREADER_CONFIG_DEFAULT_FONT_GAMMA = 15
-- DCREREADER_CONFIG_DARKER_FONT_GAMMA = 25
-- crereader line space percentage
DCREREADER_CONFIG_LINE_SPACE_PERCENT_X_TINY = 70,
DCREREADER_CONFIG_LINE_SPACE_PERCENT_TINY = 75,
DCREREADER_CONFIG_LINE_SPACE_PERCENT_XX_SMALL = 80,
DCREREADER_CONFIG_LINE_SPACE_PERCENT_X_SMALL = 85,
DCREREADER_CONFIG_LINE_SPACE_PERCENT_SMALL = 90,
DCREREADER_CONFIG_LINE_SPACE_PERCENT_L_SMALL = 95,
DCREREADER_CONFIG_LINE_SPACE_PERCENT_MEDIUM = 100,
DCREREADER_CONFIG_LINE_SPACE_PERCENT_L_MEDIUM = 105,
DCREREADER_CONFIG_LINE_SPACE_PERCENT_XL_MEDIUM = 110,
DCREREADER_CONFIG_LINE_SPACE_PERCENT_XXL_MEDIUM = 115,
DCREREADER_CONFIG_LINE_SPACE_PERCENT_LARGE = 120,
DCREREADER_CONFIG_LINE_SPACE_PERCENT_X_LARGE = 125,
DCREREADER_CONFIG_LINE_SPACE_PERCENT_XX_LARGE = 130,
DCREREADER_CONFIG_LINE_SPACE_PERCENT_X_TINY = 70
DCREREADER_CONFIG_LINE_SPACE_PERCENT_TINY = 75
DCREREADER_CONFIG_LINE_SPACE_PERCENT_XX_SMALL = 80
DCREREADER_CONFIG_LINE_SPACE_PERCENT_X_SMALL = 85
DCREREADER_CONFIG_LINE_SPACE_PERCENT_SMALL = 90
DCREREADER_CONFIG_LINE_SPACE_PERCENT_L_SMALL = 95
DCREREADER_CONFIG_LINE_SPACE_PERCENT_MEDIUM = 100
DCREREADER_CONFIG_LINE_SPACE_PERCENT_L_MEDIUM = 105
DCREREADER_CONFIG_LINE_SPACE_PERCENT_XL_MEDIUM = 110
DCREREADER_CONFIG_LINE_SPACE_PERCENT_XXL_MEDIUM = 115
DCREREADER_CONFIG_LINE_SPACE_PERCENT_LARGE = 120
DCREREADER_CONFIG_LINE_SPACE_PERCENT_X_LARGE = 125
DCREREADER_CONFIG_LINE_SPACE_PERCENT_XX_LARGE = 130
-- word spacing percentages
-- 1st number scales the normal width of spaces in all font
@ -156,42 +186,142 @@ DCREREADER_CONFIG_LINE_SPACE_PERCENT_XX_LARGE = 130,
-- regular width. {99, 100} allows reducing it by at least 1px.
-- (These replace the old settings DCREREADER_CONFIG_WORD_GAP_*,
-- with the equivalence: new_option = { 100, old_option }.)
DCREREADER_CONFIG_WORD_SPACING_SMALL = {75, 50},
DCREREADER_CONFIG_WORD_SPACING_MEDIUM = {95, 75},
DCREREADER_CONFIG_WORD_SPACING_LARGE = {100, 90},
DCREREADER_CONFIG_WORD_SPACING_SMALL = {75, 50}
DCREREADER_CONFIG_WORD_SPACING_MEDIUM = {95, 75}
DCREREADER_CONFIG_WORD_SPACING_LARGE = {100, 90}
-- word expansion, to reduce excessive spacing on justified line
-- by using letter spacing on the words
-- value is the max allowed added letter spacing, as a % of the font size
DCREREADER_CONFIG_WORD_EXPANSION_NONE = 0,
DCREREADER_CONFIG_WORD_EXPANSION_SOME = 5,
DCREREADER_CONFIG_WORD_EXPANSION_MORE = 15,
DCREREADER_CONFIG_WORD_EXPANSION_NONE = 0
DCREREADER_CONFIG_WORD_EXPANSION_SOME = 5
DCREREADER_CONFIG_WORD_EXPANSION_MORE = 15
-- crereader progress bar (no longer needed)
-- 0 for top "full" progress bar
-- 1 for bottom "mini" progress bar
--DCREREADER_PROGRESS_BAR = 1
-- configure "mini" progress bar
DMINIBAR_CONTAINER_HEIGHT = 14, -- Larger means more padding at the bottom, at the risk of eating into the last line
-- no longer needed
--DMINIBAR_TOC_MARKER_WIDTH = 2 -- Looses usefulness > 3
DMINIBAR_CONTAINER_HEIGHT = 14 -- Larger means more padding at the bottom, at the risk of eating into the last line
-- no longer needed
--DMINIBAR_FONT_SIZE = 14
-- no longer needed
--DMINIBAR_HEIGHT = 7 -- Should be smaller than DMINIBAR_CONTAINER_HEIGHT
-- change this to any numerical value if you want to automatically save settings when turning pages
-- no longer needed (now available in menu as an interval in minutes)
-- DAUTO_SAVE_PAGING_COUNT = nil
-- dictionary font size
-- no longer needed
--DDICT_FONT_SIZE = 20
-- Frontlight decrease of sensitivity for two-fingered pan gesture,
-- e.g. 2 changes the sensitivity by 1/2, 3 by 1/3 etc.
FRONTLIGHT_SENSITIVITY_DECREASE = 2
-- Normally, KOReader will present file lists sorted in case insensitive manner
-- when presenting an alphatically sorted list. So the Order is "A, b, C, d".
-- You can switch to a case sensitive sort ("A", "C", "b", "d") by disabling
-- insensitive sort
DALPHA_SORT_CASE_INSENSITIVE = true,
DALPHA_SORT_CASE_INSENSITIVE = true
-- no longer needed
-- Set a path to a folder that is filled by Calibre (must contain the file metadata.calibre)
-- e.g.
-- "/mnt/sd/.hidden" for Kobo with files in ".hidden" on the SD card
-- "/mnt/onboard/MyPath" for Kobo with files in "MyPath" on the device itself
-- "/mnt/us/documents/" for Kindle files in folder "documents"
--SEARCH_LIBRARY_PATH = ""
--SEARCH_LIBRARY_PATH2 = ""
--
-- Search parameters
--SEARCH_CASESENSITIVE = false
--
--SEARCH_AUTHORS = true
--SEARCH_TITLE = true
--SEARCH_TAGS = true
--SEARCH_SERIES = true
--SEARCH_PATH = true
-- 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
-- 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'
-- Network proxy settings
-- proxy url should be a string in the format of "http://localhost:3128"
-- proxy authentication is not supported yet.
NETWORK_PROXY = nil,
NETWORK_PROXY = nil
-- Experimental features
-- Use turbo library to handle async HTTP request
DUSE_TURBO_LIB = false,
DUSE_TURBO_LIB = false
-- Absolute path to stardict files (override)
-- By default they're stored in data/dict under dataDir.
STARDICT_DATA_DIR = nil,
}
STARDICT_DATA_DIR = nil
-- ####################################################################
-- following features are not supported right now
-- ####################################################################
-- set panning distance
--DSHIFT_X = 100
--DSHIFT_Y = 50
-- step to change zoom manually, default = 16%
--DSTEP_MANUAL_ZOOM = 16
--DPAN_BY_PAGE = false -- using shift_[xy] or width/height
--DPAN_MARGIN = 5 -- horizontal margin for two-column zoom (in pixels)
--DPAN_OVERLAP_VERTICAL = 30
-- tile cache configuration:
--DCACHE_MAX_MEMSIZE = 1024*1024*5 -- 5MB tile cache
--DCACHE_MAX_TTL = 20 -- time to live
-- renderer cache size
--DCACHE_DOCUMENT_SIZE = 1024*1024*8 -- FIXME random, needs testing
-- default value for battery level logging
--DBATTERY_LOGGING = false
-- delay for info messages in ms
--DINFO_NODELAY=0
--DINFO_DELAY=1500
-- toggle defaults
--DUNIREADER_SHOW_OVERLAP_ENABLE = true
--DUNIREADER_SHOW_LINKS_ENABLE = true
--DUNIREADER_COMICS_MODE_ENABLE = true
--DUNIREADER_RTL_MODE_ENABLE = false
--DUNIREADER_PAGE_MODE_ENABLE = false
--DDJVUREADER_SHOW_OVERLAP_ENABLE = true
--DDJVUREADER_SHOW_LINKS_ENABLE = false
--DDJVUREADER_COMICS_MODE_ENABLE = true
--DDJVUREADER_RTL_MODE_ENABLE = false
--DDJVUREADER_PAGE_MODE_ENABLE = false
--DKOPTREADER_SHOW_OVERLAP_ENABLE = true
--DKOPTREADER_SHOW_LINKS_ENABLE = false
--DKOPTREADER_COMICS_MODE_ENABLE = false
--DKOPTREADER_RTL_MODE_ENABLE = false
--DKOPTREADER_PAGE_MODE_ENABLE = false
--DPICVIEWER_SHOW_OVERLAP_ENABLE = false
--DPICVIEWER_SHOW_LINKS_ENABLE = false
--DPICVIEWER_COMICS_MODE_ENABLE = true
--DPICVIEWER_RTL_MODE_ENABLE = false
--DPICVIEWER_PAGE_MODE_ENABLE = false
--DKOPTREADER_CONFIG_MULTI_THREADS = 1 -- 1 = on, 0 = off
--DKOPTREADER_CONFIG_SCREEN_ROTATION = 0 -- 0, 90, 180, 270 degrees

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

@ -17,15 +17,28 @@ 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:
```
ANDROID_NDK_HOME=/ndk/location ANDROID_HOME=/sdk/location ./kodev release android
NDK=/ndk/location SDK=/sdk/location ./kodev release android
```
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.
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.
#### 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,
@ -56,6 +69,20 @@ 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.
@ -140,6 +167,14 @@ 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, 1, event.args.n))
return self["on"..event.name](self, unpack(event.args, 1, event.argc))
```
## Event system

@ -1,5 +1,6 @@
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")
@ -10,7 +11,6 @@ local InfoMessage = require("ui/widget/infomessage")
local InputDialog = require("ui/widget/inputdialog")
local LuaSettings = require("luasettings")
local Menu = require("ui/widget/menu")
local NetworkMgr = require("ui/network/manager")
local PathChooser = require("ui/widget/pathchooser")
local UIManager = require("ui/uimanager")
local WebDav = require("apps/cloudstorage/webdav")
@ -18,9 +18,18 @@ 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,
@ -44,6 +53,8 @@ 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()
@ -119,7 +130,7 @@ function CloudStorage:selectCloudType()
},
})
end
self.cloud_dialog = ButtonDialog:new{
self.cloud_dialog = ButtonDialogTitle:new{
title = _("Add new cloud storage"),
title_align = "center",
buttons = buttons,
@ -128,30 +139,14 @@ function CloudStorage:selectCloudType()
return true
end
function CloudStorage:generateDropBoxAccessToken()
if self.username or self.address == nil or self.address == "" then
-- short-lived token has been generated already in this session
-- or we have long-lived token in self.password
return true
else
local token = DropBox:getAccessToken(self.password, self.address)
if token then
self.password = token -- short-lived token
self.username = true -- flag
return true
end
end
end
function CloudStorage:openCloudServer(url)
local tbl, e
local NetworkMgr = require("ui/network/manager")
if self.type == "dropbox" then
if NetworkMgr:willRerunWhenOnline(function() self:openCloudServer(url) end) then
return
end
if self:generateDropBoxAccessToken() then
tbl, e = DropBox:run(url, self.password, self.choose_folder_mode)
end
tbl, e = DropBox:run(url, self.password, self.choose_folder_mode)
elseif self.type == "ftp" then
if NetworkMgr:willRerunWhenConnected(function() self:openCloudServer(url) end) then
return
@ -161,11 +156,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, self.choose_folder_mode)
tbl, e = WebDav:run(self.address, self.username, self.password, url)
end
if tbl then
self:switchItemTable(url, tbl)
if self.type == "dropbox" or self.type == "webdav" then
if self.type == "dropbox" then
self.onLeftButtonTap = function()
self:showPlusMenu(url)
end
@ -227,7 +222,7 @@ function CloudStorage:downloadFile(item)
})
end
local function createTitle(filename_orig, filename, path) -- title for ButtonDialog
local function createTitle(filename_orig, filename, path) -- title for ButtonDialogTitle
return T(_("Filename:\n%1\n\nDownload filename:\n%2\n\nDownload folder:\n%3"),
filename_orig, filename, BD.dirpath(path))
end
@ -243,6 +238,7 @@ 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()
@ -264,7 +260,6 @@ function CloudStorage:downloadFile(item)
{
{
text = _("Cancel"),
id = "close",
callback = function()
UIManager:close(input_dialog)
end,
@ -317,7 +312,7 @@ function CloudStorage:downloadFile(item)
},
}
self.download_dialog = ButtonDialog:new{
self.download_dialog = ButtonDialogTitle:new{
title = createTitle(filename_orig, filename, download_dir),
buttons = buttons,
}
@ -347,7 +342,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 = ButtonDialog:new{
button_dialog = ButtonDialogTitle:new{
title = title,
buttons = {
{
@ -428,38 +423,31 @@ function CloudStorage:onMenuHold(item)
end
function CloudStorage:synchronizeCloud(item)
if NetworkMgr:willRerunWhenOnline(function() self:synchronizeCloud(item) end) then
return
end
self.password = item.password
self.address = item.address
local Trapper = require("ui/trapper")
Trapper:wrap(function()
Trapper:setPausedText("Download paused.\nDo you want to continue or abort downloading files?")
if self:generateDropBoxAccessToken() then
local ok, downloaded_files, failed_files = pcall(self.downloadListFiles, self, item)
if ok and downloaded_files then
if not failed_files then failed_files = 0 end
local text
if downloaded_files == 0 and failed_files == 0 then
text = _("No files to download from Dropbox.")
else
text = T(N_("Successfully downloaded 1 file from Dropbox to local storage.", "Successfully downloaded %1 files from Dropbox to local storage.", downloaded_files), downloaded_files)
if failed_files > 0 then
text = text .. "\n" .. T(N_("Failed to download 1 file.", "Failed to download %1 files.", failed_files), failed_files)
end
end
UIManager:show(InfoMessage:new{
text = text,
timeout = 3,
})
local ok, downloaded_files, failed_files = pcall(self.downloadListFiles, self, item)
if ok and downloaded_files then
if not failed_files then failed_files = 0 end
local text
if downloaded_files == 0 and failed_files == 0 then
text = _("No files to download from Dropbox.")
else
Trapper:reset() -- close any last widget not cleaned if error
UIManager:show(InfoMessage:new{
text = _("No files to download from Dropbox.\nPlease check your configuration and connection."),
timeout = 3,
})
text = T(N_("Successfully downloaded 1 file from Dropbox to local storage.", "Successfully downloaded %1 files from Dropbox to local storage.", downloaded_files), downloaded_files)
if failed_files > 0 then
text = text .. "\n" .. T(N_("Failed to download 1 file.", "Failed to download %1 files.", failed_files), failed_files)
end
end
UIManager:show(InfoMessage:new{
text = text,
timeout = 3,
})
else
Trapper:reset() -- close any last widget not cleaned if error
UIManager:show(InfoMessage:new{
text = _("No files to download from Dropbox.\nPlease check your configuration and connection."),
timeout = 3,
})
end
end)
end
@ -480,7 +468,7 @@ function CloudStorage:downloadListFiles(item)
end
end
end
local remote_files = DropBox:showFiles(item.sync_source_folder, self.password)
local remote_files = DropBox:showFiles(item.sync_source_folder, item.password)
if #remote_files == 0 then
UI:clear()
return false
@ -511,7 +499,7 @@ function CloudStorage:downloadListFiles(item)
if not go_on then
break
end
response = DropBox:downloadFileNoUI(file.url, self.password, item.sync_dest_folder .. "/" .. file.text)
response = DropBox:downloadFileNoUI(file.url, item.password, item.sync_dest_folder .. "/" .. file.text)
if response then
success_files = success_files + 1
else
@ -527,7 +515,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 = ButtonDialog:new {
syn_dialog = ButtonDialogTitle: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 = {
@ -536,14 +524,14 @@ function CloudStorage:synchronizeSettings(item)
text = _("Choose Dropbox folder"),
callback = function()
UIManager:close(syn_dialog)
require("ui/downloadmgr"):new{
require("ui/cloudmgr"):new{
item = item,
onConfirm = function(path)
self:updateSyncFolder(item, path)
item.sync_source_folder = path
self:synchronizeSettings(item)
end,
}:chooseCloudDir()
}:chooseDir()
end,
},
},
@ -636,11 +624,7 @@ function CloudStorage:uploadFile(url)
end)
local url_base = url ~= "/" and url or ""
UIManager:tickAfterNext(function()
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
DropBox:uploadFile(url_base, self.password, file_path, callback_close)
end)
end
end
@ -678,11 +662,7 @@ function CloudStorage:createFolder(url)
end
self:openCloudServer(url)
end
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
DropBox:createFolder(url_base, self.password, folder_name, callback_close)
end,
},
}
@ -706,9 +686,8 @@ function CloudStorage:configCloud(type)
table.insert(cs_servers,{
name = fields[1],
password = fields[2],
address = fields[3],
url = fields[4],
type = "dropbox",
url = "/"
})
elseif type == "ftp" then
table.insert(cs_servers,{
@ -753,8 +732,6 @@ function CloudStorage:editCloudServer(item)
if server.name == updated_config.text and server.password == updated_config.password then
server.name = fields[1]
server.password = fields[2]
server.address = fields[3]
server.url = fields[4]
cs_servers[i] = server
break
end
@ -813,15 +790,7 @@ end
function CloudStorage:infoServer(item)
if item.type == "dropbox" then
if NetworkMgr:willRerunWhenOnline(function() self:infoServer(item) end) then
return
end
self.password = item.password
self.address = item.address
if self:generateDropBoxAccessToken() then
DropBox:info(self.password)
self.username = nil
end
DropBox:info(item.password)
elseif item.type == "ftp" then
Ftp:info(item)
elseif item.type == "webdav" then

@ -12,10 +12,6 @@ local _ = require("gettext")
local DropBox = {}
function DropBox:getAccessToken(refresh_token, app_key_colon_secret)
return DropBoxApi:getAccessToken(refresh_token, app_key_colon_secret)
end
function DropBox:run(url, password, choose_folder_mode)
return DropBoxApi:listFolder(url, password, choose_folder_mode)
end
@ -58,7 +54,11 @@ end
function DropBox:downloadFileNoUI(url, password, path)
local code_response = DropBoxApi:downloadFile(url, password, path)
return code_response == 200
if code_response == 200 then
return true
else
return false
end
end
function DropBox:uploadFile(url, password, file_path, callback_close)
@ -92,36 +92,39 @@ function DropBox:createFolder(url, password, folder_name, callback_close)
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> string.
Some of the previously generated long-lived tokens are still valid.]])
local text_name, text_token, text_appkey, text_url
local text_info = "How to generate Access Token:\n"..
"1. Open the following URL in your Browser, and log in using your account: https://www.dropbox.com/developers/apps.\n"..
"2. Click on >>Create App<<, then select >>Dropbox API app<<.\n"..
"3. Now go on with the configuration, choosing the app permissions and access restrictions to your DropBox folder.\n"..
"4. Enter the >>App Name<< that you prefer (e.g. KOReader).\n"..
"5. Now, click on the >>Create App<< button.\n" ..
"6. When your new App is successfully created, please click on the Generate button.\n"..
"7. Under the 'Generated access token' section, then enter code in Dropbox token field."
local hint_top = _("Your Dropbox name")
local text_top = ""
local hint_bottom = _("Dropbox token\n\n\n\n")
local text_bottom = ""
local title
local text_button_right = _("Add")
if item then
text_name = item.text
text_token = item.password
text_appkey = item.address
text_url = item.url
title = _("Edit Dropbox account")
text_button_right = _("Apply")
text_top = item.text
text_bottom = item.password
else
title = _("Add Dropbox account")
end
self.settings_dialog = MultiInputDialog:new {
title = _("Dropbox cloud storage"),
title = title,
fields = {
{
text = text_name,
hint = _("Cloud storage displayed name"),
},
{
text = text_token,
hint = _("Dropbox refresh token\nor long-lived token (deprecated)"),
},
{
text = text_appkey,
hint = _("Dropbox <APP_KEY>:<APP_SECRET>\n(leave blank for long-lived token)"),
text = text_top,
hint = hint_top ,
},
{
text = text_url,
hint = _("Dropbox folder (/ for root)"),
text = text_bottom,
hint = hint_bottom,
scroll = false,
},
},
buttons = {
@ -141,20 +144,29 @@ Some of the previously generated long-lived tokens are still valid.]])
end
},
{
text = _("Save"),
text = text_button_right,
callback = function()
local fields = self.settings_dialog:getFields()
if item then
callback(item, fields)
local fields = MultiInputDialog:getFields()
if fields[1] ~= "" and fields[2] ~= "" then
if item then
--edit
callback(item, fields)
else
-- add new
callback(fields)
end
self.settings_dialog:onClose()
UIManager:close(self.settings_dialog)
else
callback(fields)
UIManager:show(InfoMessage:new{
text = _("Please fill in all fields.")
})
end
self.settings_dialog:onClose()
UIManager:close(self.settings_dialog)
end
},
},
},
input_type = "text",
}
UIManager:show(self.settings_dialog)
self.settings_dialog:onShowKeyboard()
@ -162,17 +174,14 @@ end
function DropBox:info(token)
local info = DropBoxApi:fetchInfo(token)
local space_usage = DropBoxApi:fetchInfo(token, true)
if info and space_usage then
local account_type = info.account_type and info.account_type[".tag"]
local name = info.name and info.name.display_name
local space_total = space_usage.allocation and space_usage.allocation.allocated
UIManager:show(InfoMessage:new{
text = T(_"Type: %1\nName: %2\nEmail: %3\nCountry: %4\nSpace total: %5\nSpace used: %6",
account_type, name, info.email, info.country,
util.getFriendlySize(space_total), util.getFriendlySize(space_usage.used)),
})
local info_text
if info and info.name then
info_text = T(_"Type: %1\nName: %2\nEmail: %3\nCountry: %4",
"Dropbox",info.name.display_name, info.email, info.country)
else
info_text = _("No information available")
end
UIManager:show(InfoMessage:new{text = info_text})
end
return DropBox

@ -13,62 +13,36 @@ 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_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_URL_INFO = "https://api.dropboxapi.com/2/users/get_current_account"
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_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)
function DropBoxApi:fetchInfo(token)
local sink = {}
local data = "grant_type=refresh_token&refresh_token=" .. refresh_token
local request = {
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",
["Content-Length"] = string.len(data),
},
source = ltn12.source.string(data),
sink = ltn12.sink.table(sink),
}
socketutil:set_timeout()
local code, _, status = socket.skip(1, http.request(request))
socketutil:reset_timeout()
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.warn("DropBoxApi: cannot get access token:", status or code)
logger.warn("DropBoxApi: error:", result_response)
end
function DropBoxApi:fetchInfo(token, space_usage)
local url = space_usage and API_GET_SPACE_USAGE or API_URL_INFO
local sink = {}
local request = {
url = url,
url = API_URL_INFO,
method = "POST",
headers = {
["Authorization"] = "Bearer " .. token,
},
sink = ltn12.sink.table(sink),
}
socketutil:set_timeout()
local code, _, status = socket.skip(1, http.request(request))
local headers_request = socket.skip(1, http.request(request))
socketutil:reset_timeout()
local result_response = table.concat(sink)
if code == 200 and result_response ~= "" then
if headers_request == nil then
return nil
end
if result_response ~= "" then
local _, result = pcall(JSON.decode, result_response)
return result
else
return nil
end
logger.warn("DropBoxApi: cannot get account info:", status or code)
logger.warn("DropBoxApi: error:", result_response)
end
function DropBoxApi:fetchListFolders(path, token)
@ -76,6 +50,7 @@ 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",
@ -87,29 +62,34 @@ function DropBoxApi:fetchListFolders(path, token)
source = ltn12.source.string(data),
sink = ltn12.sink.table(sink),
}
socketutil:set_timeout()
local code, _, status = socket.skip(1, http.request(request))
local headers_request = socket.skip(1, http.request(request))
socketutil:reset_timeout()
if headers_request == nil then
return nil
end
local result_response = table.concat(sink)
if code == 200 and result_response ~= "" then
if 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("DropBoxApi: found additional files")
logger.dbg("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, headers, status = socket.skip(1, http.request{
local code, _, status = socket.skip(1, http.request{
url = API_DOWNLOAD_FILE,
method = "GET",
headers = {
@ -120,16 +100,14 @@ function DropBoxApi:downloadFile(path, token, local_path)
})
socketutil:reset_timeout()
if code ~= 200 then
logger.warn("DropBoxApi: cannot download file:", status or code)
logger.warn("DropBoxApi: Download failure:", status or code or "network unreachable")
end
return code, (headers or {}).etag
return code
end
function DropBoxApi:uploadFile(path, token, file_path, etag, overwrite)
function DropBoxApi:uploadFile(path, token, file_path)
local data = "{\"path\": \"" .. path .. "/" .. BaseUtil.basename(file_path) ..
"\",\"mode\":" .. (overwrite and "\"overwrite\"" or "\"add\"") ..
",\"autorename\": " .. (overwrite and "false" or "true") ..
",\"mute\": false,\"strict_conflict\": false}"
"\",\"mode\": \"add\",\"autorename\": 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,
@ -137,15 +115,14 @@ function DropBoxApi:uploadFile(path, token, file_path, etag, overwrite)
headers = {
["Authorization"] = "Bearer ".. token,
["Dropbox-API-Arg"] = data,
["Content-Type"] = "application/octet-stream",
["Content-Length"] = lfs.attributes(file_path, "size"),
["If-Match"] = etag,
["Content-Type"] = "application/octet-stream",
["Content-Length"] = lfs.attributes(file_path, "size"),
},
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: cannot upload file:", status or code)
logger.warn("DropBoxApi: Upload failure:", status or code or "network unreachable")
end
return code
end
@ -157,15 +134,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: cannot create folder:", status or code)
logger.warn("DropBoxApi: Folder creation failure:", status or code or "network unreachable")
end
return code
end
@ -289,5 +266,4 @@ function DropBoxApi:fetchAdditionalFolders(response, token)
return out
end
return DropBoxApi

@ -140,7 +140,7 @@ Username and password are optional.]])
{
text = text_button_right,
callback = function()
local fields = self.settings_dialog:getFields()
local fields = MultiInputDialog:getFields()
if fields[1] ~= "" and fields[2] ~= "" then
if item then
-- edit

@ -1,191 +0,0 @@
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,18 +7,17 @@ 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, folder_mode)
return WebDavApi:listFolder(address, user, pass, path, folder_mode)
function WebDav:run(address, user, pass, path)
return WebDavApi:listFolder(address, user, pass, path)
end
function WebDav:downloadFile(item, address, username, password, local_path, callback_close)
local code_response = WebDavApi:downloadFile(WebDavApi:getJoinedPath(address, item.url), username, password, local_path)
local code_response = WebDavApi:downloadFile(address .. WebDavApi:urlEncode( 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
@ -49,44 +48,26 @@ 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 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 title, text_name, text_address, text_username, text_password, text_folder
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")
if item then
title = _("Edit WebDAV account")
text_button_ok = _("Apply")
text_name = item.text
text_address = item.address
text_username = item.username
@ -100,24 +81,29 @@ The start folder is appended to the server path.]])
fields = {
{
text = text_name,
hint = _("Server display name"),
input_type = "string",
hint = hint_name ,
},
{
text = text_address,
hint = _("WebDAV address, for example https://example.com/dav"),
input_type = "string",
hint = hint_address ,
},
{
text = text_username,
hint = _("Username"),
input_type = "string",
hint = hint_username,
},
{
text = text_password,
input_type = "string",
text_type = "password",
hint = _("Password"),
hint = hint_password,
},
{
text = text_folder,
hint = _("Start folder, for example /books"),
input_type = "string",
hint = hint_folder,
},
},
buttons = {
@ -137,17 +123,18 @@ The start folder is appended to the server path.]])
end
},
{
text = _("Save"),
text = text_button_ok,
callback = function()
local fields = self.settings_dialog:getFields()
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("/$", "")
local fields = MultiInputDialog: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
if item then
-- edit
callback(item, fields)
@ -166,9 +153,11 @@ 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,18 +7,10 @@ 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
@ -40,7 +32,6 @@ 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
@ -60,7 +51,7 @@ function WebDavApi:urlEncode(url_data)
return url_data
end
function WebDavApi:listFolder(address, user, pass, folder_path, folder_mode)
function WebDavApi:listFolder(address, user, pass, folder_path)
local path = self:urlEncode( folder_path )
local webdav_list = {}
local webdav_file = {}
@ -70,10 +61,10 @@ function WebDavApi:listFolder(address, user, pass, folder_path, folder_mode)
if string.sub( address, -1 ) == "/" then has_trailing_slash = true end
if path == nil or path == "/" then
path = ""
elseif string.sub( path, 1, 1 ) == "/" then
elseif string.sub( path, 1, 2 ) == "/" then
if has_trailing_slash then
-- too many slashes, remove one
path = string.sub( path, 2 )
path = string.sub( path, 1 )
end
has_leading_slash = true
end
@ -81,7 +72,7 @@ function WebDavApi:listFolder(address, user, pass, folder_path, folder_mode)
address = address .. "/"
end
local webdav_url = address .. path
if string.sub(webdav_url, -1) ~= "/" then
if not has_trailing_slash then
webdav_url = webdav_url .. "/"
end
@ -101,16 +92,14 @@ function WebDavApi:listFolder(address, user, pass, folder_path, folder_mode)
source = ltn12.source.string(data),
sink = ltn12.sink.table(sink),
}
local code, headers, status = socket.skip(1, http.request(request))
local headers_request = socket.skip(1, http.request(request))
socketutil:reset_timeout()
if headers == nil then
logger.dbg("WebDavApi:listFolder: No response:", status or code)
if headers_request == nil then
return nil
elseif not code or code < 200 or code > 299 then
elseif headers_request < 200 or headers_request >= 300 then
-- got a response, but it wasn't a success (e.g. auth failure)
logger.dbg("WebDavApi:listFolder: Request failed:", status or code)
logger.dbg("WebDavApi:listFolder: Response headers:", headers)
logger.dbg("WebDavApi:listFolder: Response body:", table.concat(sink))
logger.dbg(headers_request)
logger.dbg(table.concat(sink))
return nil
end
@ -125,7 +114,7 @@ function WebDavApi:listFolder(address, user, pass, folder_path, folder_mode)
if string.sub( item_fullpath, -1 ) == "/" then
item_fullpath = string.sub( item_fullpath, 1, -2 )
end
local is_current_dir = self:isCurrentDirectory( util.urlDecode(item_fullpath), address, folder_path )
local is_current_dir = self:isCurrentDirectory( item_fullpath, address, path )
local item_name = util.urlDecode( FFIUtil.basename( item_fullpath ) )
item_name = util.htmlEntitiesToUtf8(item_name)
@ -134,7 +123,7 @@ function WebDavApi:listFolder(address, user, pass, folder_path, folder_mode)
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, {
@ -170,21 +159,13 @@ function WebDavApi:listFolder(address, user, pass, folder_path, folder_mode)
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
function WebDavApi:downloadFile(file_url, user, pass, local_path)
socketutil:set_timeout(socketutil.FILE_BLOCK_TIMEOUT, socketutil.FILE_TOTAL_TIMEOUT)
logger.dbg("WebDavApi: downloading file: ", file_url)
local code, headers, status = socket.skip(1, http.request{
local code, _, status = socket.skip(1, http.request{
url = file_url,
method = "GET",
sink = ltn12.sink.file(io.open(local_path, "w")),
@ -194,45 +175,8 @@ function WebDavApi:downloadFile(file_url, user, pass, local_path)
socketutil:reset_timeout()
if code ~= 200 then
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 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,245 +3,106 @@ 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 Event = require("ui/event")
local ImageViewer = require("ui/widget/imageviewer")
local InfoMessage = require("ui/widget/infomessage")
local InputDialog = require("ui/widget/inputdialog")
local TextViewer = require("ui/widget/textviewer")
local InputContainer = require("ui/widget/container/inputcontainer")
local KeyValuePage = require("ui/widget/keyvaluepage")
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{
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:"),
},
local BookInfo = InputContainer:extend{
bookinfo_menu_title = _("Book information"),
}
function BookInfo:init()
if self.document then -- only for Reader menu
if self.ui then -- only for Reader menu
self.ui.menu:registerToMainMenu(self)
end
end
function BookInfo:addToMainMenu(menu_items)
menu_items.book_info = {
text = self.title,
text = self.bookinfo_menu_title,
callback = function()
self:onShowBookInfo()
end,
}
end
-- Shows book information.
function BookInfo:isSupported(file)
return lfs.attributes(file, "mode") == "file"
end
function BookInfo:show(file, book_props)
self.prop_updated = nil
local kv_pairs = {}
-- 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 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
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:"), 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 })
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 })
-- 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
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])
-- 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
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"),
-- but custom metadata exists, it has a copy of original doc_props
-- If still no book_props (book never opened or empty "stats"), open the
-- document to get them
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
@ -269,415 +130,169 @@ function BookInfo.getDocProps(file, book_props, no_open_document)
end
end
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)
-- If still no book_props, fall back to empty ones
if not book_props then
book_props = {}
end
end
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
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])
end
end
if description then
self:showBookProp("description", util.htmlToPlainTextIfHtml(description))
authors = table.concat(authors, "\n")
else
UIManager:show(InfoMessage:new{
text = _("No book description available."),
})
authors = BD.auto(authors)
end
end
table.insert(kv_pairs, { _("Authors:"), authors })
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)
local series = book_props.series
if series == "" or series == nil then
series = _("N/A")
else
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
-- 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
end
if doc then
cover_bb = doc:getCoverPageImage()
if not is_doc then
doc:close()
-- 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")
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")
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])
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
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
keywords = table.concat(keywords, "\n")
else
keywords = BD.auto(keywords)
end
UIManager:broadcastEvent(Event:new("InvalidateMetadataCache", file))
UIManager:broadcastEvent(Event:new("BookMetadataChanged"))
end
table.insert(kv_pairs, { _("Keywords:"), keywords })
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
local description = book_props.description
if description == "" or description == nil then
description = _("N/A")
else
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)
-- Description may (often in EPUB, but not always) or may not (rarely
-- in PDF) be HTML.
description = util.htmlToPlainTextIfHtml(book_props.description)
end
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
-- (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()
end
if no_custom_metadata then
self.ui.doc_settings:getCustomMetadataFile(true) -- reset metadata file cache
if not widget then
widget = InfoMessage:new{
text = _("No cover image available"),
}
end
UIManager:show(widget)
end
self:updateBookInfo(file, book_props, prop_key, prop_value_old)
end
table.insert(kv_pairs, { _("Cover image:"), _("Tap to display"), callback=viewCoverImage })
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)
-- 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
end
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,
},
},
},
local widget = KeyValuePage:new{
title = _("Book information"),
value_overflow_align = "right",
kv_pairs = kv_pairs,
values_lang = values_lang,
}
UIManager:show(input_dialog)
input_dialog:onShowKeyboard()
UIManager:show(widget)
end
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
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
end
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)
book_props.pages = self.ui.doc_settings:readSetting("doc_pages")
self:show(self.document.file, book_props)
end
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
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."),
})
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.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)
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."),
})
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,26 +1,20 @@
local BD = require("ui/bidi")
local ButtonDialog = require("ui/widget/buttondialog")
local ConfirmBox = require("ui/widget/confirmbox")
local ButtonDialogTitle = require("ui/widget/buttondialogtitle")
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 InputContainer = require("ui/widget/container/inputcontainer")
local Menu = require("ui/widget/menu")
local ReadCollection = require("readcollection")
local SortWidget = require("ui/widget/sortwidget")
local UIManager = require("ui/uimanager")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local Screen = require("device").screen
local filemanagerutil = require("apps/filemanager/filemanagerutil")
local _ = require("gettext")
local T = require("ffi/util").template
local BaseUtil = require("ffi/util")
local util = require("util")
local _ = require("gettext")
local T = BaseUtil.template
local FileManagerCollection = WidgetContainer:extend{
title = _("Collections"),
default_collection_title = _("Favorites"),
checkmark = "\u{2713}",
local FileManagerCollection = InputContainer:extend{
coll_menu_title = _("Favorites"),
}
function FileManagerCollection:init()
@ -28,154 +22,112 @@ function FileManagerCollection:init()
end
function FileManagerCollection:addToMainMenu(menu_items)
menu_items.favorites = {
text = self.default_collection_title,
callback = function()
self:onShowColl()
end,
}
menu_items.collections = {
text = self.title,
text = self.coll_menu_title,
callback = function()
self:onShowCollList()
self:onShowColl("favorites")
end,
}
end
-- collection
function FileManagerCollection:getCollectionTitle(collection_name)
return collection_name == ReadCollection.default_collection_name
and self.default_collection_title -- favorites
or collection_name
end
function FileManagerCollection:onShowColl(collection_name)
collection_name = collection_name or ReadCollection.default_collection_name
self.coll_menu = Menu:new{
ui = self.ui,
covers_fullscreen = true, -- hint for UIManager:_repaint()
is_borderless = true,
is_popout = false,
-- item and book cover thumbnail dimensions in Mosaic and Detailed list display modes
-- must be equal in File manager, History and Collection windows to avoid image scaling
title_bar_fm_style = true,
title_bar_left_icon = "appbar.menu",
onLeftButtonTap = function() self:showCollDialog() end,
onMenuChoice = self.onMenuChoice,
onMenuHold = self.onMenuHold,
onSetRotationMode = self.MenuSetRotationModeHandler,
_manager = self,
collection_name = collection_name,
}
self.coll_menu.close_callback = function()
if self.files_updated then
if self.ui.file_chooser then
self.ui.file_chooser:refreshPath()
end
self.files_updated = nil
end
UIManager:close(self.coll_menu)
self.coll_menu = nil
end
self:updateItemTable()
UIManager:show(self.coll_menu)
return true
end
function FileManagerCollection:updateItemTable(show_last_item)
local item_table = {}
for _, item in pairs(ReadCollection.coll[self.coll_menu.collection_name]) do
table.insert(item_table, item)
end
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)
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
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
self.book_props = self.ui.coverbrowser and self.ui.coverbrowser:getBookInfo(file)
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 = {
{
{
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)
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),
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,
},
},
{
text = _("Remove from collection"),
callback = function()
UIManager:close(self.collfile_dialog)
ReadCollection:removeItem(file, self.collection_name)
self._manager:updateItemTable()
end,
{
text = _("Book information"),
enabled = FileManagerBookInfo:isSupported(item.file),
callback = function()
FileManagerBookInfo:show(item.file)
UIManager:close(self.collfile_dialog)
end,
},
},
})
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
}
-- NOTE: Duplicated from frontend/apps/filemanager/filemanager.lua
if Device:canExecuteScript(item.file) then
table.insert(buttons, {
filemanagerutil.genExecuteScriptButton(file, close_dialog_menu_callback)
{
-- @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,
}
})
end
self.collfile_dialog = ButtonDialog:new{
title = BD.filename(item.text),
self.collfile_dialog = ButtonDialogTitle:new{
title = item.text:match("([^/]+)$"),
title_align = "center",
buttons = buttons,
}
@ -183,81 +135,6 @@ 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
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)
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)
@ -273,326 +150,26 @@ function FileManagerCollection:MenuSetRotationModeHandler(rotation)
return true
end
function FileManagerCollection:onBookMetadataChanged()
if self.coll_menu then
self.coll_menu:updateItems()
end
end
-- collection list
function FileManagerCollection:onShowCollList(file_or_files, caller_callback, no_dialog)
self.selected_colections = nil
if file_or_files then -- select mode
if type(file_or_files) == "string" then -- checkmark collections containing the file
self.selected_colections = ReadCollection:getCollectionsWithFile(file_or_files)
else -- do not checkmark any
self.selected_colections = {}
end
end
self.coll_list = Menu:new{
subtitle = "",
covers_fullscreen = true,
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()
is_borderless = true,
is_popout = false,
title_bar_fm_style = true,
title_bar_left_icon = file_or_files and "check" or "appbar.menu",
onLeftButtonTap = function() self:showCollListDialog(caller_callback, no_dialog) end,
onMenuChoice = self.onCollListChoice,
onMenuHold = self.onCollListHold,
onMenuHold = self.onMenuHold,
onSetRotationMode = self.MenuSetRotationModeHandler,
_manager = self,
collection = collection,
}
self.coll_list.close_callback = function(force_close)
if force_close or self.selected_colections == nil then
UIManager:close(self.coll_list)
self.coll_list = nil
end
end
self:updateCollListItemTable(true) -- init
UIManager:show(self.coll_list)
return true
end
function FileManagerCollection:updateCollListItemTable(do_init, item_number)
local item_table
if do_init then
item_table = {}
for name, coll in pairs(ReadCollection.coll) do
local mandatory
if self.selected_colections then
mandatory = self.selected_colections[name] and self.checkmark or " "
self.coll_list.items_mandatory_font_size = self.coll_list.font_size
else
mandatory = util.tableSize(coll)
end
table.insert(item_table, {
text = self:getCollectionTitle(name),
mandatory = mandatory,
name = name,
order = ReadCollection.coll_order[name],
})
end
if #item_table > 1 then
table.sort(item_table, function(v1, v2) return v1.order < v2.order end)
end
else
item_table = self.coll_list.item_table
end
local title = T(_("Collections (%1)"), #item_table)
local subtitle
if self.selected_colections then
local selected_nb = util.tableSize(self.selected_colections)
subtitle = self.selected_colections and T(_("Selected collections: %1"), selected_nb)
if do_init and selected_nb > 0 then -- show first collection containing the long-pressed book
for i, item in ipairs(item_table) do
if self.selected_colections[item.name] then
item_number = i
break
end
end
end
end
self.coll_list:switchItemTable(title, item_table, item_number or -1, nil, subtitle)
end
function FileManagerCollection:onCollListChoice(item)
if self._manager.selected_colections then
if item.mandatory == self._manager.checkmark then
self.item_table[item.idx].mandatory = " "
self._manager.selected_colections[item.name] = nil
else
self.item_table[item.idx].mandatory = self._manager.checkmark
self._manager.selected_colections[item.name] = true
end
self._manager:updateCollListItemTable()
else
self._manager:onShowColl(item.name)
end
end
function FileManagerCollection:onCollListHold(item)
if item.name == ReadCollection.default_collection_name -- Favorites non-editable
or self._manager.selected_colections then -- select mode
return
self:updateItemTable()
self.coll_menu.close_callback = function()
UIManager:close(self.coll_menu)
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)
UIManager:show(self.coll_menu)
return true
end
function FileManagerCollection:showCollListDialog(caller_callback, no_dialog)
if no_dialog then
caller_callback()
self.coll_list.close_callback(true)
return
end
local button_dialog, buttons
local new_collection_button = {
{
text = _("New collection"),
callback = function()
UIManager:close(button_dialog)
self:addCollection()
end,
},
}
if self.selected_colections then -- select mode
buttons = {
new_collection_button,
{}, -- separator
{
{
text = _("Deselect all"),
callback = function()
UIManager:close(button_dialog)
for name in pairs(self.selected_colections) do
self.selected_colections[name] = nil
end
self:updateCollListItemTable(true)
end,
},
{
text = _("Select all"),
callback = function()
UIManager:close(button_dialog)
for name in pairs(ReadCollection.coll) do
self.selected_colections[name] = true
end
self:updateCollListItemTable(true)
end,
},
},
{
{
text = _("Apply selection"),
callback = function()
UIManager:close(button_dialog)
caller_callback()
self.coll_list.close_callback(true)
end,
},
},
}
else
buttons = {
new_collection_button,
{
{
text = _("Arrange collections"),
callback = function()
UIManager:close(button_dialog)
self:sortCollections()
end,
},
},
}
end
button_dialog = ButtonDialog:new{
buttons = buttons,
}
UIManager:show(button_dialog)
end
function FileManagerCollection:editCollectionName(editCallback, old_name)
local input_dialog
input_dialog = InputDialog:new{
title = _("Enter collection name"),
input = old_name,
input_hint = old_name,
buttons = {{
{
text = _("Cancel"),
id = "close",
callback = function()
UIManager:close(input_dialog)
end,
},
{
text = _("Save"),
callback = function()
local new_name = input_dialog:getInputText()
if new_name == "" or new_name == old_name then return end
if ReadCollection.coll[new_name] then
UIManager:show(InfoMessage:new{
text = T(_("Collection already exists: %1"), new_name),
})
else
UIManager:close(input_dialog)
editCallback(new_name)
end
end,
},
}},
}
UIManager:show(input_dialog)
input_dialog:onShowKeyboard()
end
function FileManagerCollection:addCollection()
local editCallback = function(name)
ReadCollection:addCollection(name)
local mandatory
if self.selected_colections then
self.selected_colections[name] = true
mandatory = self.checkmark
else
mandatory = 0
end
table.insert(self.coll_list.item_table, {
text = name,
mandatory = mandatory,
name = name,
order = ReadCollection.coll_order[name],
})
self:updateCollListItemTable(false, #self.coll_list.item_table) -- show added item
end
self:editCollectionName(editCallback)
end
function FileManagerCollection:renameCollection(item)
local editCallback = function(name)
ReadCollection:renameCollection(item.name, name)
self.coll_list.item_table[item.idx].text = name
self.coll_list.item_table[item.idx].name = name
self:updateCollListItemTable()
end
self:editCollectionName(editCallback, item.name)
end
function FileManagerCollection:removeCollection(item)
UIManager:show(ConfirmBox:new{
text = _("Remove collection?") .. "\n\n" .. item.text,
ok_text = _("Remove"),
ok_callback = function()
ReadCollection:removeCollection(item.name)
table.remove(self.coll_list.item_table, item.idx)
self:updateCollListItemTable()
end,
})
end
function FileManagerCollection:sortCollections()
local sort_widget
sort_widget = SortWidget:new{
title = _("Arrange collections"),
item_table = util.tableDeepCopy(self.coll_list.item_table),
callback = function()
ReadCollection:updateCollectionListOrder(sort_widget.item_table)
self:updateCollListItemTable(true) -- init
end,
}
UIManager:show(sort_widget)
end
-- external
function FileManagerCollection:genAddToCollectionButton(file_or_files, caller_pre_callback, caller_post_callback, button_disabled)
return {
text = _("Add to collection"),
enabled = not button_disabled,
callback = function()
if caller_pre_callback then
caller_pre_callback()
end
local caller_callback = function()
if type(file_or_files) == "string" then
ReadCollection:addRemoveItemMultiple(file_or_files, self.selected_colections)
else -- selected files
ReadCollection:addItemsMultiple(file_or_files, self.selected_colections)
end
if caller_post_callback then
caller_post_callback()
end
end
self:onShowCollList(file_or_files, caller_callback)
end,
}
end
return FileManagerCollection

@ -55,9 +55,6 @@ end
function FileConverter:_mdFileToHtml(file, title)
local f = io.open(file, "rb")
if f == nil then
return
end
local content = f:read("*all")
f:close()
local html = self:mdToHtml(content, title)
@ -66,9 +63,6 @@ end
function FileConverter:writeStringToFile(content, file)
local f = io.open(file, "w")
if f == nil then
return
end
f:write(content)
f:close()
end
@ -110,20 +104,7 @@ function FileConverter:showConvertButtons(file, ui)
},
},
}
self.convert_dialog.onCloseWidget = function(this)
local super = getmetatable(this)
if super.onCloseWidget then
-- Call our super's method, if any
super.onCloseWidget(this)
end
-- And then do our own cleanup
self:cleanup()
end
UIManager:show(self.convert_dialog)
end
function FileConverter:cleanup()
self.convert_dialog = nil
end
return FileConverter

@ -1,340 +1,274 @@
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 DocSettings = require("docsettings")
local CenterContainer = require("ui/widget/container/centercontainer")
local DocumentRegistry = require("document/documentregistry")
local FileChooser = require("ui/widget/filechooser")
local InfoMessage = require("ui/widget/infomessage")
local InputContainer = require("ui/widget/container/inputcontainer")
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 Utf8Proc = require("ffi/utf8proc")
local filemanagerutil = require("apps/filemanager/filemanagerutil")
local lfs = require("libs/libkoreader-lfs")
local BaseUtil = require("ffi/util")
local util = require("util")
local _ = require("gettext")
local N_ = _.ngettext
local Screen = require("device").screen
local T = require("ffi/util").template
local FileSearcher = WidgetContainer:extend{
local FileSearcher = InputContainer:new{
dirs = {},
files = {},
results = {},
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: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)
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,
self.search_dialog = InputDialog:new{
title = _("Enter filename to search for"),
input = search_string or self.search_value,
buttons = {
{
{
text = _("Cancel"),
id = "close",
callback = function()
UIManager:close(search_dialog)
UIManager:close(self.search_dialog)
end,
},
{
text = _("Home folder"),
enabled = G_reader_settings:has("home_dir"),
callback = function()
self.search_string = search_dialog:getInputText()
if self.search_string == "" then return end
UIManager:close(search_dialog)
self.search_value = self.search_dialog:getInputText()
if self.search_value == "" then return end
self.path = G_reader_settings:readSetting("home_dir")
self:doSearch()
self:close()
end,
},
{
text = self.ui.file_chooser and _("Current folder") or _("Book folder"),
text = _("Current folder"),
is_enter_default = true,
callback = function()
self.search_string = search_dialog:getInputText()
if self.search_string == "" then return end
UIManager:close(search_dialog)
self.search_value = self.search_dialog:getInputText()
if self.search_value == "" then return end
self.path = self.ui.file_chooser and self.ui.file_chooser.path or self.ui:getLastDirFile()
self:doSearch()
self:close()
end,
},
},
},
}
check_button_case = CheckButton:new{
self.check_button_case = CheckButton:new{
text = _("Case sensitive"),
checked = self.case_sensitive,
parent = search_dialog,
parent = self.search_dialog,
callback = function()
self.case_sensitive = check_button_case.checked
self.case_sensitive = self.check_button_case.checked
end,
}
search_dialog:addWidget(check_button_case)
check_button_subfolders = CheckButton:new{
self.search_dialog:addWidget(self.check_button_case)
self.check_button_subfolders = CheckButton:new{
text = _("Include subfolders"),
checked = self.include_subfolders,
parent = search_dialog,
parent = self.search_dialog,
callback = function()
self.include_subfolders = check_button_subfolders.checked
self.include_subfolders = self.check_button_subfolders.checked
end,
}
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
self.search_dialog:addWidget(self.check_button_subfolders)
function FileSearcher:doSearch()
local results
local dirs, files = self:getList()
-- If we have a FileChooser instance, use it, to be able to make use of its natsort cache
if self.ui.file_chooser then
results = self.ui.file_chooser:genItemTable(dirs, files)
else
results = FileChooser:genItemTable(dirs, files)
end
if #results > 0 then
self:showSearchResults(results)
else
self:showSearchResultsMessage(true)
end
UIManager:show(self.search_dialog)
self.search_dialog:onShowKeyboard()
end
function FileSearcher:getList()
self.no_metadata_count = 0
local sys_folders = { -- do not search in sys_folders
["/dev"] = true,
["/proc"] = true,
["/sys"] = true,
function FileSearcher:showSearchResults()
local menu_container = CenterContainer:new{
dimen = Screen:getSize(),
}
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{
title = T(_("Search results (%1)"), #results),
subtitle = T(_("Query: %1"), self.search_string),
item_table = results,
ui = self.ui,
covers_fullscreen = true, -- hint for UIManager:_repaint()
is_borderless = true,
is_popout = false,
title_bar_fm_style = true,
onMenuSelect = self.onMenuSelect,
width = Screen:getWidth() - (Size.margin.fullscreen_popout * 2),
height = Screen:getHeight() - (Size.margin.fullscreen_popout * 2),
show_parent = menu_container,
onMenuHold = self.onMenuHold,
handle_hold_on_hold_release = true,
_manager = self,
}
table.insert(menu_container, self.search_menu)
self.search_menu.close_callback = function()
UIManager:close(self.search_menu)
if self.ui.file_chooser then
self.ui.file_chooser:refreshPath()
end
end
UIManager:show(self.search_menu)
if self.no_metadata_count ~= 0 then
self:showSearchResultsMessage()
UIManager:close(menu_container)
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)
table.sort(self.results, sorting)
self.search_menu:switchItemTable(T(_("Search results (%1)"), #self.results), self.results)
UIManager:show(menu_container)
end
function FileSearcher:onMenuSelect(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.close_callback()
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),
})
end
table.insert(buttons, {
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 "/")
local buttons = {
{
{
text = _("Delete"),
enabled = not is_currently_opened,
text = _("Cancel"),
callback = function()
local function post_delete_callback()
UIManager:close(dialog)
for i, menu_item in ipairs(self.item_table) do
if menu_item.path == file then
table.remove(self.item_table, i)
break
end
self:switchItemTable(T(_("Search results (%1)"), #self.item_table), self.item_table)
end
end
local FileManager = require("apps/filemanager/filemanager")
FileManager:showDeleteFileDialog(file, post_delete_callback)
UIManager:close(self.results_dialog)
end,
},
filemanagerutil.genBookInformationButton(file, bookinfo, close_dialog_callback),
})
end
table.insert(buttons, {
filemanagerutil.genShowFolderButton(file, close_dialog_menu_callback),
{
{
text = _("Show folder"),
callback = function()
UIManager:close(self.results_dialog)
self.close_callback()
self._manager:showFolder(fullpath)
end,
},
},
}
if is_file then
table.insert(buttons[1], {
text = _("Open"),
enabled = DocumentRegistry:hasProvider(file, nil, true), -- allow auxiliary providers
callback = function()
close_dialog_callback()
local FileManager = require("apps/filemanager/filemanager")
FileManager.openFile(self.ui, file, nil, self.close_callback)
UIManager:close(self.results_dialog)
self.close_callback()
ReaderUI:showReader(fullpath)
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",
self.results_dialog = ButtonDialogTitle:new{
title = fullpath,
buttons = buttons,
}
UIManager:show(dialog)
UIManager:show(self.results_dialog)
return true
end
function FileSearcher:onMenuHold(item)
if item.is_file then
if DocumentRegistry:hasProvider(item.path, nil, true) then
local FileManager = require("apps/filemanager/filemanager")
FileManager.openFile(self.ui, item.path, nil, self.close_callback)
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
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
return true
end
return FileSearcher

@ -1,33 +1,29 @@
local BD = require("ui/bidi")
local ButtonDialog = require("ui/widget/buttondialog")
local CheckButton = require("ui/widget/checkbutton")
local ButtonDialogTitle = require("ui/widget/buttondialogtitle")
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 InputContainer = require("ui/widget/container/inputcontainer")
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 util = require("util")
local _ = require("gettext")
local C_ = _.pgettext
local T = require("ffi/util").template
local T = FFIUtil.template
local FileManagerHistory = WidgetContainer:extend{
local FileManagerHistory = InputContainer:extend{
hist_menu_title = _("History"),
}
local filter_text = {
all = C_("Book status filter", "All"),
reading = C_("Book status filter", "Reading"),
local status_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()
@ -35,6 +31,7 @@ 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()
@ -43,186 +40,97 @@ function FileManagerHistory:addToMainMenu(menu_items)
}
end
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()
-- try to stay on current page
local select_number = nil
if self.hist_menu.page and self.hist_menu.perpage and self.hist_menu.page > 0 then
select_number = (self.hist_menu.page - 1) * self.hist_menu.perpage + 1
end
self.count = { all = #require("readhistory").hist,
reading = 0, abandoned = 0, complete = 0, deleted = 0, new = 0, }
local item_table = {}
for _, v in ipairs(require("readhistory").hist) do
if self:isItemMatch(v) then
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)
if not self.filter or v.status == self.filter then
table.insert(item_table, v)
end
if self.statuses_fetched then
self.count[v.status] = self.count[v.status] + 1
end
end
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)
local title = self.hist_menu_title
if self.filter then
title = title .. " (" .. status_text[self.filter] .. ")"
end
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
self.hist_menu:switchItemTable(title, item_table, select_number)
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 file = item.file
local readerui_instance = require("apps/reader/readerui"):_getRunningInstance()
local currently_opened_file = readerui_instance and readerui_instance.document and readerui_instance.document.file
self.histfile_dialog = nil
self.book_props = self.ui.coverbrowser and self.ui.coverbrowser:getBookInfo(file)
local function close_dialog_callback()
UIManager:close(self.histfile_dialog)
end
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, {
local buttons = {
{
text = _("Delete"),
enabled = not (item.dim or is_currently_opened),
callback = function()
local function post_delete_callback()
UIManager:close(self.histfile_dialog)
{
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()
self._manager.files_updated = true
end
local FileManager = require("apps/filemanager/filemanager")
FileManager:showDeleteFileDialog(file, post_delete_callback)
end,
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,
{
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,
},
},
})
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),
}
self.histfile_dialog = ButtonDialogTitle:new{
title = BD.filename(item.text:match("([^/]+)$")),
title_align = "center",
buttons = buttons,
}
@ -247,113 +155,87 @@ function FileManagerHistory:MenuSetRotationModeHandler(rotation)
return true
end
function FileManagerHistory:onShowHist(search_info)
function FileManagerHistory:onShowHist()
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, "flashui")
UIManager:show(self.hist_menu)
return true
end
function FileManagerHistory:showHistDialog()
if not self.statuses_fetched then
self:fetchStatuses(true)
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
end
local hist_dialog
local buttons = {}
local function genFilterButton(filter)
local function genFilterButton(status)
return {
text = T(_("%1 (%2)"), filter_text[filter], self.count[filter]),
text = T(_("%1 (%2)"), status_text[status], self.count[status]),
callback = function()
UIManager:close(hist_dialog)
self.filter = filter
if filter == "all" then -- reset all filters
self.search_string = nil
self.selected_colections = nil
end
self.filter = status ~= "all" and status
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, {
{
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,
},
genFilterButton("complete"),
genFilterButton("deleted"),
})
table.insert(buttons, {
{
text = _("Search in filename and book metadata"),
callback = function()
UIManager:close(hist_dialog)
self:onSearchHistory()
end,
},
genFilterButton("all"),
genFilterButton("new"),
})
if self.count.deleted > 0 then
table.insert(buttons, {}) -- separator
table.insert(buttons, {})
table.insert(buttons, {
{
text = _("Clear history of deleted files"),
callback = function()
local confirmbox = ConfirmBox:new{
UIManager:show(ConfirmBox:new{
text = _("Clear history of deleted files?"),
ok_text = _("Clear"),
ok_callback = function()
@ -361,13 +243,12 @@ function FileManagerHistory:showHistDialog()
require("readhistory"):clearMissing()
self:updateItemTable()
end,
}
UIManager:show(confirmbox)
})
end,
},
},
})
end
hist_dialog = ButtonDialog:new{
hist_dialog = ButtonDialogTitle:new{
title = _("Filter by book status"),
title_align = "center",
buttons = buttons,
@ -375,61 +256,4 @@ 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

@ -1,17 +1,16 @@
local BD = require("ui/bidi")
local CenterContainer = require("ui/widget/container/centercontainer")
local CloudStorage = require("apps/cloudstorage/cloudstorage")
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")
@ -21,7 +20,7 @@ local T = FFIUtil.template
local FileManagerMenu = InputContainer:extend{
tab_item_table = nil,
menu_items = nil, -- table, mandatory
menu_items = {},
registered_widgets = nil,
}
@ -50,31 +49,20 @@ function FileManagerMenu:init()
self.registered_widgets = {}
self:registerKeyEvents()
if Device:hasKeys() then
self.key_events = {
ShowMenu = { { "Menu" }, doc = "show menu" },
}
end
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
local DTAP_ZONE_MENU = G_defaults:readSetting("DTAP_ZONE_MENU")
local DTAP_ZONE_MENU_EXT = G_defaults:readSetting("DTAP_ZONE_MENU_EXT")
self:registerTouchZones({
{
id = "filemanager_tap",
@ -148,77 +136,69 @@ 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 FileChooser.show_hidden end,
callback = function() FileChooser:toggleShowFilesMode("show_hidden") end,
checked_func = function() return self.ui.file_chooser.show_hidden end,
callback = function() self.ui:toggleHiddenFiles() end,
},
{
text = _("Show unsupported files"),
checked_func = function() return FileChooser.show_unsupported end,
callback = function() FileChooser:toggleShowFilesMode("show_unsupported") end,
checked_func = function() return self.ui.file_chooser.show_unsupported end,
callback = function() self.ui:toggleUnsupportedFiles() end,
separator = true,
},
{
text = _("Classic mode settings"),
sub_item_table = {
{
text_func = function()
return T(_("Items per page: %1"),
G_reader_settings:readSetting("items_per_page") or FileChooser.items_per_page_default)
end,
text = _("Items per page"),
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(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,
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 = 30,
default_value = default_value,
value_max = 24,
default_value = default_perpage,
title_text = _("Items per page"),
keep_shown_on_apply = true,
callback = function(spin)
G_reader_settings:saveSetting("items_per_page", spin.value)
FileChooser:refreshPath()
touchmenu_instance:updateItems()
end,
self.ui:onRefresh()
end
}
UIManager:show(widget)
UIManager:show(items)
end,
},
{
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,
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,
value_min = 10,
value_max = 72,
default_value = default_value,
default_value = default_font_size,
keep_shown_on_apply = true,
title_text = _("Item font size"),
callback = function(spin)
if spin.value == default_value then
if spin.value == default_font_size 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
@ -226,11 +206,10 @@ function FileManagerMenu:setUpdateItemTable()
else
G_reader_settings:saveSetting("items_font_size", spin.value)
end
FileChooser:refreshPath()
touchmenu_instance:updateItems()
end,
self.ui:onRefresh()
end
}
UIManager:show(widget)
UIManager:show(items_font)
end,
},
{
@ -277,26 +256,6 @@ 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()
@ -336,14 +295,29 @@ function FileManagerMenu:setUpdateItemTable()
{
text = _("Set home folder"),
callback = function()
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()
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
end
filemanagerutil.showChooseDialog(title_header, caller_callback, current_path, default_path)
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,
})
end,
},
{
@ -353,7 +327,8 @@ function FileManagerMenu:setUpdateItemTable()
end,
callback = function()
G_reader_settings:flipNilOrTrue("shorten_home_dir")
self.ui:updateTitleBarPath()
local FileManager = require("apps/filemanager/filemanager")
if FileManager.instance then FileManager.instance:reinit() end
end,
help_text = _([[
"Shorten home folder" will display the home folder itself as "Home" instead of its full path.
@ -382,28 +357,26 @@ To:
separator = true,
},
{
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,
text = _("Info lists items per page"),
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(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,
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,
value_min = 10,
value_max = 30,
default_value = default_value,
value_max = 24,
default_value = default_perpage,
title_text = _("Info lists items per page"),
callback = function(spin)
if spin.value == default_value then
if spin.value == default_perpage 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
@ -411,13 +384,12 @@ To:
else
G_reader_settings:saveSetting("keyvalues_per_page", spin.value)
end
touchmenu_instance:updateItems()
end
}
UIManager:show(widget)
UIManager:show(items)
end,
},
},
}
}
for _, widget in pairs(self.registered_widgets) do
@ -427,66 +399,33 @@ To:
end
end
self.menu_items.sort_by = self:getSortingMenuTable()
self.menu_items.sort_by = self.ui:getSortingMenuTable()
self.menu_items.reverse_sorting = {
text = _("Reverse sorting"),
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,
checked_func = function() return self.ui.file_chooser.reverse_collate end,
callback = function() self.ui:toggleReverseCollate() end
}
self.menu_items.start_with = self:getStartWithMenuTable()
self.menu_items.start_with = self.ui:getStartWithMenuTable()
if Device:supportsScreensaver() then
self.menu_items.screensaver = {
text = _("Sleep screen"),
text = _("Screensaver"),
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"),
callback = function()
SetDefaults:ConfirmEdit()
end,
hold_callback = function()
SetDefaults:ConfirmSave()
end,
}
self.menu_items.plugin_management = {
text = _("Plugin management"),
@ -511,7 +450,10 @@ To:
-- Also remove from the Cache objet references to the cache files we've just deleted
local Cache = require("cache")
Cache.cached = {}
UIManager:askForRestart(_("Caches cleared. Please restart KOReader."))
local InfoMessage = require("ui/widget/infomessage")
UIManager:show(InfoMessage:new{
text = _("Caches cleared. Please restart KOReader."),
})
end,
})
end,
@ -549,7 +491,7 @@ To:
end
end,
},
},
}
}
if Device:isKobo() and not Device:isSunxi() then
table.insert(self.menu_items.developer_options.sub_item_table, {
@ -559,7 +501,10 @@ To:
end,
callback = function()
G_reader_settings:flipNilOrFalse("dev_startup_no_fbdepth")
UIManager:askForRestart()
local InfoMessage = require("ui/widget/infomessage")
UIManager:show(InfoMessage:new{
text = _("This will take effect on next restart."),
})
end,
})
end
@ -572,24 +517,29 @@ To:
end,
callback = function()
G_reader_settings:flipNilOrFalse("dev_abort_on_crash")
UIManager:askForRestart()
local InfoMessage = require("ui/widget/infomessage")
UIManager:show(InfoMessage:new{
text = _("This will take effect on next restart."),
})
end,
})
end
if not Device.should_restrict_JIT then
local Blitbuffer = require("ffi/blitbuffer")
table.insert(self.menu_items.developer_options.sub_item_table, {
text = _("Disable C blitter"),
enabled_func = function()
return Blitbuffer.has_cblitbuffer
end,
checked_func = function()
return G_reader_settings:isTrue("dev_no_c_blitter")
end,
callback = function()
G_reader_settings:flipNilOrFalse("dev_no_c_blitter")
Blitbuffer:enableCBB(G_reader_settings:nilOrFalse("dev_no_c_blitter"))
end,
})
end
local Blitbuffer = require("ffi/blitbuffer")
table.insert(self.menu_items.developer_options.sub_item_table, {
text = _("Disable C blitter"),
enabled_func = function()
return Blitbuffer.has_cblitbuffer
end,
checked_func = function()
return G_reader_settings:isTrue("dev_no_c_blitter")
end,
callback = function()
G_reader_settings:flipNilOrFalse("dev_no_c_blitter")
Blitbuffer:enableCBB(G_reader_settings:nilOrFalse("dev_no_c_blitter"))
end,
})
if Device:hasEinkScreen() and Device:canHWDither() then
table.insert(self.menu_items.developer_options.sub_item_table, {
text = _("Disable HW dithering"),
@ -629,30 +579,6 @@ 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, {
@ -675,7 +601,10 @@ To:
mxcfb_bypass_wait_for = not Device:hasReliableMxcWaitFor()
end
G_reader_settings:saveSetting("mxcfb_bypass_wait_for", not mxcfb_bypass_wait_for)
UIManager:askForRestart()
local InfoMessage = require("ui/widget/infomessage")
UIManager:show(InfoMessage:new{
text = _("This will take effect on next restart."),
})
end,
})
end
@ -692,7 +621,10 @@ To:
end,
callback = function()
G_reader_settings:flipNilOrFalse("pb_ignore_b288_quirks")
UIManager:askForRestart()
local InfoMessage = require("ui/widget/infomessage")
UIManager:show(InfoMessage:new{
text = _("This will take effect on next restart."),
})
end,
})
end
@ -712,7 +644,10 @@ To:
end,
callback = function()
G_reader_settings:flipNilOrTrue("use_xtext")
UIManager:askForRestart()
local InfoMessage = require("ui/widget/infomessage")
UIManager:show(InfoMessage:new{
text = _("This will take effect on next restart."),
})
end,
})
table.insert(self.menu_items.developer_options.sub_item_table, {
@ -725,7 +660,10 @@ To:
end,
callback = function()
G_reader_settings:flipNilOrFalse("dev_reverse_ui_layout_mirroring")
UIManager:askForRestart()
local InfoMessage = require("ui/widget/infomessage")
UIManager:show(InfoMessage:new{
text = _("This will take effect on next restart."),
})
end
},
{
@ -735,10 +673,13 @@ To:
end,
callback = function()
G_reader_settings:flipNilOrFalse("dev_reverse_ui_text_direction")
UIManager:askForRestart()
local InfoMessage = require("ui/widget/infomessage")
UIManager:show(InfoMessage:new{
text = _("This will take effect on next restart."),
})
end
},
},
}
}
})
table.insert(self.menu_items.developer_options.sub_item_table, {
text_func = function()
@ -761,30 +702,11 @@ To:
touchmenu_instance:updateItems()
end,
})
table.insert(self.menu_items.developer_options.sub_item_table, {
text = _("Dump the fontlist cache"),
callback = function()
local FontList = require("fontlist")
FontList:dumpFontList()
end,
})
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")
end,
callback = function()
G_reader_settings:toggle("pm_debug_entry_failure")
end,
})
end
self.menu_items.cloud_storage = {
text = _("Cloud storage"),
callback = function()
local cloud_storage = require("apps/cloudstorage/cloudstorage"):new{}
local cloud_storage = CloudStorage:new{}
UIManager:show(cloud_storage)
local filemanagerRefresh = function() self.ui:onRefresh() end
function cloud_storage:onClose()
@ -806,7 +728,7 @@ The sorting order is the same as in filemanager.
Tap a book in the search results to open it.]]),
callback = function()
self.ui.filesearcher:onShowFileSearch()
self.ui:handleEvent(Event:new("ShowFileSearch"))
end
}
@ -872,66 +794,6 @@ 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)
@ -998,11 +860,10 @@ function FileManagerMenu:onShowMenu(tab_index)
end
function FileManagerMenu:onCloseFileManagerMenu()
if not self.menu_container then return true end
if not self.menu_container then return 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
@ -1014,10 +875,10 @@ function FileManagerMenu:_getTabIndexFromLocation(ges)
if not ges then
return last_tab_index
-- if the start position is far right
elseif ges.pos.x > Screen:getWidth() * (2/3) then
elseif ges.pos.x > 2 * Screen:getWidth() / 3 then
return BD.mirroredUILayout() and 1 or #self.tab_item_table
-- if the start position is far left
elseif ges.pos.x < Screen:getWidth() * (1/3) then
elseif ges.pos.x < Screen:getWidth() / 3 then
return BD.mirroredUILayout() and #self.tab_item_table or 1
-- if center return the last index
else
@ -1040,19 +901,11 @@ function FileManagerMenu:onSwipeShowMenu(ges)
end
function FileManagerMenu:onSetDimensions(dimen)
-- This widget doesn't support in-place layout updates, so, close & reopen
if self.menu_container then
self:onCloseFileManagerMenu()
self:onShowMenu()
self:onCloseFileManagerMenu()
-- update listening according to new screen dimen
if Device:isTouchDevice() then
self:initGesListener()
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)

@ -1,288 +1,373 @@
local CenterContainer = require("ui/widget/container/centercontainer")
local ConfirmBox = require("ui/widget/confirmbox")
local DataStorage = require("datastorage")
local Device = require("device")
local InfoMessage = require("ui/widget/infomessage")
local InputContainer = require("ui/widget/container/inputcontainer")
local InputDialog = require("ui/widget/inputdialog")
local Menu = require("ui/widget/menu")
local MultiInputDialog = require("ui/widget/multiinputdialog")
local Size = require("ui/size")
local UIManager = require("ui/uimanager")
local dump = require("dump")
local isAndroid, android = pcall(require, "android")
local logger = require("logger")
local ffiUtil = require("ffi/util")
local util = require("util")
local util = require("ffi/util")
local _ = require("gettext")
local Screen = require("device").screen
local SetDefaultsWidget = CenterContainer:extend{
state = nil,
menu_entries = nil,
defaults_menu = nil,
local is_appimage = os.getenv("APPIMAGE")
local function getDefaultsPath()
local defaults_path = DataStorage:getDataDir() .. "/defaults.lua"
if isAndroid then
defaults_path = android.dir .. "/defaults.lua"
elseif is_appimage then
defaults_path = "defaults.lua"
end
return defaults_path
end
local defaults_path = getDefaultsPath()
local persistent_defaults_path = DataStorage:getDataDir() .. "/defaults.persistent.lua"
local SetDefaults = InputContainer:new{
defaults_name = {},
defaults_value = {},
results = {},
defaults_menu = {},
changed = {},
settings_changed = false,
}
function SetDefaultsWidget:init()
-- This would usually be passed to the constructor, as CenterContainer's paintTo does *NOT* set/update self.dimen...
self.dimen = Screen:getSize()
-- Don't refresh the FM behind us. May leave stray bits of overflowed InputDialog behind in the popout border space.
self.covers_fullscreen = true
function SetDefaults:ConfirmEdit()
if not SetDefaults.EditConfirmed then
UIManager:show(ConfirmBox:new{
text = _("Some changes will not work until the next restart. Be careful; the wrong settings might crash KOReader!\nAre you sure you want to continue?"),
ok_callback = function()
self.EditConfirmed = true
self:init()
end,
})
else
self:init()
end
end
-- Then deal with our child widgets and our internal variables
function SetDefaults:init()
self.screen_width = Screen:getWidth()
self.screen_height = Screen:getHeight()
self.dialog_width = math.floor(math.min(self.screen_width, self.screen_height) * 0.95)
-- Keep track of what's an actual default, and what's been customized without actually touching the real data yet...
self.state = {}
local ro_defaults, rw_defaults = G_defaults:getDataTables()
for k, v in pairs(ro_defaults) do
self.state[k] = {
idx = 1,
value = v,
custom = false,
dirty = false,
default_value = v,
}
self.results = {}
local defaults = {}
local load_defaults = loadfile(defaults_path)
setfenv(load_defaults, defaults)
load_defaults()
local file = io.open(persistent_defaults_path, "r")
if file ~= nil then
file:close()
load_defaults = loadfile(persistent_defaults_path)
setfenv(load_defaults, defaults)
load_defaults()
end
for k, v in pairs(rw_defaults) do
self.state[k].value = v
self.state[k].custom = true
local idx = 1
for n, v in util.orderedPairs(defaults) do
self.defaults_name[idx] = n
self.defaults_value[idx] = v
idx = idx + 1
end
local menu_container = CenterContainer:new{
dimen = Screen:getSize(),
}
--- @fixme
-- in this use case (an input dialog is closed and the menu container is
-- opened immediately) we need to set the full screen dirty because
-- otherwise only the input dialog part of the screen is refreshed.
menu_container.onShow = function()
UIManager:setDirty(nil, "ui")
end
self.defaults_menu = Menu:new{
width = self.screen_width - (Size.margin.fullscreen_popout * 2),
height = self.screen_height - (Size.margin.fullscreen_popout * 2),
show_parent = menu_container,
_manager = self,
}
-- Prevent menu from closing when editing a value
function self.defaults_menu:onMenuSelect(item)
item.callback()
end
-- Prepare our menu entires
self.menu_entries = {}
table.insert(menu_container, self.defaults_menu)
self.defaults_menu.close_callback = function()
logger.dbg("Closing defaults menu")
self:saveBeforeExit()
UIManager:close(menu_container)
end
local set_dialog
local cancel_button = {
text = _("Cancel"),
id = "close",
enabled = true,
callback = function()
UIManager:close(set_dialog)
self:close()
end,
}
local i = 0
for k, t in ffiUtil.orderedPairs(self.state) do
local v = t.value
i = i + 1
self.state[k].idx = i
local value_type = type(v)
if value_type == "boolean" then
for i=1, #self.defaults_name do
self.changed[i] = false
local setting_name = self.defaults_name[i]
local setting_type = type(_G[setting_name])
if setting_type == "boolean" then
local editBoolean = function()
set_dialog = InputDialog:new{
title = k,
input = tostring(self.state[k].value),
self.set_dialog = InputDialog:new{
title = setting_name,
input = tostring(self.defaults_value[i]),
buttons = {
{
cancel_button,
{
text = _("Default"),
enabled = self.state[k].value ~= self.state[k].default_value,
callback = function()
UIManager:close(set_dialog)
self:update_menu_entry(k, self.state[k].default_value, value_type)
end
},
{
text = "true",
enabled = true,
callback = function()
UIManager:close(set_dialog)
self:update_menu_entry(k, true, value_type)
self.defaults_value[i] = true
_G[setting_name] = true
self.settings_changed = true
self.changed[i] = true
self.results[i].text = self:build_setting(i)
self:close()
self.defaults_menu:switchItemTable("Defaults", self.results, i)
end
},
{
text = "false",
enabled = true,
callback = function()
UIManager:close(set_dialog)
self:update_menu_entry(k, false, value_type)
self.defaults_value[i] = false
_G[setting_name] = false
self.settings_changed = true
self.changed[i] = true
self.results[i].text = self:build_setting(i)
self.defaults_menu:switchItemTable("Defaults", self.results, i)
self:close()
end
},
},
},
input_type = value_type,
input_type = setting_type,
width = self.dialog_width,
}
UIManager:show(set_dialog)
set_dialog:onShowKeyboard()
UIManager:show(self.set_dialog)
self.set_dialog:onShowKeyboard()
end
table.insert(self.menu_entries, {
text = self:gen_menu_entry(k, self.state[k].value, value_type),
bold = self.state[k].custom,
table.insert(self.results, {
text = self:build_setting(i),
callback = editBoolean
})
elseif value_type == "table" then
elseif setting_type == "table" then
local editTable = function()
local fields = {}
for key, value in ffiUtil.orderedPairs(self.state[k].value) do
for k, v in util.orderedPairs(_G[setting_name]) do
table.insert(fields, {
text = tostring(key) .. " = " .. tostring(value),
input_type = type(value),
text = tostring(k) .. " = " .. tostring(v),
hint = "",
padding = Screen:scaleBySize(2),
margin = Screen:scaleBySize(2),
})
end
set_dialog = MultiInputDialog:new{
title = k,
self.set_dialog = MultiInputDialog:new{
title = setting_name,
fields = fields,
buttons = {
{
cancel_button,
{
text = _("Default"),
enabled = not util.tableEquals(self.state[k].value, self.state[k].default_value),
callback = function()
UIManager:close(set_dialog)
self:update_menu_entry(k, self.state[k].default_value, value_type)
end
},
{
text = _("OK"),
enabled = true,
is_enter_default = true,
callback = function()
UIManager:close(set_dialog)
local new_table = {}
for _, field in ipairs(set_dialog:getFields()) do
for _, field in ipairs(MultiInputDialog:getFields()) do
local key, value = field:match("^[^= ]+"), field:match("[^= ]+$")
new_table[tonumber(key) or key] = tonumber(value) or value
end
self:update_menu_entry(k, new_table, value_type)
_G[setting_name] = new_table
self.defaults_value[i] = _G[setting_name]
self.settings_changed = true
self.changed[i] = true
self.results[i].text = self:build_setting(i)
self:close()
self.defaults_menu:switchItemTable("Defaults", self.results, i)
end,
},
},
},
width = self.dialog_width,
}
UIManager:show(set_dialog)
set_dialog:onShowKeyboard()
UIManager:show(self.set_dialog)
self.set_dialog:onShowKeyboard()
end
table.insert(self.menu_entries, {
text = self:gen_menu_entry(k, self.state[k].value, value_type),
bold = self.state[k].custom,
table.insert(self.results, {
text = self:build_setting(i),
callback = editTable
})
else
local editNumStr = function()
set_dialog = InputDialog:new{
title = k,
input = tostring(self.state[k].value),
self.set_dialog = InputDialog:new{
title = setting_name,
input = tostring(self.defaults_value[i]),
buttons = {
{
cancel_button,
{
text = _("Default"),
enabled = self.state[k].value ~= self.state[k].default_value,
callback = function()
UIManager:close(set_dialog)
self:update_menu_entry(k, self.state[k].default_value, value_type)
end
},
{
text = _("OK"),
is_enter_default = true,
enabled = true,
callback = function()
UIManager:close(set_dialog)
local new_value = set_dialog:getInputValue()
self:update_menu_entry(k, new_value, value_type)
local new_value = self.set_dialog:getInputValue()
if _G[setting_name] ~= new_value then
_G[setting_name] = new_value
self.defaults_value[i] = new_value
self.settings_changed = true
self.changed[i] = true
self.results[i].text = self:build_setting(i)
end
self:close()
self.defaults_menu:switchItemTable("Defaults", self.results, i)
end,
},
},
},
input_type = value_type,
input_type = setting_type,
width = self.dialog_width,
}
UIManager:show(set_dialog)
set_dialog:onShowKeyboard()
UIManager:show(self.set_dialog)
self.set_dialog:onShowKeyboard()
end
table.insert(self.menu_entries, {
text = self:gen_menu_entry(k, self.state[k].value, value_type),
bold = self.state[k].custom,
table.insert(self.results, {
text = self:build_setting(i),
callback = editNumStr
})
end
end
self.defaults_menu:switchItemTable("Defaults", self.results)
UIManager:show(menu_container)
end
-- Now that we have stuff to display, instantiate our Menu
self.defaults_menu = Menu:new{
width = self.screen_width - (Size.margin.fullscreen_popout * 2),
height = self.screen_height - (Size.margin.fullscreen_popout * 2),
show_parent = self,
item_table = self.menu_entries,
title = _("Defaults"),
}
-- Prevent menu from closing when editing a value
function self.defaults_menu:onMenuSelect(item)
item.callback()
end
self.defaults_menu.close_callback = function()
logger.dbg("Closing defaults menu")
self:saveBeforeExit()
UIManager:close(self)
end
function SetDefaults:close()
UIManager:close(self.set_dialog)
end
table.insert(self, self.defaults_menu)
function SetDefaults:ConfirmSave()
UIManager:show(ConfirmBox:new{
text = _('Are you sure you want to save the settings to "defaults.persistent.lua"?'),
ok_callback = function()
self:saveSettings()
end,
})
end
function SetDefaultsWidget:gen_menu_entry(k, v, v_type)
local ret = k .. " = "
if v_type == "boolean" then
return ret .. tostring(v)
elseif v_type == "table" then
function SetDefaults:build_setting(j)
local setting_name = self.defaults_name[j]
local ret = setting_name .. " = "
if type(_G[setting_name]) == "boolean" then
return ret .. tostring(self.defaults_value[j])
elseif type(_G[setting_name]) == "table" then
return ret .. "{...}"
elseif tonumber(v) then
return ret .. tostring(tonumber(v))
elseif tonumber(self.defaults_value[j]) then
return ret .. tostring(tonumber(self.defaults_value[j]))
else
return ret .. "\"" .. tostring(v) .. "\""
return ret .. "\"" .. tostring(self.defaults_value[j]) .. "\""
end
end
function SetDefaultsWidget:update_menu_entry(k, v, v_type)
local idx = self.state[k].idx
self.state[k].value = v
self.state[k].dirty = true
self.settings_changed = true
self.menu_entries[idx].text = self:gen_menu_entry(k, v, v_type)
if util.tableEquals(v, self.state[k].default_value) then
self.menu_entries[idx].bold = false
else
self.menu_entries[idx].bold = true
function SetDefaults:saveSettings()
self.results = {}
local persisted_defaults = {}
local file = io.open(persistent_defaults_path, "r")
if file ~= nil then
file:close()
local load_defaults = loadfile(persistent_defaults_path)
setfenv(load_defaults, persisted_defaults)
load_defaults()
end
local checked = {}
for j=1, #self.defaults_name do
if not self.changed[j] then checked[j] = true end
end
self.defaults_menu:switchItemTable(nil, self.menu_entries, idx)
end
function SetDefaultsWidget:saveSettings()
-- Update dirty keys for real
for k, t in pairs(self.state) do
if t.dirty then
G_defaults:saveSetting(k, t.value)
-- load default value for defaults
local defaults = {}
local load_defaults = loadfile(defaults_path)
setfenv(load_defaults, defaults)
load_defaults()
-- handle case "found in persistent" and changed, replace/delete it
for k, v in pairs(persisted_defaults) do
for j=1, #self.defaults_name do
if not checked[j]
and k == self.defaults_name[j] then
-- remove from persist if value got reverted back to the
-- default one
if defaults[k] == self.defaults_value[j] then
persisted_defaults[k] = nil
else
persisted_defaults[k] = self.defaults_value[j]
end
checked[j] = true
end
end
end
-- And flush to disk
G_defaults:flush()
UIManager:show(InfoMessage:new{
text = _("Default settings saved."),
})
-- handle case "not in persistent and different in non-persistent", add to
-- persistent
for j=1, #self.defaults_name do
if not checked[j] then
persisted_defaults[self.defaults_name[j]] = self.defaults_value[j]
end
end
file = io.open(persistent_defaults_path, "w")
if file then
file:write("-- For configuration changes that persists between updates\n")
for k, v in pairs(persisted_defaults) do
local line = {}
table.insert(line, k)
table.insert(line, " = ")
table.insert(line, dump(v))
table.insert(line, "\n")
file:write(table.concat(line))
end
file:close()
UIManager:show(InfoMessage:new{
text = _("Default settings saved."),
})
end
self.settings_changed = false
end
function SetDefaultsWidget:saveBeforeExit(callback)
function SetDefaults:saveBeforeExit(callback)
local save_text = _("Save and quit")
if Device:canRestart() then
save_text = _("Save and restart")
end
if self.settings_changed then
UIManager:show(ConfirmBox:new{
dismissable = false,
text = _("KOReader needs to be restarted to apply the new default settings."),
ok_text = save_text,
ok_callback = function()
self.settings_changed = false
self:saveSettings()
if Device:canRestart() then
UIManager:restartKOReader()
@ -293,25 +378,12 @@ function SetDefaultsWidget:saveBeforeExit(callback)
cancel_text = _("Discard changes"),
cancel_callback = function()
logger.info("discard defaults")
pcall(dofile, defaults_path)
pcall(dofile, persistent_defaults_path)
self.settings_changed = false
end,
})
end
end
local SetDefaults = {}
function SetDefaults:ConfirmEdit()
if not SetDefaults.EditConfirmed then
UIManager:show(ConfirmBox:new{
text = _("Some changes will not work until the next restart. Be careful; the wrong settings might crash KOReader!\nAre you sure you want to continue?"),
ok_callback = function()
SetDefaults.EditConfirmed = true
UIManager:show(SetDefaultsWidget:new{})
end,
})
else
UIManager:show(SetDefaultsWidget:new{})
end
end
return SetDefaults

@ -1,190 +1,205 @@
local BD = require("ui/bidi")
local ButtonDialog = require("ui/widget/buttondialog")
local InfoMessage = require("ui/widget/infomessage")
local InputContainer = require("ui/widget/container/inputcontainer")
local InputDialog = require("ui/widget/inputdialog")
local Menu = require("ui/widget/menu")
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"),
local FileManagerShortcuts = InputContainer:extend{
folder_shortcuts = G_reader_settings:readSetting("folder_shortcuts", {}),
}
function FileManagerShortcuts:updateItemTable()
function FileManagerShortcuts:updateItemTable(select_callback)
local item_table = {}
for folder, item in pairs(self.folder_shortcuts) do
for _, item in ipairs(self.folder_shortcuts) do
table.insert(item_table, {
text = string.format("%s (%s)", item.text, folder),
folder = folder,
name = item.text,
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,
})
end
table.sort(item_table, function(l, r)
return l.text < r.text
end)
self.shortcuts_menu:switchItemTable(nil, item_table, -1)
end
function FileManagerShortcuts:hasFolderShortcut(folder)
return self.folder_shortcuts[folder] and true or false
end
-- try to stay on current page
local select_number
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
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
end
self.fm_bookmark:switchItemTable(nil,
item_table, select_number)
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
},
},
}
dialog = ButtonDialog:new{
title = item.name .. "\n" .. BD.dirpath(item.folder),
title_align = "center",
buttons = buttons,
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
}
UIManager:show(dialog)
return true
UIManager:show(path_chooser)
end
function FileManagerShortcuts:removeShortcut(folder)
self.folder_shortcuts[folder] = nil
if self.shortcuts_menu then
self.fm_updated = true
self:updateItemTable()
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
end
table.insert(self.folder_shortcuts, {
text = friendly_name,
folder = folder,
})
self:updateItemTable()
end
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()
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
end
if self.shortcuts_menu then
self.fm_updated = true
self:updateItemTable()
end
end,
},
}},
}
UIManager:show(input_dialog)
input_dialog:onShowKeyboard()
},
{
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)
end
},
},
}
}
UIManager:show(folder_shortcuts_dialog)
return true
end
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,
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,
},
}
},
}
UIManager:show(path_chooser)
UIManager:show(edit_folder_input)
edit_folder_input:onShowKeyboard()
end
function FileManagerShortcuts:genShowFolderShortcutsButton(pre_callback)
return {
text = self.title,
callback = function()
pre_callback()
self:onShowFolderShortcutsDialog()
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()
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,
}
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
end
self:updateItemTable()
end
function FileManagerShortcuts:onSetDimensions(dimen)
@ -193,7 +208,7 @@ end
function FileManagerShortcuts:MenuSetRotationModeHandler(rotation)
if rotation ~= nil and rotation ~= Screen:getRotationMode() then
UIManager:close(self._manager.shortcuts_menu)
UIManager:close(self._manager.fm_bookmark)
if self._manager.ui.view and self._manager.ui.view.onSetRotationMode then
self._manager.ui.view:onSetRotationMode(rotation)
elseif self._manager.ui.onSetRotationMode then
@ -207,32 +222,26 @@ function FileManagerShortcuts:MenuSetRotationModeHandler(rotation)
end
function FileManagerShortcuts:onShowFolderShortcutsDialog(select_callback)
self.shortcuts_menu = Menu:new{
title = self.title,
covers_fullscreen = true,
is_borderless = true,
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,
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,
is_borderless = true,
curr_path = self.ui.file_chooser and self.ui.file_chooser.path or self.ui:getLastDirFile(),
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.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)
self:updateItemTable(select_callback)
UIManager:show(self.fm_bookmark)
end
return FileManagerShortcuts

@ -2,16 +2,10 @@
This module contains miscellaneous helper functions for FileManager
]]
local BD = require("ui/bidi")
local Device = require("device")
local DocSettings = require("docsettings")
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 util = require("ffi/util")
local _ = require("gettext")
local T = ffiutil.template
local filemanagerutil = {}
@ -23,7 +17,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 or path == home_dir .. "/" then
if path == home_dir then
return _("Home")
end
local len = home_dir:len()
@ -35,61 +29,28 @@ function filemanagerutil.abbreviate(path)
return path
end
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
-- 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()
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_paging = true,
bookmarks_rolling = true,
bookmarks_sorted = 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 = ffiutil.realpath(file)
local file_abs_path = util.realpath(file)
if file_abs_path then
local doc_settings = DocSettings:open(file_abs_path)
for k in pairs(doc_settings.data) do
@ -98,300 +59,8 @@ function filemanagerutil.resetDocumentSettings(file)
end
end
doc_settings:makeTrue("docsettings_reset_done") -- for readertypeset block_rendering_mode
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)
doc_settings:close()
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,8 +2,10 @@
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")
@ -26,16 +28,15 @@ 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:extend{
lipc_handle = nil,
}
ReaderActivityIndicator = EventListener:new{}
function ReaderActivityIndicator:isStub() return false end
function ReaderActivityIndicator:init()
local haslipc, lipc = pcall(require, "liblipclua")
if haslipc then
if (pcall(require, "liblipclua")) then
self.lipc_handle = lipc.init("com.github.koreader.activityindicator")
end
end
@ -67,15 +68,15 @@ function ReaderActivityIndicator:onStopActivityIndicator()
"clientId":"com.github.koreader.activityindicator", \
"priority":true}}')
self.indicator_started = false
util.usleep(1000000)
end
return true
end
function ReaderActivityIndicator:onCloseWidget()
function ReaderActivityIndicator:coda()
if self.lipc_handle then
self.lipc_handle:close()
end
self.lipc_handle = nil
end
return ReaderActivityIndicator

@ -1,423 +0,0 @@
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

@ -13,14 +13,16 @@ local _ = require("gettext")
-- additionally handles a location stack for each visited page or
-- page view change (when scrolling in a same page)
local ReaderBack = EventListener:extend{
location_stack = nil, -- array
local ReaderBack = EventListener:new{
location_stack = {},
-- a limit not intended to be a practical limit but just a failsafe
max_stack = 5000,
}
function ReaderBack:init()
self:registerKeyEvents()
if Device:hasKeys() then
self.ui.key_events.Back = { {Device.input.group.Back}, doc = "Reader back" }
end
-- Regular function wrapping our method, to avoid re-creating
-- an anonymous function at each page turn
self._addPreviousLocationToStackCallback = function()
@ -28,14 +30,6 @@ 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

@ -1,13 +1,14 @@
local ConfigDialog = require("ui/widget/configdialog")
local Device = require("device")
local Event = require("ui/event")
local Geom = require("ui/geometry")
local InputContainer = require("ui/widget/container/inputcontainer")
local UIManager = require("ui/uimanager")
local CreOptions = require("ui/data/creoptions")
local KoptOptions = require("ui/data/koptoptions")
local _ = require("gettext")
local ReaderConfig = InputContainer:extend{
local ReaderConfig = InputContainer:new{
last_panel_index = 1,
}
@ -19,33 +20,21 @@ function ReaderConfig:init()
end
self.configurable:loadDefaults(self.options)
self:registerKeyEvents()
if not self.dimen then self.dimen = Geom:new{} end
if Device:hasKeys() then
self.key_events = {
ShowConfigMenu = { {{"Press","AA"}}, doc = "show config dialog" },
}
end
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
local DTAP_ZONE_CONFIG = G_defaults:readSetting("DTAP_ZONE_CONFIG")
local DTAP_ZONE_CONFIG_EXT = G_defaults:readSetting("DTAP_ZONE_CONFIG_EXT")
self.ui:registerTouchZones({
{
id = "readerconfigmenu_tap",
@ -127,12 +116,12 @@ end
function ReaderConfig:onShowConfigMenu()
self.config_dialog = ConfigDialog:new{
dimen = self.dimen:copy(),
document = self.document,
ui = self.ui,
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"))
@ -158,11 +147,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
-- init basically calls update & initGesListener and nothing else, which is exactly what we want.
self.config_dialog:init()
self.config_dialog:closeDialog()
end
end

@ -8,16 +8,19 @@ local logger = require("logger")
local T = require("ffi/util").template
local _ = require("gettext")
local ReaderCoptListener = EventListener:extend{}
local ReaderCoptListener = EventListener:new{}
local CRE_HEADER_DEFAULT_SIZE = 20
function ReaderCoptListener:onReadSettings(config)
local view_mode_name = self.document.configurable.view_mode == 0 and "page" or "scroll"
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"
-- 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.document:setViewMode(view_mode_name)
self.ui.document:setViewMode(view_mode_name)
-- ReaderView is the holder of the view_mode state
self.view.view_mode = view_mode_name
@ -32,24 +35,20 @@ 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.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.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:onTimeFormatChanged()
-- Enable or disable crengine header status line (note that for crengine, 0=header enabled, 1=header disabled)
self.ui:handleEvent(Event:new("SetStatusLine", self.document.configurable.status_line))
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.old_battery_level = self.ui.rolling:updateBatteryState()
@ -69,144 +68,38 @@ function ReaderCoptListener:onReadSettings(config)
self:rescheduleHeaderRefreshIfNeeded() -- schedule (or not) first refresh
end
function ReaderCoptListener:onReaderReady()
-- custom metadata support for alt status bar and cre synthetic cover
for prop_key in pairs(self.document.prop_to_cre_prop) do
local orig_prop_value = self.ui.doc_settings:readSetting(prop_key)
local custom_prop_key = prop_key == "title" and "display_title" or prop_key
local custom_prop_value = self.ui.doc_props[custom_prop_key]
if custom_prop_value ~= orig_prop_value then
self.document:setAltDocumentProp(prop_key, custom_prop_value)
end
end
end
function ReaderCoptListener:updatePageInfoOverride(pageno)
if not (self.document.configurable.status_line == 0 and self.view.view_mode == "page" and self.page_info_override) then
return
end
-- There are a few cases where we may not be updated on change, at least:
-- - when toggling ReaderPageMap's "Use reference page numbers"
-- - when changing footer's nb of digits after decimal point
-- but we will update on next page turn. Let's not bother.
local page_pre = ""
local page_number = pageno
local page_sep = " / "
local page_count = self.ui.document:getPageCount()
local page_post = ""
local percentage = page_number / page_count
local percentage_pre = ""
local percentage_post = ""
-- Let's use the same setting for nb of digits after decimal point as configured for the footer
local percentage_digits = self.ui.view.footer.settings.progress_pct_format
local percentage_fmt = "%." .. percentage_digits .. "f%%"
-- We want the same output as with ReaderFooter's page_progress() and percentage()
-- but here each item (page number, page counte, percentage) is individually toggable,
-- so try to get something that make sense when not all are enabled
if self.ui.pagemap and self.ui.pagemap:wantsPageLabels() then
-- These become strings here
page_number = self.ui.pagemap:getCurrentPageLabel(true)
page_count = self.ui.pagemap:getLastPageLabel(true)
elseif self.ui.document:hasHiddenFlows() then
local flow = self.ui.document:getPageFlow(pageno)
page_number = tostring(self.ui.document:getPageNumberInFlow(pageno))
page_count = tostring(self.ui.document:getTotalPagesInFlow(flow))
percentage = page_number / page_count
if flow == 0 then
page_sep = " // "
else
page_pre = "["
page_post = "]"..tostring(flow)
percentage_pre = "["
percentage_post = "]"
end
end
local page_info = ""
if self.page_number or self.page_count then
page_info = page_info .. page_pre
if self.page_number then
page_info = page_info .. page_number
if self.page_count then
page_info = page_info .. page_sep
end
end
if self.page_count then
page_info = page_info .. page_count
end
page_info = page_info .. page_post
if self.reading_percent then
page_info = page_info .. " " -- (double space as done by crengine's own drawing)
end
end
if self.reading_percent then
page_info = page_info .. percentage_pre .. percentage_fmt:format(percentage*100) .. percentage_post
end
self.document:setPageInfoOverride(page_info)
end
function ReaderCoptListener:onPageUpdate(pageno)
self:updatePageInfoOverride(pageno)
end
function ReaderCoptListener:onPosUpdate(pos, pageno)
self:updatePageInfoOverride(pageno)
end
function ReaderCoptListener:onBookMetadataChanged(prop_updated)
-- custom metadata support for alt status bar and cre synthetic cover
local prop_key = prop_updated and prop_updated.metadata_key_updated
if prop_key and self.document.prop_to_cre_prop[prop_key] then
self.document:setAltDocumentProp(prop_key, prop_updated.doc_props[prop_key])
self:updateHeader()
end
end
function ReaderCoptListener:onConfigChange(option_name, option_value)
-- font_size and line_spacing are historically and sadly shared by both mupdf and cre reader modules,
-- but fortunately they can be distinguished by their different ranges
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:onTimeFormatChanged()
self.document._document:setIntProperty("window.status.clock.12hours", G_reader_settings:isTrue("twelve_hour_clock") and 1 or 0)
function ReaderCoptListener:onNotCharging()
self:headerRefresh()
end
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
function ReaderCoptListener:onTimeFormatChanged()
self.ui.document._document:setIntProperty("window.status.clock.12hours", G_reader_settings:isTrue("twelve_hour_clock") and 1 or 0)
end
function ReaderCoptListener:updateHeader()
-- Have crengine display accurate time and battery on its next drawing
self.document:resetBufferCache() -- be sure next repaint is a redrawing
self.ui.document:resetBufferCache() -- be sure next repaint is a redrawing
-- Force a refresh if we're not hidden behind another widget
if self:shouldHeaderBeRepainted() then
if UIManager:getTopWidget() == "ReaderUI" then
UIManager:setDirty(self.view.dialog, "ui",
Geom:new{
x = 0, y = 0,
w = Device.screen:getWidth(),
h = self.document:getHeaderHeight(),
h = self.ui.document:getHeaderHeight(),
}
)
end
@ -254,6 +147,10 @@ function ReaderCoptListener:onResume()
self:headerRefresh()
end
function ReaderCoptListener:onLeaveStandby()
self:headerRefresh()
end
function ReaderCoptListener:onOutOfScreenSaver()
if not self._delayed_screensaver then
return
@ -266,12 +163,11 @@ end
-- Unschedule on these events
ReaderCoptListener.onCloseDocument = ReaderCoptListener.unscheduleHeaderRefresh
ReaderCoptListener.onSuspend = ReaderCoptListener.unscheduleHeaderRefresh
ReaderCoptListener.onEnterStandby = ReaderCoptListener.unscheduleHeaderRefresh
function ReaderCoptListener:setAndSave(setting, property, value)
self.document._document:setIntProperty(property, value)
self.ui.document._document:setIntProperty(property, value)
G_reader_settings:saveSetting(setting, value)
self.page_info_override = self.page_number == 1 or self.page_count == 1 or self.reading_percent == 1
self.document:setPageInfoOverride("")
-- Have crengine redraw it (even if hidden by the menu at this time)
self.ui.rolling:updateBatteryState()
self:updateHeader()
@ -292,7 +188,7 @@ function ReaderCoptListener:getAltStatusBarMenu()
separator = true,
sub_item_table = {
{
text = _("About alt status bar"),
text = _("About alternate status bar"),
keep_menu_open = true,
callback = function()
UIManager:show(InfoMessage:new{
@ -375,18 +271,15 @@ function ReaderCoptListener:getAltStatusBarMenu()
},
{
text_func = function()
local status = _("Battery status")
local status = _("off")
if self.battery == 1 then
if self.battery_percent == 1 then
status = _("Battery status: percentage")
status = _("percentage")
else
status = _("Battery status: icon")
status = _("icon")
end
end
return status
end,
checked_func = function()
return self.battery == 1
return T(_("Battery status: %1"), status)
end,
sub_item_table = {
{

@ -5,15 +5,15 @@ local CenterContainer = require("ui/widget/container/centercontainer")
local Event = require("ui/event")
local FrameContainer = require("ui/widget/container/framecontainer")
local Geom = require("ui/geometry")
local InputContainer = require("ui/widget/container/inputcontainer")
local Math = require("optmath")
local UIManager = require("ui/uimanager")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local VerticalGroup = require("ui/widget/verticalgroup")
local Device = require("device")
local Screen = Device.screen
local _ = require("gettext")
local ReaderCropping = WidgetContainer:extend{}
local ReaderCropping = InputContainer:new{}
function ReaderCropping:onPageCrop(mode)
self.ui:handleEvent(Event:new("CloseConfigMenu"))
@ -21,6 +21,8 @@ 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
@ -55,6 +57,8 @@ 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"),
@ -167,9 +171,7 @@ function ReaderCropping:setZoomMode(mode)
end
function ReaderCropping:onReadSettings(config)
if config:has("bbox") then
self.document.bbox = config:readSetting("bbox")
end
self.document.bbox = config:readSetting("bbox")
end
function ReaderCropping:onSaveSettings()

@ -2,15 +2,15 @@ local ConfirmBox = require("ui/widget/confirmbox")
local Device = require("device")
local Event = require("ui/event")
local InfoMessage = require("ui/widget/infomessage")
local InputContainer = require("ui/widget/container/inputcontainer")
local SpinWidget = require("ui/widget/spinwidget")
local UIManager = require("ui/uimanager")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local powerd = Device:getPowerDevice()
local _ = require("gettext")
local C_ = _.pgettext
local T = require("ffi/util").template
local ReaderDeviceStatus = WidgetContainer:extend{
local ReaderDeviceStatus = InputContainer:new{
battery_confirm_box = nil,
memory_confirm_box = nil,
}
@ -64,14 +64,13 @@ function ReaderDeviceStatus:init()
if statm then
local dummy, rss = statm:read("*number", "*number")
statm:close()
rss = math.floor(rss * (4096 / 1024 / 1024))
rss = math.floor(rss * 4096 / 1024 / 1024)
if rss >= self.memory_threshold then
if self.memory_confirm_box then
UIManager:close(self.memory_confirm_box)
end
if Device:canRestart() then
local top_wg = UIManager:getTopmostVisibleWidget() or {}
if top_wg.name == "ReaderUI"
if UIManager:getTopWidget() == "ReaderUI"
and G_reader_settings:isTrue("device_status_memory_auto_restart") then
UIManager:show(InfoMessage:new{
text = _("High memory usage!\n\nKOReader is restarting…"),
@ -201,6 +200,7 @@ High level threshold is checked when the device is charging.]]),
right_max = 100,
right_default = 100,
right_hold_step = 5,
default_values = true,
unit = "%",
callback = function(left_value, right_value)
self.battery_threshold = left_value

@ -6,6 +6,7 @@ local DictQuickLookup = require("ui/widget/dictquicklookup")
local Event = require("ui/event")
local Geom = require("ui/geometry")
local InfoMessage = require("ui/widget/infomessage")
local InputContainer = require("ui/widget/container/inputcontainer")
local InputDialog = require("ui/widget/inputdialog")
local JSON = require("json")
local KeyValuePage = require("ui/widget/keyvaluepage")
@ -15,11 +16,7 @@ local NetworkMgr = require("ui/network/manager")
local SortWidget = require("ui/widget/sortwidget")
local Trapper = require("ui/trapper")
local UIManager = require("ui/uimanager")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local ffi = require("ffi")
local C = ffi.C
local ffiUtil = require("ffi/util")
local lfs = require("libs/libkoreader-lfs")
local logger = require("logger")
local time = require("ui/time")
local util = require("util")
@ -60,8 +57,9 @@ local function getIfosInDir(path)
return ifos
end
local ReaderDictionary = WidgetContainer:extend{
local ReaderDictionary = InputContainer:new{
data_dir = nil,
dict_window_list = {},
lookup_msg = _("Searching dictionary for:\n%1"),
}
@ -102,12 +100,11 @@ 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)
end
self.data_dir = G_defaults:readSetting("STARDICT_DATA_DIR") or
self.data_dir = STARDICT_DATA_DIR or
os.getenv("STARDICT_DATA_DIR") or
DataStorage:getDataDir() .. "/data/dict"
@ -135,9 +132,8 @@ function ReaderDictionary:init()
if f then
local content = f:read("*all")
f:close()
local dictname = content:match("\nbookname=(.-)\r?\n")
local dictname = content:match("\nbookname=(.-)\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, {
@ -146,7 +142,6 @@ 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
@ -158,7 +153,7 @@ function ReaderDictionary:init()
self:updateSdcvDictNamesOptions()
if not lookup_history then
lookup_history = LuaData:open(DataStorage:getSettingsDir() .. "/lookup_history.lua", "LookupHistory")
lookup_history = LuaData:open(DataStorage:getSettingsDir() .. "/lookup_history.lua", { name = "LookupHistory" })
end
end
@ -207,9 +202,6 @@ 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()
@ -271,32 +263,18 @@ function ReaderDictionary:addToMainMenu(menu_items)
},
{
text = _("Download dictionaries"),
sub_item_table_func = function() return self:_genDownloadDictionariesMenu() end,
sub_item_table = self:_genDownloadDictionariesMenu()
},
{
text_func = function()
local text = _("Enable fuzzy search")
if G_reader_settings:nilOrFalse("disable_fuzzy_search") then
text = text .. ""
end
return text
end,
text = _("Enable fuzzy search"),
checked_func = function()
if self.ui.doc_settings then
return not self.disable_fuzzy_search
end
return not self.disable_fuzzy_search_fm
return not self.disable_fuzzy_search == true
end,
callback = function()
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
self.disable_fuzzy_search = not self.disable_fuzzy_search
end,
hold_callback = function(touchmenu_instance)
self:toggleFuzzyDefault(touchmenu_instance)
hold_callback = function()
self:toggleFuzzyDefault()
end,
separator = true,
},
@ -424,23 +402,17 @@ function ReaderDictionary:addToMainMenu(menu_items)
end
end
function ReaderDictionary:onLookupWord(word, is_sane, boxes, highlight, link)
function ReaderDictionary:onLookupWord(word, is_sane, boxes, highlight, link, tweak_buttons_func)
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 disable_fuzzy_search, boxes, link)
self:stardictLookup(word, self.enabled_dict_names, not self.disable_fuzzy_search, boxes, link, tweak_buttons_func)
end)
return true
end
@ -503,19 +475,10 @@ 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
@ -628,9 +591,6 @@ 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
@ -677,14 +637,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("\u{00A0}", ' ')
text = text:gsub("\xC2\xA0", ' ') -- U+00A0 no-break space
-- 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("\u{2019}", "'") -- Right single quotation mark
text = text:gsub("\xE2\x80\x99", "'") -- U+2019 (right single quotation mark)
-- Strip punctuation characters around selection
text = util.stripPunctuation(text)
-- Strip some common english grammatical construct
@ -810,16 +770,7 @@ function ReaderDictionary:rawSdcv(words, dict_names, fuzzy_search, lookup_progre
-- definition found, sdcv will output some message on stderr, and
-- let stdout empty) by appending an "echo":
cmd = cmd .. "; echo"
-- NOTE: Bionic doesn't support rpath, but does honor LD_LIBRARY_PATH...
-- Give it a shove so it can actually find the STL.
if Device:isAndroid() then
C.setenv("LD_LIBRARY_PATH", "./libs", 1)
end
local completed, results_str = Trapper:dismissablePopen(cmd, lookup_progress_msg)
if Device:isAndroid() then
-- NOTE: It's unset by default, so this is perfectly fine.
C.unsetenv("LD_LIBRARY_PATH")
end
lookup_cancelled = not completed
if results_str and results_str ~= "\n" then -- \n is when lookup was cancelled
-- sdcv can return multiple results if we passed multiple words to
@ -932,12 +883,18 @@ function ReaderDictionary:startSdcv(word, dict_names, fuzzy_search)
return results
end
function ReaderDictionary:stardictLookup(word, dict_names, fuzzy_search, boxes, link)
function ReaderDictionary:stardictLookup(word, dict_names, fuzzy_search, boxes, link, tweak_buttons_func)
if word == "" then
return
end
local book_title = self.ui.doc_props and self.ui.doc_props.display_title or _("Dictionary lookup")
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
-- Event for plugin to catch lookup with book title
self.ui:handleEvent(Event:new("WordLookedUp", word, book_title))
@ -992,15 +949,17 @@ function ReaderDictionary:stardictLookup(word, dict_names, fuzzy_search, boxes,
return
end
self:showDict(word, tidyMarkup(results), boxes, link)
self:showDict(word, tidyMarkup(results), boxes, link, tweak_buttons_func)
end
function ReaderDictionary:showDict(word, results, boxes, link)
function ReaderDictionary:showDict(word, results, boxes, link, tweak_buttons_func)
if results and results[1] then
logger.dbg("showing quick lookup window", #DictQuickLookup.window_list+1, ":", word, results)
logger.dbg("showing quick lookup window", #self.dict_window_list+1, ":", word, results)
self.dict_window = DictQuickLookup:new{
window_list = self.dict_window_list,
ui = self.ui,
highlight = self.highlight,
tweak_buttons_func = tweak_buttons_func,
dialog = self.dialog,
-- original lookup word
word = word,
@ -1021,6 +980,7 @@ function ReaderDictionary:showDict(word, results, boxes, link)
self:onHtmlDictionaryLinkTapped(dictionary, html_link)
end,
}
table.insert(self.dict_window_list, self.dict_window)
if self.lookup_progress_msg then
-- If we have a lookup InfoMessage that ended up being displayed, make
-- it *not* refresh on close if it is hidden by our DictQuickLookup
@ -1080,6 +1040,7 @@ function ReaderDictionary:downloadDictionaryPrep(dict, size)
local dummy, filename = util.splitFilePathName(dict.url)
local download_location = string.format("%s/%s", self.data_dir, filename)
local lfs = require("libs/libkoreader-lfs")
if lfs.attributes(download_location) then
UIManager:show(ConfirmBox:new{
text = _("File already exists. Overwrite?"),
@ -1113,24 +1074,15 @@ function ReaderDictionary:downloadDictionary(dict, download_location, continue)
--logger.dbg(headers)
file_size = headers and headers["content-length"]
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
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
UIManager:nextTick(function()
UIManager:show(InfoMessage:new{
@ -1141,16 +1093,14 @@ function ReaderDictionary:downloadDictionary(dict, download_location, continue)
end
socketutil:set_timeout(socketutil.FILE_BLOCK_TIMEOUT, socketutil.FILE_TOTAL_TIMEOUT)
local code, headers, status = socket.skip(1, http.request{
local c = socket.skip(1, http.request{
url = dict.url,
sink = ltn12.sink.file(io.open(download_location, "w")),
})
socketutil:reset_timeout()
if code == 200 then
if c == 200 then
logger.dbg("file downloaded to", download_location)
else
logger.dbg("ReaderDictionary: Request failed:", status or code)
logger.dbg("ReaderDictionary: Response headers:", headers)
UIManager:show(InfoMessage:new{
text = _("Could not save file to:\n") .. BD.filepath(download_location),
--timeout = 3,
@ -1158,15 +1108,9 @@ function ReaderDictionary:downloadDictionary(dict, download_location, continue)
return false
end
-- 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)
local ok, error = Device:unpackArchive(download_location, self.data_dir)
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{
@ -1181,24 +1125,6 @@ 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
@ -1222,6 +1148,7 @@ 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
@ -1250,7 +1177,7 @@ function ReaderDictionary:onTogglePreferredDict(dict)
return true
end
function ReaderDictionary:toggleFuzzyDefault(touchmenu_instance)
function ReaderDictionary:toggleFuzzyDefault()
local disable_fuzzy_search = G_reader_settings:isTrue("disable_fuzzy_search")
UIManager:show(MultiConfirmBox:new{
text = T(
@ -1273,14 +1200,12 @@ 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

@ -2,13 +2,13 @@ local BD = require("ui/bidi")
local Device = require("device")
local Geom = require("ui/geometry")
local IconWidget = require("ui/widget/iconwidget")
local InputContainer = require("ui/widget/container/inputcontainer")
local RightContainer = require("ui/widget/container/rightcontainer")
local VerticalGroup = require("ui/widget/verticalgroup")
local VerticalSpan = require("ui/widget/verticalspan")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local Screen = Device.screen
local ReaderDogear = WidgetContainer:extend{}
local ReaderDogear = InputContainer:new{}
function ReaderDogear:init()
-- This image could be scaled for DPI (with scale_for_dpi=true, scale_factor=0.7),
@ -17,8 +17,8 @@ function ReaderDogear:init()
-- to not overwrite the book text.
-- For other documents, there is no easy way to know if valuable content
-- may be hidden by the icon (kopt's page_margin is quite obscure).
self.dogear_min_size = math.ceil(math.min(Screen:getWidth(), Screen:getHeight()) * (1/40))
self.dogear_max_size = math.ceil(math.min(Screen:getWidth(), Screen:getHeight()) * (1/32))
self.dogear_min_size = math.ceil(math.min(Screen:getWidth(), Screen:getHeight()) / 40)
self.dogear_max_size = math.ceil(math.min(Screen:getWidth(), Screen:getHeight()) / 32)
self.dogear_size = nil
self.dogear_y_offset = 0
self.top_pad = nil
@ -54,17 +54,24 @@ function ReaderDogear:setupDogear(new_dogear_size)
end
function ReaderDogear:onReadSettings(config)
if self.ui.rolling then
if not self.ui.document.info.has_pages then
-- Adjust to CreDocument margins (as done in ReaderTypeset)
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 }
local h_margins = config:readSetting("copt_h_page_margins")
or G_reader_settings:readSetting("copt_h_page_margins")
or 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 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 DCREREADER_CONFIG_B_MARGIN_SIZES_LARGE
local margins = { h_margins[1], t_margin, h_margins[2], b_margin }
self:onSetPageMargins(margins)
end
end
function ReaderDogear:onSetPageMargins(margins)
if not self.ui.rolling then
if self.ui.document.info.has_pages then
-- we may get called by readerfooter (when hiding the footer)
-- on pdf documents and get margins=nil
return
@ -79,7 +86,7 @@ function ReaderDogear:onSetPageMargins(margins)
end
function ReaderDogear:updateDogearOffset()
if not self.ui.rolling then
if self.ui.document.info.has_pages then
return
end
self.dogear_y_offset = 0
@ -94,14 +101,10 @@ function ReaderDogear:updateDogearOffset()
end
end
function ReaderDogear:onReaderReady()
self:updateDogearOffset()
end
function ReaderDogear:onDocumentRerendered()
function ReaderDogear:onUpdatePos()
-- Catching the top status bar toggling with :onSetStatusLine()
-- would be too early. But "DocumentRerendered" is sent after
-- it has been applied
-- would be too early. But "UpdatePos" is sent after it has
-- been applied
self:updateDogearOffset()
end

@ -1,47 +1,23 @@
local Geom = require("ui/geometry")
local IconWidget = require("ui/widget/iconwidget")
local InputContainer = require("ui/widget/container/inputcontainer")
local LeftContainer = require("ui/widget/container/leftcontainer")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local Screen = require("device").screen
local ReaderFlipping = WidgetContainer:extend{
-- 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",
},
local ReaderFlipping = InputContainer:new{
orig_reflow_mode = 0,
}
function ReaderFlipping:init()
local icon_size = Screen:scaleBySize(32)
self.flipping_widget = IconWidget:new{
local 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 = self.flipping_widget:getSize().h},
self.flipping_widget,
dimen = Geom:new{w = Screen:getWidth(), h = widget:getSize().h},
widget,
}
self:resetLayout()
end
@ -54,63 +30,4 @@ 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,76 +1,60 @@
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 Font = require("ui/font")
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 logger = require("logger")
local util = require("util")
local _ = require("gettext")
local T = require("ffi/util").template
local _ = require("gettext")
local C_ = _.pgettext
local optionsutil = require("ui/data/optionsutil")
local ReaderFont = InputContainer:extend{
local ReaderFont = InputContainer:new{
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},
gestureScale = Screen:getWidth() * FRONTLIGHT_SENSITIVITY_DECREASE,
}
-- Keep a list of the new fonts seen at launch
local newly_added_fonts = nil -- not yet filled
function ReaderFont:init()
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")
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
-- Build face_table for menu
self.face_table = {}
-- Font settings
table.insert(self.face_table, {
text = _("Font settings"),
sub_item_table = self:getFontSettingsTable(),
})
table.insert(self.face_table, {
text_func = function()
local nb_family_fonts = 0
local g_font_family_fonts = G_reader_settings:readSetting("cre_font_family_fonts", {})
for family, name in pairs(g_font_family_fonts) do
if self.font_family_fonts[family] then
nb_family_fonts = nb_family_fonts + 1
elseif g_font_family_fonts[family] and self.font_family_fonts[family] ~= false then
nb_family_fonts = nb_family_fonts + 1
end
end
if nb_family_fonts > 0 then
-- @translators 'font-family' is a CSS property name, keep it untranslated
return T(_("Font-family fonts (%1)"), nb_family_fonts)
end
return _("Font-family fonts")
end,
sub_item_table_func = function() return self:getFontFamiliesTable() end,
separator = true,
})
-- 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
for k,v in ipairs(face_list) do
local font_filename, font_faceindex, is_monospace = cre.getFontFaceFilenameAndFaceIndex(v)
table.insert(self.face_table, {
text_func = function()
@ -94,9 +78,6 @@ function ReaderFont:setupFaceMenuTable()
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)
@ -108,10 +89,6 @@ function ReaderFont:setupFaceMenuTable()
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)
@ -122,42 +99,12 @@ function ReaderFont:setupFaceMenuTable()
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
-- 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
self.ui.menu:registerToMainMenu(self)
end
ReaderFont.onPhysicalKeyboardConnected = ReaderFont.registerKeyEvents
function ReaderFont:onSetDimensions(dimen)
self.dimen = dimen
end
@ -168,21 +115,57 @@ function ReaderFont:onReadSettings(config)
or self.ui.document.default_font
self.ui.document:setFontFace(self.font_face)
local header_font = G_reader_settings:readSetting("header_font") or self.ui.document.header_font
self.ui.document:setHeaderFont(header_font)
self.ui.document:setFontSize(Screen:scaleBySize(self.configurable.font_size))
self.ui.document:setFontBaseWeight(self.configurable.font_base_weight)
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()
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 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 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 DCREREADER_CONFIG_DEFAULT_FONT_GAMMA
or 15 -- gamma = 1.0
self.ui.document:setGammaIndex(self.gamma_index)
-- Dirty hack: we have to add following call in order to set
-- m_is_rendered(member of LVDocView) to true. Otherwise position inside
@ -193,34 +176,64 @@ 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:onSetFontSize(self.configurable.font_size + delta)
self.font_size = self.font_size + delta
self.ui:handleEvent(Event:new("SetFontSize", self.font_size))
return true
end
function ReaderFont:onSetFontSize(size)
size = math.max(12, math.min(size, 255))
self.configurable.font_size = size
self.ui.document:setFontSize(Screen:scaleBySize(size))
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))
self.ui:handleEvent(Event:new("UpdatePos"))
Notification:notify(T(_("Font size set to: %1."), size))
Notification:notify(T(_("Font size set to: %1."), self.font_size))
return true
end
function ReaderFont:onSetLineSpace(space)
space = math.max(50, math.min(space, 200))
self.configurable.line_spacing = space
self.ui.document:setInterlineSpacePercent(space)
self.line_space_percent = math.min(200, math.max(50, space))
self.ui.document:setInterlineSpacePercent(self.line_space_percent)
self.ui:handleEvent(Event:new("UpdatePos"))
Notification:notify(T(_("Line spacing set to: %1%."), space))
Notification:notify(T(_("Line spacing set to: %1%."), self.line_space_percent))
return true
end
function ReaderFont:onSetFontBaseWeight(weight)
self.configurable.font_base_weight = weight
self.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)))
@ -228,7 +241,7 @@ function ReaderFont:onSetFontBaseWeight(weight)
end
function ReaderFont:onSetFontHinting(mode)
self.configurable.font_hinting = mode
self.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)))
@ -236,7 +249,7 @@ function ReaderFont:onSetFontHinting(mode)
end
function ReaderFont:onSetFontKerning(mode)
self.configurable.font_kerning = mode
self.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)))
@ -244,7 +257,7 @@ function ReaderFont:onSetFontKerning(mode)
end
function ReaderFont:onSetWordSpacing(values)
self.configurable.word_spacing = values
self.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]))
@ -252,7 +265,7 @@ function ReaderFont:onSetWordSpacing(values)
end
function ReaderFont:onSetWordExpansion(value)
self.configurable.word_expansion = value
self.word_expansion = value
self.ui.document:setWordExpansion(value)
self.ui:handleEvent(Event:new("UpdatePos"))
Notification:notify(T(_("Word expansion set to: %1%."), value))
@ -260,7 +273,7 @@ function ReaderFont:onSetWordExpansion(value)
end
function ReaderFont:onSetCJKWidthScaling(value)
self.configurable.cjk_width_scaling = value
self.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))
@ -268,8 +281,8 @@ function ReaderFont:onSetCJKWidthScaling(value)
end
function ReaderFont:onSetFontGamma(gamma)
self.configurable.font_gamma = gamma
self.ui.document:setGammaIndex(gamma)
self.gamma_index = gamma
self.ui.document:setGammaIndex(self.gamma_index)
local gamma_level = self.ui.document:getGammaLevel()
self.ui:handleEvent(Event:new("RedrawCurrentView"))
Notification:notify(T(_("Font gamma set to: %1."), gamma_level))
@ -278,7 +291,16 @@ end
function ReaderFont:onSaveSettings()
self.ui.doc_settings:saveSetting("font_face", self.font_face)
self.ui.doc_settings:saveSetting("font_family_fonts", self.font_family_fonts)
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)
end
function ReaderFont:onSetFont(face)
@ -297,7 +319,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 (🄼)?"), face), -- [M] is U+1F13C
text = T(_("Would you like %1 to be used as the default font (★), or the monospace font (\u{1F13C})?"), face),
choice1_text = _("Default"),
choice1_callback = function()
G_reader_settings:saveSetting("cre_font", face)
@ -334,319 +356,42 @@ 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_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
sub_item_table = self.face_table,
}
end
function ReaderFont:gesToFontSize(ges)
-- Dispatcher feeds us a number, not a gesture
if type(ges) ~= "table" then return ges end
if ges.distance == nil then
ges.distance = 1
end
-- Compute the scaling based on the gesture's direction (for pinch/spread)
local step
if ges.direction and ges.direction == "vertical" then
step = math.ceil(2 * #self.steps * ges.distance / Screen:getHeight())
elseif ges.direction and ges.direction == "horizontal" then
step = math.ceil(2 * #self.steps * ges.distance / Screen:getWidth())
elseif ges.direction and ges.direction == "diagonal" then
local screen_diagonal = math.sqrt(Screen:getWidth()^2 + Screen:getHeight()^2)
step = math.ceil(2 * #self.steps * ges.distance / screen_diagonal)
else
step = math.ceil(2 * #self.steps * ges.distance / math.min(Screen:getWidth(), Screen:getHeight()))
end
local step = math.ceil(2 * #self.steps * ges.distance / self.gestureScale)
local delta_int = self.steps[step] or self.steps[#self.steps]
return delta_int
end
function ReaderFont:onIncreaseFontSize(ges)
local delta_int = self:gesToFontSize(ges)
Notification:notify(_("Increasing font size…"), nil, true)
Notification:notify(_("Increasing font size…"), true)
self:onChangeSize(delta_int)
return true
end
function ReaderFont:onDecreaseFontSize(ges)
local delta_int = self:gesToFontSize(ges)
Notification:notify(_("Decreasing font size…"), nil, true)
Notification:notify(_("Decreasing font size…"), true)
self:onChangeSize(-delta_int)
return true
end
local font_family_info_text = _([[
In HTML/CSS based documents like EPUBs, stylesheets can specify to use fonts by family instead of a specific font name.
Except for monospace and math, KOReader uses your default font for any family name.
You can associate a specific font to each family if you care about the distinction.
A long-press on a font name will make the association global (), so it applies to all your books. This is the preferred approach.
A tap will only affect the current book.
If you encounter a book where such families are abused to the point where your default font is hardly used, you can quickly disable a family font for this book by unchecking the association.]])
local FONT_FAMILIES = {
-- On 1st page
-- @translators These are typography font family names as used in CSS, they can be kept untranslated if they are used more commonly than their translation
{ "serif", _("Serif") },
{ "sans-serif", _("Sans-serif") },
{ "monospace", _("Monospace") },
-- On 2nd page
{ "cursive", _("Cursive") },
{ "fantasy", _("Fantasy") },
{ "emoji", _("Emoji") .. " 😊" }, -- U+1F60A
{ "fangsong", _("Fang Song") .. " 仿宋" }, -- U+4EFF U+5B8B
{ "math", _("Math") },
}
function ReaderFont:updateFontFamilyFonts()
-- Note: when no font is specified for a family, we provide an empty string to
-- crengine, which is enough to have it kill any "css_ff_inherit" and have the
-- font picking code select a new font for a node - which will pick the main
-- font (we have here in self.font_face) because of its increased bias (or the
-- monospace font we also added with bias).
-- So, we don't need to insert self.font_face in the list for unset family fonts,
-- which would otherwise need us to call updateFontFamilyFonts() everytime we
-- change the main font face.
local g_font_family_fonts = G_reader_settings:readSetting("cre_font_family_fonts", {})
local family_fonts = {}
for i, family in ipairs(FONT_FAMILIES) do
local family_tag = family[1]
if self.font_family_fonts[family_tag] then
family_fonts[family_tag] = self.font_family_fonts[family_tag]
elseif g_font_family_fonts[family_tag] and self.font_family_fonts[family_tag] ~= false then
family_fonts[family_tag] = g_font_family_fonts[family_tag]
elseif family_tag == "math" then
-- If no math font set, force using our math-enabled shipped FreeSerif (which
-- saves crengine some work iterating its hardcoded list of math fonts).
family_fonts[family_tag] = "FreeSerif"
end
end
self.ui.document:setFontFamilyFontFaces(family_fonts, G_reader_settings:isTrue("cre_font_family_ignore_font_names"))
self.ui:handleEvent(Event:new("UpdatePos"))
end
function ReaderFont:getFontFamiliesTable()
local g_font_family_fonts = G_reader_settings:readSetting("cre_font_family_fonts", {})
local families_table = {
{
text = _("About font-family fonts"),
callback = function()
UIManager:show(InfoMessage:new{
text = font_family_info_text,
})
end,
keep_menu_open = true,
separator = true,
},
{
text = _("Ignore publisher font names when font-family is set"),
checked_func = function()
return G_reader_settings:isTrue("cre_font_family_ignore_font_names")
end,
callback = function()
G_reader_settings:flipNilOrFalse("cre_font_family_ignore_font_names")
self:updateFontFamilyFonts()
end,
help_text = _([[
In a CSS font-family declaration, publishers may precede a family name with one or more font names, that are to be used if found among embedded fonts or your own fonts.
Enabling this will ignore such font names and make sure your preferred family fonts are used.]]),
keep_menu_open = true,
separator = true,
},
max_per_page = 5,
}
local face_to_filename = {}
local face_list = cre.getFontFaces()
for i, family in ipairs(FONT_FAMILIES) do
local family_tag, family_name = family[1], family[2]
-- If none family font is set, crengine will use the main user set font,
-- except for 2 specific cases.
local unset_font_main_text = _("(main font)")
local unset_font_choice_text = _("Use main font")
if family_tag == "monospace" then
local monospace_font = G_reader_settings:readSetting("monospace_font") or self.ui.document.monospace_font
unset_font_main_text = _("(default monospace font)")
unset_font_choice_text = T(_("Use default monospace font: %1"), monospace_font)
elseif family_tag == "math" then
unset_font_main_text = _("(default math font)")
unset_font_choice_text = _("Use default math font")
-- The default math font would be FreeSerif, but crengine would pick a better
-- one among a hardcoded list if any is found. So, don't say more than that.
end
local family_table = {
menu_item_id = family_tag,
text_func = function()
local text
if self.font_family_fonts[family_tag] then
text = BD.wrap(self.font_family_fonts[family_tag])
elseif g_font_family_fonts[family_tag] then
-- Show it even if self.font_family_fonts[family_tag]==false,
-- the checkbox will indicate whether it is used or not
text = BD.wrap(g_font_family_fonts[family_tag]) .. ""
else
text = unset_font_main_text .. ""
end
return T("%1: %2", family_name, text)
end,
font_func = function(size)
if G_reader_settings:nilOrTrue("font_menu_use_font_face") then
local font_name
if self.font_family_fonts[family_tag] then
font_name = self.font_family_fonts[family_tag]
elseif g_font_family_fonts[family_tag] then
font_name = g_font_family_fonts[family_tag]
end
if font_name and face_to_filename[font_name] then
local filename_idx = face_to_filename[font_name]
return Font:getFace(filename_idx[1], size, filename_idx[2])
end
end
end,
checked_func = function()
if self.font_family_fonts[family_tag] then
return true
elseif g_font_family_fonts[family_tag] and self.font_family_fonts[family_tag] ~= false then
return true
end
return false
end,
checkmark_callback = function()
if self.font_family_fonts[family_tag] then
if g_font_family_fonts[family_tag] then
self.font_family_fonts[family_tag] = false
else
self.font_family_fonts[family_tag] = nil
end
else
if g_font_family_fonts[family_tag] and self.font_family_fonts[family_tag] ~= false then
self.font_family_fonts[family_tag] = false
else
self.font_family_fonts[family_tag] = nil
end
end
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,
},
{
text_func = function()
local text = unset_font_choice_text
if not g_font_family_fonts[family_tag] then
text = text .. ""
end
return text
end,
callback = function()
self.font_family_fonts[family_tag] = false
self:updateFontFamilyFonts()
end,
hold_callback = function(touchmenu_instance)
g_font_family_fonts[family_tag] = nil
self.font_family_fonts[family_tag] = nil
self:updateFontFamilyFonts()
if touchmenu_instance then touchmenu_instance:updateItems() end
end,
checked_func = function()
if self.font_family_fonts[family_tag] == false then
return true
end
return not self.font_family_fonts[family_tag] and not g_font_family_fonts[family_tag]
end,
separator = true,
},
max_per_page = 5,
},
}
for k, v in ipairs(face_list) do
local font_filename, font_faceindex, is_monospace, has_ot_math, has_emojis = cre.getFontFaceFilenameAndFaceIndex(v)
if i == 1 then
face_to_filename[v] = { font_filename, font_faceindex }
end
local ignore = false
if family_tag == "monospace" and not is_monospace then
ignore = true
end
if family_tag == "math" and not has_ot_math then
ignore = true
end
if family_tag == "emoji" and not has_emojis then
ignore = true
end
if not ignore then
table.insert(family_table.sub_item_table, {
text_func = function()
local text = v
if font_filename and font_faceindex then
text = FontList:getLocalizedFontName(font_filename, font_faceindex) or text
end
if g_font_family_fonts[family_tag] == v then
text = text .. ""
end
return text
end,
font_func = function(size)
if G_reader_settings:nilOrTrue("font_menu_use_font_face") then
if font_filename and font_faceindex then
return Font:getFace(font_filename, size, font_faceindex)
end
end
end,
callback = function()
if g_font_family_fonts[family_tag] == v then
self.font_family_fonts[family_tag] = nil
else
self.font_family_fonts[family_tag] = v
-- We don't use :notify() as we don't want this notification to be masked,
-- to let the user know it's not global (so he has to use long-press)
UIManager:show(Notification:new{ text = _("Font family font set for this book only.") })
-- Be sure it is shown before the re-rendering (which may take some time)
UIManager:forceRePaint()
end
self:updateFontFamilyFonts()
end,
hold_callback = function(touchmenu_instance)
g_font_family_fonts[family_tag] = v
self.font_family_fonts[family_tag] = nil
self:updateFontFamilyFonts()
if touchmenu_instance then touchmenu_instance:updateItems() end
end,
checked_func = function()
if self.font_family_fonts[family_tag] then
return self.font_family_fonts[family_tag] == v
elseif g_font_family_fonts[family_tag] == v and self.font_family_fonts[family_tag] ~= false then
return true
end
return false
end,
menu_item_id = family_tag .. "_" .. v,
})
end
end
family_table.sub_item_table.open_on_menu_item_id_func = function()
if self.font_family_fonts[family_tag] then
return family_tag .. "_" .. self.font_family_fonts[family_tag]
elseif g_font_family_fonts[family_tag] and self.font_family_fonts[family_tag] ~= false then
return family_tag .. "_" .. g_font_family_fonts[family_tag]
end
end
table.insert(families_table, family_table)
end
return families_table
end
function ReaderFont:getFontSettingsTable()
local settings_table = {}
@ -666,35 +411,6 @@ 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,
})
@ -757,7 +473,7 @@ This may help with Greek words among Latin text (as Latin fonts often do not hav
G_reader_settings:saveSetting("cre_monospace_scaling", scale)
self.ui.document:setMonospaceFontScaling(scale)
self.ui:handleEvent(Event:new("UpdatePos"))
end,
end
})
end,
help_text = _([[
@ -770,75 +486,16 @@ This setting allows scaling all monospace fonts by this percentage so they can f
text = _("Generate font test document"),
callback = function()
UIManager:show(ConfirmBox:new{
text = _("Would you like to generate an HTML document showing some sample text rendered with each available font?"),
text = _("Would you like to generate an HTML document showing some sample text rendered with each available font?");
ok_callback = function()
self:buildFontsTestDocument()
end,
end
})
end,
})
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

@ -1,12 +1,13 @@
local Event = require("ui/event")
local InputContainer = require("ui/widget/container/inputcontainer")
local InputDialog = require("ui/widget/inputdialog")
local SkimToWidget = require("ui/widget/skimtowidget")
local UIManager = require("ui/uimanager")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local _ = require("gettext")
local T = require("ffi/util").template
local ReaderGoto = WidgetContainer:extend{}
local ReaderGoto = InputContainer:new{
}
function ReaderGoto:init()
self.ui.menu:registerToMainMenu(self)
@ -28,7 +29,12 @@ function ReaderGoto:addToMainMenu(menu_items)
end
function ReaderGoto:onShowGotoDialog()
local curr_page = self.ui:getCurrentPage()
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 input_hint
if self.ui.pagemap and self.ui.pagemap:wantsPageLabels() then
input_hint = T("@%1 (%2 - %3)", self.ui.pagemap:getCurrentPageLabel(true),
@ -53,7 +59,16 @@ x for an absolute page number
text = _("Skim"),
callback = function()
self:close()
self:onShowSkimtoDialog()
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)
end,
},
{
@ -181,28 +196,4 @@ function ReaderGoto:onGoToEnd()
return true
end
function ReaderGoto:onGoToRandomPage()
local page_count = self.document:getPageCount()
if page_count == 1 then return true end
local current_page = self.ui:getCurrentPage()
if self.pages_pool == nil then
self.pages_pool = {}
end
if #self.pages_pool == 0 or (#self.pages_pool == 1 and self.pages_pool[1] == current_page) then
for i = 1, page_count do
self.pages_pool[i] = i
end
end
while true do
local random_page_idx = math.random(1, #self.pages_pool)
local random_page = self.pages_pool[random_page_idx]
if random_page ~= current_page then
table.remove(self.pages_pool, random_page_idx)
self.ui.link:addCurrentLocationToStack()
self.ui:handleEvent(Event:new("GotoPage", random_page))
return true
end
end
end
return ReaderGoto

@ -1,719 +0,0 @@
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

@ -1,15 +1,9 @@
local EventListener = require("ui/widget/eventlistener")
local DHINTCOUNT = G_defaults:readSetting("DHINTCOUNT")
local ReaderHinting = EventListener:extend{
hinting_states = nil, -- array
local ReaderHinting = EventListener:new{
hinting_states = {}
}
function ReaderHinting:init()
self.hinting_states = {}
end
function ReaderHinting:onHintPage()
if not self.view.hinting then return true end
for i=1, DHINTCOUNT do

@ -2,8 +2,9 @@ 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{}
local ReaderKoptListener = EventListener:new{}
function ReaderKoptListener:setZoomMode(zoom_mode)
if self.document.configurable.text_wrap == 1 then
@ -18,10 +19,15 @@ 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 = ReaderZooming.zoom_mode_label[normal_zoom_mode] and normal_zoom_mode or ReaderZooming.DEFAULT_ZOOM_MODE
normal_zoom_mode = util.arrayContains(ReaderZooming.available_zoom_modes, 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.ui:handleEvent(Event:new("GammaUpdate", self.document.configurable.contrast))
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))
-- 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
@ -62,16 +68,13 @@ end
function ReaderKoptListener:onDocLangUpdate(lang)
if lang == "chi_sim" or lang == "chi_tra" or
lang == "jpn" or lang == "kor" then
self.document.configurable.word_spacing = G_defaults:readSetting("DKOPTREADER_CONFIG_WORD_SPACINGS")[1]
self.document.configurable.word_spacing = DKOPTREADER_CONFIG_WORD_SPACINGS[1]
else
self.document.configurable.word_spacing = G_defaults:readSetting("DKOPTREADER_CONFIG_WORD_SPACINGS")[3]
self.document.configurable.word_spacing = DKOPTREADER_CONFIG_WORD_SPACINGS[3]
end
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

@ -13,10 +13,10 @@ local Screen = Device.screen
local _ = require("gettext")
local T = require("ffi/util").template
local ReaderMenu = InputContainer:extend{
local ReaderMenu = InputContainer:new{
tab_item_table = nil,
menu_items = nil, -- table, mandatory
registered_widgets = nil, -- array
menu_items = {},
registered_widgets = {},
}
function ReaderMenu:init()
@ -56,50 +56,38 @@ 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.PressMenu = { { "Menu" } }
self.key_events.TapShowMenu = { { "Menu" }, doc = "show menu", }
if Device:hasFewKeys() then
self.key_events.PressMenu = { { { "Menu", "Right" } } }
self.key_events.TapShowMenu = { { { "Menu", "Right" } }, doc = "show menu", }
end
else
-- 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" } }
-- map menu key to only top menu because bottom menu is only
-- designed for touch devices
self.key_events.ShowMenu = { { "Menu" }, doc = "show menu", }
if Device:hasFewKeys() then
self.key_events.ShowMenu = { { { "Menu", "Right" } } }
self.key_events.ShowMenu = { { { "Menu", "Right" } }, doc = "show menu", }
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:initGesListener()
function ReaderMenu:onReaderReady()
-- deligate gesture listener to readerui
self.ges_events = {}
self.onGesture = nil
if not Device:isTouchDevice() then return end
local DTAP_ZONE_MENU = G_defaults:readSetting("DTAP_ZONE_MENU")
local DTAP_ZONE_MENU_EXT = G_defaults:readSetting("DTAP_ZONE_MENU_EXT")
self.ui:registerTouchZones({
{
id = "readermenu_tap",
@ -179,8 +167,6 @@ function ReaderMenu:initGesListener()
})
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)
@ -190,46 +176,23 @@ function ReaderMenu:setUpdateItemTable()
end
-- typeset tab
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,
},
{
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,
})
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)
end,
},
},
})
end,
}
self.menu_items.page_overlap = require("ui/elements/page_overlap")
-- settings tab
@ -239,12 +202,10 @@ 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")
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")
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
-- insert DjVu render mode submenu just before the last entry (show advanced)
-- this is a bit of a hack
@ -254,14 +215,10 @@ function ReaderMenu:setUpdateItemTable()
if Device:supportsScreensaver() then
local ss_book_settings = {
text = _("Do not show this book cover on sleep screen"),
text = _("Exclude this book's cover from screensaver"),
enabled_func = function()
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
return not (self.ui == nil or self.ui.document == nil)
and G_reader_settings:readSetting("screensaver_type") == "cover"
end,
checked_func = function()
return self.ui and self.ui.doc_settings and self.ui.doc_settings:isTrue("exclude_screensaver")
@ -285,7 +242,7 @@ function ReaderMenu:setUpdateItemTable()
end
table.insert(screensaver_sub_item_table, ss_book_settings)
self.menu_items.screensaver = {
text = _("Sleep screen"),
text = _("Screensaver"),
sub_item_table = screensaver_sub_item_table,
}
end
@ -346,21 +303,6 @@ 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
@ -418,7 +360,6 @@ function ReaderMenu:onShowMenu(tab_index)
end
local menu_container = CenterContainer:new{
covers_header = true,
ignore = "height",
dimen = Screen:getSize(),
}
@ -443,7 +384,7 @@ function ReaderMenu:onShowMenu(tab_index)
end
main_menu.close_callback = function()
self:onCloseReaderMenu()
self.ui:handleEvent(Event:new("CloseReaderMenu"))
end
main_menu.touch_menu_callback = function ()
@ -458,24 +399,16 @@ function ReaderMenu:onShowMenu(tab_index)
end
function ReaderMenu:onCloseReaderMenu()
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
if self.menu_container then
self.last_tab_index = self.menu_container[1].last_index
self:onSaveSettings()
UIManager:close(self.menu_container)
end
return true
end
function ReaderMenu:onSetDimensions(dimen)
-- 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()
self:onCloseReaderMenu()
end
function ReaderMenu:onCloseDocument()
@ -498,10 +431,10 @@ function ReaderMenu:_getTabIndexFromLocation(ges)
if not ges then
return self.last_tab_index
-- if the start position is far right
elseif ges.pos.x > Screen:getWidth() * (2/3) then
elseif ges.pos.x > 2 * Screen:getWidth() / 3 then
return BD.mirroredUILayout() and 1 or #self.tab_item_table
-- if the start position is far left
elseif ges.pos.x < Screen:getWidth() * (1/3) then
elseif ges.pos.x < Screen:getWidth() / 3 then
return BD.mirroredUILayout() and #self.tab_item_table or 1
-- if center return the last index
else
@ -514,7 +447,7 @@ function ReaderMenu:onSwipeShowMenu(ges)
if G_reader_settings:nilOrTrue("show_bottom_menu") then
self.ui:handleEvent(Event:new("ShowConfigMenu"))
end
self:onShowMenu(self:_getTabIndexFromLocation(ges))
self.ui:handleEvent(Event:new("ShowMenu", self:_getTabIndexFromLocation(ges)))
self.ui:handleEvent(Event:new("HandledAsSwipe")) -- cancel any pan scroll made
return true
end
@ -525,21 +458,13 @@ function ReaderMenu:onTapShowMenu(ges)
if G_reader_settings:nilOrTrue("show_bottom_menu") then
self.ui:handleEvent(Event:new("ShowConfigMenu"))
end
self:onShowMenu(self:_getTabIndexFromLocation(ges))
self.ui:handleEvent(Event:new("ShowMenu", 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:onCloseReaderMenu()
self.ui:handleEvent(Event:new("CloseReaderMenu"))
self.ui:handleEvent(Event:new("CloseConfigMenu"))
end
@ -551,11 +476,6 @@ 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

@ -6,18 +6,18 @@ local Font = require("ui/font")
local FrameContainer = require("ui/widget/container/framecontainer")
local Geom = require("ui/geometry")
local GestureRange = require("ui/gesturerange")
local InputContainer = require("ui/widget/container/inputcontainer")
local Menu = require("ui/widget/menu")
local MultiConfirmBox = require("ui/widget/multiconfirmbox")
local OverlapGroup = require("ui/widget/overlapgroup")
local TextBoxWidget = require("ui/widget/textboxwidget")
local TextWidget = require("ui/widget/textwidget")
local UIManager = require("ui/uimanager")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local Screen = Device.screen
local T = require("ffi/util").template
local _ = require("gettext")
local ReaderPageMap = WidgetContainer:extend{
local ReaderPageMap = InputContainer:new{
label_font_face = "ffont",
label_default_font_size = 14,
-- Black so it's readable (and non-gray-flashing on GloHD)
@ -87,7 +87,9 @@ function ReaderPageMap:resetLayout()
end
function ReaderPageMap:onReadSettings(config)
local h_margins = self.ui.document.configurable.h_page_margins
local h_margins = config:readSetting("copt_h_page_margins")
or G_reader_settings:readSetting("copt_h_page_margins")
or DCREREADER_CONFIG_H_MARGIN_SIZES_MEDIUM
self.max_left_label_width = Screen:scaleBySize(h_margins[1])
self.max_right_label_width = Screen:scaleBySize(h_margins[2])
@ -280,11 +282,8 @@ 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, idx, count = self.ui.document:getPageMapCurrentPageLabel()
if clean_label then
label = self:cleanPageLabel(label)
end
return label, idx, count
local label = self.ui.document:getPageMapCurrentPageLabel()
return clean_label and self:cleanPageLabel(label) or label
end
function ReaderPageMap:getFirstPageLabel(clean_label)
@ -409,7 +408,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,6 +4,7 @@ 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")
@ -26,135 +27,87 @@ local function copyPageState(page_state)
end
local ReaderPaging = InputContainer:extend{
local ReaderPaging = InputContainer:new{
pan_rate = 30, -- default 30 ops, will be adjusted in readerui
current_page = 0,
number_of_pages = 0,
visible_area = nil,
page_area = nil,
overlap = Screen:scaleBySize(G_defaults:readSetting("DOVERLAPPIXELS")),
overlap = Screen:scaleBySize(DOVERLAPPIXELS),
page_flipping_mode = false,
bookmark_flipping_mode = false,
flip_steps = {0, 1, 2, 5, 10, 20, 50, 100},
flip_steps = {0,1,2,5,10,20,50,100},
}
function ReaderPaging:init()
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 = {}
if Device:hasKeys() then
self.key_events.GotoNextPage = {
{ { "RPgFwd", "LPgFwd", not Device:hasFewKeys() and "Right" } },
event = "GotoViewRel",
args = 1,
{ {"RPgFwd", "LPgFwd", "Right" } }, doc = "go to next page",
event = "GotoViewRel", args = 1,
}
self.key_events.GotoPrevPage = {
{ { "RPgBack", "LPgBack", not Device:hasFewKeys() and "Left" } },
event = "GotoViewRel",
args = -1,
{ { "RPgBack", "LPgBack", "Left" } }, doc = "go to previous page",
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" },
event = "GotoPosRel",
args = 1,
{ {"Down" } }, doc = "go to next position",
event = "GotoPosRel", args = 1,
}
self.key_events.GotoPrevPos = {
{ "Up" },
event = "GotoPosRel",
args = -1,
{ { "Up" } }, doc = "go to previous position",
event = "GotoPosRel", args = -1,
}
end
if Device:hasKeyboard() then
self.key_events.GotoFirst = {
{ "1" },
event = "GotoPercent",
args = 0,
{"1"}, doc = "go to start", event = "GotoPercent", args = 0,
}
self.key_events.Goto11 = {
{ "2" },
event = "GotoPercent",
args = 11,
{"2"}, doc = "go to 11%", event = "GotoPercent", args = 11,
}
self.key_events.Goto22 = {
{ "3" },
event = "GotoPercent",
args = 22,
{"3"}, doc = "go to 22%", event = "GotoPercent", args = 22,
}
self.key_events.Goto33 = {
{ "4" },
event = "GotoPercent",
args = 33,
{"4"}, doc = "go to 33%", event = "GotoPercent", args = 33,
}
self.key_events.Goto44 = {
{ "5" },
event = "GotoPercent",
args = 44,
{"5"}, doc = "go to 44%", event = "GotoPercent", args = 44,
}
self.key_events.Goto55 = {
{ "6" },
event = "GotoPercent",
args = 55,
{"6"}, doc = "go to 55%", event = "GotoPercent", args = 55,
}
self.key_events.Goto66 = {
{ "7" },
event = "GotoPercent",
args = 66,
{"7"}, doc = "go to 66%", event = "GotoPercent", args = 66,
}
self.key_events.Goto77 = {
{ "8" },
event = "GotoPercent",
args = 77,
{"8"}, doc = "go to 77%", event = "GotoPercent", args = 77,
}
self.key_events.Goto88 = {
{ "9" },
event = "GotoPercent",
args = 88,
{"9"}, doc = "go to 88%", event = "GotoPercent", args = 88,
}
self.key_events.GotoLast = {
{ "0" },
event = "GotoPercent",
args = 100,
{"0"}, doc = "go to end", 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()
@ -212,6 +165,10 @@ 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()
@ -247,7 +204,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 ~= 0 and pos or nil
self.page_positions[page] = pos
self.ui:handleEvent(Event:new("PagePositionUpdated"))
end
@ -289,11 +246,15 @@ 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
@ -581,7 +542,7 @@ end
function ReaderPaging:onGotoPercent(percent)
logger.dbg("goto document offset in percent:", percent)
local dest = math.floor(self.number_of_pages * percent * (1/100))
local dest = math.floor(self.number_of_pages * percent / 100)
if dest < 1 then dest = 1 end
if dest > self.number_of_pages then
dest = self.number_of_pages
@ -694,10 +655,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(
@ -712,9 +673,7 @@ function ReaderPaging:onInitScrollPageStates(orig_mode)
blank_area.h = blank_area.h - self.view.page_gap.height
end
if blank_area.h > 0 then
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")
self:_gotoPage(self.current_page + 1, "scrolling")
end
end
self:_gotoPage(self.orig_page, "scrolling")
@ -736,7 +695,7 @@ function ReaderPaging:onUpdateScrollPageGamma(gamma)
return true
end
function ReaderPaging:getNextPageState(blank_area, image_offset)
function ReaderPaging:getNextPageState(blank_area, offset)
local page_area = self.view:getPageArea(
self.view.state.page,
self.view.state.zoom,
@ -744,25 +703,21 @@ function ReaderPaging:getNextPageState(blank_area, image_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, image_offset.x, image_offset.y)
visible_area = visible_area:shrinkInside(page_area, offset.x, 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 = page_offset,
offset = Geom:new{ x = self.view.state.offset.x, y = 0},
visible_area = visible_area,
page_area = page_area,
}
end
function ReaderPaging:getPrevPageState(blank_area, image_offset)
function ReaderPaging:getPrevPageState(blank_area, offset)
local page_area = self.view:getPageArea(
self.view.state.page,
self.view.state.zoom,
@ -771,19 +726,15 @@ function ReaderPaging:getPrevPageState(blank_area, image_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, image_offset.x, image_offset.y)
visible_area = visible_area:shrinkInside(page_area, offset.x, 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 = page_offset,
offset = Geom:new{ x = self.view.state.offset.x, y = 0},
visible_area = visible_area,
page_area = page_area,
}
@ -796,7 +747,7 @@ function ReaderPaging:updateTopPageState(state, blank_area, offset)
w = blank_area.w,
h = blank_area.h,
}
if self.ui.document:getNextPage(state.page) == 0 then -- last page
if state.page == self.number_of_pages then
visible_area:offsetWithin(state.page_area, offset.x, offset.y)
else
visible_area = visible_area:shrinkInside(state.page_area, offset.x, offset.y)
@ -813,7 +764,7 @@ function ReaderPaging:updateBottomPageState(state, blank_area, offset)
w = blank_area.w,
h = blank_area.h,
}
if self.ui.document:getPrevPage(state.page) == 0 then -- first page
if state.page == 1 then
visible_area:offsetWithin(state.page_area, offset.x, offset.y)
else
visible_area = visible_area:shrinkInside(state.page_area, offset.x, offset.y)
@ -841,10 +792,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
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())
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{})
table.insert(page_states, state)
end
end
@ -866,23 +817,13 @@ 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
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())
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{})
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
@ -890,7 +831,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
@ -927,14 +868,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 self.ui.document:getNextPage(last_page_state.page) == 0 and
if last_page_state.page == self.number_of_pages 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{
@ -944,7 +885,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)
@ -967,8 +908,8 @@ function ReaderPaging:onGotoPageRel(diff)
local x_pan_off, y_pan_off = 0, 0
local right_to_left = self.ui.document.configurable.writing_direction and self.ui.document.configurable.writing_direction > 0
local bottom_to_top = self.ui.zooming.zoom_bottom_to_top
local h_progress = 1 - self.ui.zooming.zoom_overlap_h * (1/100)
local v_progress = 1 - self.ui.zooming.zoom_overlap_v * (1/100)
local h_progress = 1 - self.ui.zooming.zoom_overlap_h / 100
local v_progress = 1 - self.ui.zooming.zoom_overlap_v / 100
local old_va = self.visible_area
local old_page = self.current_page
local x, y, w, h = "x", "y", "w", "h"
@ -1024,41 +965,14 @@ function ReaderPaging:onGotoPageRel(diff)
goto_end(x, -x_diff)
end
local function goto_next_page()
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
local new_page = self.current_page + diff
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
@ -1167,18 +1081,7 @@ function ReaderPaging:onGotoPage(number, pos)
end
function ReaderPaging:onGotoRelativePage(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)
self:_gotoPage(self.current_page + number)
return true
end

@ -2,7 +2,7 @@ local InputContainer = require("ui/widget/container/inputcontainer")
local Device = require("device")
local _ = require("gettext")
local ReaderPanning = InputContainer:extend{
local ReaderPanning = InputContainer:new{
-- defaults
panning_steps = {
normal = 50,
@ -13,49 +13,35 @@ 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" },
event = "Panning",
args = {0, -1}
},
{ "Up" }, doc = "move visible area up",
event = "Panning", args = {0, -1} },
MoveDown = {
{ "Down" },
event = "Panning",
args = {0, 1}
},
{ "Down" }, doc = "move visible area down",
event = "Panning", args = {0, 1} },
MoveLeft = {
{ "Left" },
event = "Panning",
args = {-1, 0}
},
{ "Left" }, doc = "move visible area left",
event = "Panning", args = {-1, 0} },
MoveRight = {
{ "Right" },
event = "Panning",
args = {1, 0}
},
{ "Right" }, doc = "move visible area right",
event = "Panning", args = {1, 0} },
}
end
end
ReaderPanning.onPhysicalKeyboardConnected = ReaderPanning.registerKeyEvents
function ReaderPanning:onSetDimensions(dimensions)
self.dimen = dimensions
end
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.view.visible_area.w * (1/100),
dy * self.panning_steps.normal * self.view.visible_area.h * (1/100))
dx * self.panning_steps.normal * self.dimen.w / 100,
dy * self.panning_steps.normal * self.dimen.h / 100)
return true
end

File diff suppressed because it is too large Load Diff

@ -3,38 +3,26 @@ local Device = require("device")
local Event = require("ui/event")
local _ = require("gettext")
local ReaderRotation = InputContainer:extend{
current_rotation = 0,
local ReaderRotation = InputContainer:new{
current_rotation = 0
}
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" },
event = "Rotate",
args = -90
},
{"J"},
doc = "rotate left by 90 degrees",
event = "Rotate", args = -90 },
RotateRight = {
{ "K" },
event = "Rotate",
args = 90
},
{"K"},
doc = "rotate right by 90 degrees",
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)

@ -1,7 +1,7 @@
local Device = require("device")
local Event = require("ui/event")
local InputContainer = require("ui/widget/container/inputcontainer")
local UIManager = require("ui/uimanager")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local logger = require("logger")
local time = require("ui/time")
local _ = require("gettext")
@ -16,7 +16,7 @@ local SCROLL_METHOD_CLASSIC = "classic"
local SCROLL_METHOD_TURBO = "turbo"
local SCROLL_METHOD_ON_RELEASE = "on_release"
local ReaderScrolling = WidgetContainer:extend{
local ReaderScrolling = InputContainer:new{
-- Available scrolling methods (make them available to other reader modules)
SCROLL_METHOD_CLASSIC = SCROLL_METHOD_CLASSIC,
SCROLL_METHOD_TURBO = SCROLL_METHOD_TURBO,
@ -221,6 +221,9 @@ function ReaderScrolling:applyScrollSettings()
end
function ReaderScrolling:setupTouchZones()
self.ges_events = {}
self.onGesture = nil
local zones = {
{
id = "inertial_scrolling_touch",
@ -381,11 +384,11 @@ function ReaderScrolling:_setupAction()
end
-- Decrease velocity at each step
self._velocity = self._velocity * self.scroll_friction^self._inertial_scroll_interval
self._velocity = self._velocity * math.pow(self.scroll_friction, self._inertial_scroll_interval)
local dist = math.floor(self._velocity * self._inertial_scroll_interval)
if math.abs(dist) < self.end_scroll_dist then
-- Decrease it even more so scrolling stops sooner
self._velocity = self._velocity * (2/3)
self._velocity = self._velocity / 1.5
end
-- self._stats_scroll_iterations = self._stats_scroll_iterations + 1
-- self._stats_scroll_distance = self._stats_scroll_distance + dist

@ -3,23 +3,16 @@ local ButtonDialog = require("ui/widget/buttondialog")
local CheckButton = require("ui/widget/checkbutton")
local Device = require("device")
local InfoMessage = require("ui/widget/infomessage")
local InputContainer = require("ui/widget/container/inputcontainer")
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
local DGENERIC_ICON_SIZE = G_defaults:readSetting("DGENERIC_ICON_SIZE")
local ReaderSearch = WidgetContainer:extend{
local ReaderSearch = InputContainer:new{
direction = 0, -- 0 for search forward, 1 for search backward
case_insensitive = true, -- default to case insensitive
@ -31,11 +24,7 @@ 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 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,
max_hits = 2048, -- maximum hits for search; timinges tested on a Tolino
-- internal: whether we expect results on previous pages
-- (can be different from self.direction, if, from a page in the
@ -80,108 +69,22 @@ 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 == 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)
-- if reverse ~= 0 search backwards
function ReaderSearch:searchCallback(reverse)
local search_text = self.input_dialog:getInputText()
if search_text == "" then return end
self.last_search_text = search_text
local regex_error
if text then -- from highlight dialog
self.use_regex = false
self.case_insensitive = true
else -- from input dialog
-- search_text comes from our keyboard, and may contain multiple diacritics ordered
-- in any order: we'd rather have them normalized, and expect the book content to
-- be proper and normalized text.
search_text = Utf8Proc.normalize_NFC(search_text)
self.use_regex = self.check_button_regex.checked
self.case_insensitive = not self.check_button_case.checked
regex_error = self.use_regex and self.ui.document:checkRegex(search_text)
end
self.use_regex = self.check_button_regex.checked
self.case_insensitive = not self.check_button_case.checked
local regex_error = self.use_regex and self.ui.document:checkRegex(search_text)
if self.use_regex and regex_error ~= 0 then
logger.dbg("ReaderSearch: regex error", regex_error, SRELL_ERROR_CODES[regex_error])
local error_message
@ -193,15 +96,7 @@ function ReaderSearch:searchCallback(reverse, text)
UIManager:show(InfoMessage:new{ text = error_message })
else
UIManager:close(self.input_dialog)
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
self:onShowSearchDialog(search_text, reverse, self.use_regex, self.case_insensitive)
end
end
@ -214,7 +109,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 or self.ui.doc_settings:readSetting("fulltext_search_last_search_text"),
input = self.last_search_text,
buttons = {
{
{
@ -224,13 +119,6 @@ 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()
@ -288,10 +176,7 @@ 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.paging then
if not current_page then -- initial search
current_page = self.ui.paging.current_page
end
if self.ui.document.info.has_pages then
no_results = false
self.ui.link:onGotoLink({page = res.page - 1}, neglect_current_location)
self.view.highlight.temp[res.page] = res
@ -360,24 +245,8 @@ function ReaderSearch:onShowSearchDialog(text, direction, regex, case_insensitiv
self.ui.link:onGotoLink({xpointer=valid_link}, neglect_current_location)
end
end
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
-- Don't add result pages to location ("Go back") stack
neglect_current_location = true
end
if no_results then
local notification_text
@ -411,12 +280,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)()
do_search(func, pattern, param, regex, case_insensitive)()
UIManager:close(self.wait_button)
end)
end
else
return do_search(func, pattern, param)
return do_search(func, pattern, param, regex, case_insensitive)
end
end
self.search_dialog = ButtonDialog:new{
@ -439,6 +308,7 @@ 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,
},
@ -465,14 +335,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)()
do_search(self.searchFromCurrent, text, direction, regex, case_insensitive)()
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)()
do_search(self.searchFromCurrent, text, direction, regex, case_insensitive)()
UIManager:show(self.search_dialog)
--- @todo regional
UIManager:setDirty(self.dialog, "partial")
@ -493,14 +363,7 @@ 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()
local regex_retval = regex and self.ui.document:getAndClearRegexSearchError();
if regex and regex_retval ~= 0 then
local error_message
if SRELL_ERROR_CODES[regex_retval] then
@ -512,12 +375,13 @@ function ReaderSearch:showErrorNotification(words_found, regex, max_hits)
text = error_message,
timeout = false,
})
elseif words_found and words_found >= max_hits then
elseif words_found and words_found > self.max_hits then
UIManager:show(Notification:new{
text =_("Too many hits"),
timeout = 4,
})
end
return retval
end
function ReaderSearch:searchFromStart(pattern, _, regex, case_insensitive)
@ -545,102 +409,4 @@ 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,23 +1,31 @@
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 InputContainer = require("ui/widget/container/inputcontainer")
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{
local ReaderStatus = InputContainer:new {
document = nil,
summary = {
rating = 0,
note = nil,
status = "",
modified = "",
},
enabled = true,
total_pages = 0,
total_pages = 0
}
function ReaderStatus:init()
if self.ui.document.is_pic then
self.enabled = false
return
else
self.total_pages = self.document:getPageCount()
self.ui.menu:registerToMainMenu(self)
@ -35,42 +43,45 @@ 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 == QuickStart.quickstart_filename then
-- Like onOpenNextDocumentInFolder, delay this so as not to break instance lifecycle
UIManager:nextTick(function()
self:openFileBrowser()
end)
if last_file and last_file == QuickStart.quickstart_filename then
self:openFileBrowser()
return
end
if G_reader_settings:readSetting("collate") == "access" then
collate = false
end
-- Should we start by marking the book as finished?
-- Should we start by marking the book as read?
if G_reader_settings:isTrue("end_document_auto_mark") then
self:onMarkBook(true)
end
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
if (settings == "pop-up" or settings == nil) and UIManager:getTopWidget() ~= "end_document" then
local buttons = {
{
{
text_func = function()
return self.summary.status == "complete" and _("Mark as reading") or _("Mark as finished")
if self.settings.data.summary and self.settings.data.summary.status == "complete" then
return _("Mark as reading")
else
return _("Mark as read")
end
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,
},
@ -79,16 +90,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 = next_file_enabled,
enabled = collate,
callback = function()
UIManager:close(button_dialog)
self:onOpenNextDocumentInFolder()
self:openNextFile(self.document.file)
UIManager:close(choose_action)
end,
},
},
@ -96,40 +107,49 @@ function ReaderStatus:onEndOfBook()
{
text = _("Delete file"),
callback = function()
UIManager:close(button_dialog)
self:deleteFile()
self:deleteFile(self.document.file, false)
UIManager:close(choose_action)
end,
},
{
text = _("File browser"),
callback = function()
UIManager:close(button_dialog)
-- Ditto
UIManager:nextTick(function()
self:openFileBrowser()
end)
self:openFileBrowser()
UIManager:close(choose_action)
end,
},
},
{
{
text = _("Cancel"),
callback = function()
UIManager:close(choose_action)
end,
},
},
}
button_dialog = ButtonDialogTitle:new{
choose_action = 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(button_dialog)
UIManager:show(choose_action)
elseif settings == "book_status" then
self:onShowBookStatus()
elseif settings == "next_file" then
if next_file_enabled then
if G_reader_settings:readSetting("collate") ~= "access" then
local info = InfoMessage:new{
text = _("Searching next file…"),
}
UIManager:show(info)
UIManager:forceRePaint()
UIManager:close(info)
self:onOpenNextDocumentInFolder()
-- 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)
else
UIManager:show(InfoMessage:new{
text = _("Could not open next file. Sort by last read date does not support this feature."),
@ -145,7 +165,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 finished."),
text = _("You've reached the end of the document.\nThe current book is marked as read."),
timeout = 3
})
elseif settings == "book_status_file_browser" then
@ -157,7 +177,7 @@ function ReaderStatus:onEndOfBook()
elseif settings == "delete_file" then
-- Ditto
UIManager:nextTick(function()
self:deleteFile()
self:deleteFile(self.document.file, true)
end)
end
end
@ -170,15 +190,15 @@ function ReaderStatus:openFileBrowser()
end
end
function ReaderStatus:onOpenNextDocumentInFolder()
local FileChooser = require("ui/widget/filechooser")
local next_file = FileChooser:getNextFile(self.document.file)
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()
if next_file then
-- 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)
self.ui:switchDocument(next_file)
else
UIManager:show(InfoMessage:new{
text = _("This is the last file in the current folder. No next file to open."),
@ -186,23 +206,34 @@ function ReaderStatus:onOpenNextDocumentInFolder()
end
end
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()
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"
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)
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,
})
end
function ReaderStatus:onShowBookStatus(before_show_callback)
local status_page = BookStatusWidget:new {
thumbnail = FileManagerBookInfo:getCoverImage(self.document),
props = self.ui.doc_props,
thumbnail = self.document:getCoverPageImage(),
props = self.document:getProps(),
document = self.document,
settings = self.settings,
ui = self.ui,
@ -215,19 +246,28 @@ function ReaderStatus:onShowBookStatus(before_show_callback)
return true
end
-- 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.
-- 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.
function ReaderStatus:onMarkBook(mark_read)
self.summary.status = (not mark_read and self.summary.status == "complete") and "reading" or "complete"
self.summary.modified = os.date("%Y-%m-%d", os.time())
-- If History is called over Reader, it will read the file to get the book status, so save and flush
self.settings:saveSetting("summary", self.summary)
self.settings:flush()
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
end
function ReaderStatus:onReadSettings(config)
self.settings = config
self.summary = config:readSetting("summary") or {}
end
return ReaderStatus

@ -1,12 +1,10 @@
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")
@ -21,17 +19,15 @@ local TextBoxWidget = require("ui/widget/textboxwidget")
local UIManager = require("ui/uimanager")
local VerticalGroup = require("ui/widget/verticalgroup")
local VerticalSpan = require("ui/widget/verticalspan")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
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
-- Simple widget for showing tweak info
local TweakInfoWidget = InputContainer:extend{
local TweakInfoWidget = InputContainer:new{
tweak = nil,
is_global_default = nil,
toggle_global_default_callback = function() end,
@ -53,7 +49,11 @@ function TweakInfoWidget:init()
}
}
end
self:registerKeyEvents()
if Device:hasKeys() then
self.key_events = {
Close = { {Device.input.group.Back}, doc = "cancel" }
}
end
local content = VerticalGroup:new{
TextBoxWidget:new{
@ -126,34 +126,25 @@ function TweakInfoWidget:init()
local buttons = {
{
{
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 = _("Close"),
callback = function()
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,
},
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,
buttons = buttons,
button_font_face = "cfont",
button_font_size = 20,
buttons = { buttons },
zero_sep = true,
show_parent = self,
}
@ -178,14 +169,6 @@ 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
@ -243,13 +226,12 @@ end
-- Reader component for managing tweaks. The aggregated css_text
-- is actually requested from us and applied by ReaderTypeset
local ReaderStyleTweak = WidgetContainer:extend{
local ReaderStyleTweak = InputContainer:new{
tweaks_by_id = nil,
tweaks_table = nil, -- sub-menu items
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)
@ -440,23 +422,12 @@ 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 = {}
@ -527,9 +498,6 @@ 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)
@ -561,26 +529,27 @@ 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,
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
})
end,
callback = function()
-- enable/disable only for this book
self:onToggleStyleTweak(item.id, item, true) -- no notification
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
end,
separator = item.separator,
})
@ -697,7 +666,6 @@ 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)
@ -714,67 +682,14 @@ function ReaderStyleTweak:addToMainMenu(menu_items)
}
end
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
local BOOK_TWEAK_SAMPLE_CSS = [[
p.someTitleClassName { text-indent: 0; }
function ReaderStyleTweak:onDispatcherRegisterActions()
for tweak_id, tweak_title in pairs(self.tweaks_in_dispatcher) do
dispatcherRegisterStyleTweak(tweak_id, tweak_title)
end
end
DIV.advertisement { display: none !important; }
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;
}
/* Help getting some alternative ToC when no headings */
.someSeparatorClassName {
-cr-hint: toc-level1;
break-before: always;
}
/* Hide annoying content */
DIV.someAdvertisement {
display: none !important;
margin: 0 !important;
}
]]
@ -783,126 +698,6 @@ 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
@ -955,7 +750,7 @@ function ReaderStyleTweak:editBookTweak(touchmenu_instance)
add_nav_bar = true,
scroll_by_pan = true,
buttons = {{
-- First buttons on first row (row will be completed with Reset|Save|Close)
-- First button on first row (row will be completed with Reset|Save|Close)
{
id = tweak_button_id,
text_func = function()
@ -973,110 +768,6 @@ 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
@ -1097,10 +788,10 @@ function ReaderStyleTweak:editBookTweak(touchmenu_instance)
end
end
end,
-- Store/retrieve view and cursor position callback
-- Set/save view and cursor position callback
view_pos_callback = function(top_line_num, charpos)
-- 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.
-- This same callback is called with no argument to get initial position,
-- and with arguments to give back final position when closed.
if top_line_num and charpos then
self.book_style_tweak_last_edit_pos = {top_line_num, charpos}
else
@ -1184,7 +875,8 @@ function ReaderStyleTweak:editBookTweak(touchmenu_instance)
-- being shown: having them identical will hide that.
end
end,
close_discarded_notif_text = NOT_MODIFIED_MSG,
close_discarded_notif_text = NOT_MODIFIED_MSG;
}
UIManager:show(editor)
editor:onShowKeyboard(true)

@ -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,7 +18,7 @@ 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:new{}
function ReaderThumbnail:init()
if not Device:isTouchDevice() then
@ -68,12 +68,6 @@ function ReaderThumbnail:addToMainMenu(menu_items)
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,
}
menu_items.page_browser = {
text = _("Page browser"),
@ -83,11 +77,10 @@ function ReaderThumbnail:addToMainMenu(menu_items)
}
end
function ReaderThumbnail:onShowBookMap(overview_mode)
function ReaderThumbnail:onShowBookMap()
local BookMapWidget = require("ui/widget/bookmapwidget")
UIManager:show(BookMapWidget:new{
ui = self.ui,
overview_mode = overview_mode,
})
return true
end
@ -130,7 +123,7 @@ function ReaderThumbnail:setupCache()
local max_bytes = math.ceil(N * Screen:getWidth() * Screen:getHeight() * Blitbuffer.TYPE_TO_BPP[self.bb_type] / 8)
-- We don't really care about limiting any number of slots, so allow
-- for at least 5 pages of 10x10 tiles
local avg_itemsize = math.ceil(max_bytes * (1/500))
local avg_itemsize = math.ceil(max_bytes / 500)
self.tile_cache = Cache:new{
size = max_bytes,
avg_itemsize = avg_itemsize, -- will make slots=500
@ -186,11 +179,10 @@ function ReaderThumbnail:removeFromCache(hash_subs, remove_only_non_matching)
return nb_removed, size_removed
end
function ReaderThumbnail:resetCachedPagesForBookmarks(annotations)
function ReaderThumbnail:resetCachedPagesForBookmarks(...)
-- Multiple bookmarks may be provided
local start_page, end_page
for i = 1, #annotations do
local bm = annotations[i]
for _, bm in ipairs({...}) do
if self.ui.rolling then
-- Look at all properties that may be xpointers
for _, k in ipairs({"page", "pos0", "pos1"}) do
@ -206,15 +198,12 @@ function ReaderThumbnail:resetCachedPagesForBookmarks(annotations)
end
else
if bm.page and type(bm.page) == "number" then
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
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
end
end
end
@ -250,10 +239,6 @@ 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
@ -278,10 +263,6 @@ 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
@ -322,11 +303,6 @@ 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
@ -427,24 +403,16 @@ 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.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.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.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
-- (We keep "nighmode_images" as it was set: we may get and cache a different bb whether nightmode is on or off)
self.ui.document:setNightmodeImages(false) -- We don't invert page images even if nightmode set: keep images as-is
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
@ -522,10 +490,6 @@ 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()
@ -534,9 +498,10 @@ function ReaderThumbnail:onColorRenderingUpdate()
end
-- CRE: emitted after a re-rendering
ReaderThumbnail.onDocumentRerendered = ReaderThumbnail.resetCache
ReaderThumbnail.onDocumentPartiallyRerendered = ReaderThumbnail.resetCache
ReaderThumbnail.onTocReset = ReaderThumbnail.resetCache
-- Emitted When adding/removing/updating bookmarks and highlights
ReaderThumbnail.onAnnotationsModified = ReaderThumbnail.resetCachedPagesForBookmarks
ReaderThumbnail.onBookmarkAdded = ReaderThumbnail.resetCachedPagesForBookmarks
ReaderThumbnail.onBookmarkRemoved = ReaderThumbnail.resetCachedPagesForBookmarks
ReaderThumbnail.onBookmarkUpdated = ReaderThumbnail.resetCachedPagesForBookmarks
return ReaderThumbnail

@ -20,21 +20,28 @@ local N_ = _.ngettext
local Screen = Device.screen
local T = require("ffi/util").template
local ReaderToc = InputContainer:extend{
local ReaderToc = InputContainer:new{
toc = nil,
toc_depth = nil,
ticks = nil,
ticks_flattened = nil,
ticks_flattened_filtered = nil,
collapsed_toc = nil, -- table
collapsed_toc = {},
collapse_depth = 2,
expanded_nodes = nil, -- table
expanded_nodes = {},
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()
self:registerKeyEvents()
if Device:hasKeyboard() then
self.key_events = {
ShowToc = {
{ "T" },
doc = "show Table of Content menu" },
}
end
if G_reader_settings:hasNot("toc_items_per_page") then
-- The TOC items per page and items' font size can now be
@ -52,34 +59,8 @@ 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")
@ -95,7 +76,7 @@ end
function ReaderToc:cleanUpTocTitle(title, replace_empty)
title = title:gsub("\13", "")
if replace_empty and title:match("^%s*$") then
title = "\u{2013}" -- En-Dash
title = "\xE2\x80\x93" -- U+2013 En-Dash
end
return title
end
@ -106,7 +87,6 @@ end
function ReaderToc:resetToc()
self.toc = nil
self.toc_menu_items_built = false
self.toc_depth = nil
self.ticks = nil
self.ticks_flattened = nil
@ -119,12 +99,9 @@ end
function ReaderToc:onUpdateToc()
self:resetToc()
self.ui:handleEvent(Event:new("TocReset"))
return true
end
-- Be sure to update the ToC after a CRE rerendering
function ReaderToc:onDocumentRerendered()
self:onUpdateToc()
--- @note: Let this propagate, plugins/statistics uses it to react to changes in document pagination
--return true
end
function ReaderToc:onPageUpdate(pageno)
@ -193,10 +170,8 @@ 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 item = toc[i]
local page = item.page
local page = toc[i].page
if page < cur_page then
has_bogus = true
break
@ -204,15 +179,9 @@ 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)
local depth = item.depth
if depth > max_depth then
max_depth = depth
if toc[i].depth > max_depth then
max_depth = toc[i].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")
@ -224,7 +193,6 @@ 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
@ -237,17 +205,10 @@ 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
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
if toc[i].depth > max_depth then
max_depth = toc[i].depth
end
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
local page = toc[i].fixed_page or toc[i].page
if page >= cur_page then
cur_page = page
else
@ -301,7 +262,7 @@ function ReaderToc:validateAndFixToc()
logger.dbg(" fix next", j, toc[j].page, "=>", fixed_page)
end
end
cur_page = item.fixed_page or item.page
cur_page = toc[i].fixed_page or toc[i].page
end
end
if nb_bogus > 0 then
@ -317,37 +278,6 @@ 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
@ -355,7 +285,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 = 0
local prev_index = 1
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
@ -370,7 +300,7 @@ function ReaderToc:getTocIndexByPage(pn_or_xp, skip_ignored_ticks)
prev_index = _k
end
end
return prev_index > 0 and prev_index or nil
return prev_index
end
function ReaderToc:getAccurateTocIndexByXPointer(xptr, skip_ignored_ticks)
@ -603,54 +533,78 @@ function ReaderToc:isChapterEnd(cur_pageno)
end
function ReaderToc:getChapterPageCount(pageno)
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
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)
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)
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
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
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
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
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
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()
@ -695,16 +649,11 @@ 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_menu_items_built then
self.toc_menu_items_built = true
if items_show_chapter_length then
self:completeTocWithChapterLengths()
end
if #self.toc > 0 and not self.toc[1].text then
-- Have the width of 4 spaces be the unit of indentation
local tmp = TextWidget:new{
text = " ",
@ -718,11 +667,6 @@ 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
@ -810,7 +754,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 = self:getTitle(),
title = _("Table of Contents"),
item_table = self.collapsed_toc,
state_w = can_collapse and button_size.w or 0,
ui = self.ui,
@ -992,7 +936,7 @@ function ReaderToc:addToMainMenu(menu_items)
-- insert table to main reader menu
menu_items.table_of_contents = {
text_func = function()
return self:getTitle()
return self.ui.document:isTocAlternativeToc() and self.alt_toc_menu_title or self.toc_menu_title
end,
callback = function()
self:onShowToc()
@ -1005,22 +949,13 @@ 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") .. " " .. self.alt_toc_symbol,
help_text_func = function()
local help_text = _([[
text = _("Alternative table of contents"),
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.]])
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,
See Style tweaks Miscellaneous Alternative ToC hints.]]),
checked_func = function()
return self.ui.document:isTocAlternativeToc()
end,
@ -1191,17 +1126,6 @@ Enabling this option will restrict display to the chapter titles of progress bar
UIManager:show(items_font)
end,
}
menu_items.toc_items_show_chapter_length = {
text = _("Show chapter length"),
keep_menu_open = true,
checked_func = function()
return not G_reader_settings:nilOrFalse("toc_items_show_chapter_length")
end,
callback = function()
G_reader_settings:flipNilOrFalse("toc_items_show_chapter_length")
self.toc_menu_items_built = false
end
}
menu_items.toc_items_with_dots = {
text = _("With dots"),
keep_menu_open = true,

@ -2,10 +2,10 @@ local BD = require("ui/bidi")
local ConfirmBox = require("ui/widget/confirmbox")
local Event = require("ui/event")
local InfoMessage = require("ui/widget/infomessage")
local InputContainer = require("ui/widget/container/inputcontainer")
local UIManager = require("ui/uimanager")
local Math = require("optmath")
local Notification = require("ui/widget/notification")
local UIManager = require("ui/uimanager")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local lfs = require("libs/libkoreader-lfs")
local optionsutil = require("ui/data/optionsutil")
local _ = require("gettext")
@ -13,10 +13,11 @@ local C_ = _.pgettext
local Screen = require("device").screen
local T = require("ffi/util").template
local ReaderTypeset = WidgetContainer:extend{
local ReaderTypeset = InputContainer:new{
-- @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,
}
@ -26,24 +27,38 @@ end
function ReaderTypeset:onReadSettings(config)
self.css = config:readSetting("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
or G_reader_settings:readSetting("copt_css")
or self.ui.document.default_css
local tweaks_css = self.ui.styletweak:getCssText()
self.ui.document:setStyleSheet(self.css, tweaks_css)
-- default to enable embedded fonts
self.ui.document:setEmbeddedFonts(self.configurable.embedded_fonts)
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 CSS
self.ui.document:setEmbeddedStyleSheet(self.configurable.embedded_css)
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)
-- Block rendering mode: stay with legacy rendering for books
-- previously opened so bookmarks and highlights stay valid.
@ -59,98 +74,104 @@ 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.configurable.block_rendering_mode = self.block_rendering_mode
self.ui.document.configurable.block_rendering_mode = self.block_rendering_mode
end
self:setBlockRenderingMode(self.block_rendering_mode)
-- default to 96 dpi
self.ui.document:setRenderDPI(self.configurable.render_dpi)
-- 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)
-- uncomment if we want font size to follow DPI changes
-- self.ui.document:setRenderScaleFontWithDPI(1)
-- set page margins
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 }
local h_margins = config:readSetting("copt_h_page_margins")
or G_reader_settings:readSetting("copt_h_page_margins")
or 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 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 DCREREADER_CONFIG_B_MARGIN_SIZES_LARGE
self.unscaled_margins = { h_margins[1], t_margin, h_margins[2], b_margin }
self:onSetPageMargins(self.unscaled_margins)
self.sync_t_b_page_margins = self.configurable.sync_t_b_page_margins == 1 and true or false
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
-- default to disable TXT formatting as it does more harm than good (the setting is not in UI)
-- default to disable TXT formatting as it does more harm than good
self.txt_preformatted = config:readSetting("txt_preformatted")
or G_reader_settings:readSetting("txt_preformatted")
or 1
self.ui.document:setTxtPreFormatted(self.txt_preformatted)
self:toggleTxtPreFormatted(self.txt_preformatted)
-- default to disable smooth scaling
self.ui.document:setImageScaling(self.configurable.smooth_scaling == 1)
-- 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 automagic nightmode-friendly handling of images
self.ui.document:setNightmodeImages(self.configurable.nightmode_images == 1)
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)
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)
local text
self:toggleEmbeddedStyleSheet(toggle)
if toggle then
self.configurable.embedded_css = 1
text = _("Enabled embedded styles.")
Notification:notify(_("Enabled embedded styles."))
else
self.configurable.embedded_css = 0
text = _("Disabled embedded styles.")
Notification:notify(_("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)
local text
self:toggleEmbeddedFonts(toggle)
if toggle then
self.configurable.embedded_fonts = 1
text = _("Enabled embedded fonts.")
Notification:notify(_("Enabled embedded fonts."))
else
self.configurable.embedded_fonts = 0
text = _("Disabled embedded fonts.")
Notification:notify(_("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.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)
self:toggleImageScaling(toggle)
Notification:notify(T( _("Image scaling set to: %1"), optionsutil:getOptionText("ToggleImageScaling", toggle)))
return true
end
function ReaderTypeset:onToggleNightmodeImages(toggle)
self.configurable.nightmode_images = toggle and 1 or 0
self.ui.document:setNightmodeImages(toggle)
self.ui:handleEvent(Event:new("UpdatePos"))
self:toggleNightmodeImages(toggle)
return true
end
function ReaderTypeset:onSetBlockRenderingMode(mode)
self: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)
Notification:notify(T( _("Render mode set to: %1"), optionsutil:getOptionText("SetBlockRenderingMode", mode)))
return true
end
@ -170,18 +191,23 @@ 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, description, fb2_compatible, separator)
local getStyleMenuItem = function(text, css_file, separator)
return {
text_func = function()
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 "")
return text .. (css_file == G_reader_settings:readSetting("copt_css") 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, description, touchmenu_instance)
self:makeDefaultStyleSheet(css_file, text, touchmenu_instance)
end,
checked_func = function()
if not css_file then -- "Auto"
@ -189,16 +215,6 @@ 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
@ -206,18 +222,8 @@ function ReaderTypeset:genStyleSheetMenu()
local style_table = {}
local obsoleted_table = {}
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
))
table.insert(style_table, getStyleMenuItem(_("None"), ""))
table.insert(style_table, getStyleMenuItem(_("Auto"), nil, true))
local css_files = {}
for f in lfs.dir("./data") do
@ -227,39 +233,15 @@ function ReaderTypeset:genStyleSheetMenu()
end
-- Add the 3 main styles
if css_files["epub.css"] then
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
))
table.insert(style_table, getStyleMenuItem(_("HTML / EPUB (epub.css)"), css_files["epub.css"]))
css_files["epub.css"] = nil
end
if css_files["html5.css"] then
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
))
table.insert(style_table, getStyleMenuItem(_("HTML5 (html5.css)"), css_files["html5.css"]))
css_files["html5.css"] = nil
end
if css_files["fb2.css"] then
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
))
table.insert(style_table, getStyleMenuItem(_("FictionBook (fb2.css)"), css_files["fb2.css"], true))
css_files["fb2.css"] = nil
end
-- Add the obsoleted ones to the Obsolete sub menu
@ -267,7 +249,7 @@ This stylesheet is to be used only with FB2 and FB3 documents, which are not cla
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], _("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.")))
table.insert(obsoleted_table, getStyleMenuItem(css, css_files[css]))
css_files[css] = nil
end
end
@ -278,7 +260,7 @@ This stylesheet is to be used only with FB2 and FB3 documents, which are not cla
end
table.sort(user_files)
for __, css in ipairs(user_files) do
table.insert(style_table, getStyleMenuItem(css, css_files[css], _("This is a user added stylesheet.")))
table.insert(style_table, getStyleMenuItem(css, css_files[css]))
end
style_table[#style_table].separator = true
@ -317,7 +299,6 @@ function ReaderTypeset:setStyleSheet(new_css)
end
end
-- Not used
function ReaderTypeset:setEmbededStyleSheetOnly()
if self.css ~= nil then
-- clear applied css
@ -328,6 +309,30 @@ 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
@ -399,6 +404,39 @@ 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 = {
@ -407,20 +445,11 @@ function ReaderTypeset:addToMainMenu(menu_items)
}
end
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
function ReaderTypeset:makeDefaultStyleSheet(css, text, touchmenu_instance)
UIManager:show(ConfirmBox:new{
text = text,
text = T( _("Set default style to %1?"), BD.filename(text)),
ok_callback = function()
if self.ui.document.is_fb2 then
G_reader_settings:saveSetting("copt_fb2_css", css)
else
G_reader_settings:saveSetting("copt_css", css)
end
G_reader_settings:saveSetting("copt_css", css)
if touchmenu_instance then touchmenu_instance:updateItems() end
end,
})
@ -436,7 +465,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.configurable.b_page_margin = t_margin
self.ui.document.configurable.b_page_margin = t_margin
end
self.ui:handleEvent(Event:new("SetPageMargins", self.unscaled_margins, when_applied_callback))
end
@ -446,7 +475,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.configurable.t_page_margin = b_margin
self.ui.document.configurable.t_page_margin = b_margin
end
self.ui:handleEvent(Event:new("SetPageMargins", self.unscaled_margins, when_applied_callback))
end
@ -457,7 +486,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.configurable.sync_t_b_page_margins = 0
self.ui.document.configurable.sync_t_b_page_margins = 0
end
self.ui:handleEvent(Event:new("SetPageMargins", self.unscaled_margins, when_applied_callback))
end
@ -474,8 +503,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.configurable.t_page_margin = mean_margin
self.configurable.b_page_margin = mean_margin
self.ui.document.configurable.t_page_margin = mean_margin
self.ui.document.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,11 +1,10 @@
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 InputContainer = require("ui/widget/container/inputcontainer")
local MultiConfirmBox = require("ui/widget/multiconfirmbox")
local UIManager = require("ui/uimanager")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local logger = require("logger")
local util = require("util")
local _ = require("gettext")
@ -13,7 +12,7 @@ local C_ = _.pgettext
local T = require("ffi/util").template
local Screen = Device.screen
local ReaderTypography = WidgetContainer:extend{}
local ReaderTypography = InputContainer:new{}
-- This is used to migrate old hyph settings, and to show the currently
-- used hyph dict language in the hyphenation menu.
@ -84,9 +83,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" },
@ -152,7 +151,6 @@ When the book's language tag is not among our presets, no specific features will
-- Show infos about TextLangMan seen lang_tags and loaded hyph dicts
local lang_infos = {}
local seen_hyph_dicts = {} -- to avoid outputing count and size for shared hyph dicts
local cre = require("document/credocument"):engineInit()
local main_lang_tag, main_lang_active_hyph_dict, loaded_lang_infos = cre.getTextLangStatus() -- luacheck: no unused
-- First output main lang tag
local main_lang_info = loaded_lang_infos[main_lang_tag]
@ -197,10 +195,11 @@ 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_type = "code",
text_face = Font:getFace("smallinfont"),
height = math.floor(Screen:getHeight() * 0.8),
})
end,
@ -361,7 +360,6 @@ When the book's language tag is not among our presets, no specific features will
end,
callback = function()
local DoubleSpinWidget = require("/ui/widget/doublespinwidget")
local cre = require("document/credocument"):engineInit()
local hyph_alg, alg_left_hyphen_min, alg_right_hyphen_min = cre.getSelectedHyphDict() -- luacheck: no unused
local hyph_limits_widget = DoubleSpinWidget:new{
-- Min (1) and max (10) values are enforced by crengine
@ -380,6 +378,7 @@ When the book's language tag is not among our presets, no specific features will
-- let room on the widget sides so we can see
-- the hyphenation changes happening
width_factor = 0.6,
default_values = true,
default_text = T(_("Language defaults: %1 / %2"), alg_left_hyphen_min, alg_right_hyphen_min),
title_text = _("Hyphenation limits"),
info_text = _([[
@ -776,8 +775,7 @@ 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 = FileManagerBookInfo.getCustomProp("language", self.ui.document.file)
or self.ui.document:getProps().language
local doc_language = 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

@ -16,7 +16,7 @@ local T = require("ffi/util").template
-- and some `if NORM then` branches can be simplified.
local NORM = false
local ReaderUserHyph = WidgetContainer:extend{
local ReaderUserHyph = WidgetContainer:new{
-- return values from setUserHyphenationDict (crengine's UserHyphDict::init())
USER_DICT_RELOAD = 0,
USER_DICT_NOCHANGE = 1,
@ -34,7 +34,6 @@ end
-- if reload==true, force a reload
-- Unload is done automatically when a new dictionary is loaded.
function ReaderUserHyph:loadDictionary(name, reload, no_scrubbing)
local cre = require("document/credocument"):engineInit()
if G_reader_settings:isTrue("hyph_user_dict") and lfs.attributes(name, "mode") == "file" then
logger.dbg("set user hyphenation dict", name, reload, no_scrubbing)
local ret = cre.setUserHyphenationDict(name, reload)
@ -266,7 +265,6 @@ function ReaderUserHyph:modifyUserEntry(word)
word = Utf8Proc.normalize_NFC(word)
end
local cre = require("document/credocument"):engineInit()
local suggested_hyphenation = cre.getHyphenationForWord(word)
-- word may have some strange punctuation marks (as the upper dot),

@ -22,20 +22,34 @@ 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
local ReaderView = OverlapGroup:extend{
document = nil,
view_modules = nil, -- array
-- single page state
state = nil, -- table
outer_page_color = Blitbuffer.gray(G_defaults:readSetting("DOUTER_PAGE_COLOR") * (1/15)),
state = {
page = nil,
pos = 0,
zoom = 1.0,
rotation = 0,
gamma = 1.0,
offset = nil,
bbox = nil,
},
outer_page_color = Blitbuffer.gray(DOUTER_PAGE_COLOR / 15),
-- highlight with "lighten" or "underscore" or "strikeout" or "invert"
highlight = nil, -- table
highlight = {
lighten_factor = G_reader_settings:readSetting("highlight_lighten_factor", 0.2),
note_mark = G_reader_settings:readSetting("highlight_note_marker"),
temp_drawer = "invert",
temp = {},
saved_drawer = "lighten",
saved = {},
indicator = nil, -- geom: non-touch highlight position indicator: {x = 50, y=50}
},
highlight_visible = true,
note_mark_line_w = 3, -- side line thickness
note_mark_sign = nil,
@ -43,16 +57,18 @@ local ReaderView = OverlapGroup:extend{
note_mark_pos_x2 = nil, -- page 2 in two-page mode
-- PDF/DjVu continuous paging
page_scroll = nil,
page_bgcolor = Blitbuffer.gray(G_defaults:readSetting("DBACKGROUND_COLOR") * (1/15)),
page_states = nil, -- table
page_bgcolor = Blitbuffer.gray(DBACKGROUND_COLOR / 15),
page_states = {},
-- properties of the gap drawn between each page in scroll mode:
page_gap = nil, -- table
page_gap = {
-- color (0 = white, 8 = gray, 15 = black)
color = Blitbuffer.gray((G_reader_settings:readSetting("page_gap_color") or 8) / 15),
},
-- DjVu page rendering mode (used in djvu.c:drawPage())
render_mode = nil, -- default to COLOR, will be set in onReadSettings()
render_mode = DRENDER_MODE, -- default to COLOR
-- Crengine view mode
view_mode = G_defaults:readSetting("DCREREADER_VIEW_MODE"), -- default to page mode
view_mode = DCREREADER_VIEW_MODE, -- default to page mode
hinting = true,
emitHintPageEvent = nil,
-- visible area within current viewing page
visible_area = nil,
@ -71,52 +87,22 @@ 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()
self.view_modules = {}
-- fix recalculate from close document pageno
self.state.page = nil
self.state = {
page = nil,
pos = 0,
zoom = 1.0,
rotation = 0,
gamma = 1.0,
offset = nil,
bbox = nil,
}
self.highlight = {
lighten_factor = G_reader_settings:readSetting("highlight_lighten_factor", 0.2),
note_mark = G_reader_settings:readSetting("highlight_note_marker"),
temp_drawer = "invert",
temp = {},
saved_drawer = "lighten",
indicator = nil, -- geom: non-touch highlight position indicator: {x = 50, y=50}
}
self.page_states = {}
self.page_gap = {
-- color (0 = white, 8 = gray, 15 = black)
color = Blitbuffer.gray((G_reader_settings:readSetting("page_gap_color") or 8) * (1/15)),
}
-- Reset the various areas across documents
self.visible_area = Geom:new{x = 0, y = 0, w = 0, h = 0}
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))
end
-- We've subclassed OverlapGroup, go through its init, because it does some funky stuff with self.dimen...
OverlapGroup.init(self)
end
function ReaderView:addWidgets()
@ -199,36 +185,21 @@ 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
-- mark last read area of overlapped pages
-- dim last read area
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)
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
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)
end
end
-- draw saved highlight
if self.highlight_visible then
self:drawSavedHighlight(bb, x, y)
@ -249,9 +220,10 @@ function ReaderView:paintTo(bb, x, y)
if self.footer_visible then
self.footer:paintTo(bb, x, y)
end
-- paint top left corner indicator
self.flipping:paintTo(bb, x, y)
-- paint view modules
-- paint flipping
if self.flipping_visible then
self.flipping:paintTo(bb, x, y)
end
for _, m in pairs(self.view_modules) do
m:paintTo(bb, x, y)
end
@ -264,29 +236,18 @@ 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()
-- 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
-- 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
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 and G_reader_settings:nilOrTrue("refresh_on_pages_with_images") then
if self.state.drawn == false 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
@ -429,7 +390,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)
@ -474,7 +435,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
@ -533,7 +494,6 @@ 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
@ -543,24 +503,30 @@ end
function ReaderView:drawPageSavedHighlight(bb, x, y)
local pages = self:getCurrentPageList()
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
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
end
end
end
end
end -- end for each box
end -- end if boxes
end -- end for each highlight
end
end
end -- end for each page
end
function ReaderView:drawXPointerSavedHighlight(bb, x, y)
@ -568,38 +534,46 @@ 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).
-- 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)
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)
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
end -- end for each box
end -- end if boxes
end
end
end -- end for each highlight
end
end
end -- end for all saved highlight
end
function ReaderView:drawHighlightRect(bb, _x, _y, rect, drawer, draw_note_mark)
@ -607,23 +581,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, Size.line.thick, Blitbuffer.COLOR_GRAY_4)
bb:paintRect(x, y + h - 1, w, 2, Blitbuffer.COLOR_GRAY)
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, Size.line.medium, Blitbuffer.COLOR_BLACK)
bb:paintRect(x, line_y, w, 2, 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, Size.line.medium, Blitbuffer.COLOR_BLACK)
bb:paintRect(x, y + h - 1, w, 2, Blitbuffer.COLOR_BLACK)
else
local note_mark_pos_x
if self.ui.paging or
(self.document:getVisiblePageCount() == 1) or -- one-page mode
(self.ui.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
@ -670,8 +644,7 @@ 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
-- 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
if self.ui.zooming.zoom_bottom_to_top then
-- starts from bottom of page_area
self.visible_area.y = self.page_area.y + self.page_area.h - self.visible_area.h
else
@ -791,38 +764,18 @@ end
function ReaderView:onSetRotationMode(rotation)
if rotation ~= nil then
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
if rotation == Screen:getRotationMode() then
return true
end
Screen:setRotationMode(rotation)
end
UIManager:setDirty(nil, "full") -- SetDimensions will only request a partial, we want a flash
UIManager:setDirty(self.dialog, "full")
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
return true
end
function ReaderView:onSetDimensions(dimensions)
@ -866,22 +819,28 @@ In combination with zoom to fit page, page height, content height, content or co
end
function ReaderView:onReadSettings(config)
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")
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
end
end
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
if rotation_mode then
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
@ -889,8 +848,42 @@ 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_enable = config:isTrue("show_overlap_enable") or G_reader_settings:isTrue("page_overlap_enable") or DSHOWOVERLAP
self.page_overlap_style = config:readSetting("page_overlap_style") or G_reader_settings:readSetting("page_overlap_style") or "dim"
self.page_gap.height = Screen:scaleBySize(config:readSetting("kopt_page_gap_height")
or G_reader_settings:readSetting("kopt_page_gap_height")
@ -947,15 +940,19 @@ 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
-- We don't need to do anything if reclaim is enabled ;).
-- 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...
if not self.footer.settings.reclaim_height then
-- 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).
-- 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
end
self.ui:handleEvent(Event:new("ViewRecalculate", self.visible_area, self.page_area))
end
end
@ -964,7 +961,7 @@ function ReaderView:onGammaUpdate(gamma)
if self.page_scroll then
self.ui:handleEvent(Event:new("UpdateScrollPageGamma", gamma))
end
Notification:notify(T(_("Contrast set to: %1."), gamma))
Notification:notify(T(_("Font gamma set to: %1."), gamma))
end
-- For ReaderKOptListener
@ -995,10 +992,8 @@ function ReaderView:onSWDitheringUpdate(toggle)
end
function ReaderView:onFontSizeUpdate(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
self.ui:handleEvent(Event:new("ReZoom", font_size))
Notification:notify(T(_("Font zoom set to: %1."), font_size))
end
function ReaderView:onDefectSizeUpdate()
@ -1032,22 +1027,21 @@ function ReaderView:onPageGapUpdate(page_gap)
end
function ReaderView:onSaveSettings()
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
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)
-- Don't etch the current rotation in stone when sticky rotation is enabled
if G_reader_settings:nilOrFalse("lock_rotation") then
self.document.configurable.rotation_mode = Screen:getRotationMode() -- will be saved by ReaderConfig
local locked = G_reader_settings:isTrue("lock_rotation")
if not locked then
self.ui.doc_settings:saveSetting("rotation_mode", Screen:getRotationMode())
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)
@ -1076,13 +1070,10 @@ function ReaderView:getRenderModeMenuTable()
}
end
function ReaderView:onCloseWidget()
-- Stop any pending HintPage event
function ReaderView:onCloseDocument()
self.hinting = false
-- stop any in fly 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()
@ -1121,7 +1112,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.document.configurable.text_wrap == 1))
or (self.ui.paging.zoom_mode == "page" and self.ui.paging.is_reflowed))
and not self.ui.paging.zoom_mode:find("height")
else
return self.view_mode ~= "page"
@ -1129,14 +1120,20 @@ function ReaderView:isOverlapAllowed()
end
function ReaderView:setupTouchZones()
(self.ui.rolling or self.ui.paging):setupTouchZones()
if self.ui.rolling then
self.ui.rolling:setupTouchZones()
else
self.ui.paging:setupTouchZones()
end
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
Notification:notify(is_rtl and _("RTL page turning.") or _("LTR page turning."))
UIManager:show(Notification:new{
text = is_rtl and _("RTL page turning.") or _("LTR page turning."),
})
return true
end
@ -1144,22 +1141,20 @@ function ReaderView:getTapZones()
local forward_zone, backward_zone
local tap_zones_type = G_reader_settings:readSetting("page_turns_tap_zones", "default")
if tap_zones_type == "default" then
local DTAP_ZONE_FORWARD = G_defaults:readSetting("DTAP_ZONE_FORWARD")
forward_zone = {
ratio_x = DTAP_ZONE_FORWARD.x, ratio_y = DTAP_ZONE_FORWARD.y,
ratio_w = DTAP_ZONE_FORWARD.w, ratio_h = DTAP_ZONE_FORWARD.h,
}
local DTAP_ZONE_BACKWARD = G_defaults:readSetting("DTAP_ZONE_BACKWARD")
backward_zone = {
ratio_x = DTAP_ZONE_BACKWARD.x, ratio_y = DTAP_ZONE_BACKWARD.y,
ratio_w = DTAP_ZONE_BACKWARD.w, ratio_h = DTAP_ZONE_BACKWARD.h,
}
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 = G_reader_settings:readSetting("page_turns_tap_zone_backward_size_ratio", G_defaults:readSetting("DTAP_ZONE_BACKWARD").w)
local tap_zone_forward_w = G_reader_settings:readSetting("page_turns_tap_zone_forward_size_ratio", DTAP_ZONE_FORWARD.w)
local tap_zone_backward_w = 1 - tap_zone_forward_w
if tap_zones_type == "left_right" then
forward_zone = {
ratio_x = 1 - tap_zone_forward_w, ratio_y = 0,
ratio_x = tap_zone_backward_w, ratio_y = 0,
ratio_w = tap_zone_forward_w, ratio_h = 1,
}
backward_zone = {
@ -1168,7 +1163,7 @@ function ReaderView:getTapZones()
}
else
forward_zone = {
ratio_x = 0, ratio_y = 1 - tap_zone_forward_w,
ratio_x = 0, ratio_y = tap_zone_backward_w,
ratio_w = 1, ratio_h = tap_zone_forward_w,
}
backward_zone = {
@ -1215,17 +1210,17 @@ function ReaderView:setupNoteMarkPosition()
self.note_mark_pos_x1 = screen_w - sign_gap - sign_w
end
else
local doc_margins = self.document:getPageMargins()
local doc_margins = self.ui.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.document:getVisiblePageCount() == 1 then
if self.ui.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.document:getPageOffsetX(self.document:getCurrentPage(true)+1)
local page2_x = self.ui.document:getPageOffsetX(self.ui.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
@ -1238,41 +1233,4 @@ 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,6 +1,7 @@
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")
local InfoMessage = require("ui/widget/infomessage")
local InputDialog = require("ui/widget/inputdialog")
local KeyValuePage = require("ui/widget/keyvaluepage")
@ -11,7 +12,6 @@ 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")
@ -31,7 +31,7 @@ function ReaderWikipedia:init()
self.wiki_languages = {}
self.ui.menu:registerToMainMenu(self)
if not wikipedia_history then
wikipedia_history = LuaData:open(DataStorage:getSettingsDir() .. "/wikipedia_history.lua", "WikipediaHistory")
wikipedia_history = LuaData:open(DataStorage:getSettingsDir() .. "/wikipedia_history.lua", { name = "WikipediaHistory" })
end
end
@ -113,17 +113,6 @@ 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 = {
@ -181,26 +170,113 @@ 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,
help_text = _([[
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 = _([[
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.]]),
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)
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
end
end
filemanagerutil.showChooseDialog(title_header, caller_callback, current_path, default_path)
-- If setting exists, or no home_dir found, let user choose directory
choose_directory()
end,
},
{ -- setting used by dictquicklookup
@ -211,41 +287,6 @@ You can choose an existing folder, or use a default folder named "Wikipedia" in
callback = function()
G_reader_settings:flipNilOrFalse("wikipedia_save_in_book_dir")
end,
},
{ -- setting used in wikipedia.lua
text_func = function()
local include_images = _("ask")
if G_reader_settings:readSetting("wikipedia_epub_include_images") == true then
include_images = _("always")
elseif G_reader_settings:readSetting("wikipedia_epub_include_images") == false then
include_images = _("never")
end
return T(_("Include images in EPUB: %1"), include_images)
end,
sub_item_table = {
genChoiceMenuEntry(_("Ask"), "wikipedia_epub_include_images", nil),
genChoiceMenuEntry(_("Include images"), "wikipedia_epub_include_images", true),
genChoiceMenuEntry(_("Don't include images"), "wikipedia_epub_include_images", false),
},
},
{ -- setting used in wikipedia.lua
text_func = function()
local images_quality = _("ask")
if G_reader_settings:readSetting("wikipedia_epub_highres_images") == true then
images_quality = _("higher")
elseif G_reader_settings:readSetting("wikipedia_epub_highres_images") == false then
images_quality = _("standard")
end
return T(_("Images quality in EPUB: %1"), images_quality)
end,
enabled_func = function()
return G_reader_settings:readSetting("wikipedia_epub_include_images") ~= false
end,
sub_item_table = {
genChoiceMenuEntry(_("Ask"), "wikipedia_epub_highres_images", nil),
genChoiceMenuEntry(_("Standard quality"), "wikipedia_epub_highres_images", false),
genChoiceMenuEntry(_("Higher quality"), "wikipedia_epub_highres_images", true),
},
separator = true,
},
{
@ -329,7 +370,7 @@ function ReaderWikipedia:initLanguages(word)
end
-- use book and UI languages
if self.view then
addLanguage(self.ui.doc_props.language)
addLanguage(self.view.document:getProps().language)
end
addLanguage(G_reader_settings:readSetting("language"))
if #self.wiki_languages == 0 and word then
@ -385,7 +426,13 @@ 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_props and self.ui.doc_props.display_title or _("Wikipedia lookup")
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
wikipedia_history:addTableItem("wikipedia_history", {
book_title = book_title,
time = os.time(),
@ -440,7 +487,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 (page.length and _("No introduction.")) or no_result_text
local definition = page.extract or no_result_text
if page.length then
-- we get 'length' only for intro results
-- let's append it to definition so we know
@ -506,11 +553,11 @@ function ReaderWikipedia:getWikiLanguages(first_lang)
end
end
local update_wiki_languages_on_close = false
if DictQuickLookup.rotated_update_wiki_languages_on_close ~= nil then
if self.dict_window_list.rotated_update_wiki_languages_on_close ~= nil then
-- Flag set by DictQuickLookup when rotating, forwarding the flag
-- of the rotated out DictQuickLookup instance: trust it
update_wiki_languages_on_close = DictQuickLookup.rotated_update_wiki_languages_on_close
DictQuickLookup.rotated_update_wiki_languages_on_close = nil
update_wiki_languages_on_close = self.dict_window_list.rotated_update_wiki_languages_on_close
self.dict_window_list.rotated_update_wiki_languages_on_close = nil
else
-- Not a rotation. Only if it's the first request with the current
-- first language, we will have it (and any lang rotation from it)
@ -520,8 +567,8 @@ function ReaderWikipedia:getWikiLanguages(first_lang)
-- from them) won't update it.
if is_first_lang then
update_wiki_languages_on_close = true
for i = #DictQuickLookup.window_list-1, 1, -1 do -- (ignore the last one, which is the one calling this)
if DictQuickLookup.window_list[i].is_wiki then
for i=1, #self.dict_window_list-1 do -- (ignore the last one, which is the one calling this)
if self.dict_window_list[i].is_wiki then
-- Another upper Wikipedia result: only this one may update it
update_wiki_languages_on_close = false
break

@ -8,14 +8,15 @@ 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
local T = require("ffi/util").template
local ReaderZooming = InputContainer:extend{
local ReaderZooming = InputContainer:new{
zoom = 1.0,
available_zoom_modes = { -- const
available_zoom_modes = {
"page",
"pagewidth",
"pageheight",
@ -26,37 +27,26 @@ 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
zoom_genus_to_mode = {
[4] = "page",
[3] = "content",
[2] = "columns",
[1] = "rows",
[0] = "manual",
},
zoom_mode_to_genus = { -- const
zoom_mode_to_genus = {
page = 4,
content = 3,
columns = 2,
rows = 1,
manual = 0,
},
zoom_type_to_mode = { -- const
zoom_type_to_mode = {
[2] = "",
[1] = "width",
[0] = "height",
},
zoom_mode_to_type = { -- const
zoom_mode_to_type = {
[""] = 2,
width = 1,
height = 0,
@ -68,23 +58,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 = {
"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
paged_modes = {
page = _("Zoom to fit page works best with page view."),
pageheight = _("Zoom to fit page height works best with page view."),
contentheight = _("Zoom to fit content height works best with page view."),
@ -94,63 +81,57 @@ 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 },
event = "Zoom",
args = "in",
doc = "zoom in",
event = "Zoom", args = "in"
},
ZoomOut = {
{ "Shift", Input.group.PgBack },
event = "Zoom",
args = "out",
doc = "zoom out",
event = "Zoom", args = "out"
},
ZoomToFitPage = {
{ "A" },
event = "SetZoomMode",
args = "page",
doc = "zoom to fit page",
event = "SetZoomMode", args = "page"
},
ZoomToFitContent = {
{ "Shift", "A" },
event = "SetZoomMode",
args = "content",
doc = "zoom to fit content",
event = "SetZoomMode", args = "content"
},
ZoomToFitPageWidth = {
{ "S" },
event = "SetZoomMode",
args = "pagewidth",
doc = "zoom to fit page width",
event = "SetZoomMode", args = "pagewidth"
},
ZoomToFitContentWidth = {
{ "Shift", "S" },
event = "SetZoomMode",
args = "contentwidth",
doc = "zoom to fit content width",
event = "SetZoomMode", args = "contentwidth"
},
ZoomToFitPageHeight = {
{ "D" },
event = "SetZoomMode",
args = "pageheight",
doc = "zoom to fit page height",
event = "SetZoomMode", args = "pageheight"
},
ZoomToFitContentHeight = {
{ "Shift", "D" },
event = "SetZoomMode",
args = "contentheight",
doc = "zoom to fit content height",
event = "SetZoomMode", args = "contentheight"
},
ZoomManual = {
{ "Shift", "M" },
event = "SetZoomMode",
args = "manual",
doc = "manual zoom mode",
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
@ -220,7 +201,9 @@ function ReaderZooming:onReadSettings(config)
local zoom_mode = config:readSetting("zoom_mode")
if zoom_mode then
-- Validate it first
zoom_mode = self.zoom_mode_label[zoom_mode] and zoom_mode or self.DEFAULT_ZOOM_MODE
zoom_mode = util.arrayContains(self.available_zoom_modes, 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)
@ -230,13 +213,16 @@ 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")
zoom_mode = self:combo_to_mode(zoom_mode_genus, 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
-- Validate it
zoom_mode = self.zoom_mode_label[zoom_mode] and zoom_mode or self.DEFAULT_ZOOM_MODE
zoom_mode = util.arrayContains(self.available_zoom_modes, zoom_mode)
and zoom_mode
or self.DEFAULT_ZOOM_MODE
end
-- Import legacy zoom_factor settings
@ -252,23 +238,18 @@ 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
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
for _, setting in ipairs(self.zoom_pan_settings) do
self[setting] = config:readSetting(setting)
or G_reader_settings:readSetting(setting)
or self[setting]
end
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)
@ -342,12 +323,16 @@ end
function ReaderZooming:onDefineZoom(btn, when_applied_callback)
local config = self.ui.document.configurable
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 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_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]
@ -369,7 +354,7 @@ function ReaderZooming:onDefineZoom(btn, when_applied_callback)
zoom_mode = zoom_mode_genus
self.ui:handleEvent(Event:new("SetScrollMode", false))
end
zoom_mode = self.zoom_mode_label[zoom_mode] and zoom_mode or self.DEFAULT_ZOOM_MODE
zoom_mode = util.arrayContains(self.available_zoom_modes, zoom_mode) and zoom_mode or self.DEFAULT_ZOOM_MODE
settings.zoom_mode = zoom_mode
if settings.right_to_left then
@ -424,7 +409,7 @@ function ReaderZooming:onDefineZoom(btn, when_applied_callback)
horizontal overlap: %3 %
vertical overlap: %5 %
zoom factor: %6]]),
self.zoom_mode_label[zoom_mode],
zoom_mode,
("%.2f"):format(self:getNumberOf("columns", settings.zoom_overlap_h)),
settings.zoom_overlap_h,
("%.2f"):format(self:getNumberOf("rows", settings.zoom_overlap_v)),
@ -715,10 +700,9 @@ 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
@ -734,12 +718,4 @@ 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,11 +21,8 @@ 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")
@ -35,7 +32,6 @@ 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")
@ -62,19 +58,17 @@ 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 Input = Device.input
local Screen = Device.screen
local Screen = require("device").screen
local T = ffiUtil.template
local ReaderUI = InputContainer:extend{
local ReaderUI = InputContainer:new{
name = "ReaderUI",
active_widgets = nil, -- array
active_widgets = {},
-- if we have a parent container, it must be referenced for now
dialog = nil,
@ -86,7 +80,7 @@ local ReaderUI = InputContainer:extend{
password = nil,
postInitCallback = nil,
postReaderReadyCallback = nil,
postReaderCallback = nil,
}
function ReaderUI:registerModule(name, ui_module, always_active)
@ -105,21 +99,18 @@ function ReaderUI:registerPostInitCallback(callback)
table.insert(self.postInitCallback, callback)
end
function ReaderUI:registerPostReaderReadyCallback(callback)
table.insert(self.postReaderReadyCallback, callback)
function ReaderUI:registerPostReadyCallback(callback)
table.insert(self.postReaderCallback, callback)
end
function ReaderUI:init()
self.active_widgets = {}
-- 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.postReaderReadyCallback = {}
self.postReaderCallback = {}
-- if we are not the top level dialog ourselves, it must be given in the table
if not self.dialog then
self.dialog = self
@ -129,7 +120,9 @@ function ReaderUI:init()
-- Handle local settings migration
SettingsMigration:migrateSettings(self.doc_settings)
self:registerKeyEvents()
if Device:hasKeys() then
self.key_events.Home = { {"Home"}, doc = "open file browser" }
end
-- a view container (so it must be child #1!)
-- all paintable widgets need to be a child of reader view
@ -166,13 +159,6 @@ 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,
@ -185,12 +171,6 @@ 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{
@ -335,14 +315,12 @@ 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
@ -361,7 +339,6 @@ function ReaderUI:init()
})
-- rolling controller
self:registerModule("rolling", ReaderRolling:new{
configurable = self.document.configurable,
pan_rate = pan_rate,
dialog = self.dialog,
view = self.view,
@ -436,12 +413,6 @@ 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(
@ -459,6 +430,15 @@ function ReaderUI:init()
end
end
if Device:hasWifiToggle() then
local NetworkListener = require("ui/network/networklistener")
self:registerModule("networklistener", NetworkListener:new {
document = self.document,
view = self.view,
ui = self,
})
end
-- Allow others to change settings based on external factors
-- Must be called after plugins are loaded & before setting are read.
self:handleEvent(Event:new("DocSettingsLoad", self.doc_settings, self.document))
@ -470,40 +450,22 @@ function ReaderUI:init()
end
self.postInitCallback = nil
-- 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
-- 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())
-- 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.postReaderReadyCallback) do
for _,v in ipairs(self.postReaderCallback) do
v()
end
self.postReaderReadyCallback = nil
self.postReaderCallback = 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
@ -519,27 +481,6 @@ 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)
@ -569,6 +510,7 @@ function ReaderUI:showFileManager(file)
local last_dir, last_file
if file then
last_dir = util.splitFilePathName(file)
last_dir = last_dir:match("(.*)/")
last_file = file
else
last_dir, last_file = self:getLastDirFile(true)
@ -598,20 +540,19 @@ 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, seamless)
function ReaderUI:showReader(file, provider)
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(filemanagerutil.abbreviate(file)))
text = T(_("File '%1' does not exist."), BD.filepath(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(filemanagerutil.abbreviate(file)))
text = T(_("File '%1' is not supported."), BD.filepath(file))
})
self:showFileManager(file)
return
@ -621,30 +562,26 @@ function ReaderUI:showReader(file, provider, seamless)
UIManager:broadcastEvent(Event:new("ShowingReader"))
provider = provider or DocumentRegistry:getProvider(file)
if provider.provider then
self:showReaderCoroutine(file, provider, seamless)
self:showReaderCoroutine(file, provider)
end
end
function ReaderUI:showReaderCoroutine(file, provider, seamless)
function ReaderUI:showReaderCoroutine(file, provider)
UIManager:show(InfoMessage:new{
text = T(_("Opening file '%1'."), BD.filepath(filemanagerutil.abbreviate(file))),
text = T(_("Opening file '%1'."), BD.filepath(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, seamless)
self:doShowReader(file, provider)
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.")
})
@ -653,10 +590,7 @@ function ReaderUI:showReaderCoroutine(file, provider, seamless)
end)
end
function ReaderUI:doShowReader(file, provider, seamless)
if seamless then
UIManager:avoidFlashOnNextRepaint()
end
function ReaderUI:doShowReader(file, provider)
logger.info("opening file", file)
-- Only keep a single instance running
if ReaderUI.instance then
@ -683,14 +617,22 @@ function ReaderUI:doShowReader(file, provider, seamless)
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,
}
Screen:setWindowTitle(reader.doc_props.display_title)
Device:notifyBookState(reader.doc_props.display_title, 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)
-- This is mostly for the few callers that bypass the coroutine shenanigans and call doShowReader directly,
-- instead of showReader...
@ -701,7 +643,14 @@ function ReaderUI:doShowReader(file, provider, seamless)
FileManager.instance:onClose()
end
UIManager:show(reader, seamless and "ui" or "full")
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
end
function ReaderUI:unlockDocumentWithPassword(document, try_again)
@ -714,6 +663,7 @@ function ReaderUI:unlockDocumentWithPassword(document, try_again)
{
text = _("Cancel"),
id = "close",
enabled = true,
callback = function()
self:closeDialog()
coroutine.resume(self._coroutine)
@ -721,6 +671,7 @@ function ReaderUI:unlockDocumentWithPassword(document, try_again)
},
{
text = _("OK"),
enabled = true,
callback = function()
local success = self:onVerifyPassword(document)
self:closeDialog()
@ -760,12 +711,8 @@ function ReaderUI:saveSettings()
G_reader_settings:flush()
end
function ReaderUI:onFlushSettings(show_notification)
function ReaderUI:onFlushSettings()
self:saveSettings()
if show_notification then
-- Invoked from dispatcher to explicitely flush settings
Notification:notify(_("Book metadata saved."))
end
end
function ReaderUI:closeDocument()
@ -784,9 +731,9 @@ function ReaderUI:notifyCloseDocument()
self:closeDocument()
else
UIManager:show(ConfirmBox:new{
text = _("Write highlights into this PDF?"),
ok_text = _("Write"),
dismissable = false,
text = _("Do you want to save this document?"),
ok_text = _("Save"),
cancel_text = _("Don't save"),
ok_callback = function()
self:closeDocument()
end,
@ -813,10 +760,9 @@ function ReaderUI:onClose(full_refresh)
if self.dialog ~= self then
self:saveSettings()
end
-- Serialize the most recently displayed page for later launch
DocCache:serialize()
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")
self:notifyCloseDocument()
end
@ -839,6 +785,10 @@ 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
@ -849,9 +799,6 @@ 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
@ -864,11 +811,7 @@ function ReaderUI:onHome()
return true
end
function ReaderUI:onReload()
self:reloadDocument()
end
function ReaderUI:reloadDocument(after_close_callback, seamless)
function ReaderUI:reloadDocument(after_close_callback)
local file = self.document.file
local provider = getmetatable(self.document).__index
@ -878,7 +821,6 @@ function ReaderUI:reloadDocument(after_close_callback, seamless)
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
@ -886,7 +828,7 @@ function ReaderUI:reloadDocument(after_close_callback, seamless)
after_close_callback(file, provider)
end
self:showReader(file, provider, seamless)
self:showReader(file, provider)
end
function ReaderUI:switchDocument(new_file)
@ -909,7 +851,11 @@ function ReaderUI:onOpenLastDoc()
end
function ReaderUI:getCurrentPage()
return self.paging and self.paging.current_page or self.document:getCurrentPage()
if self.document.info.has_pages then
return self.paging.current_page
else
return self.document:getCurrentPage()
end
end
return ReaderUI

@ -6,7 +6,11 @@ local lfs = require("libs/libkoreader-lfs")
local logger = require("logger")
local lru = require("ffi/lru")
local md5 = require("ffi/sha2").md5
local util = require("util")
local CanvasContext = require("document/canvascontext")
if CanvasContext.should_restrict_JIT then
jit.off(true, true)
end
local Cache = {
-- Cache configuration:
@ -74,6 +78,95 @@ function Cache:_getDiskCache()
return cached
end
-- For documentation purposes, here's a battle-tested shell version of calcFreeMem
--[[
if grep -q 'MemAvailable' /proc/meminfo ; then
# We'll settle for 85% of available memory to leave a bit of breathing room
tmpfs_size="$(awk '/MemAvailable/ {printf "%d", $2 * 0.85}' /proc/meminfo)"
elif grep -q 'Inactive(file)' /proc/meminfo ; then
# Basically try to emulate the kernel's computation, c.f., https://unix.stackexchange.com/q/261247
# Again, 85% of available memory
tmpfs_size="$(awk -v low=$(grep low /proc/zoneinfo | awk '{k+=$2}END{printf "%d", k}') \
'{a[$1]=$2}
END{
printf "%d", (a["MemFree:"]+a["Active(file):"]+a["Inactive(file):"]+a["SReclaimable:"]-(12*low))*0.85;
}' /proc/meminfo)"
else
# Ye olde crap workaround of Free + Buffers + Cache...
# Take it with a grain of salt, and settle for 80% of that...
tmpfs_size="$(awk \
'{a[$1]=$2}
END{
printf "%d", (a["MemFree:"]+a["Buffers:"]+a["Cached:"])*0.80;
}' /proc/meminfo)"
fi
--]]
-- And here's our simplified Lua version...
function Cache:_calcFreeMem()
local memtotal, memfree, memavailable, buffers, cached
local meminfo = io.open("/proc/meminfo", "r")
if meminfo then
for line in meminfo:lines() do
if not memtotal then
memtotal = line:match("^MemTotal:%s-(%d+) kB")
if memtotal then
-- Next!
goto continue
end
end
if not memfree then
memfree = line:match("^MemFree:%s-(%d+) kB")
if memfree then
-- Next!
goto continue
end
end
if not memavailable then
memavailable = line:match("^MemAvailable:%s-(%d+) kB")
if memavailable then
-- Best case scenario, we're done :)
break
end
end
if not buffers then
buffers = line:match("^Buffers:%s-(%d+) kB")
if buffers then
-- Next!
goto continue
end
end
if not cached then
cached = line:match("^Cached:%s-(%d+) kB")
if cached then
-- Ought to be the last entry we care about, we're done
break
end
end
::continue::
end
meminfo:close()
else
-- Not on Linux?
return 0, 0
end
if memavailable then
-- Leave a bit of margin, and report 85% of that...
return math.floor(memavailable * 0.85) * 1024, memtotal * 1024
else
-- Crappy Free + Buffers + Cache version, because the zoneinfo approach is a tad hairy...
-- So, leave an even larger margin, and only report 75% of that...
return math.floor((memfree + buffers + cached) * 0.75) * 1024, memtotal * 1024
end
end
function Cache:insert(key, object)
-- If this object is single-handledly too large for the cache, don't cache it.
if not self:willAccept(object.size) then
@ -140,6 +233,64 @@ function Cache:willAccept(size)
return size*4 < self.size*3
end
function Cache:serialize()
if not self.disk_cache then
return
end
-- Calculate the current disk cache size
local cached_size = 0
local sorted_caches = {}
for _, file in pairs(self.cached) do
table.insert(sorted_caches, {file=file, time=lfs.attributes(file, "access")})
cached_size = cached_size + (lfs.attributes(file, "size") or 0)
end
table.sort(sorted_caches, function(v1, v2) return v1.time > v2.time end)
-- Only serialize the second most recently used cache item (as the MRU would be the *hinted* page).
local mru_key
local mru_found = 0
for key, item in self.cache:pairs() do
-- Only dump cache items that actually request persistence
if item.persistent and item.dump then
mru_key = key
mru_found = mru_found + 1
if mru_found >= 2 then
-- We found the second MRU item, i.e., the *displayed* page
break
end
end
end
if mru_key then
local cache_full_path = self.cache_path .. md5(mru_key)
local cache_file_exists = lfs.attributes(cache_full_path)
if not cache_file_exists then
logger.dbg("Dumping cache item", mru_key)
local cache_item = self.cache:get(mru_key)
local cache_size = cache_item:dump(cache_full_path)
if cache_size then
cached_size = cached_size + cache_size
end
end
end
-- Allocate the same amount of storage to the disk cache than the memory cache
while cached_size > self.size do
-- discard the least recently used cache
local discarded = table.remove(sorted_caches)
if discarded then
cached_size = cached_size - lfs.attributes(discarded.file, "size")
os.remove(discarded.file)
else
logger.warn("Cache accounting is broken")
break
end
end
-- We may have updated the disk cache's content, so refresh its state
self:refreshSnapshot()
end
-- Blank the cache
function Cache:clear()
self.cache:clear()
@ -147,10 +298,10 @@ end
-- Terribly crappy workaround: evict half the cache if we appear to be redlining on free RAM...
function Cache:memoryPressureCheck()
local memfree, memtotal = util.calcFreeMem()
local memfree, memtotal = self:_calcFreeMem()
-- Nonsensical values? (!Linux), skip this.
if memtotal == nil then
if memtotal == 0 then
return
end
@ -158,9 +309,7 @@ function Cache:memoryPressureCheck()
local free_fraction = memfree / memtotal
if free_fraction < 0.20 then
logger.warn(string.format("Running low on memory (~%d%%, ~%.2f/%d MiB), evicting half of the cache...",
free_fraction * 100,
memfree / (1024 * 1024),
memtotal / (1024 * 1024)))
free_fraction * 100, memfree / 1024 / 1024, memtotal / 1024 / 1024))
self.cache:chop()
-- And finish by forcing a GC sweep now...

@ -9,15 +9,12 @@ local CacheItem = {
--- and the most common items we cache are Geom-like tables (i.e., 4 key-value pairs).
--- That's generally a low estimation, especially for larger tables, where memory allocation trickery may be happening.
function CacheItem:extend(o)
function CacheItem:new(o)
o = o or {}
setmetatable(o, self)
self.__index = self
return o
end
-- NOTE: There's no object-specific initialization, the entirety of the new data *is* the table we pass to new.
-- We only keep the distinction as a matter of semantics, to differentiate class declarations from object instantiations.
CacheItem.new = CacheItem.extend
-- Called on eviction.
-- We generally use it to free C/FFI resources *immediately* (as opposed to relying on our Userdata/FFI finalizers to do it "later" on GC).

@ -38,6 +38,9 @@ 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

@ -1,319 +0,0 @@
--[[--
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

@ -19,16 +19,34 @@ These functions don't do anything when debugging is turned off.
--]]--
local logger = require("logger")
local dump = require("dump")
local isAndroid, android = pcall(require, "android")
local Dbg = {
-- set to nil so first debug:turnOff call won't be skipped
is_on = nil,
is_verbose = nil,
ev_log = nil,
}
local Dbg_mt = {}
local LvDEBUG = logger.LvDEBUG
local function LvDEBUG(lv, ...)
local line = ""
for i,v in ipairs({...}) do
if type(v) == "table" then
line = line .. " " .. dump(v, lv)
else
line = line .. " " .. tostring(v)
end
end
if isAndroid then
android.LOGV(line)
else
io.stdout:write(string.format("# %s %s\n", os.date("%x-%X"), line))
io.stdout:flush()
end
end
--- Turn on debug mode.
-- This should only be used in tests and at the user's request.
@ -37,7 +55,7 @@ function Dbg:turnOn()
self.is_on = true
logger:setLevel(logger.levels.dbg)
Dbg_mt.__call = function(_, ...) return LvDEBUG(...) end
Dbg_mt.__call = function(dbg, ...) LvDEBUG(math.huge, ...) end
--- Pass a guard function to detect bad input values.
Dbg.guard = function(_, mod, method, pre_guard, post_guard)
local old_method = mod[method]
@ -45,11 +63,11 @@ function Dbg:turnOn()
if pre_guard then
pre_guard(...)
end
local values = table.pack(old_method(...))
local values = {old_method(...)}
if post_guard then
post_guard(...)
end
return unpack(values, 1, values.n)
return unpack(values)
end
end
--- Use this instead of a regular Lua @{assert}().
@ -57,6 +75,19 @@ function Dbg:turnOn()
assert(check, msg)
return check
end
-- create or clear ev log file
--- @note: On Linux, use CLOEXEC to avoid polluting the fd table of our child processes.
--- Otherwise, it can be problematic w/ wpa_supplicant & USBMS...
--- Note that this is entirely undocumented, but at least LuaJIT passes the mode as-is to fopen, so, we're good.
local open_flags = "w"
if jit.os == "Linux" then
-- Oldest Kindle devices are too old to support O_CLOEXEC...
if os.getenv("KINDLE_LEGACY") ~= "yes" then
open_flags = "we"
end
end
self.ev_log = io.open("ev.log", open_flags)
end
--- Turn off debug mode.
@ -65,12 +96,15 @@ function Dbg:turnOff()
if self.is_on == false then return end
self.is_on = false
logger:setLevel(logger.levels.info)
Dbg_mt.__call = function() end
-- NOTE: This doesn't actually disengage previously wrapped methods!
Dbg.guard = function() end
function Dbg_mt.__call() end
function Dbg.guard() end
Dbg.dassert = function(check)
return check
end
if self.ev_log then
self.ev_log:close()
self.ev_log = nil
end
end
--- Turn on verbose mode.
@ -82,20 +116,24 @@ end
--- Simple table dump.
function Dbg:v(...)
if self.is_verbose then
return LvDEBUG(...)
LvDEBUG(math.huge, ...)
end
end
--- Conditional logging with a stable ref.
function Dbg.log(...)
if Dbg.is_on then
return LvDEBUG(...)
--- Log @{ui.event|Event} to dedicated log file.
function Dbg:logEv(ev)
local ev_value = tostring(ev.value)
local log = ev.type.."|"..ev.code.."|"
..ev_value.."|"..ev.time.sec.."|"..ev.time.usec.."\n"
if self.ev_log then
self.ev_log:write(log)
self.ev_log:flush()
end
end
--- Simple traceback.
function Dbg:traceback()
return LvDEBUG(debug.traceback())
LvDEBUG(math.huge, debug.traceback())
end
setmetatable(Dbg, Dbg_mt)

@ -1,11 +1,9 @@
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")
@ -69,12 +67,11 @@ local external = require("device/thirdparty"):new{
end,
}
local Device = Generic:extend{
local Device = Generic:new{
isAndroid = yes,
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,
@ -114,24 +111,32 @@ local Device = Generic:extend{
android.dictLookup(text, app, action)
end
end,
-- Android is very finicky, and the LuaJIT allocator has a tremendously hard time getting the system
-- to allocate memory for it in the very specific memory range it requires to store generated code (mcode).
-- Failure to do so at runtime (mcode_alloc) will *tank* performance
-- (much worse than if the JIT were simply disabled).
-- The first line of defense is left to android-luajit-launcher,
-- which will try to grab the full mcode region in one go right at startup.
-- The second line of defense is *this* flag, which disables the JIT in a few code-hungry modules,
-- but not necessarily performance critical ones, to hopefully limit the amount of mcode memory
-- required for those modules where it *really* matters (e.g., ffi/blitbuffer.lua).
-- This is also why we try to actually avoid entering actual loops in the Lua blitter on Android,
-- and instead attempt to do most of everything via the C implementation.
-- NOTE: Since https://github.com/koreader/android-luajit-launcher/pull/283, we've patched LuaJIT
-- to ensure that the initial mcode alloc works, and sticks around, which is why this is no longer enabled.
should_restrict_JIT = false,
}
function Device:init()
self.screen = require("ffi/framebuffer_android"):new{device = self, debug = logger.dbg}
self.powerd = require("device/android/powerd"):new{device = self}
local event_map = require("device/android/event_map")
if android.prop.is_tolino then
-- dpad left/right as page back/forward
event_map[21] = "LPgBack"
event_map[22] = "LPgFwd"
end
self.input = require("device/input"):new{
device = self,
event_map = event_map,
event_map = require("device/android/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"))
@ -204,7 +209,7 @@ function Device:init()
end
elseif ev.code == C.APP_CMD_PAUSE then
if not android.prop.brokenLifecycle then
UIManager:broadcastEvent(Event:new("RequestSuspend"))
UIManager:broadcastEvent(Event:new("Suspend"))
end
elseif ev.code == C.AEVENT_POWER_CONNECTED then
UIManager:broadcastEvent(Event:new("Charging"))
@ -281,10 +286,6 @@ 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()
@ -297,13 +298,12 @@ function Device:initNetworkManager(NetworkMgr)
android.openWifiSettings()
end
function NetworkMgr:isConnected()
function NetworkMgr:isWifiOn()
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)
@ -336,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
@ -385,13 +385,12 @@ 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
-- (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
self.input:registerEventAdjustHook(
self.input.adjustTouchTranslate,
{x = 0 + self.viewport.x, y = 0 + self.viewport.y})
end
local viewport = Geom:new{x=0, y=statusbar_height, w=width, h=new_height}
@ -399,11 +398,10 @@ function Device:_toggleStatusBarVisibility()
0, statusbar_height, width, new_height))
self.screen:setViewport(viewport)
if is_fullscreen and self.viewport and self.viewport.y ~= 0 then
if is_fullscreen and self.viewport 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
@ -430,8 +428,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 on %4\nBuild flavor: %5\n"),
android.prop.product, getCodename(), Device.firmware_rev, jit.arch, android.prop.flavor)
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 platform_text = ""
if product_type ~= "android" then
@ -451,18 +449,12 @@ 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.LOGI(string.format("Stopping %s main activity", android.prop.name));
android.lib.ANativeActivity_finish(android.app.activity)
end
@ -494,6 +486,7 @@ 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
@ -502,18 +495,15 @@ 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)
@ -532,6 +522,7 @@ 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()
@ -552,6 +543,8 @@ 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,17 +6,25 @@ 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 the frontlight switch was off, turn it on.
-- if frontlight switch was toggled of, 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()
@ -58,13 +66,11 @@ function AndroidPowerD:turnOffFrontlightHW()
return
end
android.setScreenBrightness(self.fl_min)
if android.hasStandaloneWarmth() then
android.setScreenWarmth(self.fl_warmth_min)
end
self.is_fl_on = false
broadcastLightChanges()
end
function AndroidPowerD:turnOnFrontlightHW(done_callback)
function AndroidPowerD:turnOnFrontlightHW()
if self:isFrontlightOn() and self:isFrontlightOnHW() then
return
end
@ -73,10 +79,8 @@ function AndroidPowerD:turnOnFrontlightHW(done_callback)
android.setScreenBrightness(math.floor(self.fl_intensity * self.bright_diff / self.fl_max))
if android.hasStandaloneWarmth() then
android.setScreenWarmth(math.floor(self.fl_warmth / self.warm_diff))
end
return false
self.is_fl_on = true
broadcastLightChanges()
end
return AndroidPowerD

@ -12,6 +12,20 @@ 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")
@ -20,7 +34,7 @@ local function isMassStorageSupported()
return true
end
local Cervantes = Generic:extend{
local Cervantes = Generic:new{
model = "Cervantes",
isCervantes = yes,
isAlwaysPortrait = yes,
@ -32,11 +46,9 @@ 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?
@ -53,33 +65,33 @@ local Cervantes = Generic:extend{
canHWInvert = yes,
}
-- Cervantes Touch
local CervantesTouch = Cervantes:extend{
local CervantesTouch = Cervantes:new{
model = "CervantesTouch",
display_dpi = 167,
hasFrontlight = no,
hasMultitouch = no,
}
-- Cervantes TouchLight / Fnac Touch Plus
local CervantesTouchLight = Cervantes:extend{
local CervantesTouchLight = Cervantes:new{
model = "CervantesTouchLight",
display_dpi = 167,
hasMultitouch = no,
}
-- Cervantes 2013 / Fnac Touch Light
local Cervantes2013 = Cervantes:extend{
local Cervantes2013 = Cervantes:new{
model = "Cervantes2013",
display_dpi = 212,
hasMultitouch = no,
--- @fixme: Possibly requires canHWInvert = no, as it seems to be based on a similar board as the Kobo Aura...
}
-- Cervantes 3 / Fnac Touch Light 2
local Cervantes3 = Cervantes:extend{
local Cervantes3 = Cervantes:new{
model = "Cervantes3",
display_dpi = 300,
hasMultitouch = no,
}
-- Cervantes 4
local Cervantes4 = Cervantes:extend{
local Cervantes4 = Cervantes:new{
model = "Cervantes4",
display_dpi = 300,
hasNaturalLight = yes,
@ -94,10 +106,14 @@ local Cervantes4 = Cervantes:extend{
-- input events
function Cervantes:initEventAdjustHooks()
if self.touch_switch_xy and self.touch_mirrored_x then
if self.touch_switch_xy then
self.input:registerEventAdjustHook(self.input.adjustTouchSwitchXY)
end
if self.touch_mirrored_x then
self.input:registerEventAdjustHook(
self.input.adjustTouchSwitchAxesAndMirrorX,
(self.screen:getWidth() - 1)
self.input.adjustTouchMirrorX,
self.screen:getWidth()
)
end
@ -159,10 +175,10 @@ function Cervantes:initNetworkManager(NetworkMgr)
complete_callback()
end
end
function NetworkMgr:turnOnWifi(complete_callback, interactive)
function NetworkMgr:turnOnWifi(complete_callback)
logger.info("Cervantes: enabling Wi-Fi")
os.execute("./enable-wifi.sh")
return self:reconnectOrShowNetworkMenu(complete_callback, interactive)
self:reconnectOrShowNetworkMenu(complete_callback)
end
function NetworkMgr:getNetworkInterfaceName()
return "eth0"
@ -177,8 +193,33 @@ function Cervantes:initNetworkManager(NetworkMgr)
function NetworkMgr:restoreWifiAsync()
os.execute("./restore-wifi-async.sh")
end
NetworkMgr.isWifiOn = NetworkMgr.sysfsWifiOn
NetworkMgr.isConnected = NetworkMgr.ifHasAnAddress
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
end
-- power functions: suspend, resume, reboot, poweroff
@ -189,86 +230,10 @@ function Cervantes:resume()
os.execute("./resume.sh")
end
function Cervantes:reboot()
os.execute("sleep 1 && reboot &")
os.execute("reboot")
end
function Cervantes:powerOff()
os.execute("sleep 1 && halt &")
end
-- This method is the same as the one in kobo/device.lua except the sleep cover part.
function Cervantes:setEventHandlers(UIManager)
-- We do not want auto suspend procedure to waste battery during
-- 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:onPowerEvent("Suspend")
end
UIManager.event_handlers.Resume = function()
self:onPowerEvent("Resume")
end
UIManager.event_handlers.PowerPress = function()
-- Always schedule power off.
-- Press the power button for 2+ seconds to shutdown directly from suspend.
UIManager:scheduleIn(2, UIManager.poweroff_action)
end
UIManager.event_handlers.PowerRelease = function()
if not UIManager._entered_poweroff_stage then
UIManager:unschedule(UIManager.poweroff_action)
-- resume if we were suspended
if self.screen_saver_mode then
if self.screen_saver_lock then
UIManager.event_handlers.Suspend()
else
UIManager.event_handlers.Resume()
end
else
UIManager.event_handlers.Suspend()
end
end
end
UIManager.event_handlers.Light = function()
self:getPowerDevice():toggleFrontlight()
end
-- USB plug events with a power-only charger
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 and not self.screen_saver_lock then
UIManager.event_handlers.Suspend()
end
end
UIManager.event_handlers.NotCharging = function()
-- 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 and not self.screen_saver_lock then
UIManager.event_handlers.Suspend()
end
end
-- USB plug events with a data-aware host
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 and not self.screen_saver_lock then
UIManager.event_handlers.Suspend()
elseif not self.screen_saver_lock then
-- Potentially start an USBMS session
local MassStorage = require("ui/elements/mass_storage")
MassStorage:start()
end
end
UIManager.event_handlers.UsbPlugOut = function()
-- 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 and not self.screen_saver_lock then
UIManager.event_handlers.Suspend()
elseif not self.screen_saver_lock then
-- Potentially dismiss the USBMS ConfirmBox
local MassStorage = require("ui/elements/mass_storage")
MassStorage:dismiss()
end
end
os.execute("halt")
end
-------------- device probe ------------

@ -51,13 +51,10 @@ 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")
@ -135,29 +132,19 @@ function CervantesPowerD:isChargingHW()
end
function CervantesPowerD:beforeSuspend()
-- 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
if self.fl == nil then return end
-- just turn off frontlight without remembering its state
self.fl:setBrightness(0)
end
function CervantesPowerD:afterResume()
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
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)
end
self:invalidateCapacityCache()
-- Restore user input and emit the Resume event.
self.device:_afterResume()
end
return CervantesPowerD

@ -1,6 +1,7 @@
local ConfirmBox = require("ui/widget/confirmbox")
local Device = require("device")
local Event = require("ui/event")
local EventListener = require("ui/widget/eventlistener")
local InputContainer = require("ui/widget/container/inputcontainer")
local Notification = require("ui/widget/notification")
local Screen = Device.screen
local UIManager = require("ui/uimanager")
@ -8,7 +9,19 @@ local bit = require("bit")
local _ = require("gettext")
local T = require("ffi/util").template
local DeviceListener = EventListener:extend{}
local DeviceListener = InputContainer:new{}
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")
@ -42,68 +55,90 @@ function DeviceListener:onShowIntensity()
return true
end
function DeviceListener:onShowWarmth()
if not Device:hasNaturalLight() then return true end
-- Display it in the native scale, like FrontLightWidget
function DeviceListener:onShowWarmth(value)
local powerd = Device:getPowerDevice()
Notification:notify(T(_("Warmth set to %1."), powerd:toNativeWarmth(powerd:frontlightWarmth())))
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
return true
end
-- frontlight controller
if Device:hasFrontlight() then
local function calculateGestureDelta(ges, direction, min, max)
local delta_int
if type(ges) == "table" then
local gesture_multiplier
if ges.ges == "two_finger_swipe" or ges.ges == "swipe" then
gesture_multiplier = 0.8
-- 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
if ges.ges == "two_finger_swipe" then
-- for backward compatibility
scale_multiplier = FRONTLIGHT_SENSITIVITY_DECREASE * 0.8
elseif ges.ges == "swipe" then
scale_multiplier = 0.8
else
gesture_multiplier = 1
scale_multiplier = 1
end
local gestureScale
if ges.direction == "south" or ges.direction == "north" then
gestureScale = Screen:getHeight() * gesture_multiplier
gestureScale = Screen:getHeight() * scale_multiplier
elseif ges.direction == "west" or ges.direction == "east" then
gestureScale = Screen:getWidth() * gesture_multiplier
gestureScale = Screen:getWidth() * scale_multiplier
else
local width = Screen:getWidth()
local height = Screen:getHeight()
-- diagonal
gestureScale = math.sqrt(width^2 + height^2) * gesture_multiplier
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)
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
-- 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)
local step = math.ceil(#steps_tbl * ges.distance / gestureScale)
delta_int = steps_tbl[step] or steps_tbl[#steps_tbl]
else
-- The ges arg passed by our caller wasn't a gesture, but an absolute integer increment
-- received amount to change
delta_int = ges
end
if direction ~= -1 and direction ~= 1 then
-- If the caller didn't specify, opt to *increase* by default
-- set default value (increase frontlight)
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 = calculateGestureDelta(ges, direction, powerd.fl_min, powerd.fl_max)
local delta_int
--received gesture
direction, delta_int = calculateGestureDelta(ges, direction, powerd.fl_min, powerd.fl_max)
local new_intensity = powerd:frontlightIntensity() + delta
-- when new_intensity <= 0, toggle light off
local new_intensity = powerd.fl_intensity + direction * delta_int
if new_intensity == nil then return true end
-- when new_intensity <=0, toggle light off
self:onSetFlIntensity(new_intensity)
self:onShowIntensity()
return true
@ -132,17 +167,15 @@ if Device:hasFrontlight() then
-- direction +1 - increase frontlight warmth
-- direction -1 - decrease frontlight warmth
function DeviceListener:onChangeFlWarmth(ges, direction)
if not Device:hasNaturalLight() then return true end
local powerd = Device:getPowerDevice()
local delta = calculateGestureDelta(ges, direction, powerd.fl_warmth_min, powerd.fl_warmth_max)
if powerd.fl_warmth == nil then return false end
local delta_int
--received gesture
-- 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)
direction, delta_int = calculateGestureDelta(ges, direction, powerd.fl_warmth_min, powerd.fl_warmth_max)
local warmth = math.floor(powerd.fl_warmth + direction * delta_int * 100 / powerd.fl_warmth_max)
self:onSetFlWarmth(warmth)
self:onShowWarmth()
return true
@ -169,20 +202,15 @@ if Device:hasFrontlight() then
function DeviceListener:onToggleFrontlight()
local powerd = Device:getPowerDevice()
powerd:toggleFrontlight()
local new_text
if powerd:isFrontlightOn() then
new_text = _("Frontlight disabled.")
else
if powerd.is_fl_on then
new_text = _("Frontlight enabled.")
else
new_text = _("Frontlight disabled.")
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()
@ -191,9 +219,9 @@ if Device:hasFrontlight() then
end
if Device:hasGSensor() then
if Device:canToggleGSensor() then
function DeviceListener:onToggleGSensor()
G_reader_settings:flipNilOrFalse("input_ignore_gsensor")
_toggleSetting("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
@ -201,20 +229,9 @@ if Device:hasGSensor() then
else
new_text = _("Accelerometer rotation events on.")
end
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)
UIManager:show(Notification:new{
text = new_text,
})
return true
end
end
@ -225,10 +242,9 @@ if not Device:isAlwaysFullscreen() then
end
end
function DeviceListener:onIterateRotation(ccw)
-- Simply rotate by 90° CW or CCW
local step = ccw and -1 or 1
local arg = bit.band(Screen:getRotationMode() + step, 3)
function DeviceListener:onIterateRotation()
-- Simply rotate by 90° CW
local arg = bit.band(Screen:getRotationMode() + 1, 3)
self.ui:handleEvent(Event:new("SetRotationMode", arg))
return true
end
@ -274,84 +290,64 @@ end
function DeviceListener:onSetFlashOnChapterBoundaries(toggle)
if toggle == true then
G_reader_settings:makeTrue("refresh_on_chapter_boundaries")
_setSetting("refresh_on_chapter_boundaries")
else
G_reader_settings:delSetting("refresh_on_chapter_boundaries")
_unsetSetting("refresh_on_chapter_boundaries")
end
end
function DeviceListener:onToggleFlashOnChapterBoundaries()
G_reader_settings:flipNilOrFalse("refresh_on_chapter_boundaries")
_toggleSetting("refresh_on_chapter_boundaries")
end
function DeviceListener:onSetNoFlashOnSecondChapterPage(toggle)
if toggle == true then
G_reader_settings:makeTrue("no_refresh_on_second_chapter_page")
_setSetting("no_refresh_on_second_chapter_page")
else
G_reader_settings:delSetting("no_refresh_on_second_chapter_page")
_unsetSetting("no_refresh_on_second_chapter_page")
end
end
function DeviceListener:onToggleNoFlashOnSecondChapterPage()
G_reader_settings:flipNilOrFalse("no_refresh_on_second_chapter_page")
_toggleSetting("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")
if Device:canReboot() then
function DeviceListener:onReboot()
UIManager:show(ConfirmBox:new{
text = _("Are you sure you want to reboot the device?"),
ok_text = _("Reboot"),
ok_callback = function()
UIManager:nextTick(UIManager.reboot_action)
end,
})
end
end
function DeviceListener:onToggleFlashOnPagesWithImages()
G_reader_settings:flipNilOrTrue("refresh_on_pages_with_images")
end
function DeviceListener:onSwapPageTurnButtons()
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")
if Device:canPowerOff() then
function DeviceListener:onPowerOff()
UIManager:show(ConfirmBox:new{
text = _("Are you sure you want to power off the device?"),
ok_text = _("Power off"),
ok_callback = function()
UIManager:nextTick(UIManager.poweroff_action)
end,
})
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
function DeviceListener:onRequestSuspend()
function DeviceListener:onSuspendEvent()
UIManager:suspend()
end
function DeviceListener:onRequestReboot()
UIManager:askForReboot()
end
function DeviceListener:onRequestPowerOff()
UIManager:askForPowerOff()
end
function DeviceListener:onExit(callback)
self.ui.menu:exitOrRestart(callback)
end
function DeviceListener:onRestart()
self.ui.menu:exitOrRestart(function() UIManager:restartKOReader() end)
end
function DeviceListener:onFullRefresh()
if self.ui and self.ui.view then
self.ui:handleEvent(Event:new("UpdateFooter", self.ui.view.footer_visible))
@ -359,10 +355,4 @@ 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

@ -4,7 +4,7 @@ local logger = require("logger")
local function yes() return true end
local function no() return false end
local Device = Generic:extend{
local Device = Generic:new{
model = "dummy",
hasKeyboard = no,
hasKeys = no,

@ -5,31 +5,26 @@ 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,
screen_saver_lock = false,
charging_mode = false,
survive_screen_saver = false,
is_cover_closed = false,
should_restrict_JIT = 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
@ -41,31 +36,24 @@ 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
@ -74,10 +62,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,
@ -142,7 +130,7 @@ local Device = {
canExternalDictLookup = no,
}
function Device:extend(o)
function Device:new(o)
o = o or {}
setmetatable(o, self)
self.__index = self
@ -171,6 +159,7 @@ function Device:invertButtons()
end
function Device:init()
assert(self ~= nil)
if not self.screen then
error("screen/framebuffer must be implemented")
end
@ -214,18 +203,12 @@ 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)
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
self.input:registerEventAdjustHook(
self.input.adjustTouchTranslate,
{x = 0 - self.viewport.x, y = 0 - self.viewport.y})
end
-- Handle button mappings shenanigans
@ -235,13 +218,8 @@ function Device:init()
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
@ -253,47 +231,6 @@ 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)
@ -311,6 +248,7 @@ 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
@ -321,47 +259,38 @@ 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.
logger.dbg("Pressed power while asleep in screen saver mode with a closed sleepcover, going back to suspend...")
-- don't let power key press wake up device when the cover is in closed state
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(0, 0, self.screen:getWidth(), self.screen:getHeight()) end)
UIManager:scheduleIn(1, function() self.screen:refreshFull() end)
end
self.screen_saver_mode = false
self.powerd:afterResume()
end
elseif ev == "Suspend" then
-- 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
-- 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, suspending...")
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")
@ -375,31 +304,35 @@ 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(0, 0, self.screen:getWidth(), self.screen:getHeight())
self.screen:refreshFull()
end
-- 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()
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
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()
-- 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()
end
end)
end
end
function Device:showLightDialog()
local FrontLightWidget = require("ui/widget/frontlightwidget")
local UIManager = require("ui/uimanager")
UIManager:show(FrontLightWidget:new{})
end
@ -408,6 +341,8 @@ 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?"),
@ -415,19 +350,11 @@ function Device:install()
ok_callback = function()
local save_quit = function()
self:saveSettings()
UIManager:quit(85)
UIManager:quit()
UIManager._exit_code = 85
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
@ -457,11 +384,9 @@ function Device:suspend() end
-- Hardware specific method to resume the device
function Device:resume() end
-- NOTE: These two should ideally run in the background, and only trip the action after a small delay,
-- to give us time to quit first.
-- e.g., os.execute("sleep 1 && shutdown -r now &")
-- Hardware specific method to power off the device
function Device:powerOff() end
-- Hardware specific method to reboot the device
function Device:reboot() end
@ -496,17 +421,8 @@ function Device:performHapticFeedback(type) end
-- Device specific method for toggling input events
function Device:setIgnoreInput(enable) return true 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
-- Device specific method for toggling the GSensor
function Device:toggleGSensor(toggle) 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)
@ -541,400 +457,56 @@ 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 (and is responsible for setting the canKeyRepeat cap)
-- Device specific method to get the current key repeat setup
function Device:getKeyRepeat() end
-- Device specific method to disable key repeat
function Device:disableKeyRepeat() end
-- Device specific method to restore the initial key repeat config
-- Device specific method to restore key repeat
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()
-- Flush settings to disk
G_reader_settings:close()
-- I/O teardown
self.input.teardown()
end
-- 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)
require("ffi/input"):closeAll()
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...
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
if self:isKindle() and self:hasDPad() then
-- 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
pingok = os.execute("ping -q -c 2 `ip r | grep default | tail -n 1 | cut -d ' ' -f 3` > /dev/null")
else
return os.execute("ping -q -c1 -w2 " .. ip .. " > /dev/null") == 0
pingok = os.execute("ping -q -w 3 -c 2 `ip r | grep default | tail -n 1 | cut -d ' ' -f 3` > /dev/null")
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
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
if pingok == 0 then
result = result .. "Gateway ping successful"
else
-- 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
result = result .. "Gateway ping FAILED"
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
result = result .. "No default gateway to ping"
end
::continue::
return result
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
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
else
table.insert(results, _("No default gateway to ping"))
end
return table.concat(results, "\n")
end
function Device:setTime(hour, min)
@ -981,14 +553,13 @@ 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, with_stripped_root)
function Device:unpackArchive(archive, extract_to)
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, with_stripped_root)
ok = self:untar(archive, extract_to)
else
return false, T(_("Couldn't extract archive:\n\n%1\n\nUnrecognized filename extension."), BD.filepath(archive))
end
@ -998,147 +569,8 @@ function Device:unpackArchive(archive, extract_to, with_stripped_root)
return true
end
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(uimgr)
if self:canReboot() then
UIManager.event_handlers.Reboot = function(message_text)
local ConfirmBox = require("ui/widget/confirmbox")
UIManager:show(ConfirmBox:new{
text = message_text or _("Are you sure you want to reboot the device?"),
ok_text = _("Reboot"),
ok_callback = function()
UIManager:nextTick(UIManager.reboot_action)
end,
})
end
else
UIManager.event_handlers.Reboot = function() end
end
if self:canPowerOff() then
UIManager.event_handlers.PowerOff = function(message_text)
local ConfirmBox = require("ui/widget/confirmbox")
UIManager:show(ConfirmBox:new{
text = message_text or _("Are you sure you want to power off the device?"),
ok_text = _("Power off"),
ok_callback = function()
UIManager:nextTick(UIManager.poweroff_action)
end,
})
end
else
UIManager.event_handlers.PowerOff = function() end
end
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 implementing this method.
function Device:setEventHandlers(uimgr)
-- These will most probably be overwritten by device-specific `setEventHandlers` implementations
UIManager.event_handlers.Suspend = function()
self.powerd:beforeSuspend()
end
UIManager.event_handlers.Resume = function()
self.powerd:afterResume()
end
end
-- The common operations that should be performed before suspending the device.
function Device:_beforeSuspend(inhibit)
UIManager:flushSettings()
UIManager:broadcastEvent(Event:new("Suspend"))
if inhibit ~= false then
-- Block input events unrelated to power management
self.input:inhibitInput(true)
-- Disable key repeat to avoid useless chatter (especially where Sleep Covers are concerned...)
self:disableKeyRepeat()
end
end
-- The common operations that should be performed after resuming the device.
function Device:_afterResume(inhibit)
if inhibit ~= false then
-- 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
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()
-- 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()
self.powerd:invalidateCapacityCache()
self:_updateChargingLED()
UIManager:broadcastEvent(Event:new("NotCharging"))
function Device:untar(archive, extract_to)
return os.execute(("./tar xf %q -C %q"):format(archive, extract_to))
end
return Device

@ -1,6 +1,5 @@
local Event = require("ui/event")
local UIManager -- will be updated when available
local Math = require("optmath")
local UIManager
local logger = require("logger")
local time = require("ui/time")
local BasePowerD = {
@ -42,11 +41,12 @@ function BasePowerD:new(o)
return o
end
function BasePowerD:init() end
--- @note: This should *always* call self:_decideFrontlightState() in its coda (unless you have a custom isFrontlightOn implementation)!
function BasePowerD:setIntensityHW(intensity)
self:_decideFrontlightState()
function BasePowerD:readyUI()
UIManager = require("ui/uimanager")
end
function BasePowerD:init() end
function BasePowerD:setIntensityHW(intensity) 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,52 +62,23 @@ 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
--- @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: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)?
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).
-- 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
function BasePowerD:beforeSuspend() end
-- Anything that needs to be done after doing a real hardware resume.
-- (Such as restoring front light state).
-- 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:afterResume() end
function BasePowerD:isFrontlightOn()
assert(self ~= nil)
return self.is_fl_on
end
function BasePowerD:_decideFrontlightState()
assert(self ~= nil)
assert(self.device:hasFrontlight())
self.is_fl_on = self:isFrontlightOnHW()
end
@ -117,6 +88,7 @@ function BasePowerD:isFrontlightOff()
end
function BasePowerD:frontlightIntensity()
assert(self ~= nil)
if not self.device:hasFrontlight() then return 0 end
if self:isFrontlightOff() then return 0 end
--- @note: We assume that nothing other than us will set the frontlight level,
@ -125,41 +97,39 @@ function BasePowerD:frontlightIntensity()
return self.fl_intensity
end
function BasePowerD:toggleFrontlight(done_callback)
function BasePowerD:toggleFrontlight()
assert(self ~= nil)
if not self.device:hasFrontlight() then return false end
if self:isFrontlightOn() then
return self:turnOffFrontlight(done_callback)
return self:turnOffFrontlight()
else
return self:turnOnFrontlight(done_callback)
return self:turnOnFrontlight()
end
end
function BasePowerD:turnOffFrontlight(done_callback)
function BasePowerD:turnOffFrontlight()
assert(self ~= nil)
if not self.device:hasFrontlight() then return end
if self:isFrontlightOff() then return false end
local cb_handled = self:turnOffFrontlightHW(done_callback)
self:turnOffFrontlightHW()
self.is_fl_on = false
self:stateChanged()
if not cb_handled and done_callback then
done_callback()
end
return true
end
function BasePowerD:turnOnFrontlight(done_callback)
function BasePowerD:turnOnFrontlight()
assert(self ~= nil)
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?
local cb_handled = self:turnOnFrontlightHW(done_callback)
self:turnOnFrontlightHW()
self.is_fl_on = true
self:stateChanged()
if not cb_handled and done_callback then
done_callback()
end
return true
end
function BasePowerD:frontlightWarmth()
assert(self ~= nil)
if not self.device:hasNaturalLight() then
return 0
end
@ -185,7 +155,7 @@ function BasePowerD:unchecked_read_int_file(file)
fd:close()
return int
else
return nil
return
end
end
@ -210,6 +180,7 @@ 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()
@ -309,6 +280,7 @@ 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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -51,10 +51,8 @@ least one key in this table must match.
E.g.:
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
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,8 +1,6 @@
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{
@ -29,22 +27,17 @@ 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(done_callback)
function KindlePowerD:turnOnFrontlightHW()
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 KindlePowerD:turnOnFrontlight(done_callback)
function BasePowerD:turnOnFrontlight()
if not self.device:hasFrontlight() then return end
if self:isFrontlightOn() then return false end
local cb_handled = self:turnOnFrontlightHW(done_callback)
self:turnOnFrontlightHW()
self.is_fl_on = true
self:stateChanged()
if not cb_handled and done_callback then
done_callback()
end
return true
end
@ -84,14 +77,6 @@ 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
@ -113,17 +98,13 @@ 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.
ffiUtil.writeToSysfs(intensity, self.fl_intensity_file)
os.execute("printf '%s' ".. 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
ffiUtil.writeToSysfs(intensity, self.warmth_intensity_file)
os.execute("printf '%s' ".. 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()
@ -180,24 +161,27 @@ function KindlePowerD:isChargedHW()
return false
end
function KindlePowerD:hasHallSensor()
return self.hall_file ~= nil
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: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
end
function KindlePowerD:toggleSuspend()
if self.lipc_handle then
self.lipc_handle:set_int_property("com.lab126.powerd", "powerButton", 1)
@ -222,15 +206,15 @@ end
function KindlePowerD:checkUnexpectedWakeup()
local state = self:getPowerdState()
logger.dbg("Powerd resume state:", state)
logger.info("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(90) then
if self.device.wakeup_mgr:isWakeupAlarmScheduled() and self.device.wakeup_mgr:wakeupAction() then
logger.info("Kindle scheduled wakeup")
else
logger.warn("Kindle unscheduled wakeup")
logger.info("Kindle unscheduled wakeup")
end
end
@ -243,15 +227,18 @@ function KindlePowerD:initWakeupMgr()
if not self.device:supportsScreensaver() then return end
if self.lipc_handle == nil then return end
function KindlePowerD:wakeupFromSuspend(ts)
function KindlePowerD:wakeupFromSuspend()
logger.info("Kindle wakeupFromSuspend")
-- 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(delay)
function KindlePowerD:readyToSuspend()
logger.info("Kindle readyToSuspend")
if self.device.wakeup_mgr:isWakeupAlarmScheduled() then
local now = os.time()
local alarm = self.device.wakeup_mgr:getWakeupAlarmEpoch()
@ -268,50 +255,7 @@ function KindlePowerD:initWakeupMgr()
self.device.wakeup_mgr = WakeupMgr:new{rtc = require("device/kindle/mockrtc")}
end
-- 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.
--- @fixme: This won't ever fire, 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 UIManager
local ffiUtil = require("ffi/util")
local RTC = require("ffi/rtc")
-- Here, we only deal with the real hw intensity.
@ -20,13 +20,12 @@ local KoboPowerD = BasePowerD:new{
fl_was_on = nil,
}
--- @todo Remove G_defaults:readSetting("KOBO_LIGHT_ON_START")
--- @todo Remove KOBO_LIGHT_ON_START
function KoboPowerD:_syncKoboLightOnStart()
local new_intensity = nil
local is_frontlight_on = nil
local new_warmth = nil
local kobo_light_on_start = tonumber(G_defaults:readSetting("KOBO_LIGHT_ON_START"))
local kobo_light_on_start = tonumber(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)
@ -43,7 +42,7 @@ function KoboPowerD:_syncKoboLightOnStart()
-- ColorSetting is stored as a color temperature scale in Kelvin,
-- from 1500 to 6400
-- so normalize this to [0, 100] on our end.
new_warmth = (100 - Math.round((new_color - 1500) * (1/49)))
new_warmth = (100 - Math.round((new_color - 1500) / 49))
end
end
if is_frontlight_on == nil then
@ -133,17 +132,8 @@ function KoboPowerD:init()
self.initial_is_fl_on = true
if self.device:hasFrontlight() then
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.
-- If this device has natural light (currently only KA1 & Forma)
-- 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
@ -156,8 +146,6 @@ 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")
@ -182,7 +170,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, true)
self:setWarmth(self.fl_warmth)
end
-- Use setIntensity to ensure it sets fl_intensity, and because we don't want the ramping behavior of turnOn
self:setIntensity(self:frontlightIntensityHW())
@ -215,7 +203,7 @@ function KoboPowerD:saveSettings()
G_reader_settings:saveSetting("frontlight_warmth", cur_warmth)
end
-- And to "Kobo eReader.conf" if needed
if G_defaults:readSetting("KOBO_SYNC_BRIGHTNESS_WITH_NICKEL") then
if KOBO_SYNC_BRIGHTNESS_WITH_NICKEL then
if NickelConf.frontLightState.get() ~= nil then
if NickelConf.frontLightState.get() ~= cur_is_fl_on then
NickelConf.frontLightState.set(cur_is_fl_on)
@ -250,10 +238,10 @@ function KoboPowerD:isFrontlightOnHW()
self.initial_is_fl_on = nil
return ret
end
return self.hw_intensity > 0 and not self.fl_ramp_down_running
return self.hw_intensity > 0
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)
@ -269,11 +257,6 @@ 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()
@ -314,109 +297,34 @@ function KoboPowerD:isChargedHW()
return false
end
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)
function KoboPowerD:turnOffFrontlightHW()
if not self:isFrontlightOnHW() then
return
end
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
ffiUtil.runInSubProcess(function()
for i = 1,5 do
self:setIntensityHW(math.floor(self.fl_intensity - ((self.fl_intensity / 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)
end
end
end
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
end
-- 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, 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
end
self:_decideFrontlightState()
end
function KoboPowerD:turnOnFrontlightHW(done_callback)
function KoboPowerD:turnOnFrontlightHW()
-- 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).
@ -426,77 +334,55 @@ function KoboPowerD:turnOnFrontlightHW(done_callback)
if self:isFrontlightOnHW() then
return
end
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
ffiUtil.runInSubProcess(function()
for i = 1,5 do
self:setIntensityHW(math.ceil(self.fl_min + ((self.fl_intensity / 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)
end
end
end
else
-- If UIManager is not initialized yet, just turn it on immediately
self:setIntensityHW(self.fl_intensity)
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
end
-- We consume done_callback ourselves, make sure Generic's PowerD gets the memo
return true
self:_decideFrontlightState()
end
-- Turn off front light before suspend.
function KoboPowerD:beforeSuspend()
-- 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
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()
end
-- Restore front light state after resume.
function KoboPowerD:afterResume()
-- 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)
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)
end
end
end
-- Turn the frontlight back on
self:turnOnFrontlight()
function KoboPowerD:UIManagerReadyHW(uimgr)
UIManager = uimgr
-- Set the system clock to the hardware clock's time.
RTC:HCToSys()
end
return KoboPowerD

@ -1,6 +1,4 @@
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
@ -13,13 +11,17 @@ 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
local ext_path = "/mnt/ext1/system/config/extensions.cfg"
local app_name = "koreader.app"
local PocketBook = Generic:extend{
local PocketBook = Generic:new{
model = "PocketBook",
isPocketBook = yes,
hasOTAUpdates = yes,
@ -56,11 +58,9 @@ local PocketBook = Generic:extend{
-- Works same as input.event_map, but for raw input EV_KEY translation
keymap = { [scan] = event },
}]]
-- 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,
-- Runtime state: whether raw input is actually used
--- @fixme: Never actually set anywhere?
is_using_raw_input = nil,
-- Will be set appropriately at init
isB288SoC = no,
@ -188,7 +188,6 @@ 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",
@ -213,36 +212,40 @@ 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,
}
-- If InkView translates buttons for us, disable our own translation map
if self.inkview_translates_buttons then
self.input:disableRotationMap()
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 tells us this device has a gsensor enable the event based functionality
if inkview.QueryGSensor() ~= 0 then
self.hasGSensor = yes
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
-- 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
-- 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)
self._model_init()
-- 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
if (not self.input.raw_input) or (not pcall(self.input.open, self.input, self.raw_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.
@ -257,31 +260,14 @@ 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)
local fn = document and document.file or nil
logger.dbg("Notify book state", title or "[nil]", fn or "[nil]")
os.remove("/tmp/.current")
if fn then
local fo = io.open("/tmp/.current", "w+")
if fo then
fo:write(fn)
fo:close()
end
fo:write(fn)
fo:close()
end
inkview.SetSubtaskInfo(inkview.GetCurrentTask(), 0, title and (title .. " - koreader") or "koreader", fn or _("N/A"))
end
@ -350,6 +336,8 @@ 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)
@ -383,18 +371,9 @@ function PocketBook:initNetworkManager(NetworkMgr)
end
end
function NetworkMgr:isConnected()
function NetworkMgr:isWifiOn()
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()
@ -409,44 +388,6 @@ function PocketBook:getDefaultCoverPath()
return "/mnt/ext1/system/logo/offlogo/cover.bmp"
end
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.powerd:beforeSuspend()
end
UIManager.event_handlers.Resume = function()
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
@ -456,7 +397,7 @@ local function landscape_ccw() return {
} end
-- PocketBook Mini (515)
local PocketBook515 = PocketBook:extend{
local PocketBook515 = PocketBook:new{
model = "PB515",
display_dpi = 200,
isTouchDevice = no,
@ -466,7 +407,7 @@ local PocketBook515 = PocketBook:extend{
}
-- PocketBook Basic 4 (606)
local PocketBook606 = PocketBook:extend{
local PocketBook606 = PocketBook:new{
model = "PB606",
display_dpi = 212,
isTouchDevice = no,
@ -476,7 +417,7 @@ local PocketBook606 = PocketBook:extend{
}
-- PocketBook Basic (611)
local PocketBook611 = PocketBook:extend{
local PocketBook611 = PocketBook:new{
model = "PB611",
display_dpi = 167,
isTouchDevice = no,
@ -486,19 +427,18 @@ local PocketBook611 = PocketBook:extend{
}
-- PocketBook Basic (613)
local PocketBook613 = PocketBook:extend{
local PocketBook613 = PocketBook:new{
model = "PB613B",
display_dpi = 167,
isTouchDevice = no,
hasWifiToggle = no,
hasSeamlessWifiToggle = no,
hasFrontlight = no,
hasDPad = yes,
hasFewKeys = yes,
}
-- PocketBook Basic 2 / Basic 3 (614/614W)
local PocketBook614W = PocketBook:extend{
local PocketBook614W = PocketBook:new{
model = "PB614W",
display_dpi = 167,
isTouchDevice = no,
@ -508,7 +448,7 @@ local PocketBook614W = PocketBook:extend{
}
-- PocketBook Basic Lux / 615 Plus (615/615W)
local PocketBook615 = PocketBook:extend{
local PocketBook615 = PocketBook:new{
model = "PBBLux",
display_dpi = 212,
isTouchDevice = no,
@ -517,7 +457,7 @@ local PocketBook615 = PocketBook:extend{
}
-- PocketBook Basic Lux 2 (616/616W)
local PocketBook616 = PocketBook:extend{
local PocketBook616 = PocketBook:new{
model = "PBBLux2",
display_dpi = 212,
isTouchDevice = no,
@ -526,7 +466,7 @@ local PocketBook616 = PocketBook:extend{
}
-- PocketBook Basic Lux 3 (617)
local PocketBook617 = PocketBook:extend{
local PocketBook617 = PocketBook:new{
model = "PBBLux3",
display_dpi = 212,
isTouchDevice = no,
@ -535,54 +475,48 @@ 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{
local PocketBook622 = PocketBook:new{
model = "PBTouch",
display_dpi = 167,
hasFrontlight = no,
}
-- PocketBook Touch Lux (623)
local PocketBook623 = PocketBook:extend{
local PocketBook623 = PocketBook:new{
model = "PBTouchLux",
display_dpi = 212,
}
-- PocketBook Basic Touch (624)
local PocketBook624 = PocketBook:extend{
local PocketBook624 = PocketBook:new{
model = "PBBasicTouch",
display_dpi = 167,
hasFrontlight = no,
}
-- PocketBook Basic Touch 2 (625)
local PocketBook625 = PocketBook:extend{
local PocketBook625 = PocketBook:new{
model = "PBBasicTouch2",
display_dpi = 167,
hasFrontlight = no,
}
-- PocketBook Touch Lux 2 / Touch Lux 3 (626)
local PocketBook626 = PocketBook:extend{
local PocketBook626 = PocketBook:new{
model = "PBLux3",
display_dpi = 212,
}
-- PocketBook Touch Lux 4 (627)
local PocketBook627 = PocketBook:extend{
local PocketBook627 = PocketBook:new{
model = "PBLux4",
display_dpi = 212,
isAlwaysPortrait = yes,
}
-- PocketBook Touch Lux 5 (628)
local PocketBook628 = PocketBook:extend{
local PocketBook628 = PocketBook:new{
model = "PBTouchLux5",
display_dpi = 212,
isAlwaysPortrait = yes,
@ -590,22 +524,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{
local PocketBook630 = PocketBook:new{
model = "PBSense",
display_dpi = 212,
}
-- PocketBook Touch HD / Touch HD 2 (631)
local PocketBook631 = PocketBook:extend{
local PocketBook631 = PocketBook:new{
model = "PBTouchHD",
display_dpi = 300,
-- see https://github.com/koreader/koreader/pull/6531#issuecomment-676629182
@ -613,7 +539,7 @@ local PocketBook631 = PocketBook:extend{
}
-- PocketBook Touch HD Plus / Touch HD 3 (632)
local PocketBook632 = PocketBook:extend{
local PocketBook632 = PocketBook:new{
model = "PBTouchHDPlus",
display_dpi = 300,
isAlwaysPortrait = yes,
@ -622,72 +548,43 @@ local PocketBook632 = PocketBook:extend{
}
-- PocketBook Color (633)
local PocketBook633 = PocketBook:extend{
local PocketBook633 = PocketBook:new{
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{
local PocketBook640 = PocketBook:new{
model = "PBAqua",
display_dpi = 167,
}
-- PocketBook Aqua 2 (641)
local PocketBook641 = PocketBook:extend{
local PocketBook641 = PocketBook:new{
model = "PBAqua2",
display_dpi = 212,
}
-- PocketBook Ultra (650)
local PocketBook650 = PocketBook:extend{
local PocketBook650 = PocketBook:new{
model = "PBUltra",
display_dpi = 212,
}
-- PocketBook Era (700)
local PocketBook700 = PocketBook:extend{
local PocketBook700 = PocketBook:new{
model = "PB700",
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{
local PocketBook740 = PocketBook:new{
model = "PBInkPad3",
display_dpi = 300,
isAlwaysPortrait = yes,
@ -696,7 +593,7 @@ local PocketBook740 = PocketBook:extend{
}
-- PocketBook InkPad 3 Pro (740_2)
local PocketBook740_2 = PocketBook:extend{
local PocketBook740_2 = PocketBook:new{
model = "PBInkPad3Pro",
display_dpi = 300,
isAlwaysPortrait = yes,
@ -713,76 +610,31 @@ local PocketBook740_2 = PocketBook:extend{
}
-- PocketBook InkPad Color (741)
local PocketBook741 = PocketBook:extend{
local PocketBook741 = PocketBook:new{
model = "PBInkPadColor",
display_dpi = 300,
hasColorScreen = yes,
canHWDither = yes, -- Adjust color saturation with inkview
canUseCBB = no, -- 24bpp
isAlwaysPortrait = yes,
usingForcedRotation = landscape_ccw,
}
function PocketBook741._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 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)
function PocketBook741._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{
local PocketBookColorLux = PocketBook:new{
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.DEVICE_ROTATED_UPRIGHT
self.screen.native_rotation_mode = self.screen.DEVICE_ROTATED_UPRIGHT
self.screen.blitbuffer_rotation_mode = self.screen.ORIENTATION_PORTRAIT
self.screen.native_rotation_mode = self.screen.ORIENTATION_PORTRAIT
end
function PocketBookColorLux._fb_init(fb, finfo, vinfo)
-- Pocketbook Color Lux reports bits_per_pixel = 8, but actually uses an RGB24 framebuffer
@ -792,13 +644,13 @@ function PocketBookColorLux._fb_init(fb, finfo, vinfo)
end
-- PocketBook InkPad / InkPad 2 (840)
local PocketBook840 = PocketBook:extend{
local PocketBook840 = PocketBook:new{
model = "PBInkPad",
display_dpi = 250,
}
-- PocketBook InkPad Lite (970)
local PocketBook970 = PocketBook:extend{
local PocketBook970 = PocketBook:new{
model = "PB970",
display_dpi = 150,
isAlwaysPortrait = yes,
@ -806,7 +658,7 @@ local PocketBook970 = PocketBook:extend{
}
-- PocketBook InkPad X (1040)
local PocketBook1040 = PocketBook:extend{
local PocketBook1040 = PocketBook:new{
model = "PB1040",
display_dpi = 227,
isAlwaysPortrait = yes,
@ -816,91 +668,71 @@ local PocketBook1040 = PocketBook:extend{
logger.info('SoftwareVersion: ', PocketBook:getSoftwareVersion())
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
local codename = PocketBook:getDeviceModel()
if codename == "515" then
if codename == "PocketBook 515" then
return PocketBook515
elseif codename == "606" then
elseif codename == "PB606" or codename == "PocketBook 606" then
return PocketBook606
elseif codename == "611" then
elseif codename == "PocketBook 611" then
return PocketBook611
elseif codename == "613" then
elseif codename == "PocketBook 613" then
return PocketBook613
elseif codename == "614" or codename == "614W" then
elseif codename == "PocketBook 614" or codename == "PocketBook 614W" then
return PocketBook614W
elseif codename == "615" or codename == "615W" then
elseif codename == "PB615" or codename == "PB615W" or
codename == "PocketBook 615" or codename == "PocketBook 615W" then
return PocketBook615
elseif codename == "616" or codename == "616W" then
elseif codename == "PB616" or codename == "PB616W" or
codename == "PocketBook 616" or codename == "PocketBook 616W" then
return PocketBook616
elseif codename == "617" then
elseif codename == "PB617" or codename == "PocketBook 617" then
return PocketBook617
elseif codename == "618" then
return PocketBook618
elseif codename == "622" then
elseif codename == "PocketBook 622" then
return PocketBook622
elseif codename == "623" then
elseif codename == "PocketBook 623" then
return PocketBook623
elseif codename == "624" then
elseif codename == "PocketBook 624" then
return PocketBook624
elseif codename == "625" then
elseif codename == "PB625" then
return PocketBook625
elseif codename == "626" or codename == "626(2)-TL3" then
elseif codename == "PB626" or codename == "PB626(2)-TL3" or
codename == "PocketBook 626" then
return PocketBook626
elseif codename == "627" then
elseif codename == "PB627" then
return PocketBook627
elseif codename == "628" then
elseif codename == "PB628" then
return PocketBook628
elseif codename == "629" then
return PocketBook629
elseif codename == "630" then
elseif codename == "PocketBook 630" then
return PocketBook630
elseif codename == "631" then
elseif codename == "PB631" or codename == "PocketBook 631" then
return PocketBook631
elseif codename == "632" then
elseif codename == "PB632" then
return PocketBook632
elseif codename == "633" then
elseif codename == "PB633" then
return PocketBook633
elseif codename == "634" then
return PocketBook634
elseif codename == "640" then
elseif codename == "PB640" or codename == "PocketBook 640" then
return PocketBook640
elseif codename == "641" then
elseif codename == "PB641" then
return PocketBook641
elseif codename == "650" then
elseif codename == "PB650" or codename == "PocketBook 650" then
return PocketBook650
elseif codename == "700" then
elseif codename == "PB700" or codename == "PocketBook 700" then
return PocketBook700
elseif codename == "700K3" then
return PocketBook700K3
elseif codename == "740" then
elseif codename == "PB740" then
return PocketBook740
elseif codename == "740-2" or codename == "740-3" then
elseif codename == "PB740-2" or codename == "PB740-3" then
return PocketBook740_2
elseif codename == "741" then
elseif codename == "PB741" then
return PocketBook741
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
elseif codename == "PocketBook 840" then
return PocketBook840
elseif codename == "970" then
elseif codename == "PB970" then
return PocketBook970
elseif codename == "1040" then
elseif codename == "PB1040" then
return PocketBook1040
elseif codename == "Color Lux" then
elseif codename == "PocketBook Color Lux" then
return PocketBookColorLux
else
error("unrecognized PocketBook model " .. full_codename)
error("unrecognized PocketBook model " .. codename)
end

@ -41,9 +41,6 @@ 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()
@ -76,16 +73,4 @@ 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

@ -29,14 +29,12 @@ local wacom_scale_x = screen_width / wacom_width
local wacom_scale_y = screen_height / wacom_height
local isRm2, rm_model = getModel()
local Remarkable = Generic:extend{
local Remarkable = Generic:new{
isRemarkable = yes,
model = rm_model,
hasKeys = yes,
needsScreenRefreshAfterResume = no,
hasOTAUpdates = yes,
hasFastWifiStatusQuery = yes,
hasWifiManager = yes,
canReboot = yes,
canPowerOff = yes,
canSuspend = yes,
@ -49,7 +47,7 @@ local Remarkable = Generic:extend{
home_dir = "/home/root",
}
local Remarkable1 = Remarkable:extend{
local Remarkable1 = Remarkable:new{
mt_width = 767, -- unscaled_size_check: ignore
mt_height = 1023, -- unscaled_size_check: ignore
input_wacom = "/dev/input/event0",
@ -61,7 +59,8 @@ 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
@ -71,7 +70,7 @@ function Remarkable1:adjustTouchEvent(ev, by)
end
end
local Remarkable2 = Remarkable:extend{
local Remarkable2 = Remarkable:new{
mt_width = 1403, -- unscaled_size_check: ignore
mt_height = 1871, -- unscaled_size_check: ignore
input_wacom = "/dev/input/event1",
@ -83,7 +82,8 @@ 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,6 +116,7 @@ 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))
@ -145,27 +146,6 @@ 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
@ -173,37 +153,13 @@ function Remarkable:init()
local scalex = screen_width / self.mt_width
local scaley = screen_height / self.mt_height
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
self.input:registerEventAdjustHook(adjustAbsEvt)
self.input:registerEventAdjustHook(self.adjustTouchEvent, {mt_scale_x=scalex, mt_scale_y=scaley})
-- USB plug/unplug, battery charge/not charging are generated as fake events
self.input.open("fake_events")
local rotation_mode = self.screen.DEVICE_ROTATED_UPRIGHT
local rotation_mode = self.screen.ORIENTATION_PORTRAIT
self.screen.native_rotation_mode = rotation_mode
self.screen.cur_rotation_mode = rotation_mode
@ -218,9 +174,11 @@ end
function Remarkable:supportsScreensaver() return true end
function Remarkable:initNetworkManager(NetworkMgr)
function NetworkMgr:turnOnWifi(complete_callback, interactive)
function NetworkMgr:turnOnWifi(complete_callback)
os.execute("./enable-wifi.sh")
return self:reconnectOrShowNetworkMenu(complete_callback, interactive)
self:reconnectOrShowNetworkMenu(function()
self:connectivityCheck(1, complete_callback)
end)
end
function NetworkMgr:turnOffWifi(complete_callback)
@ -236,8 +194,9 @@ function Remarkable:initNetworkManager(NetworkMgr)
NetworkMgr:setWirelessBackend("wpa_supplicant", {ctrl_interface = "/var/run/wpa_supplicant/wlan0"})
NetworkMgr.isWifiOn = NetworkMgr.sysfsWifiOn
NetworkMgr.isConnected = NetworkMgr.ifHasAnAddress
NetworkMgr.isWifiOn = function()
return NetworkMgr:isConnected()
end
end
function Remarkable:setDateTime(year, month, day, hour, min, sec)
@ -273,33 +232,6 @@ function Remarkable:getDefaultCoverPath()
return "/usr/share/remarkable/poweroff.png"
end
function Remarkable:setEventHandlers(UIManager)
UIManager.event_handlers.Suspend = function()
self:onPowerEvent("Suspend")
end
UIManager.event_handlers.Resume = function()
self:onPowerEvent("Resume")
end
UIManager.event_handlers.PowerPress = function()
UIManager:scheduleIn(2, UIManager.poweroff_action)
end
UIManager.event_handlers.PowerRelease = function()
if not UIManager._entered_poweroff_stage then
UIManager:unschedule(UIManager.poweroff_action)
-- resume if we were suspended
if self.screen_saver_mode then
if self.screen_saver_lock then
UIManager.event_handlers.Suspend()
else
UIManager.event_handlers.Resume()
end
else
UIManager.event_handlers.Suspend()
end
end
end
end
if isRm2 then
if not os.getenv("RM2FB_SHIM") then
error("reMarkable2 requires RM2FB to work (https://github.com/ddvk/remarkable2-framebuffer)")

@ -22,16 +22,4 @@ 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

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

Loading…
Cancel
Save