Compare commits

..

No commits in common. 'master' and 'v2020.07.1' have entirely different histories.

@ -9,17 +9,52 @@ set +e
if [ -z "${CIRCLE_PULL_REQUEST}" ] && [ "${CIRCLE_BRANCH}" = 'master' ]; then
echo "CIRCLE_NODE_INDEX: ${CIRCLE_NODE_INDEX}"
if [ "${CIRCLE_NODE_INDEX}" = 1 ]; then
echo -e "\\n${ANSI_GREEN}Updating translation source file."
make pot
pushd l10n && {
git checkout master
# If only one line was added and removed, it was just the timestamp.
git diff --numstat | grep "1[[:space:]]1[[:space:]]templates/koreader.pot" && echo -e "\\n${ANSI_GREEN}No updated translations found." || {
git -c user.name="KOReader build bot" -c user.email="non-reply@koreader.rocks" \
commit templates/koreader.pot -m "Updated translation source file"
git push --quiet "https://${TRANSLATIONS_GITHUB_TOKEN}@github.com/koreader/koreader-translations.git" master
echo -e "\\n${ANSI_GREEN}Translation update pushed."
}
} && popd || exit
echo -e "\\n${ANSI_GREEN}Checking out koreader/doc for update."
git clone git@github.com:koreader/doc.git koreader_doc
# push doc update
pushd doc && {
luajit "$(command -v ldoc)" . 2>/dev/null
if [ ! -d html ]; then
echo "Failed to generate documents..."
exit 1
fi
} && popd || exit
cp -r doc/html/* koreader_doc/
pushd koreader_doc && {
git add -A
echo -e "\\n${ANSI_GREEN}Pushing document update..."
git -c user.name="KOReader build bot" -c user.email="non-reply@koreader.rocks" \
commit -a --amend -m 'Automated documentation build from travis-ci.'
git push -f --quiet "https://${DOCS_GITHUB_TOKEN}@github.com/koreader/doc.git" gh-pages >/dev/null
echo -e "\\n${ANSI_GREEN}Documentation update pushed."
} && popd || exit
echo -e "\\n${ANSI_GREEN}Running make testfront for timings."
make testfront BUSTED_OVERRIDES="--output=junit -Xoutput junit-test-results.xml"
fi
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
fi
else
echo -e "\\n${ANSI_GREEN}Not on official master branch. Skipping coverage."
echo -e "\\n${ANSI_GREEN}Not on official master branch. Skipping documentation update and coverage."
fi

@ -1,42 +0,0 @@
#!/usr/bin/env bash
CI_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=/dev/null
source "${CI_DIR}/common.sh"
set +e
echo -e "\\n${ANSI_GREEN}Updating translation source file."
make pot
pushd l10n && {
git checkout master
# If only one line was added and removed, it was just the timestamp.
git diff --numstat | grep "1[[:space:]]1[[:space:]]templates/koreader.pot" && echo -e "\\n${ANSI_GREEN}No updated translations found." || {
git -c user.name="KOReader build bot" -c user.email="non-reply@koreader.rocks" \
commit templates/koreader.pot -m "Updated translation source file"
git push --quiet "https://${TRANSLATIONS_GITHUB_TOKEN}@github.com/koreader/koreader-translations.git" master
echo -e "\\n${ANSI_GREEN}Translation update pushed."
}
} && popd || exit
echo -e "\\n${ANSI_GREEN}Checking out koreader/doc for update."
git clone git@github.com:koreader/doc.git koreader_doc
# push doc update
pushd doc && {
luajit "$(command -v ldoc)" .
if [ ! -d html ]; then
echo "Failed to generate documents..."
exit 1
fi
} && popd || exit
cp -r doc/html/* koreader_doc/
pushd koreader_doc && {
git add -A
echo -e "\\n${ANSI_GREEN}Pushing document update..."
git -c user.name="KOReader build bot" -c user.email="non-reply@koreader.rocks" \
commit -a --amend -m 'Automated documentation build from travis-ci.'
git push -f --quiet "https://${DOCS_GITHUB_TOKEN}@github.com/koreader/doc.git" gh-pages >/dev/null
echo -e "\\n${ANSI_GREEN}Documentation update pushed."
} && popd || exit

@ -0,0 +1,17 @@
#!/usr/bin/env bash
# don't do this for clang
if [ "${CXX}" = "g++" ]; then
export CXX="g++-5" CC="gcc-5"
fi
# in case anything ignores the environment variables, override through PATH
mkdir bin
ln -s "$(command -v gcc-5)" bin/cc
ln -s "$(command -v gcc-5)" bin/gcc
ln -s "$(command -v c++)" bin/c++
ln -s "$(command -v g++-5)" bin/g++
# Travis only makes a shallow clone of --depth=50. KOReader is small enough that
# we can just grab it all. This is necessary to generate the version number,
# without which some tests will fail.
# git fetch --unshallow

@ -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,25 @@
#!/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.rc13-0
#- mv -f $HOME/.luarocks/bin/busted_bootstrap $HOME/.luarocks/bin/busted
# Apply junit testcase time fix. This can be removed once there is a busted 2.0.rc13 or final
# See https://github.com/Olivine-Labs/busted/commit/830f175c57ca3f9e79f95b8c4eaacf58252453d7
sed -i 's|testcase_node.time = formatDuration(element.duration)|testcase_node:set_attrib("time", formatDuration(element.duration))|' "${HOME}/.luarocks/share/lua/5.1/busted/outputHandlers/junit.lua"
travis_retry luarocks --local install luacheck
travis_retry luarocks --local install lanes # for parallel luacheck
# used only on master branch but added to cache for better speed
travis_retry luarocks --local install ldoc
travis_retry luarocks --local install luacov

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

@ -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.7.0"
SHELLCHECK_URL="https://storage.googleapis.com/shellcheck/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.0.1/shfmt_v3.0.1_linux_amd64"
if [ "$(shfmt --version)" != "v3.0.1" ]; 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,79 @@
version: "2.1"
# Parameters. {{{
parameters:
# Bump this to reset all caches.
cache_epoch:
type: integer
default: 0
# }}}
# Executors. {{{
executors:
base:
docker:
- image: koreader/kobase:0.3.2-20.04
auth:
username: $DOCKER_USERNAME
password: $DOCKER_PASSWORD
# }}}
# Jobs. {{{
version: 2
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"
docker:
- image: koreader/koappimage:0.1.7
environment:
EMULATE_READER: 1
# this is for shellcheck 0.4.5 and lower; can be removed for 0.4.6
LC_ALL: en_US.UTF8
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.
- build-{{ arch }}-{{ checksum "git-rev-base" }}
- run: echo 'export PATH=${HOME}/bin:${PATH}' >> $BASH_ENV
- 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.
name: setup
command: .ci/before_install.sh
# 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: docs-and-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. {{{
docs:
executor: base
resource_class: small
environment:
BASH_ENV: "~/.bashrc"
parallelism: 1
steps:
- checkout
- run:
name: fetch
command: .ci/fetch.sh
# docs, coverage, and test timing (can we use two outputs at once?); master branch only
- run:
name: docs-and-translation
command: .ci/after_success_docs_translation.sh
# }}}
# }}}
# Workflows. {{{
path: koreader-emulator-x86_64-linux-gnu/koreader/junit-test-results.xml
workflows:
version: 2
build:
jobs:
- build
- docs:
- build:
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,27 +15,17 @@ 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.
Android won't have a crash.log file because Google restricts what apps can log, so you'll need to obtain logs using `adb logcat KOReader:I ActivityManager:* AndroidRuntime:* *:F`.
Please try to include the relevant sections in your issue description.
You can upload the whole `crash.log` file (zipped if necessary) on GitHub by dragging and dropping it onto this textbox.
If your issue doesn't directly concern a Lua crash, we'll quite likely need you to reproduce the issue with *verbose* debug logging enabled before providing the logs to us.
To do so, from the file manager, go to [Tools] → More tools → Developer options, and tick both `Enable debug logging` and `Enable verbose debug logging`.
You'll need to restart KOReader after toggling these on.
If you instead opt to inline it, please do so behind a spoiler tag:
<details>
<summary>crash.log</summary>
```
<Paste crash.log content here>
```
</details>
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.

@ -1,6 +0,0 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"

@ -1,181 +0,0 @@
name: macos
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on: [push, pull_request]
defaults:
run:
shell: bash
jobs:
macos:
# macos-11, macos-12 & macos-13 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'
steps:
- name: XCode version
run: xcode-select -p
# Checkout / fetch. {{{
- name: Checkout
uses: actions/checkout@v4
with:
clean: false
fetch-depth: 0
filter: tree:0
show-progress: false
- name: Fetch
run: make fetchthirdparty
# }}}
# Restore / setup caches. {{{
- name: Generate cache key
run: make -C base TARGET= cache-key
- name: Restore build directory
id: build-restore
uses: actions/cache/restore@v4
with:
path: base/build
key: ${{ env.CACHE_EPOCH }}-${{ runner.os }}-build-${{ hashFiles('base/cache-key') }}
- name: Restore build cache
id: ccache-restore
if: steps.build-restore.outputs.cache-hit != 'true'
uses: actions/cache/restore@v4
with:
path: /Users/runner/Library/Caches/ccache
key: ${{ env.CACHE_EPOCH }}-${{ runner.os }}-ccache-${{ hashFiles('base/cache-key') }}
restore-keys: ${{ env.CACHE_EPOCH }}-${{ runner.os }}-ccache-
- name: Install ccache
if: steps.build-restore.outputs.cache-hit != 'true'
run: |
wget --progress=dot:mega https://github.com/ccache/ccache/releases/download/v4.9.1/ccache-4.9.1-darwin.tar.gz
tar xf ccache-4.9.1-darwin.tar.gz
printf '%s\n' "$PWD/ccache-4.9.1-darwin" >>"${GITHUB_PATH}"
- name: Setup build cache
if: steps.build-restore.outputs.cache-hit != 'true'
run: |
set -x
which ccache
ccache --version
ccache --zero-stats
ccache --max-size=256M
ccache --show-config
# }}}
# Install dependencies. {{{
- name: Setup Python
if: steps.build-restore.outputs.cache-hit != 'true'
uses: actions/setup-python@v5
with:
# Note: Python 3.12 removal of `distutils` breaks GLib's build.
python-version: '3.11'
- name: Install homebrew dependencies
# Compared to the README, adds p7zip.
run: |
packages=(
nasm binutils coreutils libtool autoconf automake cmake make
sdl2 lua@5.1 luarocks gettext pkg-config wget
gnu-getopt grep p7zip ninja
)
# Lua 5.1 is disabled, so we need to work around that:
# - fetch all packages
brew fetch "${packages[@]}"
# - disable auto-updates
export HOMEBREW_NO_AUTO_UPDATE=1
# - install lua@5.1 from cache
brew install "$(brew --cache lua@5.1)"
# - and install the rest
brew install "${packages[@]}"
- name: Update PATH
run: >
printf '%s\n'
"$(brew --prefix)/opt/gettext/bin"
"$(brew --prefix)/opt/gnu-getopt/bin"
"$(brew --prefix)/opt/grep/libexec/gnubin"
"$(brew --prefix)/opt/make/libexec/gnubin"
| tee "${GITHUB_PATH}"
# }}}
# Build. {{{
- name: Build
if: steps.build-restore.outputs.cache-hit != 'true'
run: make base
- name: Dump binaries runtime path & dependencies
run: make bindeps
# }}}
# Clean / save caches. {{{
- name: Clean caches
if: steps.build-restore.outputs.cache-hit != 'true' && always()
run: |
set -x
# Trim the build directory.
rm -rf base/build/{cmake,staging,thirdparty}
ccache --cleanup >/dev/null
ccache --show-stats --verbose
- name: Save build cache
uses: actions/cache/save@v4
if: steps.build-restore.outputs.cache-hit != 'true' && steps.ccache-restore.outputs.cache-hit != 'true'
with:
path: /Users/runner/Library/Caches/ccache
key: ${{ steps.ccache-restore.outputs.cache-primary-key }}
- name: Save build directory
uses: actions/cache/save@v4
if: steps.build-restore.outputs.cache-hit != 'true'
with:
path: base/build
key: ${{ steps.build-restore.outputs.cache-primary-key }}
# }}}
# Generate / upload artifact. {{{
- name: Generate artifact
run: make update --assume-old=base
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: koreader-macos
path: '*.7z'
# }}}
# vim: foldmethod=marker foldlevel=0

4
.gitmodules vendored

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

@ -5,13 +5,121 @@ 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",
"DCREREADER_VIEW_MODE",
"DSHOWOVERLAP",
"DSHOWHIDDENFILES",
"DLANDSCAPE_CLOCKWISE_ROTATION",
"DCREREADER_TWO_PAGE_THRESHOLD",
"DOVERLAPPIXELS",
"FOLLOW_LINK_TIMEOUT",
"DTAP_ZONE_MENU",
"DTAP_ZONE_CONFIG",
"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 = {
@ -26,11 +134,9 @@ exclude_files = {
-- don't balk on busted stuff in spec
files["spec/unit/*"].std = "+busted"
files["spec/unit/*"].globals = {
"match", -- can be removed once luacheck 0.24.0 or higher is used
"package",
"requireBackgroundRunner",
"stopBackgroundRunner",
"notifyBackgroundJobsUpdated",
}
-- TODO: clean up and enforce max line width (631)

@ -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,32 @@
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
endif
IS_RELEASE := $(if $(or $(IS_RELEASE),$(APPIMAGE),$(DEBIAN),$(MACOS)),1,)
ANDROID_ARCH?=arm
# 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 +38,101 @@ 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
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 .
@echo "[*] Install afterupdate marker"
@echo "# If this file is here, there are no afterupdate scripts in /sdcard/koreader/scripts/afterupdate." > $(INSTALL_DIR)/koreader/afterupdate.marker
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 +149,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 +170,375 @@ 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)/fmon/README.txt $(INSTALL_DIR)/README_kobo.txt
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 script
mkdir -p $(INSTALL_DIR)/applications
mkdir -p $(INSTALL_DIR)/system/bin
mkdir -p $(INSTALL_DIR)/system/config
cp $(POCKETBOOK_DIR)/koreader.app $(INSTALL_DIR)/applications
cp $(POCKETBOOK_DIR)/koreader.app $(INSTALL_DIR)/system/bin
cp $(POCKETBOOK_DIR)/extensions.cfg $(INSTALL_DIR)/system/config
cp -rfL $(INSTALL_DIR)/koreader $(INSTALL_DIR)/applications
# 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
mkdir -p $(ANDROID_LAUNCHER_DIR)/assets/module
-rm $(ANDROID_LAUNCHER_DIR)/assets/module/koreader-*
# in runtime luajit-launcher's libluajit.so will be loaded
-rm $(INSTALL_DIR)/koreader/libs/libluajit.so
# assets are compressed manually and stored inside the APK.
cd $(INSTALL_DIR)/koreader && zip -r9 \
../../$(ANDROID_LAUNCHER_DIR)/assets/module/koreader-$(VERSION).zip * \
--exclude=*resources/fonts* \
--exclude=*resources/icons/src* \
--exclude=*share/man* \
--exclude=*spec* \
--exclude=*COPYING* \
--exclude=*README.md*
# 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 $(MACOS_DIR)/koreader.sh $(INSTALL_DIR)/bundle/Contents/MacOS/koreader
cp resources/koreader.icns $(INSTALL_DIR)/bundle/Contents/Resources/icon.icns
cp -LR $(INSTALL_DIR)/koreader $(INSTALL_DIR)/bundle/Contents/Resources
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), 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 +570,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,6 @@
[![Weblate Status][badge-weblate]][link-weblate]
[Download](https://github.com/koreader/koreader/releases) •
[User guide](http://koreader.rocks/user_guide/) •
[Wiki](https://github.com/koreader/koreader/wiki) •
[Developer docs](http://koreader.rocks/doc/)
@ -19,21 +18,19 @@
* **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.
* **integrated** with *calibre* (search metadata, receive ebooks wirelessly, browse library via OPDS), *Wallabag*, *Wikipedia*, *Google Translate* and other content providers.
* **integrated** with *calibre* (search metadata, receive ebooks wirelessly, browse library via OPDS), *Evernote* (export hightlights), *Wallabag*, *Wikipedia*, *Google Translate* and other content providers.
* **optimized for e-ink devices**: custom UI without animation, with paginated menus, adjustable text contrast, and easy zoom to fit content or page in paged media.
* **extensible**: via plugins
* **fast**: on some older devices, it has been measured to have less than half the page-turn delay as the built in reading software.
* **and much more**: look up words with StarDict dictionaries / Wikipedia, add your own online OPDS catalogs and RSS feeds, share ebooks with other KOReader devices wirelessly, online over-the-air software updates, an FTP client, an SSH server, …
* **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 [wiki][link-wiki] to discover more features and to help us document them.
## Screenshots
@ -56,7 +53,7 @@ Please follow the model specific steps for your device:
## Development
[Setting up a build environment](doc/Building.md) •
[Setting a build environment](doc/Building.md) •
[Collaborating with Git](doc/Collaborating_with_Git.md) •
[Building targets](doc/Building_targets.md) •
[Porting](doc/Porting.md) •
@ -71,13 +68,24 @@ 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.
Also if you have and old Pocketbook device you don't want, we might find it useful to tinker a bit with that platform. Please contact us through the forum or GitHub.
## Contributors
[![Last commit][badge-last-commit]][link-gh-commits]
[![Commit activity][badge-commit-activity]][link-gh-insights]
[![0](https://sourcerer.io/fame/Frenzie/koreader/koreader/images/0)](https://sourcerer.io/fame/Frenzie/koreader/koreader/links/0)
[![1](https://sourcerer.io/fame/Frenzie/koreader/koreader/images/1)](https://sourcerer.io/fame/Frenzie/koreader/koreader/links/1)
[![2](https://sourcerer.io/fame/Frenzie/koreader/koreader/images/2)](https://sourcerer.io/fame/Frenzie/koreader/koreader/links/2)
[![3](https://sourcerer.io/fame/Frenzie/koreader/koreader/images/3)](https://sourcerer.io/fame/Frenzie/koreader/koreader/links/3)
[![4](https://sourcerer.io/fame/Frenzie/koreader/koreader/images/4)](https://sourcerer.io/fame/Frenzie/koreader/koreader/links/4)
[![5](https://sourcerer.io/fame/Frenzie/koreader/koreader/images/5)](https://sourcerer.io/fame/Frenzie/koreader/koreader/links/5)
[![6](https://sourcerer.io/fame/Frenzie/koreader/koreader/images/6)](https://sourcerer.io/fame/Frenzie/koreader/koreader/links/6)
[![7](https://sourcerer.io/fame/Frenzie/koreader/koreader/images/7)](https://sourcerer.io/fame/Frenzie/koreader/koreader/links/7)
[badge-bountysource]:https://img.shields.io/bountysource/team/koreader/activity?color=red
[badge-circleci]:https://circleci.com/gh/koreader/koreader.svg?style=shield
[badge-coverage]:https://codecov.io/gh/koreader/koreader/branch/master/graph/badge.svg
@ -101,4 +109,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 67474697169dd88800bc37af5b7fb87ce0596ee8
Subproject commit 19a87bbc1019f413976758287e9b754667a505e0

@ -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,13 @@ 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
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")
end
elseif os.getenv("APPIMAGE") or os.getenv("KO_MULTIUSER") then
data_dir = string.format("%s/%s/%s", os.getenv("HOME"), ".config", "koreader")
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 +37,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 +52,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",
"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,175 @@
-- 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*10
-- 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*60
-- 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,
-- generic icon size
DGENERIC_ICON_SIZE = 40,
DOUTER_PAGE_COLOR = 0
-- 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 = 1/8, y = 0, w = 3/4, h = 1/8}
DTAP_ZONE_CONFIG = {x = 1/8, y = 7/8, w = 3/4, h = 1/8}
DTAP_ZONE_MINIBAR = {x = 0, y = 31/32, w = 1, h = 1/32}
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 = 6/8, y = 0, w = 2/8, h = 2/8}
DDOUBLE_TAP_ZONE_PREV_CHAPTER = {x = 0, y = 0, w = 2/8, h = 2/8}
-- 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 +181,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,16 +22,17 @@ 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 libsdl2-dev \
libssl-dev libffi-dev libsdl2-dev libc6-dev-i386 xutils-dev linux-libc-dev:i386 zlib1g:i386
```
### Fedora/Red Hat
Install the prerequisites using DNF:
Install the `libstdc++-static`, `SDL` and `SDL-devel` packages 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
```
### macOS
@ -39,13 +40,13 @@ sudo dnf install libstdc++-static SDL SDL-devel patch wget unzip git cmake luaro
Install the prerequisites using [Homebrew](https://brew.sh/):
```
brew install nasm binutils coreutils libtool autoconf automake cmake makedepend \
sdl2 lua@5.1 luarocks gettext pkg-config wget gnu-getopt grep bison
brew install nasm ragel binutils coreutils libtool autoconf automake cmake makedepend \
sdl2 lua@5.1 luarocks gettext pkg-config wget
```
You will also have to ensure Homebrew's gettext, gnu-getopt, bison & grep are in your path, e.g., via
You will also have to ensure Homebrew's gettext is in your path, e.g., via
```
export PATH="$(brew --prefix)/opt/gettext/bin:$(brew --prefix)/opt/gnu-getopt/bin:$(brew --prefix)/opt/bison/bin:$(brew --prefix)/opt/grep/libexec/gnubin:${PATH}"
export PATH="/usr/local/opt/gettext/bin:${PATH}"
```
See also `brew info gettext` for details on how to make that permanent in your shell.
@ -61,6 +62,7 @@ export MACOSX_DEPLOYMENT_TARGET=10.09
```
*Note:* On Catalina (10.15), you will currently *NOT* want to deploy for `10.15`, as [XCode is currently broken in that configuration](https://forums.developer.apple.com/thread/121887)! (i.e., deploy for `10.14` instead).
## Getting the source

@ -4,6 +4,7 @@ KOReader is available for multiple platforms. Here are instructions to build ins
These instructions are intended for a Linux OS. MacOS and Windows users are suggested to develop in a Linux VM.
## Prerequisites
This instructions asume that you [have a development environment ready to run](Building.md) KOReader. If not then please install common prerequisites first.
@ -17,25 +18,41 @@ 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:
##### e-Ink devices (e.g., Kindle, Kobo, Cervantes, reMarkable, PocketBook)
##### Kindle and Cervantes
```
sudo apt-get install gcc-arm-linux-gnueabi g++-arm-linux-gnueabi
```
##### Kobo and 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
**NOTE:** While, for some targets (specifically, Cervantes, Kindle & Kobo), we make *some* effort to support Linaro/Ubuntu TCs,
they do *not* exactly target the proper devices. While your build *may* go fine, this will *probably* lead to runtime failure.
As time goes by, and/or the more bleeding-edge your distro is, the greater the risk for mismatch gets.
```
/usr/lib/gcc-cross/arm-linux-gnueabihf/4.8/../../../../arm-linux-gnueabihf/bin/ld: cannot find -lglib-2.0
```
Which means, that, unless you are *very* sure you know what you're doing, you'll want to use the exact same TCs we do, ones that target their respective platforms properly.
We have a distribution-agnostic solution to make that mostly painless: [koxtoolchain](https://github.com/koreader/koxtoolchain)!
This will allow you to build the *exact* same TCs used to build the nightlies, thanks to the magic of [crosstool-ng](https://github.com/crosstool-ng/crosstool-ng). These are also included precompiled in the Docker images for the respective targets.
**NOTE 2:** In the specific case of Cervantes, Kindle & Kobo targets, while we make some effort to support these Linaro/Ubuntu TCs,
they do *not* exactly target the proper devices. While your build will go fine, this may lead to runtime failure.
As time goes by, and/or the more bleeding-edge your distro is, the greater the risk for mismatch gets.
Thankfully, we have a distribution-agnostic solution for you: [koxtoolchain](https://github.com/koreader/koxtoolchain)!
This will allow you to build the *exact* same TCs used to build the nightlies, thanks to the magic of [crosstool-ng](https://github.com/crosstool-ng/crosstool-ng). These are also included precompiled in the Docker images for the respective targets.
**NOTE 3:** The vendor toolchain will be downloaded automatically by `./kodev release pocketbook`
### Additional packages
@ -56,9 +73,25 @@ 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.
You can check out our [nightlybuild script][nb-script] to see how to build a
package from scratch.
### Android
@ -72,11 +105,13 @@ You can check out our [nightlybuild script][nb-script] to see how to build a pac
ANDROID_ARCH=x86 ./kodev release android
```
### Desktop Linux
### Cervantes
#### Emulator
```
./kodev release cervantes
```
See [Building](https://github.com/koreader/koreader/blob/master/doc/Building.md).
### Desktop Linux
#### AppImage (x86_64)
@ -102,46 +137,34 @@ See [Building](https://github.com/koreader/koreader/blob/master/doc/Building.md)
./kodev release debian-armhf
```
### Desktop macOS
```
./kodev release macos
```
### e-Ink devices
#### Cervantes
```
./kodev release cervantes
```
#### Kindle
### Kindle
```
./kodev release kindle
```
#### Kobo
### Kobo
```
./kodev release kobo
```
#### Pocketbook
### Pocketbook
```
./kodev release pocketbook
```
#### reMarkable
### Ubuntu Touch
```
./kodev release remarkable
./kodev release ubuntu-touch
```
## Porting to a new target.
See [Porting.md](Porting.md)
[nb-script]:https://gitlab.com/koreader/nightly-builds/blob/master/build_release.sh

@ -10,7 +10,6 @@ to a widget, you can simply invoke the handleEvent method like the following:
```lua
widget_foo:handleEvent(Event:new("Timeout"))
```
If the widget can be destroyed during the event you should call @{ui.uimanager:sendEvent|UIManager:sendEvent} to propagate the event from the topmost widget or @{ui.uimanager:broadcastEvent|UIManager:broadcastEvent} to send the event to all widgets.
Events are passed to child Widgets (or child containers) before their own handler sees them. See the implementation of WidgetContainer:handleEvent(). So a child widget, for instance a text input widget, gets the input events before the layout manager. The child widgets can "consume" an event by returning `true` from the event handler. Thus a text input widget just implements an input handler and consumes left/right presses, returning `true` in those cases. It can even make its return code dependent on whether the cursor is on the last position (do not consume press to right) or first position (do not consume press to left) to have proper focus movement in those cases.
@ -26,11 +25,12 @@ recalculate the view based on the new typesetting.
## Event propagation ##
Most UI components are a subclass of @{ui.widget.container.widgetcontainer|WidgetContainer}.
A WidgetContainer is an array that stores a list of children widgets.
Most of the UI components is a subclass of
@{ui.widget.container.widgetcontainer|WidgetContainer}. A WidgetContainer is an array that
stores a list of children widgets.
When @{ui.widget.container.widgetcontainer:handleEvent|WidgetContainer:handleEvent} is called with a new event,
it will run roughly the following code:
When @{ui.widget.container.widgetcontainer:handleEvent|WidgetContainer:handleEvent} is called with a new
event, it will run roughly the following code:
```lua
-- First propagate event to its children
@ -40,8 +40,8 @@ for _, widget in ipairs(self) do
return true
end
end
-- If not consumed by children, consume it ourself
return self["on"..event.name](self, unpack(event.args, 1, event.args.n))
-- If not consumed by children, try consume by itself
return self["on"..event.name](self, unpack(event.args))
```
## Event system

@ -13,18 +13,11 @@ logger.dbg("table a: ", a)
Anything printed by `logger.dbg` starts with `DEBUG`.
On most target platforms, log output is saved to `crash.log` in the `koreader` directory.
```
04/06/17-21:44:53 DEBUG foo
```
In production code, remember that arguments are *always* evaluated in Lua, so,
don't inline complex computations in logger functions' arguments.
If you *really* have to, hide the whole thing behind a `dbg.is_on` branch,
like in [frontend/device/input.lua](https://github.com/koreader/koreader/blob/ba6fef4d7ba217ca558072f090849000e72ba142/frontend/device/input.lua#L1131-L1134).
## Bug hunting in KPV (KOReader's predecessor)
## Bug hunting in kpv
A real example of bug hunting in KPV's cache system: <https://github.com/koreader/kindlepdfviewer/pull/475>

@ -2,12 +2,12 @@
Unit tests are automatically performed using [busted](http://olivinelabs.com/busted/). It depends on `luarocks`.
To grab busted, install the same version [as used in the automated tests](https://github.com/koreader/koreader/blob/master/.ci/install.sh). At the time of writing that is 2.0.0-1:
To grab busted, install the same version [as used in the automated tests](https://github.com/koreader/koreader/blob/master/.ci/install.sh). At the time of writing that is 2.0.rc12-1:
```bash
mkdir $HOME/.luarocks
cp /etc/luarocks/config.lua $HOME/.luarocks/config.lua
echo "wrap_bin_scripts = false" >> $HOME/.luarocks/config.lua
luarocks --local install busted 2.0.0-1
luarocks --local install busted 2.0.rc12-1
```
Then you can set up the environment variables with `./kodev activate`.

@ -20,13 +20,7 @@ format = 'markdown'
sort_modules = true
file = {
'../frontend',
'../plugins',
'../base/ffi',
'../platform/android/luajit-launcher/assets',
exclude = {'../base/ffi/sha2.lua',
'../base/ffi/qrencode.lua',
'../plugins/exporter.koplugin/template/slt2.lua',
'../plugins/newsdownloader.koplugin/lib/handler.lua',
'../plugins/newsdownloader.koplugin/lib/xml.lua',
},
exclude = {'../base/ffi/sha2.lua'},
}

@ -1,41 +1,37 @@
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 DataStorage = require("datastorage")
local DropBox = require("apps/cloudstorage/dropbox")
local FFIUtil = require("ffi/util")
local Ftp = require("apps/cloudstorage/ftp")
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")
local lfs = require("libs/libkoreader-lfs")
local logger = require("logger")
local _ = require("gettext")
local N_ = _.ngettext
local T = require("ffi/util").template
local _ = require("gettext")
local Screen = require("device").screen
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,
is_borderless = true,
title = _("Cloud storage"),
}
local server_types = {
dropbox = _("Dropbox"),
ftp = _("FTP"),
webdav = _("WebDAV"),
title = _("Cloud storage")
}
function CloudStorage:init()
--- @todo: Probably a good candidate for the new readSetting API
self.cs_settings = self:readSettings()
self.show_parent = self
if self.item then
@ -44,10 +40,8 @@ function CloudStorage:init()
else
self.item_table = self:genItemTableFromRoot()
end
self.title_bar_left_icon = "plus"
self.onLeftButtonTap = function() -- add new cloud storage
self:selectCloudType()
end
self.width = Screen:getWidth()
self.height = Screen:getHeight()
Menu.init(self)
if self.item then
self.item_table[1].callback()
@ -56,11 +50,16 @@ end
function CloudStorage:genItemTableFromRoot()
local item_table = {}
table.insert(item_table, {
text = _("Add new cloud storage"),
callback = function()
self:selectCloudType()
end,
})
local added_servers = self.cs_settings:readSetting("cs_servers") or {}
for _, server in ipairs(added_servers) do
table.insert(item_table, {
text = server.name,
mandatory = server_types[server.type],
address = server.address,
username = server.username,
password = server.password,
@ -107,83 +106,80 @@ function CloudStorage:genItemTable(item)
end
function CloudStorage:selectCloudType()
local buttons = {}
for server_type, name in FFIUtil.orderedPairs(server_types) do
table.insert(buttons, {
local buttons = {
{
{
text = name,
text = _("Dropbox"),
callback = function()
UIManager:close(self.cloud_dialog)
self:configCloud(server_type)
self:configCloud("dropbox")
end,
},
})
end
self.cloud_dialog = ButtonDialog:new{
title = _("Add new cloud storage"),
title_align = "center",
buttons = buttons,
},
{
{
text = _("FTP"),
callback = function()
UIManager:close(self.cloud_dialog)
self:configCloud("ftp")
end,
},
},
{
{
text = _("WebDAV"),
callback = function()
UIManager:close(self.cloud_dialog)
self:configCloud("webdav")
end,
},
},
}
self.cloud_dialog = ButtonDialogTitle:new{
title = _("Choose cloud storage type"),
title_align = "center",
buttons = buttons,
}
UIManager:show(self.cloud_dialog)
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 tbl
local NetworkMgr = require("ui/network/manager")
if self.type == "dropbox" then
if NetworkMgr:willRerunWhenOnline(function() self:openCloudServer(url) end) then
if not NetworkMgr:isOnline() then
NetworkMgr:promptWifiOn()
return
end
if self:generateDropBoxAccessToken() then
tbl, e = DropBox:run(url, self.password, self.choose_folder_mode)
end
tbl = DropBox:run(url, self.password, self.choose_folder_mode)
elseif self.type == "ftp" then
if NetworkMgr:willRerunWhenConnected(function() self:openCloudServer(url) end) then
if not NetworkMgr:isConnected() then
NetworkMgr:promptWifiOn()
return
end
tbl, e = Ftp:run(self.address, self.username, self.password, url)
tbl = Ftp:run(self.address, self.username, self.password, url)
elseif self.type == "webdav" then
if NetworkMgr:willRerunWhenConnected(function() self:openCloudServer(url) end) then
if not NetworkMgr:isConnected() then
NetworkMgr:promptWifiOn()
return
end
tbl, e = WebDav:run(self.address, self.username, self.password, url, self.choose_folder_mode)
tbl = WebDav:run(self.address, self.username, self.password, url)
end
if tbl then
if tbl and #tbl > 0 then
self:switchItemTable(url, tbl)
if self.type == "dropbox" or self.type == "webdav" then
self.onLeftButtonTap = function()
self:showPlusMenu(url)
end
else
self:setTitleBarLeftIcon("home")
self.onLeftButtonTap = function()
self:init()
end
end
return true
else
logger.err("CloudStorage:", e)
elseif not tbl then
UIManager:show(InfoMessage:new{
text = _("Cannot fetch list of folder contents\nPlease check your configuration or network connection."),
timeout = 3,
})
table.remove(self.paths)
return false
else
UIManager:show(InfoMessage:new{ text = _("Empty folder") })
return false
end
end
@ -211,115 +207,119 @@ function CloudStorage:onMenuSelect(item)
end
function CloudStorage:downloadFile(item)
local function startDownloadFile(unit_item, address, username, password, path_dir, callback_close)
local lastdir = G_reader_settings:readSetting("lastdir")
local cs_settings = self:readSettings()
local download_dir = cs_settings:readSetting("download_dir") or lastdir
local path = download_dir .. '/' .. item.text
self:cloudFile(item, path)
end
function CloudStorage:cloudFile(item, path)
local download_text = _("Downloading. This might take a moment.")
local function dropboxDownloadFile(unit_item, password, path_dir, callback_close)
UIManager:scheduleIn(1, function()
if self.type == "dropbox" then
DropBox:downloadFile(unit_item, password, path_dir, callback_close)
elseif self.type == "ftp" then
Ftp:downloadFile(unit_item, address, username, password, path_dir, callback_close)
elseif self.type == "webdav" then
WebDav:downloadFile(unit_item, address, username, password, path_dir, callback_close)
end
DropBox:downloadFile(unit_item, password, path_dir, callback_close)
end)
UIManager:show(InfoMessage:new{
text = _("Downloading. This might take a moment."),
text = download_text,
timeout = 1,
})
end
local function createTitle(filename_orig, filename, path) -- title for ButtonDialog
return T(_("Filename:\n%1\n\nDownload filename:\n%2\n\nDownload folder:\n%3"),
filename_orig, filename, BD.dirpath(path))
local function ftpDownloadFile(unit_item, address, username, password, path_dir, callback_close)
UIManager:scheduleIn(1, function()
Ftp:downloadFile(unit_item, address, username, password, path_dir, callback_close)
end)
UIManager:show(InfoMessage:new{
text = download_text,
timeout = 1,
})
end
local cs_settings = self:readSettings()
local download_dir = cs_settings:readSetting("download_dir") or G_reader_settings:readSetting("lastdir")
local filename_orig = item.text
local filename = filename_orig
local function webdavDownloadFile(unit_item, address, username, password, path_dir, callback_close)
UIManager:scheduleIn(1, function()
WebDav:downloadFile(unit_item, address, username, password, path_dir, callback_close)
end)
UIManager:show(InfoMessage:new{
text = download_text,
timeout = 1,
})
end
local path_dir = path
local overwrite_text = _("File already exists. Would you like to overwrite it?")
local buttons = {
{
{
text = _("Choose folder"),
callback = function()
require("ui/downloadmgr"):new{
onConfirm = function(path)
self.cs_settings:saveSetting("download_dir", path)
self.cs_settings:flush()
download_dir = path
self.download_dialog:setTitle(createTitle(filename_orig, filename, download_dir))
end,
}:chooseDir(download_dir)
end,
},
{
text = _("Change filename"),
text = _("Download file"),
callback = function()
local input_dialog
input_dialog = InputDialog:new{
title = _("Enter filename"),
input = filename,
input_hint = filename_orig,
buttons = {
{
{
text = _("Cancel"),
id = "close",
callback = function()
UIManager:close(input_dialog)
end,
},
{
text = _("Set filename"),
is_enter_default = true,
callback = function()
filename = input_dialog:getInputValue()
if filename == "" then
filename = filename_orig
end
UIManager:close(input_dialog)
self.download_dialog:setTitle(createTitle(filename_orig, filename, download_dir))
end,
},
}
},
}
UIManager:show(input_dialog)
input_dialog:onShowKeyboard()
if self.type == "dropbox" then
local callback_close = function()
self:onClose()
end
UIManager:close(self.download_dialog)
if lfs.attributes(path) then
UIManager:show(ConfirmBox:new{
text = overwrite_text,
ok_callback = function()
dropboxDownloadFile(item, self.password, path_dir, callback_close)
end
})
else
dropboxDownloadFile(item, self.password, path_dir, callback_close)
end
elseif self.type == "ftp" then
local callback_close = function()
self:onClose()
end
UIManager:close(self.download_dialog)
if lfs.attributes(path) then
UIManager:show(ConfirmBox:new{
text = overwrite_text,
ok_callback = function()
ftpDownloadFile(item, self.address, self.username, self.password, path_dir, callback_close)
end
})
else
ftpDownloadFile(item, self.address, self.username, self.password, path_dir, callback_close)
end
elseif self.type == "webdav" then
local callback_close = function()
self:onClose()
end
UIManager:close(self.download_dialog)
if lfs.attributes(path) then
UIManager:show(ConfirmBox:new{
text = overwrite_text,
ok_callback = function()
webdavDownloadFile(item, self.address, self.username, self.password, path_dir, callback_close)
end
})
else
webdavDownloadFile(item, self.address, self.username, self.password, path_dir, callback_close)
end
end
end,
},
},
{
{
text = _("Cancel"),
text = _("Choose download directory"),
callback = function()
UIManager:close(self.download_dialog)
end,
},
{
text = _("Download"),
callback = function()
UIManager:close(self.download_dialog)
local path_dir = (download_dir ~= "/" and download_dir or "") .. '/' .. filename
local callback_close = function() self:onClose() end
if lfs.attributes(path_dir) then
UIManager:show(ConfirmBox:new{
text = _("File already exists. Would you like to overwrite it?"),
ok_callback = function()
startDownloadFile(item, self.address, self.username, self.password, path_dir, callback_close)
end
})
else
startDownloadFile(item, self.address, self.username, self.password, path_dir, callback_close)
end
require("ui/downloadmgr"):new{
show_hidden = G_reader_settings:readSetting("show_hidden"),
onConfirm = function(path_download)
self.cs_settings:saveSetting("download_dir", path_download)
self.cs_settings:flush()
path_dir = path_download .. '/' .. item.text
end,
}:chooseDir()
end,
},
},
}
self.download_dialog = ButtonDialog:new{
title = createTitle(filename_orig, filename, download_dir),
buttons = buttons,
buttons = buttons
}
UIManager:show(self.download_dialog)
end
@ -344,10 +344,10 @@ end
function CloudStorage:onMenuHold(item)
if item.type == "folder_long_press" then
local title = T(_("Choose this folder?\n\n%1"), BD.dirpath(item.url))
local title = T(_("Select this directory?\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 = {
{
@ -358,7 +358,7 @@ function CloudStorage:onMenuHold(item)
end,
},
{
text = _("Choose"),
text = _("Select"),
callback = function()
if onConfirm then
onConfirm(item.url)
@ -378,6 +378,7 @@ function CloudStorage:onMenuHold(item)
{
{
text = _("Info"),
enabled = true,
callback = function()
UIManager:close(cs_server_dialog)
self:infoServer(item)
@ -385,6 +386,7 @@ function CloudStorage:onMenuHold(item)
},
{
text = _("Edit"),
enabled = true,
callback = function()
UIManager:close(cs_server_dialog)
self:editCloudServer(item)
@ -393,6 +395,7 @@ function CloudStorage:onMenuHold(item)
},
{
text = _("Delete"),
enabled = true,
callback = function()
UIManager:close(cs_server_dialog)
self:deleteCloudServer(item)
@ -412,6 +415,7 @@ function CloudStorage:onMenuHold(item)
},
{
text = _("Synchronize settings"),
enabled = true,
callback = function()
UIManager:close(cs_server_dialog)
self:synchronizeSettings(item)
@ -428,38 +432,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.")
elseif downloaded_files > 0 and failed_files == 0 then
text = T(_("Successfully downloaded %1 files from Dropbox to local storage."), downloaded_files)
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(_("Successfully downloaded %1 files from Dropbox to local storage.\nFailed to download %2 files."),
downloaded_files, failed_files)
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 +477,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 +508,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 +524,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 +533,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,
},
},
@ -575,129 +572,6 @@ function CloudStorage:synchronizeSettings(item)
UIManager:show(syn_dialog)
end
function CloudStorage:showPlusMenu(url)
local button_dialog
button_dialog = ButtonDialog:new{
buttons = {
{
{
text = _("Upload file"),
callback = function()
UIManager:close(button_dialog)
self:uploadFile(url)
end,
},
},
{
{
text = _("New folder"),
callback = function()
UIManager:close(button_dialog)
self:createFolder(url)
end,
},
},
{},
{
{
text = _("Return to cloud storage list"),
callback = function()
UIManager:close(button_dialog)
self:init()
end,
},
},
},
}
UIManager:show(button_dialog)
end
function CloudStorage:uploadFile(url)
local path_chooser
path_chooser = PathChooser:new{
select_directory = false,
path = self.last_path,
onConfirm = function(file_path)
self.last_path = file_path:match("(.*)/")
if self.last_path == "" then self.last_path = "/" end
if lfs.attributes(file_path, "size") > 157286400 then
UIManager:show(InfoMessage:new{
text = _("File size must be less than 150 MB."),
})
else
local callback_close = function()
self:openCloudServer(url)
end
UIManager:nextTick(function()
UIManager:show(InfoMessage:new{
text = _("Uploading…"),
timeout = 1,
})
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
end)
end
end
}
UIManager:show(path_chooser)
end
function CloudStorage:createFolder(url)
local input_dialog, check_button_enter_folder
input_dialog = InputDialog:new{
title = _("New folder"),
buttons = {
{
{
text = _("Cancel"),
id = "close",
callback = function()
UIManager:close(input_dialog)
end,
},
{
text = _("Create"),
is_enter_default = true,
callback = function()
local folder_name = input_dialog:getInputText()
if folder_name == "" then return end
UIManager:close(input_dialog)
local url_base = url ~= "/" and url or ""
local callback_close = function()
if check_button_enter_folder.checked then
table.insert(self.paths, {
url = url,
})
url = url_base .. "/" .. folder_name
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
end,
},
}
},
}
check_button_enter_folder = CheckButton:new{
text = _("Enter folder after creation"),
checked = false,
parent = input_dialog,
}
input_dialog:addWidget(check_button_enter_folder)
UIManager:show(input_dialog)
input_dialog:onShowKeyboard()
end
function CloudStorage:configCloud(type)
local callbackAdd = function(fields)
local cs_settings = self:readSettings()
@ -706,9 +580,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 +626,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 +684,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
@ -849,17 +712,4 @@ function CloudStorage:onReturn()
return true
end
function CloudStorage:onHoldReturn()
if #self.paths > 1 then
local path = self.paths[1]
if path then
for i = #self.paths, 2, -1 do
table.remove(self.paths)
end
self:openCloudServer(path.url)
end
end
return true
end
return CloudStorage

@ -7,15 +7,12 @@ local MultiInputDialog = require("ui/widget/multiinputdialog")
local UIManager = require("ui/uimanager")
local ReaderUI = require("apps/reader/readerui")
local util = require("util")
local Screen = require("device").screen
local T = require("ffi/util").template
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
@ -24,7 +21,7 @@ function DropBox:showFiles(url, password)
return DropBoxApi:showFiles(url, password)
end
function DropBox:downloadFile(item, password, path, callback_close)
function DropBox:downloadFile(item, password, path, close)
local code_response = DropBoxApi:downloadFile(item.url, password, path)
if code_response == 200 then
local __, filename = util.splitFilePathName(path)
@ -37,13 +34,7 @@ function DropBox:downloadFile(item, password, path, callback_close)
text = T(_("File saved to:\n%1\nWould you like to read the downloaded book now?"),
BD.filepath(path)),
ok_callback = function()
local Event = require("ui/event")
UIManager:broadcastEvent(Event:new("SetupShowReader"))
if callback_close then
callback_close()
end
close()
ReaderUI:showReader(path)
end
})
@ -58,77 +49,53 @@ end
function DropBox:downloadFileNoUI(url, password, path)
local code_response = DropBoxApi:downloadFile(url, password, path)
return code_response == 200
end
function DropBox:uploadFile(url, password, file_path, callback_close)
local code_response = DropBoxApi:uploadFile(url, password, file_path)
local __, filename = util.splitFilePathName(file_path)
if code_response == 200 then
UIManager:show(InfoMessage:new{
text = T(_("File uploaded:\n%1"), filename),
})
if callback_close then
callback_close()
end
return true
else
UIManager:show(InfoMessage:new{
text = T(_("Could not upload file:\n%1"), filename),
})
end
end
function DropBox:createFolder(url, password, folder_name, callback_close)
local code_response = DropBoxApi:createFolder(url, password, folder_name)
if code_response == 200 then
if callback_close then
callback_close()
end
else
UIManager:show(InfoMessage:new{
text = T(_("Could not create folder:\n%1"), folder_name),
})
return false
end
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_top,
hint = hint_top ,
},
{
text = text_appkey,
hint = _("Dropbox <APP_KEY>:<APP_SECRET>\n(leave blank for long-lived token)"),
},
{
text = text_url,
hint = _("Dropbox folder (/ for root)"),
text = text_bottom,
hint = hint_bottom,
scroll = false,
},
},
buttons = {
{
{
text = _("Cancel"),
id = "close",
callback = function()
self.settings_dialog:onClose()
UIManager:close(self.settings_dialog)
@ -141,20 +108,31 @@ 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
},
},
},
width = math.floor(Screen:getWidth() * 0.95),
height = math.floor(Screen:getHeight() * 0.2),
input_type = "text",
}
UIManager:show(self.settings_dialog)
self.settings_dialog:onShowKeyboard()
@ -162,17 +140,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\nCounty: %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

@ -1,173 +1,93 @@
local DocumentRegistry = require("document/documentregistry")
local JSON = require("json")
local http = require("socket.http")
local lfs = require("libs/libkoreader-lfs")
local logger = require("logger")
local ltn12 = require("ltn12")
local socket = require("socket")
local socketutil = require("socketutil")
local util = require("util")
local BaseUtil = require("ffi/util")
local http = require('socket.http')
local https = require('ssl.https')
local ltn12 = require('ltn12')
local socket = require('socket')
local url = require('socket.url')
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_LIST_ADD_FOLDER = "https://api.dropboxapi.com/2/files/list_folder/continue"
local API_CREATE_FOLDER = "https://api.dropboxapi.com/2/files/create_folder_v2"
local API_DOWNLOAD_FILE = "https://content.dropboxapi.com/2/files/download"
local API_UPLOAD_FILE = "https://content.dropboxapi.com/2/files/upload"
function DropBoxApi:getAccessToken(refresh_token, app_key_colon_secret)
local sink = {}
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 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"
function DropBoxApi:fetchInfo(token)
local request, sink = {}, {}
local parsed = url.parse(API_URL_INFO)
request['url'] = API_URL_INFO
request['method'] = 'POST'
local headers = { ["Authorization"] = "Bearer ".. token }
request['headers'] = headers
request['sink'] = ltn12.sink.table(sink)
http.TIMEOUT = 5
https.TIMEOUT = 5
local httpRequest = parsed.scheme == 'http' and http.request or https.request
local headers_request = socket.skip(1, httpRequest(request))
local result_response = table.concat(sink)
if code == 200 and result_response ~= "" then
local _, result = pcall(JSON.decode, result_response)
return result["access_token"]
if headers_request == nil then
return nil
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,
method = "POST",
headers = {
["Authorization"] = "Bearer " .. token,
},
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
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)
local request, sink = {}, {}
if path == nil or path == "/" then path = "" end
local parsed = url.parse(API_LIST_FOLDER)
request['url'] = API_LIST_FOLDER
request['method'] = 'POST'
local data = "{\"path\": \"" .. path .. "\",\"recursive\": false,\"include_media_info\": false,"..
"\"include_deleted\": false,\"include_has_explicit_shared_members\": false}"
local sink = {}
local request = {
url = API_LIST_FOLDER,
method = "POST",
headers = {
["Authorization"] = "Bearer ".. token,
["Content-Type"] = "application/json",
["Content-Length"] = #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 headers = { ["Authorization"] = "Bearer ".. token,
["Content-Type"] = "application/json" ,
["Content-Length"] = #data}
request['headers'] = headers
request['source'] = ltn12.source.string(data)
request['sink'] = ltn12.sink.table(sink)
http.TIMEOUT = 5
https.TIMEOUT = 5
local httpRequest = parsed.scheme == 'http' and http.request or https.request
local headers_request = socket.skip(1, httpRequest(request))
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")
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 parsed = url.parse(API_DOWNLOAD_FILE)
local url_api = API_DOWNLOAD_FILE
local data1 = "{\"path\": \"" .. path .. "\"}"
socketutil:set_timeout(socketutil.FILE_BLOCK_TIMEOUT, socketutil.FILE_TOTAL_TIMEOUT)
local code, headers, status = socket.skip(1, http.request{
url = API_DOWNLOAD_FILE,
method = "GET",
headers = {
["Authorization"] = "Bearer ".. token,
["Dropbox-API-Arg"] = data1,
},
sink = ltn12.sink.file(io.open(local_path, "w")),
})
socketutil:reset_timeout()
if code ~= 200 then
logger.warn("DropBoxApi: cannot download file:", status or code)
end
return code, (headers or {}).etag
end
function DropBoxApi:uploadFile(path, token, file_path, etag, overwrite)
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}"
socketutil:set_timeout(socketutil.FILE_BLOCK_TIMEOUT, socketutil.FILE_TOTAL_TIMEOUT)
local code, _, status = socket.skip(1, http.request{
url = API_UPLOAD_FILE,
method = "POST",
headers = {
["Authorization"] = "Bearer ".. token,
["Dropbox-API-Arg"] = data,
["Content-Type"] = "application/octet-stream",
["Content-Length"] = lfs.attributes(file_path, "size"),
["If-Match"] = etag,
},
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)
end
return code
end
function DropBoxApi:createFolder(path, token, folder_name)
local data = "{\"path\": \"" .. path .. "/" .. folder_name .. "\",\"autorename\": false}"
socketutil:set_timeout()
local code, _, status = socket.skip(1, http.request{
url = API_CREATE_FOLDER,
method = "POST",
headers = {
["Authorization"] = "Bearer ".. token,
["Content-Type"] = "application/json",
["Content-Length"] = #data,
},
source = ltn12.source.string(data),
})
socketutil:reset_timeout()
if code ~= 200 then
logger.warn("DropBoxApi: cannot create folder:", status or code)
end
return code
local headers = { ["Authorization"] = "Bearer ".. token,
["Dropbox-API-Arg"] = data1}
http.TIMEOUT = 5
https.TIMEOUT = 5
local httpRequest = parsed.scheme == 'http' and http.request or https.request
local _, code_return, _ = httpRequest{
url = url_api,
method = 'GET',
headers = headers,
sink = ltn12.sink.file(io.open(local_path, "w"))
}
return code_return
end
-- folder_mode - set to true when we want to see only folder.
@ -194,7 +114,6 @@ function DropBoxApi:listFolder(path, token, folder_mode)
or G_reader_settings:isTrue("show_unsupported")) and not folder_mode then
table.insert(dropbox_file, {
text = text,
mandatory = util.getFriendlySize(files.size),
url = files.path_display,
type = tag,
})
@ -210,7 +129,7 @@ function DropBoxApi:listFolder(path, token, folder_mode)
-- Add special folder.
if folder_mode then
table.insert(dropbox_list, 1, {
text = _("Long-press to choose current folder"),
text = _("Long-press to select current directory"),
url = path,
type = "folder_long_press",
})
@ -218,7 +137,6 @@ function DropBoxApi:listFolder(path, token, folder_mode)
for _, files in ipairs(dropbox_file) do
table.insert(dropbox_list, {
text = files.text,
mandatory = files.mandatory,
url = files.url,
type = files.type,
})
@ -245,49 +163,4 @@ function DropBoxApi:showFiles(path, token)
return dropbox_files
end
function DropBoxApi:fetchAdditionalFolders(response, token)
local out = response
local cursor = response.cursor
repeat
local data = "{\"cursor\": \"" .. cursor .. "\"}"
local sink = {}
socketutil:set_timeout()
local request = {
url = API_LIST_ADD_FOLDER,
method = "POST",
headers = {
["Authorization"] = "Bearer ".. token,
["Content-Type"] = "application/json",
["Content-Length"] = #data,
},
source = ltn12.source.string(data),
sink = ltn12.sink.table(sink),
}
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)
local ret, result = pcall(JSON.decode, result_response)
if not ret then
return nil
end
for __, v in ipairs(result.entries) do
table.insert(out.entries, v)
end
if result.has_more then
cursor = result.cursor
end
until not result.has_more
return out
end
return DropBoxApi

@ -5,8 +5,8 @@ local FtpApi = require("apps/cloudstorage/ftpapi")
local InfoMessage = require("ui/widget/infomessage")
local MultiInputDialog = require("ui/widget/multiinputdialog")
local ReaderUI = require("apps/reader/readerui")
local Screen = require("device").screen
local UIManager = require("ui/uimanager")
local ltn12 = require("ltn12")
local logger = require("logger")
local util = require("util")
local _ = require("gettext")
@ -19,19 +19,15 @@ function Ftp:run(address, user, pass, path)
return FtpApi:listFolder(url, path)
end
function Ftp:downloadFile(item, address, user, pass, path, callback_close)
function Ftp:downloadFile(item, address, user, pass, path, close)
local url = FtpApi:generateUrl(address, util.urlEncode(user), util.urlEncode(pass)) .. item.url
logger.dbg("downloadFile url", url)
path = util.fixUtf8(path, "_")
local file, err = io.open(path, "w")
if not file then
UIManager:show(InfoMessage:new{
text = T(_("Could not save file to %1:\n%2"), BD.filepath(path), err),
})
return
end
local response = FtpApi:ftpGet(url, "retr", ltn12.sink.file(file))
local response = FtpApi:ftpGet(url, "retr")
if response ~= nil then
path = util.fixUtf8(path, "_")
local file = io.open(path, "w")
file:write(response)
file:close()
local __, filename = util.splitFilePathName(path)
if G_reader_settings:isTrue("show_unsupported") and not DocumentRegistry:hasProvider(filename) then
UIManager:show(InfoMessage:new{
@ -42,13 +38,7 @@ function Ftp:downloadFile(item, address, user, pass, path, callback_close)
text = T(_("File saved to:\n%1\nWould you like to read the downloaded book now?"),
BD.filepath(path)),
ok_callback = function()
local Event = require("ui/event")
UIManager:broadcastEvent(Event:new("SetupShowReader"))
if callback_close then
callback_close()
end
close()
ReaderUI:showReader(path)
end
})
@ -62,12 +52,9 @@ function Ftp:downloadFile(item, address, user, pass, path, callback_close)
end
function Ftp:config(item, callback)
local text_info = _([[
The FTP address must be in the following format:
ftp://example.domain.com
An IP address is also supported, for example:
ftp://10.10.10.1
Username and password are optional.]])
local text_info = "FTP address must be in the format ftp://example.domain.com\n"..
"Also supported is format with IP e.g: ftp://10.10.10.1\n"..
"Username and password are optional."
local hint_name = _("Your FTP name")
local text_name = ""
local hint_address = _("FTP address eg ftp://example.com")
@ -125,7 +112,6 @@ Username and password are optional.]])
{
{
text = _("Cancel"),
id = "close",
callback = function()
self.settings_dialog:onClose()
UIManager:close(self.settings_dialog)
@ -140,7 +126,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
@ -160,6 +146,8 @@ Username and password are optional.]])
},
},
},
width = math.floor(Screen:getWidth() * 0.95),
height = math.floor(Screen:getHeight() * 0.2),
input_type = "text",
}
UIManager:show(self.settings_dialog)

@ -20,15 +20,16 @@ function FtpApi:generateUrl(address, user, pass)
return generated_url
end
function FtpApi:ftpGet(u, command, sink)
function FtpApi:ftpGet(u, command)
local t = {}
local p = url.parse(u)
p.user = util.urlDecode(p.user)
p.password = util.urlDecode(p.password)
p.command = command
p.sink = sink
p.sink = ltn12.sink.table(t)
p.type = "i" -- binary
local r, e = ftp.get(p)
return r, e
return r and table.concat(t), e
end
function FtpApi:listFolder(address_path, folder_path)
@ -37,16 +38,12 @@ function FtpApi:listFolder(address_path, folder_path)
local type
local extension
local file_name
local tbl = {}
local sink = ltn12.sink.table(tbl)
local ls_ftp, e = self:ftpGet(address_path, "nlst", sink)
if ls_ftp == nil then
return false, e
end
local ls_ftp = self:ftpGet(address_path, "nlst")
if ls_ftp == nil then return false end
if folder_path == "/" then
folder_path = ""
end
for item in (table.concat(tbl)..'\n'):gmatch'(.-)\r?\n' do
for item in (ls_ftp..'\n'):gmatch'(.-)\r?\n' do
if item ~= '' then
file_name = item:match("([^/]+)$")
extension = item:match("^.+(%..+)$")

@ -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,18 @@ local UIManager = require("ui/uimanager")
local ReaderUI = require("apps/reader/readerui")
local WebDavApi = require("apps/cloudstorage/webdavapi")
local util = require("util")
local ffiutil = require("ffi/util")
local _ = require("gettext")
local Screen = require("device").screen
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)
function WebDav:downloadFile(item, address, username, password, local_path, close)
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
@ -30,13 +30,7 @@ function WebDav:downloadFile(item, address, username, password, local_path, call
text = T(_("File saved to:\n%1\nWould you like to read the downloaded book now?"),
BD.filepath(local_path)),
ok_callback = function()
local Event = require("ui/event")
UIManager:broadcastEvent(Event:new("SetupShowReader"))
if callback_close then
callback_close()
end
close()
ReaderUI:showReader(local_path)
end
})
@ -49,44 +43,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,31 +76,35 @@ 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 = {
{
{
text = _("Cancel"),
id = "close",
callback = function()
self.settings_dialog:onClose()
UIManager:close(self.settings_dialog)
@ -137,17 +117,10 @@ The start folder is appended to the server path.]])
end
},
{
text = _("Save"),
text = text_button_ok,
callback = function()
local fields = self.settings_dialog:getFields()
local fields = MultiInputDialog: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("/$", "")
end
if item then
-- edit
callback(item, fields)
@ -166,9 +139,13 @@ The start folder is appended to the server path.]])
},
},
},
width = math.floor(Screen:getWidth() * 0.95),
height = math.floor(Screen:getHeight() * 0.2),
input_type = "text",
}
UIManager:show(self.settings_dialog)
self.settings_dialog:onShowKeyboard()
end
function WebDav:info(item)

@ -1,24 +1,17 @@
local DocumentRegistry = require("document/documentregistry")
local FFIUtil = require("ffi/util")
local http = require("socket.http")
local ltn12 = require("ltn12")
local socket = require("socket")
local socketutil = require("socketutil")
local http = require('socket.http')
local https = require('ssl.https')
local ltn12 = require('ltn12')
local mime = require('mime')
local socket = require('socket')
local url = require('socket.url')
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 +33,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,20 +52,20 @@ 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 = {}
local has_trailing_slash = false
local has_leading_slash = false
if string.sub( address, -1 ) == "/" then has_trailing_slash = true end
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,60 +73,42 @@ 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
webdav_url = webdav_url .. "/"
end
local sink = {}
local request, sink = {}, {}
local parsed = url.parse(webdav_url)
local data = [[<?xml version="1.0"?><a:propfind xmlns:a="DAV:"><a:prop><a:resourcetype/></a:prop></a:propfind>]]
socketutil:set_timeout()
local request = {
url = webdav_url,
method = "PROPFIND",
headers = {
["Content-Type"] = "application/xml",
["Depth"] = "1",
["Content-Length"] = #data,
},
user = user,
password = pass,
source = ltn12.source.string(data),
sink = ltn12.sink.table(sink),
}
local code, headers, status = socket.skip(1, http.request(request))
socketutil:reset_timeout()
if headers == nil then
logger.dbg("WebDavApi:listFolder: No response:", status or code)
return nil
elseif not code or code < 200 or code > 299 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))
local auth = string.format("%s:%s", user, pass)
local headers = { ["Authorization"] = "Basic " .. mime.b64( auth ),
["Content-Type"] = "application/xml",
["Depth"] = "1",
["Content-Length"] = #data}
request["url"] = webdav_url
request["method"] = "PROPFIND"
request["headers"] = headers
request["source"] = ltn12.source.string(data)
request["sink"] = ltn12.sink.table(sink)
http.TIMEOUT = 5
https.TIMEOUT = 5
local httpRequest = parsed.scheme == "http" and http.request or https.request
local headers_request = socket.skip(1, httpRequest(request))
if headers_request == nil then
return nil
end
local res_data = table.concat(sink)
if res_data ~= "" then
-- iterate through the <d:response> tags, each containing an entry
for item in res_data:gmatch("<[^:]*:response[^>]*>(.-)</[^:]*:response>") do
for item in res_data:gmatch("<d:response>(.-)</d:response>") do
--logger.dbg("WebDav catalog item=", item)
-- <d:href> is the path and filename of the entry.
local item_fullpath = item:match("<[^:]*:href[^>]*>(.*)</[^:]*:href>")
local item_fullpath = item:match("<d:href>(.*)</d:href>")
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)
local is_not_collection = item:find("<[^:]*:resourcetype/>") or
item:find("<[^:]*:resourcetype></[^:]*:resourcetype>")
local item_path = (path == "" and has_trailing_slash) and item_name or path .. "/" .. item_name
if item:find("<[^:]*:collection[^<]*/>") then
local item_path = path .. "/" .. item_name
if item:find("<d:collection/>") then
item_name = item_name .. "/"
if not is_current_dir then
table.insert(webdav_list, {
@ -143,7 +117,7 @@ function WebDavApi:listFolder(address, user, pass, folder_path, folder_mode)
type = "folder",
})
end
elseif is_not_collection and (DocumentRegistry:hasProvider(item_name)
elseif item:find("<d:resourcetype/>") and (DocumentRegistry:hasProvider(item_name)
or G_reader_settings:isTrue("show_unsupported")) then
table.insert(webdav_file, {
text = item_name,
@ -170,69 +144,23 @@ 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{
url = file_url,
method = "GET",
sink = ltn12.sink.file(io.open(local_path, "w")),
user = user,
password = pass,
})
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
local parsed = url.parse(file_url)
local auth = string.format("%s:%s", user, pass)
local headers = { ["Authorization"] = "Basic " .. mime.b64( auth ) }
http.TIMEOUT = 5
https.TIMEOUT = 5
local httpRequest = parsed.scheme == "http" and http.request or https.request
local _, code_return, _ = httpRequest{
url = file_url,
method = "GET",
headers = headers,
sink = ltn12.sink.file(io.open(local_path, "w"))
}
return code_return
end
return WebDavApi

File diff suppressed because it is too large Load Diff

@ -3,245 +3,107 @@ 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, { _("Directory:"), BD.dirpath(filemanagerutil.abbreviate(directory)) })
table.insert(kv_pairs, "----")
-- 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
-- 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
@ -265,419 +127,166 @@ function BookInfo.getDocProps(file, book_props, no_open_document)
book_props = document:getProps()
book_props.pages = pages
end
document:close()
DocumentRegistry:closeDocument(file)
end
end
return BookInfo.extendProps(book_props, file)
end
-- If still no book_props, fall back to empty ones
if not book_props then
book_props = {}
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
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
authors = table.concat(authors, "\n")
else
authors = BD.auto(authors)
end
end
table.insert(kv_pairs, { _("Authors:"), authors })
-- 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)
local series = book_props.series
if series == "" or series == nil then
series = _("N/A")
else -- Shorten calibre series decimal number (#4.0 => #4)
series = series:gsub("(#%d+)%.0$", "%1")
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
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
end
if description then
self:showBookProp("description", util.htmlToPlainTextIfHtml(description))
keywords = table.concat(keywords, "\n")
else
UIManager:show(InfoMessage:new{
text = _("No book description available."),
})
keywords = BD.auto(keywords)
end
end
table.insert(kv_pairs, { _("Keywords:"), keywords })
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 description = book_props.description
if description == "" or description == nil then
description = _("N/A")
else
UIManager:show(InfoMessage:new{
text = _("No cover image available."),
})
-- Description may (often in EPUB, but not always) or may not (rarely
-- in PDF) be HTML.
description = util.htmlToPlainTextIfHtml(book_props.description)
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
-- (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
DocumentRegistry:closeDocument(file)
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 not widget then
widget = InfoMessage:new{
text = _("No cover image available"),
}
end
UIManager:show(widget)
end
return cover_bb
end
table.insert(kv_pairs, { _("Cover image:"), _("Tap to display"), callback=viewCoverImage })
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
-- 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
self.prop_updated = {
filepath = file,
doc_props = book_props,
metadata_key_updated = prop_updated,
metadata_value_old = prop_value_old,
}
self.kvp_widget:onClose()
self:show(file, book_props)
end
function BookInfo:setCustomCover(file, book_props)
if self.custom_book_cover then -- reset custom cover
if os.remove(self.custom_book_cover) then
DocSettings.removeSidecarDir(util.splitFilePathName(self.custom_book_cover))
self:updateBookInfo(file, book_props, "cover")
end
else -- choose an image and set custom cover
local PathChooser = require("ui/widget/pathchooser")
local path_chooser = PathChooser:new{
select_directory = false,
file_filter = function(filename)
return DocumentRegistry:isImageFile(filename)
end,
onConfirm = function(image_file)
if DocSettings:flushCustomCover(file, image_file) then
self:updateBookInfo(file, book_props, "cover")
end
end,
}
UIManager:show(path_chooser)
end
end
function BookInfo:setCustomCoverFromImage(file, image_file)
local custom_book_cover = DocSettings:findCustomCoverFile(file)
if custom_book_cover then
os.remove(custom_book_cover)
end
DocSettings:flushCustomCover(file, image_file)
if self.ui.doc_settings then
self.ui.doc_settings:getCustomCoverFile(true) -- reset cover file cache
end
UIManager:broadcastEvent(Event:new("InvalidateMetadataCache", file))
UIManager:broadcastEvent(Event:new("BookMetadataChanged"))
end
function BookInfo:setCustomMetadata(file, book_props, prop_key, prop_value)
-- in file
local custom_doc_settings, custom_props, display_title, no_custom_metadata
if self.custom_doc_settings then
custom_doc_settings = self.custom_doc_settings
else -- no custom metadata file, create new
custom_doc_settings = DocSettings.openSettingsFile()
display_title = book_props.display_title -- backup
book_props.display_title = nil
custom_doc_settings:saveSetting("doc_props", book_props) -- save a copy of original props
end
custom_props = custom_doc_settings:readSetting("custom_props", {})
local prop_value_old = custom_props[prop_key] or book_props[prop_key]
custom_props[prop_key] = prop_value -- nil when resetting a custom prop
if next(custom_props) == nil then -- no more custom metadata
os.remove(custom_doc_settings.sidecar_file)
DocSettings.removeSidecarDir(util.splitFilePathName(custom_doc_settings.sidecar_file))
no_custom_metadata = true
else
if book_props.pages then -- keep a copy of original 'pages' up to date
local original_props = custom_doc_settings:readSetting("doc_props")
original_props.pages = book_props.pages
end
custom_doc_settings:flushCustomMetadata(file)
end
book_props.display_title = book_props.display_title or display_title -- restore
-- in memory
prop_value = prop_value or custom_doc_settings:readSetting("doc_props")[prop_key] -- set custom or restore original
book_props[prop_key] = prop_value
if prop_key == "title" then -- generate when resetting the customized title and original is empty
book_props.display_title = book_props.title or filemanagerutil.splitFileNameType(file)
end
if self.document and self.document.file == file then -- currently opened document
self.ui.doc_props[prop_key] = prop_value
if prop_key == "title" then
self.ui.doc_props.display_title = book_props.display_title
end
if no_custom_metadata then
self.ui.doc_settings:getCustomMetadataFile(true) -- reset metadata file cache
end
end
self:updateBookInfo(file, book_props, prop_key, prop_value_old)
end
function BookInfo:showCustomEditDialog(file, book_props, prop_key)
local prop = book_props[prop_key]
if prop and prop_key == "description" then
prop = util.htmlToPlainTextIfHtml(prop)
end
local 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 = _("Book 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 BaseUtil = require("ffi/util")
local util = require("util")
local _ = require("gettext")
local T = require("ffi/util").template
local util = require("util")
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,571 +22,142 @@ 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 = {}
local doc_settings_or_file
if is_currently_opened then
doc_settings_or_file = self.ui.doc_settings
if not self.book_props then
self.book_props = self.ui.doc_props
self.book_props.has_cover = true
end
else
if DocSettings:hasSidecarFile(file) then
doc_settings_or_file = DocSettings:open(file)
if not self.book_props then
local props = doc_settings_or_file:readSetting("doc_props")
self.book_props = FileManagerBookInfo.extendProps(props, file)
self.book_props.has_cover = true
end
else
doc_settings_or_file = file
end
end
table.insert(buttons, filemanagerutil.genStatusButtonsRow(doc_settings_or_file, close_dialog_update_callback))
table.insert(buttons, {}) -- separator
table.insert(buttons, {
filemanagerutil.genResetSettingsButton(doc_settings_or_file, close_dialog_update_callback, is_currently_opened),
{
text = _("Remove from collection"),
callback = function()
UIManager:close(self.collfile_dialog)
ReadCollection:removeItem(file, self.collection_name)
self._manager:updateItemTable()
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
table.insert(buttons, {
filemanagerutil.genExecuteScriptButton(file, close_dialog_menu_callback)
})
end
self.collfile_dialog = ButtonDialog:new{
title = BD.filename(item.text),
title_align = "center",
buttons = buttons,
}
UIManager:show(self.collfile_dialog)
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)
if self._manager.ui.view and self._manager.ui.view.onSetRotationMode then
self._manager.ui.view:onSetRotationMode(rotation)
elseif self._manager.ui.onSetRotationMode then
self._manager.ui:onSetRotationMode(rotation)
else
Screen:setRotationMode(rotation)
end
self._manager:onShowColl()
end
return true
end
function FileManagerCollection:onBookMetadataChanged()
if self.coll_menu then
self.coll_menu:updateItems()
end
end
-- collection list
function FileManagerCollection:onShowCollList(file_or_files, caller_callback, no_dialog)
self.selected_colections = nil
if file_or_files then -- select mode
if type(file_or_files) == "string" then -- checkmark collections containing the file
self.selected_colections = ReadCollection:getCollectionsWithFile(file_or_files)
else -- do not checkmark any
self.selected_colections = {}
end
end
self.coll_list = Menu:new{
subtitle = "",
covers_fullscreen = true,
is_borderless = true,
is_popout = false,
title_bar_fm_style = true,
title_bar_left_icon = file_or_files and "check" or "appbar.menu",
onLeftButtonTap = function() self:showCollListDialog(caller_callback, no_dialog) end,
onMenuChoice = self.onCollListChoice,
onMenuHold = self.onCollListHold,
onSetRotationMode = self.MenuSetRotationModeHandler,
_manager = self,
}
self.coll_list.close_callback = function(force_close)
if force_close or self.selected_colections == nil then
UIManager:close(self.coll_list)
self.coll_list = nil
end
end
self:updateCollListItemTable(true) -- init
UIManager:show(self.coll_list)
return true
end
function FileManagerCollection:updateCollListItemTable(do_init, item_number)
local item_table
if do_init then
item_table = {}
for name, coll in pairs(ReadCollection.coll) do
local mandatory
if self.selected_colections then
mandatory = self.selected_colections[name] and self.checkmark or " "
self.coll_list.items_mandatory_font_size = self.coll_list.font_size
else
mandatory = util.tableSize(coll)
end
table.insert(item_table, {
text = self:getCollectionTitle(name),
mandatory = mandatory,
name = name,
order = ReadCollection.coll_order[name],
})
end
if #item_table > 1 then
table.sort(item_table, function(v1, v2) return v1.order < v2.order end)
end
else
item_table = self.coll_list.item_table
end
local title = T(_("Collections (%1)"), #item_table)
local subtitle
if self.selected_colections then
local selected_nb = util.tableSize(self.selected_colections)
subtitle = self.selected_colections and T(_("Selected collections: %1"), selected_nb)
if do_init and selected_nb > 0 then -- show first collection containing the long-pressed book
for i, item in ipairs(item_table) do
if self.selected_colections[item.name] then
item_number = i
break
end
end
end
end
self.coll_list:switchItemTable(title, item_table, item_number or -1, nil, subtitle)
end
function FileManagerCollection:onCollListChoice(item)
if self._manager.selected_colections then
if item.mandatory == self._manager.checkmark then
self.item_table[item.idx].mandatory = " "
self._manager.selected_colections[item.name] = nil
else
self.item_table[item.idx].mandatory = self._manager.checkmark
self._manager.selected_colections[item.name] = true
end
self._manager:updateCollListItemTable()
else
self._manager:onShowColl(item.name)
end
end
function FileManagerCollection:onCollListHold(item)
if item.name == ReadCollection.default_collection_name -- Favorites non-editable
or self._manager.selected_colections then -- select mode
return
end
local button_dialog
local buttons = {
{
{
text = _("Remove collection"),
text = _("Sort"),
callback = function()
UIManager:close(button_dialog)
self._manager:removeCollection(item)
end
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)
end,
},
{
text = _("Rename collection"),
text = _("Remove from collection"),
callback = function()
UIManager:close(button_dialog)
self._manager:renameCollection(item)
end
ReadCollection:removeItem(item.file, self._manager.coll_menu.collection)
self._manager:updateItemTable()
UIManager:close(self.collfile_dialog)
end,
},
},
}
button_dialog = ButtonDialog:new{
title = item.text,
title_align = "center",
buttons = buttons,
}
UIManager:show(button_dialog)
return true
end
function FileManagerCollection:showCollListDialog(caller_callback, no_dialog)
if no_dialog then
caller_callback()
self.coll_list.close_callback(true)
return
end
local button_dialog, buttons
local new_collection_button = {
{
text = _("New collection"),
callback = function()
UIManager:close(button_dialog)
self:addCollection()
end,
},
}
if self.selected_colections then -- select mode
buttons = {
new_collection_button,
{}, -- separator
{
{
text = _("Deselect all"),
callback = function()
UIManager:close(button_dialog)
for name in pairs(self.selected_colections) do
self.selected_colections[name] = nil
end
self:updateCollListItemTable(true)
end,
},
{
text = _("Select all"),
callback = function()
UIManager:close(button_dialog)
for name in pairs(ReadCollection.coll) do
self.selected_colections[name] = true
end
self:updateCollListItemTable(true)
end,
},
},
{
{
text = _("Apply selection"),
callback = function()
UIManager:close(button_dialog)
caller_callback()
self.coll_list.close_callback(true)
end,
},
},
}
else
buttons = {
new_collection_button,
{
{
text = _("Arrange collections"),
callback = function()
UIManager:close(button_dialog)
self:sortCollections()
end,
},
},
}
end
button_dialog = ButtonDialog:new{
buttons = buttons,
}
UIManager:show(button_dialog)
end
function FileManagerCollection:editCollectionName(editCallback, old_name)
local input_dialog
input_dialog = InputDialog:new{
title = _("Enter collection name"),
input = old_name,
input_hint = old_name,
buttons = {{
{
text = _("Cancel"),
id = "close",
text = _("Book information"),
enabled = FileManagerBookInfo:isSupported(item.file),
callback = function()
UIManager:close(input_dialog)
FileManagerBookInfo:show(item.file)
UIManager:close(self.collfile_dialog)
end,
},
},
}
-- NOTE: Duplicated from frontend/apps/filemanager/filemanager.lua
if Device:canExecuteScript(item.file) then
table.insert(buttons, {
{
text = _("Save"),
-- @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()
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
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_file = "resources/info-warn.png",
})
end
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,
self.collfile_dialog = ButtonDialogTitle:new{
title = item.text:match("([^/]+)$"),
title_align = "center",
buttons = buttons,
}
UIManager:show(sort_widget)
UIManager:show(self.collfile_dialog)
return true
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,
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,
onMenuHold = self.onMenuHold,
_manager = self,
collection = collection,
}
self:updateItemTable()
self.coll_menu.close_callback = function()
-- Close it at next tick so it stays displayed
-- while a book is opening (avoids a transient
-- display of the underlying File Browser)
UIManager:nextTick(function()
UIManager:close(self.coll_menu)
end)
end
UIManager:show(self.coll_menu)
return true
end
return FileManagerCollection

@ -38,12 +38,11 @@ local FileConverter = {
---- @string markdown the markdown fragment
---- @string title an optional title for the HTML document
---- @treturn string an HTML document
function FileConverter:mdToHtml(markdown, title, stylesheet)
function FileConverter:mdToHtml(markdown, title)
local MD = require("apps/filemanager/lib/md")
stylesheet = stylesheet and string.format("<style>\n%s\n</style>\n", stylesheet) or ""
local md_options = {
prependHead = "<!DOCTYPE html>\n<html>\n<head>\n",
insertHead = string.format("<title>%s</title>\n%s</head>\n<body>\n", title, stylesheet),
insertHead = string.format("<title>%s</title>\n</head>\n<body>\n", title),
appendTail = "\n</body>\n</html>",
}
local html, err = MD(markdown, md_options)
@ -55,9 +54,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 +62,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 +103,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,185 @@
local ButtonDialog = require("ui/widget/buttondialog")
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 Font = require("ui/font")
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 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 T = require("ffi/util").template
local Screen = require("device").screen
local FileSearcher = WidgetContainer:extend{
case_sensitive = false,
include_subfolders = true,
include_metadata = false,
}
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,
buttons = {
{
{
text = _("Cancel"),
id = "close",
callback = function()
UIManager:close(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.path = G_reader_settings:readSetting("home_dir")
self:doSearch()
end,
},
{
text = self.ui.file_chooser and _("Current folder") or _("Book 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.path = self.ui.file_chooser and self.ui.file_chooser.path or self.ui:getLastDirFile()
self:doSearch()
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)
check_button_subfolders = CheckButton:new{
text = _("Include subfolders"),
checked = self.include_subfolders,
parent = search_dialog,
callback = function()
self.include_subfolders = 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
local FileSearcher = InputContainer:new{
search_dialog = nil,
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
end
--filesearcher
-- state buffer
dirs = {},
files = {},
results = {},
items = 0,
commands = nil,
function FileSearcher:getList()
self.no_metadata_count = 0
local sys_folders = { -- do not search in sys_folders
["/dev"] = true,
["/proc"] = true,
["/sys"] = true,
}
local collate = FileChooser:getCollate()
local search_string = self.search_string
if search_string ~= "*" then -- one * to show all files
if not self.case_sensitive then
search_string = Utf8Proc.lowercase(util.fixUtf8(search_string, "?"))
end
-- replace '.' with '%.'
search_string = search_string:gsub("%.","%%%.")
-- replace '*' with '.*'
search_string = search_string:gsub("%*","%.%*")
-- replace '?' with '.'
search_string = search_string:gsub("%?","%.")
end
--filemanagersearch
use_previous_search_results = false,
lastsearch = nil,
}
local dirs, files = {}, {}
local scan_dirs = {self.path}
while #scan_dirs ~= 0 do
function FileSearcher:readDir()
self.dirs = {self.path}
self.files = {}
while #self.dirs ~= 0 do
local new_dirs = {}
-- handle each dir
for _, d in ipairs(scan_dirs) do
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 (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
for f in lfs.dir(d) do
local fullpath = d.."/"..f
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, ".")) then
table.insert(new_dirs, fullpath)
table.insert(self.files, {name = f, path = fullpath, attr = attributes})
-- Always ignore macOS resource forks, too.
elseif attributes.mode == "file" and not util.stringStartsWith(f, "._") and DocumentRegistry:hasProvider(fullpath) then
table.insert(self.files, {name = f, path = fullpath, attr = attributes})
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
self.dirs = new_dirs
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 })
function FileSearcher:setSearchResults()
local FileManager = require("apps/filemanager/filemanager")
local ReaderUI = require("apps/reader/readerui")
local keywords = self.search_value
self.results = {}
if keywords == " " then -- one space to show all files
self.results = self.files
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()
for __,f in pairs(self.files) do
if string.find(string.lower(f.name), string.lower(keywords)) and string.sub(f.name,-4) ~= ".sdr" then
if f.attr.mode == "directory" then
f.text = f.name.."/"
f.name = nil
f.callback = function()
FileManager:showFiles(f.path)
end
table.insert(self.results, f)
else
f.text = f.name
f.name = nil
f.callback = function()
ReaderUI:showReader(f.path)
end
table.insert(self.results, f)
end
self.ui.coverbrowser:extractBooksInDirectory(self.path)
end
})
end
end
self.keywords = keywords
self.items = #self.results
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,
onMenuHold = self.onMenuHold,
handle_hold_on_hold_release = true,
}
self.search_menu.close_callback = function()
UIManager:close(self.search_menu)
if self.ui.file_chooser then
self.ui.file_chooser:refreshPath()
function FileSearcher:close()
if self.search_value then
UIManager:close(self.search_dialog)
if string.len(self.search_value) > 0 then
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() --- @todo something about no results
else
UIManager:show(
InfoMessage:new{
text = BaseUtil.template(_("Found no files matching '%1'."),
self.search_value)
}
)
end
end
end
UIManager:show(self.search_menu)
if self.no_metadata_count ~= 0 then
self:showSearchResultsMessage()
end
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:onShowFileSearch()
local dummy = self.search_value
local enabled_search_home_dir = true
if not G_reader_settings:readSetting("home_dir") then
enabled_search_home_dir = false
end
self.search_dialog = InputDialog:new{
title = _("Search for books by filename"),
input = self.search_value,
width = math.floor(Screen:getWidth() * 0.9),
buttons = {
{
text = _("Delete"),
enabled = not is_currently_opened,
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)
{
text = _("Cancel"),
enabled = true,
callback = function()
self.search_dialog:onClose()
UIManager:close(self.search_dialog)
end,
},
{
text = _("Current folder"),
enabled = true,
callback = function()
self.path = self.ui.file_chooser and self.ui.file_chooser.path or self.ui:getLastDirFile()
self.search_value = self.search_dialog:getInputText()
if self.search_value == dummy then -- probably DELETE this if/else block
self.use_previous_search_results = true
else
self.use_previous_search_results = false
end
end
local FileManager = require("apps/filemanager/filemanager")
FileManager:showDeleteFileDialog(file, post_delete_callback)
end,
self:close()
end,
},
{
text = _("Home folder"),
enabled = enabled_search_home_dir,
callback = function()
self.path = G_reader_settings:readSetting("home_dir")
self.search_value = self.search_dialog:getInputText()
if self.search_value == dummy then -- probably DELETE this if/else block
self.use_previous_search_results = true
else
self.use_previous_search_results = false
end
self:close()
end,
},
},
filemanagerutil.genBookInformationButton(file, bookinfo, close_dialog_callback),
})
end
table.insert(buttons, {
filemanagerutil.genShowFolderButton(file, close_dialog_menu_callback),
{
text = _("Open"),
enabled = DocumentRegistry:hasProvider(file, nil, true), -- allow auxiliary providers
callback = function()
close_dialog_callback()
local FileManager = require("apps/filemanager/filemanager")
FileManager.openFile(self.ui, file, nil, self.close_callback)
end,
},
})
local title = file
if bookinfo then
if bookinfo.title then
title = title .. "\n\n" .. T(_("Title: %1"), bookinfo.title)
end
if bookinfo.authors then
title = title .. "\n" .. T(_("Authors: %1"), bookinfo.authors:gsub("[\n\t]", "|"))
end
end
dialog = ButtonDialog:new{
title = title .. "\n",
buttons = buttons,
}
UIManager:show(dialog)
UIManager:show(self.search_dialog)
self.search_dialog:onShowKeyboard()
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:showSearchResults()
local menu_container = CenterContainer:new{
dimen = Screen:getSize(),
}
self.search_menu = Menu:new{
width = Screen:getWidth()-15,
height = Screen:getHeight()-15,
show_parent = menu_container,
onMenuHold = self.onMenuHold,
cface = Font:getFace("smallinfofont"),
perpage = G_reader_settings:readSetting("items_per_page") or 14,
_manager = self,
}
table.insert(menu_container, self.search_menu)
self.search_menu.close_callback = function()
UIManager:close(menu_container)
end
return true
table.sort(self.results, function(v1,v2) return v1.text < v2.text end)
self.search_menu:switchItemTable(_("Search Results"), self.results)
UIManager:show(menu_container)
end
return FileSearcher

@ -1,40 +1,25 @@
local BD = require("ui/bidi")
local ButtonDialog = require("ui/widget/buttondialog")
local CheckButton = require("ui/widget/checkbutton")
local ConfirmBox = require("ui/widget/confirmbox")
local ButtonDialogTitle = require("ui/widget/buttondialogtitle")
local DocSettings = require("docsettings")
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 util = require("ffi/util")
local _ = require("gettext")
local C_ = _.pgettext
local T = require("ffi/util").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"),
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"),
}
function FileManagerHistory:init()
self.ui.menu:registerToMainMenu(self)
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 +28,94 @@ 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()
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)
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)
-- try to stay on current page
local select_number = nil
if self.hist_menu.page and self.hist_menu.perpage then
select_number = (self.hist_menu.page - 1) * self.hist_menu.perpage + 1
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(self.hist_menu_title,
require("readhistory").hist, 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.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 = _("Purge .sdr"),
enabled = item.file ~= currently_opened_file and DocSettings:hasSidecarFile(util.realpath(item.file)),
callback = function()
local ConfirmBox = require("ui/widget/confirmbox")
UIManager:show(ConfirmBox:new{
text = util.template(_("Purge .sdr to reset settings for this document?\n\n%1"), BD.filename(item.text)),
ok_text = _("Purge"),
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()
local ConfirmBox = require("ui/widget/confirmbox")
UIManager:show(ConfirmBox:new{
text = _("Are you sure that you want to delete this file?\n") .. BD.filepath(item.file) .. ("\n") .. _("If you delete a file, it is permanently lost."),
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),
{},
{
{
text = _("Clear history of deleted files"),
callback = function()
require("readhistory"):clearMissing()
self._manager:updateItemTable()
UIManager:close(self.histfile_dialog)
end,
},
},
}
self.histfile_dialog = ButtonDialogTitle:new{
title = BD.filename(item.text:match("([^/]+)$")),
title_align = "center",
buttons = buttons,
}
@ -230,206 +123,28 @@ function FileManagerHistory:onMenuHold(item)
return true
end
-- Can't *actually* name it onSetRotationMode, or it also fires in FM itself ;).
function FileManagerHistory:MenuSetRotationModeHandler(rotation)
if rotation ~= nil and rotation ~= Screen:getRotationMode() then
UIManager:close(self._manager.hist_menu)
-- Also re-layout ReaderView or FileManager itself
if self._manager.ui.view and self._manager.ui.view.onSetRotationMode then
self._manager.ui.view:onSetRotationMode(rotation)
elseif self._manager.ui.onSetRotationMode then
self._manager.ui:onSetRotationMode(rotation)
else
Screen:setRotationMode(rotation)
end
self._manager:onShowHist()
end
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
UIManager:close(self.hist_menu)
self.hist_menu = nil
G_reader_settings:saveSetting("history_filter", self.filter)
end
UIManager:show(self.hist_menu, "flashui")
-- Close it at next tick so it stays displayed
-- while a book is opening (avoids a transient
-- display of the underlying File Browser)
UIManager:nextTick(function()
UIManager:close(self.hist_menu)
end)
end
UIManager:show(self.hist_menu)
return true
end
function FileManagerHistory:showHistDialog()
if not self.statuses_fetched then
self:fetchStatuses(true)
end
local hist_dialog
local buttons = {}
local function genFilterButton(filter)
return {
text = T(_("%1 (%2)"), filter_text[filter], self.count[filter]),
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: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,
},
})
table.insert(buttons, {
{
text = _("Search in filename and book metadata"),
callback = function()
UIManager:close(hist_dialog)
self:onSearchHistory()
end,
},
})
if self.count.deleted > 0 then
table.insert(buttons, {}) -- separator
table.insert(buttons, {
{
text = _("Clear history of deleted files"),
callback = function()
local confirmbox = ConfirmBox:new{
text = _("Clear history of deleted files?"),
ok_text = _("Clear"),
ok_callback = function()
UIManager:close(hist_dialog)
require("readhistory"):clearMissing()
self:updateItemTable()
end,
}
UIManager:show(confirmbox)
end,
},
})
end
hist_dialog = ButtonDialog:new{
title = _("Filter by book status"),
title_align = "center",
buttons = buttons,
}
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

File diff suppressed because it is too large Load Diff

@ -1,288 +1,371 @@
local CenterContainer = require("ui/widget/container/centercontainer")
local ConfirmBox = require("ui/widget/confirmbox")
local DataStorage = require("datastorage")
local Device = require("device")
local Font = require("ui/font")
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
function SetDefaults:init()
self.results = {}
-- Then deal with our child widgets and our internal variables
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)
local defaults = {}
local load_defaults = loadfile(defaults_path)
setfenv(load_defaults, defaults)
load_defaults()
-- 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,
}
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, "partial")
end
self.defaults_menu = Menu:new{
width = Screen:getWidth()-15,
height = Screen:getHeight()-15,
cface = Font:getFace("smallinfofont"),
perpage = G_reader_settings:readSetting("items_per_page") or 14,
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,
width = self.dialog_width,
input_type = setting_type,
width = math.floor(Screen:getWidth() * 0.95),
}
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,
width = math.floor(Screen:getWidth() * 0.95),
height = math.floor(Screen:getHeight() * 0.2),
}
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,
width = self.dialog_width,
input_type = setting_type,
width = math.floor(Screen:getWidth() * 0.95),
}
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
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)
local checked = {}
for j=1, #self.defaults_name do
if not self.changed[j] then checked[j] = true end
end
-- 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 +376,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,238 +1,236 @@
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 = require("ffi/util").template
local FileManagerShortcuts = WidgetContainer:extend{
title = _("Folder shortcuts"),
folder_shortcuts = G_reader_settings:readSetting("folder_shortcuts", {}),
}
local FileManagerShortcuts = InputContainer:extend{}
function FileManagerShortcuts:updateItemTable()
local item_table = {}
for folder, item in pairs(self.folder_shortcuts) do
local folder_shortcuts = G_reader_settings:readSetting("folder_shortcuts") or {}
table.insert(item_table, {
text = _("Add new folder shortcut"),
callback = function()
self:addNewFolder()
end,
})
for _, item in ipairs(folder_shortcuts) do
table.insert(item_table, {
text = string.format("%s (%s)", item.text, folder),
folder = folder,
name = item.text,
})
end
table.sort(item_table, function(l, r)
return l.text < r.text
end)
self.shortcuts_menu:switchItemTable(nil, item_table, -1)
end
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)
function FileManagerShortcuts:hasFolderShortcut(folder)
return self.folder_shortcuts[folder] and true or false
end
local folder = item.folder
if folder ~= nil and lfs.attributes(folder, "mode") == "directory" then
if self.ui.file_chooser then
self.ui.file_chooser:changeToPath(folder)
else -- called from Reader
local FileManager = require("apps/filemanager/filemanager")
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
self.ui:onClose()
if FileManager.instance then
FileManager.instance:reinit(folder)
else
FileManager:showFiles(folder)
end
end
end
end,
})
end
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,
}
UIManager:show(dialog)
return true
end
-- try to stay on current page
local select_number = nil
function FileManagerShortcuts:removeShortcut(folder)
self.folder_shortcuts[folder] = nil
if self.shortcuts_menu then
self.fm_updated = true
self:updateItemTable()
if self.fm_bookmark.page and self.fm_bookmark.perpage then
select_number = (self.fm_bookmark.page - 1) * self.fm_bookmark.perpage + 1
end
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()
end
end
if self.shortcuts_menu then
self.fm_updated = true
self:updateItemTable()
end
end,
},
}},
}
UIManager:show(input_dialog)
input_dialog:onShowKeyboard()
self.fm_bookmark:switchItemTable(nil,
item_table, select_number)
end
function FileManagerShortcuts:addShortcut()
function FileManagerShortcuts:addNewFolder()
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(),
path = self.fm_bookmark.curr_path,
onConfirm = function(path)
if self:hasFolderShortcut(path) then
UIManager:show(InfoMessage:new{
text = _("Shortcut already exists."),
})
else
self:editShortcut(path)
end
end,
local add_folder_input
local friendly_name = util.basename(path) or _("my folder")
add_folder_input = InputDialog:new{
title = self.title,
input = friendly_name,
input_type = "text",
description = T(_("Title for selected folder:\n%1"), BD.dirpath(path)),
buttons = {
{
{
text = _("Cancel"),
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(path_chooser)
end
function FileManagerShortcuts:genShowFolderShortcutsButton(pre_callback)
return {
text = self.title,
callback = function()
pre_callback()
self:onShowFolderShortcutsDialog()
end,
}
function FileManagerShortcuts:addFolderFromInput(friendly_name, folder)
for __, item in ipairs(G_reader_settings:readSetting("folder_shortcuts") or {}) 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
local folder_shortcuts = G_reader_settings:readSetting("folder_shortcuts") or {}
table.insert(folder_shortcuts, {
text = friendly_name,
folder = folder,
})
G_reader_settings:saveSetting("folder_shortcuts", folder_shortcuts)
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:onMenuHold(item)
if item.deletable or item.editable then
local folder_shortcuts_dialog
folder_shortcuts_dialog = ButtonDialog:new{
buttons = {
{
{
text = _("Paste file"),
enabled = (self._manager.ui.file_chooser and self._manager.ui.clipboard) and true or false,
callback = function()
UIManager:close(folder_shortcuts_dialog)
self._manager.ui:pasteHere(item.folder)
end
},
{
text = _("Edit"),
enabled = item.editable,
callback = function()
UIManager:close(folder_shortcuts_dialog)
self._manager:editFolderShortcut(item)
end
},
{
text = _("Delete"),
enabled = item.deletable,
callback = function()
UIManager:close(folder_shortcuts_dialog)
self._manager:deleteFolderShortcut(item)
end
},
},
}
}
UIManager:show(folder_shortcuts_dialog)
return true
end
end
function FileManagerShortcuts:onSetDimensions(dimen)
self.dimen = dimen
function FileManagerShortcuts:editFolderShortcut(item)
local edit_folder_input
edit_folder_input = InputDialog:new {
title = _("Edit friendly name"),
input = item.friendly_name,
input_type = "text",
description = T(_("Rename title for selected folder:\n%1"), BD.dirpath(item.folder)),
buttons = {
{
{
text = _("Cancel"),
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(edit_folder_input)
edit_folder_input:onShowKeyboard()
end
function FileManagerShortcuts:MenuSetRotationModeHandler(rotation)
if rotation ~= nil and rotation ~= Screen:getRotationMode() then
UIManager:close(self._manager.shortcuts_menu)
if self._manager.ui.view and self._manager.ui.view.onSetRotationMode then
self._manager.ui.view:onSetRotationMode(rotation)
elseif self._manager.ui.onSetRotationMode then
self._manager.ui:onSetRotationMode(rotation)
else
Screen:setRotationMode(rotation)
function FileManagerShortcuts:renameFolderShortcut(item, new_name)
local folder_shortcuts = {}
for _, element in ipairs(G_reader_settings:readSetting("folder_shortcuts") or {}) do
if element.text == item.friendly_name and element.folder == item.folder then
element.text = new_name
end
self._manager:onShowFolderShortcutsDialog()
table.insert(folder_shortcuts, element)
end
return true
G_reader_settings:saveSetting("folder_shortcuts", folder_shortcuts)
self:updateItemTable()
end
function FileManagerShortcuts:onShowFolderShortcutsDialog(select_callback)
self.shortcuts_menu = Menu:new{
title = self.title,
covers_fullscreen = true,
is_borderless = true,
function FileManagerShortcuts:deleteFolderShortcut(item)
local folder_shortcuts = {}
for _, element in ipairs(G_reader_settings:readSetting("folder_shortcuts") or {}) do
if element.text ~= item.friendly_name or element.folder ~= item.folder then
table.insert(folder_shortcuts, element)
end
end
G_reader_settings:saveSetting("folder_shortcuts", folder_shortcuts)
self:updateItemTable()
end
function FileManagerShortcuts:onSetDimensions(dimen)
self.dimen = dimen
end
function FileManagerShortcuts:onShowFolderShortcutsDialog()
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,
onMenuHold = not select_callback and self.onMenuHold or nil,
onSetRotationMode = self.MenuSetRotationModeHandler,
is_borderless = true,
curr_path = self.ui.file_chooser and self.ui.file_chooser.path or self.ui:getLastDirFile(),
onMenuHold = self.onMenuHold,
_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)
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,363 +29,18 @@ 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
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_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)
-- Purge doc settings in sidecar directory,
function filemanagerutil.purgeSettings(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
if not settings_to_keep[k] then
doc_settings:delSetting(k)
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)
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,
},
})
os.remove(DocSettings:getSidecarFile(file_abs_path))
-- Also remove backup, otherwise it will be used if we re-open this document
-- (it also allows for the sidecar folder to be empty and removed)
os.remove(DocSettings:getSidecarFile(file_abs_path)..".old")
-- If the sidecar folder is empty, os.remove() can delete it.
-- Otherwise, the following statement has no effect.
os.remove(DocSettings:getSidecarDir(file_abs_path))
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,16 +2,18 @@ local BD = require("ui/bidi")
local Blitbuffer = require("ffi/blitbuffer")
local ConfirmBox = require("ui/widget/confirmbox")
local FrameContainer = require("ui/widget/container/framecontainer")
local OPDSBrowser = require("opdsbrowser")
local InputContainer = require("ui/widget/container/inputcontainer")
local OPDSBrowser = require("ui/widget/opdsbrowser")
local ReaderUI = require("apps/reader/readerui")
local UIManager = require("ui/uimanager")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local logger = require("logger")
local _ = require("gettext")
local Screen = require("device").screen
local T = require("ffi/util").template
local OPDSCatalog = WidgetContainer:extend{
local OPDSCatalog = InputContainer:extend{
title = _("OPDS Catalog"),
onExit = function() end,
}
function OPDSCatalog:init()
@ -20,6 +22,7 @@ function OPDSCatalog:init()
show_parent = self,
is_popout = false,
is_borderless = true,
has_close_button = true,
close_callback = function() return self:onClose() end,
file_downloaded_callback = function(downloaded_file)
UIManager:show(ConfirmBox:new{
@ -28,12 +31,7 @@ function OPDSCatalog:init()
ok_text = _("Read now"),
cancel_text = _("Read later"),
ok_callback = function()
local Event = require("ui/event")
UIManager:broadcastEvent(Event:new("SetupShowReader"))
self:onClose()
local ReaderUI = require("apps/reader/readerui")
ReaderUI:showReader(downloaded_file)
end
})
@ -50,13 +48,13 @@ end
function OPDSCatalog:onShow()
UIManager:setDirty(self, function()
return "ui", self[1].dimen -- i.e., FrameContainer
return "ui", self[1].dimen
end)
end
function OPDSCatalog:onCloseWidget()
UIManager:setDirty(nil, function()
return "ui", self[1].dimen
return "partial", self[1].dimen
end)
end
@ -65,12 +63,18 @@ function OPDSCatalog:showCatalog()
UIManager:show(OPDSCatalog:new{
dimen = Screen:getSize(),
covers_fullscreen = true, -- hint for UIManager:_repaint()
onExit = function()
--UIManager:quit()
end
})
end
function OPDSCatalog:onClose()
logger.dbg("close OPDS catalog")
UIManager:close(self)
if self.onExit then
self:onExit()
end
return true
end

@ -1,42 +1,16 @@
-- Start with a empty stub, because 99.9% of users won't actually need this.
local ReaderActivityIndicator = {}
function ReaderActivityIndicator:isStub() return true end
function ReaderActivityIndicator:onStartActivityIndicator() end
function ReaderActivityIndicator:onStopActivityIndicator() end
-- Now, if we're on Kindle, and we haven't actually murdered Pillow, see what we can do...
local Device = require("device")
if Device:isKindle() then
if os.getenv("PILLOW_HARD_DISABLED") or os.getenv("PILLOW_SOFT_DISABLED") then
-- Pillow is dead, bye!
return ReaderActivityIndicator
end
if not Device:isTouchDevice() then
-- No lipc, bye!
return ReaderActivityIndicator
end
else
-- Not on Kindle, bye!
return ReaderActivityIndicator
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 Device = require("device")
local util = require("ffi/util")
-- lipc
ReaderActivityIndicator = EventListener:extend{
lipc_handle = nil,
}
function ReaderActivityIndicator:isStub() return false end
local ReaderActivityIndicator = EventListener:new{}
function ReaderActivityIndicator:init()
local haslipc, lipc = pcall(require, "liblipclua")
if haslipc then
self.lipc_handle = lipc.init("com.github.koreader.activityindicator")
local dev_mod = Device.model
if dev_mod == "KindlePaperWhite" or dev_mod == "KindlePaperWhite2" or dev_mod == "KindleVoyage" or dev_mod == "KindleBasic" or dev_mod == "KindlePaperWhite3" or dev_mod == "KindleOasis" or dev_mod == "KindleBasic2" or dev_mod == "KindleTouch" then
if (pcall(require, "liblipclua")) then
self.lipc_handle = lipc.init("com.github.koreader.activityindicator")
end
end
end
@ -67,15 +41,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

@ -1,189 +1,89 @@
local ConfirmBox = require("ui/widget/confirmbox")
local Device = require("device")
local Event = require("ui/event")
local EventListener = require("ui/widget/eventlistener")
local Notification = require("ui/widget/notification")
local UIManager = require("ui/uimanager")
local logger = require("logger")
local util = require("util")
local _ = require("gettext")
-- This module handles the "Back" key (and the "Back" gesture action).
-- When global setting "back_in_reader" == "previous_read_page", it
-- 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,
-- allow for disabling back history, and having Back key
-- quit immediately (useful for some developers)
disabled = G_reader_settings:isFalse("enable_back_history"),
}
function ReaderBack:init()
self:registerKeyEvents()
-- Regular function wrapping our method, to avoid re-creating
-- an anonymous function at each page turn
self._addPreviousLocationToStackCallback = function()
self:_addPreviousLocationToStack()
end
end
function ReaderBack:registerKeyEvents()
if Device:hasKeys() then
self.ui.key_events.Back = { { Device.input.group.Back } }
self.ui.key_events.Back = { {"Back"}, doc = "Reader back" }
end
end
ReaderBack.onPhysicalKeyboardConnected = ReaderBack.registerKeyEvents
function ReaderBack:_getCurrentLocation()
local current_location
if self.ui.document.info.has_pages then
return self.ui.paging:getBookLocation()
current_location = self.ui.paging:getBookLocation()
else
return {
current_location = {
xpointer = self.ui.rolling:getBookLocation(),
}
end
end
function ReaderBack:_areLocationsSimilar(location1, location2)
if self.ui.document.info.has_pages then
-- locations are arrays of k/v tables
if #location1 ~= #location2 then
return false
end
for i=1, #location1 do
if not util.tableEquals(location1[i], location2[i]) then
return false
end
end
return true
else
return location1.xpointer == location2.xpointer
end
return current_location
end
function ReaderBack:_addPreviousLocationToStack()
local ignore_location
function ReaderBack:addCurrentLocationToStack()
local location_stack = self.location_stack
local new_location = self:_getCurrentLocation()
if self.cur_location and new_location then
if self:_areLocationsSimilar(self.cur_location, new_location) then
-- Unchanged, don't add it yet
return
end
table.insert(self.location_stack, self.cur_location)
if #self.location_stack > self.max_stack then
table.remove(self.location_stack, 1)
end
end
if util.tableEquals(ignore_location, new_location) then return end
table.insert(location_stack, new_location)
if new_location then
self.cur_location = new_location
if #location_stack > self.max_stack then
table.remove(location_stack, 1)
end
end
-- Scroll mode crengine
function ReaderBack:onPosUpdate()
if self.disabled then return end
self:addCurrentLocationToStack()
end
-- Paged media
function ReaderBack:onPageUpdate()
if self.disabled then return end
self:addCurrentLocationToStack()
end
-- Called when loading new document
function ReaderBack:onReadSettings(config)
self.location_stack = {}
self.cur_location = nil
end
function ReaderBack:_onViewPossiblyUpdated()
if G_reader_settings:readSetting("back_in_reader") == "previous_read_page" then
-- As multiple modules will have their :onPageUpdate()/... called,
-- and some of them will set up the new page with it, we need to
-- delay our handling after all of them are called (otherwise,
-- depending on the order of the calls, we may be have the location
-- of either the previous page or the current one).
UIManager:nextTick(self._addPreviousLocationToStackCallback)
end
self.back_resist = nil
end
function ReaderBack:onBack()
local location_stack = self.location_stack
-- Hook to events that do/may change page/view (more than one of these events
-- may be sent on a single page turn/scroll, _addPreviousLocationToStack()
-- will ignore those for the same book location):
-- Called after initial page is set up
ReaderBack.onReaderReady = ReaderBack._onViewPossiblyUpdated
-- New page on paged media or crengine in page mode
ReaderBack.onPageUpdate = ReaderBack._onViewPossiblyUpdated
-- New page on crengine in scroll mode
ReaderBack.onPosUpdate = ReaderBack._onViewPossiblyUpdated
-- View updated (possibly on the same page) on paged media
ReaderBack.onViewRecalculate = ReaderBack._onViewPossiblyUpdated
-- View updated (possibly on the same page) on paged media (needed in Reflow mode)
ReaderBack.onPagePositionUpdated = ReaderBack._onViewPossiblyUpdated
if self.disabled then
self.ui:handleEvent(Event:new("Close"))
elseif #location_stack > 1 then
local saved_location = table.remove(location_stack)
function ReaderBack:onBack()
local back_in_reader = G_reader_settings:readSetting("back_in_reader", "previous_location")
local back_to_exit = G_reader_settings:readSetting("back_to_exit", "prompt")
if back_in_reader == "previous_read_page" then
if #self.location_stack > 0 then
local saved_location = table.remove(self.location_stack)
if saved_location then
-- Reset self.cur_location, which will be updated with the restored
-- saved_location, which will then not be added to the stack
self.cur_location = nil
logger.dbg("[ReaderBack] restoring:", saved_location)
self.ui:handleEvent(Event:new('RestoreBookLocation', saved_location))
-- Ensure we always have self.cur_location updated, as in some
-- cases (same page), no event that we handle might be sent.
UIManager:nextTick(self._addPreviousLocationToStackCallback)
return true
end
elseif not self.back_resist or back_to_exit == "disable" then
-- Show a one time notification when location stack is empty.
-- On next "Back" only, proceed with the default behaviour (unless
-- it's disabled, in which case we always show this notification)
self.back_resist = true
UIManager:show(Notification:new{
text = _("Location history is empty."),
})
if saved_location then
ignore_location = self:_getCurrentLocation()
logger.dbg("[ReaderBack] restoring:", saved_location)
self.ui:handleEvent(Event:new('RestoreBookLocation', saved_location))
return true
else
self.back_resist = nil
end
elseif back_in_reader == "previous_location" then
-- ReaderLink maintains its own location_stack of less frequent jumps
-- (links or TOC entries followed, skim document...)
if back_to_exit == "disable" then
-- Let ReaderLink always show its notification if empty
self.ui.link:onGoBackLink(true) -- show_notification_if_empty=true
return true
end
if self.back_resist then
-- Notification "Location history is empty" previously shown by ReaderLink
self.back_resist = nil
elseif self.ui.link:onGoBackLink(true) then -- show_notification_if_empty=true
return true -- some location restored
else
-- ReaderLink has shown its notification that location stack is empty.
-- On next "Back" only, proceed with the default behaviour
self.back_resist = true
return true
end
elseif back_in_reader == "filebrowser" then
else
logger.dbg("[ReaderBack] no location history, closing")
self.ui:handleEvent(Event:new("Home"))
-- Filebrowser will handle next "Back" and ensure back_to_exit
return true
end
-- location stack empty, or back_in_reader == "default"
if back_to_exit == "always" then
self.ui:handleEvent(Event:new("Close"))
elseif back_to_exit == "disable" then
return true
elseif back_to_exit == "prompt" then
UIManager:show(ConfirmBox:new{
text = _("Exit KOReader?"),
ok_text = _("Exit"),
ok_callback = function()
self.ui:handleEvent(Event:new("Close"))
end
})
end
return true
end
return ReaderBack

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,22 @@ function ReaderConfig:init()
end
self.configurable:loadDefaults(self.options)
self:registerKeyEvents()
self:initGesListener()
if G_reader_settings:has("activate_menu") then
self.activation_menu = G_reader_settings:readSetting("activate_menu")
else
self.activation_menu = "swipe_tap"
end
-- delegate gesture listener to ReaderUI, NOP our own
self.ges_events = nil
end
function ReaderConfig:onGesture() end
function ReaderConfig:registerKeyEvents()
if not self.dimen then self.dimen = Geom:new{} end
if Device:hasKeys() then
self.key_events.ShowConfigMenu = { { { "Press", "AA" } } }
self.key_events = {
ShowConfigMenu = { {{"Press","AA"}}, doc = "show config dialog" },
}
end
if Device:isTouchDevice() then
self:initGesListener()
end
self.activation_menu = G_reader_settings:readSetting("activate_menu")
if self.activation_menu == nil then
self.activation_menu = "swipe_tap"
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",
@ -60,18 +50,6 @@ function ReaderConfig:initGesListener()
},
handler = function() return self:onTapShowConfigMenu() end,
},
{
id = "readerconfigmenu_ext_tap",
ges = "tap",
screen_zone = {
ratio_x = DTAP_ZONE_CONFIG_EXT.x, ratio_y = DTAP_ZONE_CONFIG_EXT.y,
ratio_w = DTAP_ZONE_CONFIG_EXT.w, ratio_h = DTAP_ZONE_CONFIG_EXT.h,
},
overrides = {
"readerconfigmenu_tap",
},
handler = function() return self:onTapShowConfigMenu() end,
},
{
id = "readerconfigmenu_swipe",
ges = "swipe",
@ -85,18 +63,6 @@ function ReaderConfig:initGesListener()
},
handler = function(ges) return self:onSwipeShowConfigMenu(ges) end,
},
{
id = "readerconfigmenu_ext_swipe",
ges = "swipe",
screen_zone = {
ratio_x = DTAP_ZONE_CONFIG_EXT.x, ratio_y = DTAP_ZONE_CONFIG_EXT.y,
ratio_w = DTAP_ZONE_CONFIG_EXT.w, ratio_h = DTAP_ZONE_CONFIG_EXT.h,
},
overrides = {
"readerconfigmenu_swipe",
},
handler = function(ges) return self:onSwipeShowConfigMenu(ges) end,
},
{
id = "readerconfigmenu_pan",
ges = "pan",
@ -110,36 +76,23 @@ function ReaderConfig:initGesListener()
},
handler = function(ges) return self:onSwipeShowConfigMenu(ges) end,
},
{
id = "readerconfigmenu_ext_pan",
ges = "pan",
screen_zone = {
ratio_x = DTAP_ZONE_CONFIG_EXT.x, ratio_y = DTAP_ZONE_CONFIG_EXT.y,
ratio_w = DTAP_ZONE_CONFIG_EXT.w, ratio_h = DTAP_ZONE_CONFIG_EXT.h,
},
overrides = {
"readerconfigmenu_pan",
},
handler = function(ges) return self:onSwipeShowConfigMenu(ges) end,
},
})
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"))
-- show last used panel when opening config dialog
self.config_dialog:onShowConfigPanel(self.last_panel_index)
UIManager:show(self.config_dialog)
self.ui:handleEvent(Event:new("HandledAsSwipe")) -- cancel any pan scroll made
return true
end
@ -158,11 +111,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

@ -1,462 +1,27 @@
local Event = require("ui/event")
local Device = require("device")
local EventListener = require("ui/widget/eventlistener")
local Geom = require("ui/geometry")
local InfoMessage = require("ui/widget/infomessage")
local UIManager = require("ui/uimanager")
local logger = require("logger")
local T = require("ffi/util").template
local _ = require("gettext")
local ReaderCoptListener = EventListener:extend{}
local CRE_HEADER_DEFAULT_SIZE = 20
local ReaderCoptListener = EventListener:new{}
function ReaderCoptListener:onReadSettings(config)
local view_mode_name = self.document.configurable.view_mode == 0 and "page" or "scroll"
-- Let crengine know of the view mode before rendering, as it can
-- cause a rendering change (2-pages would become 1-page in
-- scroll mode).
self.document:setViewMode(view_mode_name)
-- ReaderView is the holder of the view_mode state
self.view.view_mode = view_mode_name
-- crengine top status bar can only show author and title together
self.title = G_reader_settings:readSetting("cre_header_title", 1)
self.clock = G_reader_settings:readSetting("cre_header_clock", 1)
self.header_auto_refresh = G_reader_settings:readSetting("cre_header_auto_refresh", 1)
self.page_number = G_reader_settings:readSetting("cre_header_page_number", 1)
self.page_count = G_reader_settings:readSetting("cre_header_page_count", 1)
self.reading_percent = G_reader_settings:readSetting("cre_header_reading_percent", 0)
self.battery = G_reader_settings:readSetting("cre_header_battery", 1)
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: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))
self.old_battery_level = self.ui.rolling:updateBatteryState()
-- Have this ready in case auto-refresh is enabled, now or later
self.headerRefresh = function()
-- Only draw it if the header is shown...
if self.document.configurable.status_line == 0 and self.view.view_mode == "page" then
-- ...and something has changed
local new_battery_level = self.ui.rolling:updateBatteryState()
if self.clock == 1 or (self.battery == 1 and new_battery_level ~= self.old_battery_level) then
self.old_battery_level = new_battery_level
self:updateHeader()
end
end
self:rescheduleHeaderRefreshIfNeeded() -- schedule (or not) next refresh
local view_mode = config:readSetting("copt_view_mode") or
G_reader_settings:readSetting("copt_view_mode")
if view_mode == 0 then
self.ui:registerPostReadyCallback(function()
self.view:onSetViewMode("page")
end)
elseif view_mode == 1 then
self.ui:registerPostReadyCallback(function()
self.view:onSetViewMode("scroll")
end)
end
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
local status_line = config:readSetting("copt_status_line") or G_reader_settings:readSetting("copt_status_line") or 1
self.ui:handleEvent(Event:new("SetStatusLine", status_line, true))
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: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)
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
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
-- Force a refresh if we're not hidden behind another widget
if self:shouldHeaderBeRepainted() then
UIManager:setDirty(self.view.dialog, "ui",
Geom:new{
x = 0, y = 0,
w = Device.screen:getWidth(),
h = self.document:getHeaderHeight(),
}
)
end
end
function ReaderCoptListener:unscheduleHeaderRefresh()
if not self.headerRefresh then return end -- not yet set up
UIManager:unschedule(self.headerRefresh)
logger.dbg("ReaderCoptListener.headerRefresh unscheduled")
end
function ReaderCoptListener:rescheduleHeaderRefreshIfNeeded()
if not self.headerRefresh then return end -- not yet set up
local unscheduled = UIManager:unschedule(self.headerRefresh) -- unschedule if already scheduled
-- Only schedule an update if the header is actually visible
if self.header_auto_refresh == 1
and self.document.configurable.status_line == 0 -- top bar enabled
and self.view.view_mode == "page" -- not in scroll mode (which would disable the header)
and (self.clock == 1 or self.battery == 1) then -- something shown can change in next minute
UIManager:scheduleIn(61 - tonumber(os.date("%S")), self.headerRefresh)
if not unscheduled then
logger.dbg("ReaderCoptListener.headerRefresh scheduled")
else
logger.dbg("ReaderCoptListener.headerRefresh rescheduled")
end
elseif unscheduled then
logger.dbg("ReaderCoptListener.headerRefresh unscheduled")
end
end
-- Schedule or stop scheduling on these events, as they may change what is shown:
ReaderCoptListener.onSetStatusLine = ReaderCoptListener.rescheduleHeaderRefreshIfNeeded
-- configurable.status_line is set before this event is triggered
ReaderCoptListener.onSetViewMode = ReaderCoptListener.rescheduleHeaderRefreshIfNeeded
-- ReaderView:onSetViewMode(), which sets view.view_mode, is called before
-- ReaderCoptListener.onSetViewMode, so we'll get the updated value
function ReaderCoptListener:onResume()
-- Don't repaint the header until OutOfScreenSaver if screensaver_delay is enabled...
local screensaver_delay = G_reader_settings:readSetting("screensaver_delay")
if screensaver_delay and screensaver_delay ~= "disable" then
self._delayed_screensaver = true
return
end
self:headerRefresh()
end
function ReaderCoptListener:onOutOfScreenSaver()
if not self._delayed_screensaver then
return
end
self._delayed_screensaver = nil
self:headerRefresh()
end
-- Unschedule on these events
ReaderCoptListener.onCloseDocument = ReaderCoptListener.unscheduleHeaderRefresh
ReaderCoptListener.onSuspend = ReaderCoptListener.unscheduleHeaderRefresh
function ReaderCoptListener:setAndSave(setting, property, value)
self.document._document:setIntProperty(property, value)
G_reader_settings:saveSetting(setting, value)
self.page_info_override = self.page_number == 1 or self.page_count == 1 or self.reading_percent == 1
self.document:setPageInfoOverride("")
-- Have crengine redraw it (even if hidden by the menu at this time)
self.ui.rolling:updateBatteryState()
self:updateHeader()
-- And see if we should auto-refresh
self:rescheduleHeaderRefreshIfNeeded()
end
local about_text = _([[
In CRE documents, an alternative status bar can be displayed at the top of the screen, with or without the regular bottom status bar.
Enabling this alt status bar, per document or by default, can be done in the bottom menu.
The alternative status bar can be configured here.]])
function ReaderCoptListener:getAltStatusBarMenu()
return {
text = _("Alt status bar"),
separator = true,
sub_item_table = {
{
text = _("About alt status bar"),
keep_menu_open = true,
callback = function()
UIManager:show(InfoMessage:new{
text = about_text,
})
end,
separator = true,
},
{
text = _("Auto refresh"),
checked_func = function()
return self.header_auto_refresh == 1
end,
callback = function()
self.header_auto_refresh = self.header_auto_refresh == 0 and 1 or 0
G_reader_settings:saveSetting("cre_header_auto_refresh", self.header_auto_refresh)
self:rescheduleHeaderRefreshIfNeeded()
end,
separator = true
},
{
text = _("Book author and title"),
checked_func = function()
return self.title == 1
end,
callback = function()
self.title = self.title == 0 and 1 or 0
self:setAndSave("cre_header_title", "window.status.title", self.title)
end,
},
{
text = _("Current time"),
checked_func = function()
return self.clock == 1
end,
callback = function()
self.clock = self.clock == 0 and 1 or 0
self:setAndSave("cre_header_clock", "window.status.clock", self.clock)
end,
},
{
text = _("Current page"),
checked_func = function()
return self.page_number == 1
end,
callback = function()
self.page_number = self.page_number == 0 and 1 or 0
self:setAndSave("cre_header_page_number", "window.status.pos.page.number", self.page_number)
end,
},
{
text = _("Total pages"),
checked_func = function()
return self.page_count == 1
end,
callback = function()
self.page_count = self.page_count == 0 and 1 or 0
self:setAndSave("cre_header_page_count", "window.status.pos.page.count", self.page_count)
end,
},
{
text = _("Progress percentage"),
checked_func = function()
return self.reading_percent == 1
end,
callback = function()
self.reading_percent = self.reading_percent == 0 and 1 or 0
self:setAndSave("cre_header_reading_percent", "window.status.pos.percent", self.reading_percent)
end,
},
{
text = _("Chapter marks"),
checked_func = function()
return self.chapter_marks == 1
end,
callback = function()
self.chapter_marks = self.chapter_marks == 0 and 1 or 0
self:setAndSave("cre_header_chapter_marks", "crengine.page.header.chapter.marks", self.chapter_marks)
end,
},
{
text_func = function()
local status = _("Battery status")
if self.battery == 1 then
if self.battery_percent == 1 then
status = _("Battery status: percentage")
else
status = _("Battery status: icon")
end
end
return status
end,
checked_func = function()
return self.battery == 1
end,
sub_item_table = {
{
text = _("Battery icon"),
checked_func = function()
return self.battery == 1 and self.battery_percent == 0
end,
callback = function()
if self.battery == 0 then -- self.battery_percent don't care
self.battery = 1
self.battery_percent = 0
elseif self.battery == 1 and self.battery_percent == 1 then
self.battery = 1
self.battery_percent = 0
else
self.battery = 0
self.battery_percent = 0
end
self:setAndSave("cre_header_battery", "window.status.battery", self.battery)
self:setAndSave("cre_header_battery_percent", "window.status.battery.percent", self.battery_percent)
end,
},
{
text = _("Battery percentage"),
checked_func = function()
return self.battery == 1 and self.battery_percent == 1
end,
callback = function()
if self.battery == 0 then -- self.battery_percent don't care
self.battery = 1
self.battery_percent = 1
elseif self.battery == 1 and self.battery_percent == 0 then
self.battery_percent = 1
else
self.battery = 0
self.battery_percent = 0
end
self:setAndSave("cre_header_battery", "window.status.battery", self.battery)
self:setAndSave("cre_header_battery_percent", "window.status.battery.percent", self.battery_percent)
end,
},
},
separator = true,
},
{
text_func = function()
return T(_("Font size: %1"), G_reader_settings:readSetting("cre_header_status_font_size", CRE_HEADER_DEFAULT_SIZE))
end,
callback = function()
local SpinWidget = require("ui/widget/spinwidget")
local start_size = G_reader_settings:readSetting("cre_header_status_font_size", CRE_HEADER_DEFAULT_SIZE)
local size_spinner = SpinWidget:new{
value = start_size,
value_min = 8,
value_max = 36,
default_value = 14,
keep_shown_on_apply = true,
title_text = _("Size of top status bar"),
ok_text = _("Set size"),
callback = function(spin)
self:setAndSave("cre_header_status_font_size", "crengine.page.header.font.size", spin.value)
-- This will probably needs a re-rendering, so make sure it happens now.
self.ui:handleEvent(Event:new("UpdatePos"))
end
}
UIManager:show(size_spinner)
end,
},
},
}
function ReaderCoptListener:onSetFontSize(font_size)
self.document.configurable.font_size = font_size
end
return ReaderCoptListener

@ -1,23 +1,83 @@
local BBoxWidget = require("ui/widget/bboxwidget")
local Blitbuffer = require("ffi/blitbuffer")
local ButtonTable = require("ui/widget/buttontable")
local CenterContainer = require("ui/widget/container/centercontainer")
local Button = require("ui/widget/button")
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 HorizontalGroup = require("ui/widget/horizontalgroup")
local HorizontalSpan = require("ui/widget/horizontalspan")
local LeftContainer = require("ui/widget/container/leftcontainer")
local Math = require("optmath")
local RightContainer = require("ui/widget/container/rightcontainer")
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 Screen = require("device").screen
local _ = require("gettext")
local ReaderCropping = WidgetContainer:extend{}
local PageCropDialog = VerticalGroup:new{
ok_text = _("OK"),
cancel_text = _("Cancel"),
ok_callback = function() end,
cancel_callback = function() end,
button_width = math.floor(Screen:scaleBySize(70)),
}
function PageCropDialog:init()
local horizontal_group = HorizontalGroup:new{}
local ok_button = Button:new{
text = self.ok_text,
callback = self.ok_callback,
width = self.button_width,
text_font_face = "cfont",
text_font_size = 20,
show_parent = self,
}
local cancel_button = Button:new{
text = self.cancel_text,
callback = self.cancel_callback,
width = self.button_width,
text_font_face = "cfont",
text_font_size = 20,
show_parent = self,
}
local ok_container = RightContainer:new{
dimen = Geom:new{ w = math.floor(Screen:getWidth()*0.33), h = math.floor(Screen:getHeight()/12)},
ok_button,
}
local cancel_container = LeftContainer:new{
dimen = Geom:new{ w = math.floor(Screen:getWidth()*0.33), h = math.floor(Screen:getHeight()/12)},
cancel_button,
}
table.insert(horizontal_group, ok_container)
table.insert(horizontal_group, HorizontalSpan:new{ width = math.floor(Screen:getWidth()*0.34)})
table.insert(horizontal_group, cancel_container)
self[2] = FrameContainer:new{
horizontal_group,
background = Blitbuffer.COLOR_WHITE,
bordersize = 0,
padding = 0,
}
end
function PageCropDialog:onCloseWidget()
UIManager:setDirty(nil, function()
return "partial", self[1].dimen:combine(self[2].dimen)
end)
return true
end
function PageCropDialog:onShow()
UIManager:setDirty(self, function()
return "ui", self[1].dimen:combine(self[2].dimen)
end)
return true
end
local ReaderCropping = InputContainer:new{}
function ReaderCropping:onPageCrop(mode)
self.ui:handleEvent(Event:new("CloseConfigMenu"))
-- backup original zoom mode as cropping use "page" zoom mode
self.orig_zoom_mode = self.view.zoom_mode
if mode == "auto" then
@ -26,6 +86,9 @@ function ReaderCropping:onPageCrop(mode)
end
return
elseif mode == "none" then
if self.document.configurable.text_wrap ~= 1 then
self.ui:handleEvent(Event:new("SetZoomMode", "pagewidth", "cropping"))
end
return
end
-- backup original view dimen
@ -33,9 +96,6 @@ function ReaderCropping:onPageCrop(mode)
-- backup original view bgcolor
self.orig_view_bgcolor = self.view.outer_page_color
self.view.outer_page_color = Blitbuffer.COLOR_DARK_GRAY
-- backup original footer visibility
self.orig_view_footer_visibility = self.view.footer_visible
self.view.footer_visible = false
-- backup original page scroll
self.orig_page_scroll = self.view.page_scroll
self.view.page_scroll = false
@ -49,61 +109,21 @@ function ReaderCropping:onPageCrop(mode)
-- mode, just force readerview to recalculate visible_area
self.view:recalculate()
else
self.ui:handleEvent(Event:new("SetZoomMode", "page"))
end
-- prepare bottom buttons so we know the size available for the page above it
local button_table = ButtonTable:new{
width = Screen:getWidth(),
buttons = {{
{
text = _("Cancel"),
callback = function() self:onCancelPageCrop() end,
},
{
text = _("Apply crop"),
callback = function() self:onConfirmPageCrop() end,
},
}},
zero_sep = true,
show_parent = self,
}
local button_container = FrameContainer:new{
margin = 0,
bordersize = 0,
padding = 0,
background = Blitbuffer.COLOR_WHITE,
CenterContainer:new{
dimen = Geom:new{
w = Screen:getWidth(),
h = button_table:getSize().h,
},
button_table,
}
}
-- height available for page
local page_container_h = Screen:getHeight()
if Device:isTouchDevice() then
-- non-touch devices do not need cancel and apply buttons
page_container_h = page_container_h - button_table:getSize().h
self.ui:handleEvent(Event:new("SetZoomMode", "page", "cropping"))
end
local page_dimen = Geom:new{
w = Screen:getWidth(),
h = page_container_h,
}
-- resize document view to the available size
self.ui:handleEvent(Event:new("SetDimensions", page_dimen))
-- finalize crop dialog
self.ui:handleEvent(Event:new("SetDimensions",
Geom:new{w = Screen:getWidth(), h = math.floor(Screen:getHeight()*11/12)})
)
self.bbox_widget = BBoxWidget:new{
crop = self,
ui = self.ui,
view = self.view,
document = self.document,
}
self.crop_dialog = VerticalGroup:new{
align = "left",
self.crop_dialog = PageCropDialog:new{
self.bbox_widget,
(Device:isTouchDevice() and button_container) or nil, -- button bar only availble for touch devices
ok_callback = function() self:onConfirmPageCrop() end,
cancel_callback = function() self:onCancelPageCrop() end,
}
UIManager:show(self.crop_dialog)
return true
@ -132,8 +152,6 @@ function ReaderCropping:exitPageCrop(confirmed)
self.ui:handleEvent(Event:new("RestoreHinting"))
-- restore page scroll
self.view.page_scroll = self.orig_page_scroll
-- restore footer visibility
self.view.footer_visible = self.orig_view_footer_visibility
-- restore view bgcolor
self.view.outer_page_color = self.orig_view_bgcolor
-- restore reflow mode
@ -151,11 +169,11 @@ end
function ReaderCropping:setCropZoomMode(confirmed)
if confirmed then
-- if original zoom mode is "page???", set zoom mode to "content???"
local zoom_mode_type = self.orig_zoom_mode:match("page(.*)")
self:setZoomMode(zoom_mode_type
and "content"..zoom_mode_type
or self.orig_zoom_mode)
-- if original zoom mode is not "content", set zoom mode to "contentwidth"
self:setZoomMode(
self.orig_zoom_mode:find("content")
and self.orig_zoom_mode
or "contentwidth")
self.ui:handleEvent(Event:new("InitScrollPageStates"))
else
self:setZoomMode(self.orig_zoom_mode)
@ -167,9 +185,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()

@ -1,351 +1,125 @@
local ConfirmBox = require("ui/widget/confirmbox")
local ButtonDialogTitle = require("ui/widget/buttondialogtitle")
local Device = require("device")
local Event = require("ui/event")
local InfoMessage = require("ui/widget/infomessage")
local SpinWidget = require("ui/widget/spinwidget")
local Font = require("ui/font")
local InputContainer = require("ui/widget/container/inputcontainer")
local Screen = Device.screen
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{
battery_confirm_box = nil,
memory_confirm_box = nil,
local ReaderDeviceStatus = InputContainer:new{
}
function ReaderDeviceStatus:init()
if Device:hasBattery() then
self.battery_interval_m = G_reader_settings:readSetting("device_status_battery_interval_minutes", 10)
self.battery_threshold = G_reader_settings:readSetting("device_status_battery_threshold", 20)
self.battery_threshold_high = G_reader_settings:readSetting("device_status_battery_threshold_high", 100)
-- `checkLowBatteryLevel` and `checkHighMemoryUsage` are each supposed to start one second past the top of the minute,
-- as some other periodic activities do (e.g. footer). This means that the processor is woken up less often from standby.
self.checkLowBatteryLevel = function(sync)
local is_charging = powerd:isCharging()
if powerd:getCapacity() > 0 or powerd:isCharging() then
self.checkLowBattery = function()
local threshold = G_reader_settings:readSetting("low_battery_threshold") or 20
local battery_capacity = powerd:getCapacity()
if powerd:getDismissBatteryStatus() == true then -- alerts dismissed
if (is_charging and battery_capacity <= self.battery_threshold_high) or
(not is_charging and battery_capacity > self.battery_threshold) then
powerd:setDismissBatteryStatus(false)
end
else
if (is_charging and battery_capacity > self.battery_threshold_high) or
(not is_charging and battery_capacity <= self.battery_threshold) then
if self.battery_confirm_box then
UIManager:close(self.battery_confirm_box)
end
self.battery_confirm_box = ConfirmBox:new {
text = is_charging and T(_("High battery level: %1 %\n\nDismiss battery level alert?"), battery_capacity)
or T(_("Low battery level: %1 %\n\nDismiss battery level alert?"), battery_capacity),
ok_text = _("Dismiss"),
dismissable = false,
ok_callback = function()
powerd:setDismissBatteryStatus(true)
end,
if powerd:isCharging() then
powerd:setDissmisBatteryStatus(false)
elseif powerd:getDissmisBatteryStatus() ~= true and battery_capacity <= threshold then
local low_battery_info
low_battery_info = ButtonDialogTitle:new {
modal = true,
title = T(_("The battery is getting low.\n%1% remaining."), battery_capacity),
title_align = "center",
title_face = Font:getFace("infofont"),
dismissable = false,
buttons = {
{
{
text = _("Dismiss"),
callback = function()
UIManager:close(low_battery_info)
powerd:setDissmisBatteryStatus(true)
UIManager:scheduleIn(300, self.checkLowBattery)
end,
},
},
}
UIManager:show(self.battery_confirm_box)
end
}
UIManager:show(low_battery_info)
return
elseif powerd:getDissmisBatteryStatus() and battery_capacity > threshold then
powerd:setDissmisBatteryStatus(false)
end
local offset = sync and (os.date("%S") - 1) or 0
UIManager:scheduleIn(self.battery_interval_m * 60 - offset, self.checkLowBatteryLevel)
UIManager:scheduleIn(300, self.checkLowBattery)
end
self.ui.menu:registerToMainMenu(self)
self:startBatteryChecker()
else
self.checkLowBattery = nil
end
if not Device:isAndroid() then
self.memory_interval_m = G_reader_settings:readSetting("device_status_memory_interval_minutes", 5)
self.memory_threshold = G_reader_settings:readSetting("device_status_memory_threshold", 100)
-- `checkLowBatteryLevel` and `checkHighMemoryUsage` are each supposed to start one second past the top of the minute,
-- as some other periodic activities do (e.g. footer). This means that the processor is woken up less often from standby.
self.checkHighMemoryUsage = function(sync)
local statm = io.open("/proc/self/statm", "r")
if statm then
local dummy, rss = statm:read("*number", "*number")
statm:close()
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"
and G_reader_settings:isTrue("device_status_memory_auto_restart") then
UIManager:show(InfoMessage:new{
text = _("High memory usage!\n\nKOReader is restarting…"),
icon = "notice-warning",
})
UIManager:nextTick(function()
self.ui:handleEvent(Event:new("Restart"))
end)
else
self.memory_confirm_box = ConfirmBox:new {
text = T(_("High memory usage: %1 MB\n\nRestart KOReader?"), rss),
ok_text = _("Restart"),
dismissable = false,
ok_callback = function()
UIManager:show(InfoMessage:new{
text = _("High memory usage!\n\nKOReader is restarting…"),
icon = "notice-warning",
})
UIManager:nextTick(function()
self.ui:handleEvent(Event:new("Restart"))
end)
end,
}
UIManager:show(self.memory_confirm_box)
end
else
self.memory_confirm_box = ConfirmBox:new {
text = T(_("High memory usage: %1 MB\n\nExit KOReader?"), rss),
ok_text = _("Exit"),
dismissable = false,
ok_callback = function()
self.ui:handleEvent(Event:new("Exit"))
end,
}
UIManager:show(self.memory_confirm_box)
end
end
end
local offset = sync and (os.date("%S") - 1) or 0
UIManager:scheduleIn(self.memory_interval_m * 60 - offset, self.checkHighMemoryUsage)
end
self:startMemoryChecker()
end
self.ui.menu:registerToMainMenu(self)
end
end
function ReaderDeviceStatus:addToMainMenu(menu_items)
menu_items.device_status_alarm = {
text = _("Device status alerts"),
sub_item_table = {},
}
if Device:hasBattery() then
table.insert(menu_items.device_status_alarm.sub_item_table,
menu_items.battery = {
text = _("Low battery alarm"),
sub_item_table = {
{
text = _("Battery level"),
text = _("Enable"),
checked_func = function()
return G_reader_settings:isTrue("device_status_battery_alarm")
return G_reader_settings:nilOrTrue("battery_alarm")
end,
callback = function()
G_reader_settings:flipNilOrFalse("device_status_battery_alarm")
if G_reader_settings:isTrue("device_status_battery_alarm") then
self:startBatteryChecker(true)
G_reader_settings:flipNilOrTrue("battery_alarm")
if G_reader_settings:nilOrTrue("battery_alarm") then
self:startBatteryChecker()
else
self:stopBatteryChecker()
powerd:setDismissBatteryStatus(false)
powerd:setDissmisBatteryStatus(false)
end
end,
})
table.insert(menu_items.device_status_alarm.sub_item_table,
},
{
text_func = function()
return T(_("Check interval: %1 min"), self.battery_interval_m)
end,
enabled_func = function()
return G_reader_settings:isTrue("device_status_battery_alarm")
end,
keep_menu_open = true,
callback = function(touchmenu_instance)
UIManager:show(SpinWidget:new{
value = self.battery_interval_m,
value_min = 1,
value_max = 60,
default_value = 10,
unit = C_("Time", "min"),
value_hold_step = 5,
title_text = _("Battery check interval"),
callback = function(spin)
self.battery_interval_m = spin.value
G_reader_settings:saveSetting("device_status_battery_interval_minutes", self.battery_interval_m)
touchmenu_instance:updateItems()
powerd:setDismissBatteryStatus(false)
self:stopBatteryChecker()
-- schedule first check on a full minute to reduce wakeups from standby)
UIManager:scheduleIn(self.battery_interval_m * 60 - os.date("%S") + 1,
self.checkLowBatteryLevel)
end,
})
end,
})
table.insert(menu_items.device_status_alarm.sub_item_table,
{
text_func = function()
return T(_("Thresholds: %1 % / %2 %"), self.battery_threshold, self.battery_threshold_high)
end,
enabled_func = function()
return G_reader_settings:isTrue("device_status_battery_alarm")
end,
keep_menu_open = true,
callback = function(touchmenu_instance)
local DoubleSpinWidget = require("/ui/widget/doublespinwidget")
local thresholds_widget
thresholds_widget = DoubleSpinWidget:new{
title_text = _("Battery level alert thresholds"),
info_text = _([[
Low level threshold is checked when the device is not charging.
High level threshold is checked when the device is charging.]]),
left_text = _("Low"),
left_value = self.battery_threshold,
left_min = 1,
left_max = self.battery_threshold_high,
left_default = 20,
left_hold_step = 5,
right_text = _("High"),
right_value = self.battery_threshold_high,
right_min = self.battery_threshold,
right_max = 100,
right_default = 100,
right_hold_step = 5,
unit = "%",
callback = function(left_value, right_value)
self.battery_threshold = left_value
self.battery_threshold_high = right_value
G_reader_settings:saveSetting("device_status_battery_threshold", self.battery_threshold)
G_reader_settings:saveSetting("device_status_battery_threshold_high", self.battery_threshold_high)
touchmenu_instance:updateItems()
powerd:setDismissBatteryStatus(false)
end,
}
UIManager:show(thresholds_widget)
end,
separator = true,
})
end
if not Device:isAndroid() then
table.insert(menu_items.device_status_alarm.sub_item_table,
{
text = _("High memory usage"),
checked_func = function()
return G_reader_settings:isTrue("device_status_memory_alarm")
end,
text = _("Low battery threshold"),
enabled_func = function() return G_reader_settings:nilOrTrue("battery_alarm") end,
callback = function()
G_reader_settings:flipNilOrFalse("device_status_memory_alarm")
if G_reader_settings:isTrue("device_status_memory_alarm") then
self:startMemoryChecker(true)
else
self:stopMemoryChecker()
end
end,
})
table.insert(menu_items.device_status_alarm.sub_item_table,
{
text_func = function()
return T(_("Check interval: %1 min"), self.memory_interval_m)
end,
enabled_func = function()
return G_reader_settings:isTrue("device_status_memory_alarm")
end,
keep_menu_open = true,
callback = function(touchmenu_instance)
UIManager:show(SpinWidget:new{
value = self.memory_interval_m,
value_min = 1,
value_max = 60,
default_value = 5,
unit = C_("Time", "min"),
value_hold_step = 5,
title_text = _("Memory check interval"),
callback = function(spin)
self.memory_interval_m = spin.value
G_reader_settings:saveSetting("device_status_memory_interval_minutes", self.memory_interval_m)
touchmenu_instance:updateItems()
self:stopMemoryChecker()
-- schedule first check on a full minute to reduce wakeups from standby)
UIManager:scheduleIn(self.memory_interval_m * 60 - os.date("%S") + 1,
self.checkHighMemoryUsage)
end,
})
end,
})
table.insert(menu_items.device_status_alarm.sub_item_table,
{
text_func = function()
return T(_("Threshold: %1 MB"), self.memory_threshold)
end,
enabled_func = function()
return G_reader_settings:isTrue("device_status_memory_alarm")
end,
keep_menu_open = true,
callback = function(touchmenu_instance)
UIManager:show(SpinWidget:new{
value = self.memory_threshold,
value_min = 20,
value_max = 500,
default_value = 100,
unit = C_("Data storage size", "MB"),
value_step = 5,
local SpinWidget = require("ui/widget/spinwidget")
local curr_items = G_reader_settings:readSetting("low_battery_threshold") or 20
local battery_spin = SpinWidget:new {
width = math.floor(Screen:getWidth() * 0.6),
value = curr_items,
value_min = 5,
value_max = 90,
value_hold_step = 10,
title_text = _("Memory alert threshold"),
callback = function(spin)
self.memory_threshold = spin.value
G_reader_settings:saveSetting("device_status_memory_threshold", self.memory_threshold)
touchmenu_instance:updateItems()
end,
})
end,
})
table.insert(menu_items.device_status_alarm.sub_item_table,
{
text = _("Automatic restart"),
enabled_func = function()
return G_reader_settings:isTrue("device_status_memory_alarm") and Device:canRestart()
end,
checked_func = function()
return G_reader_settings:isTrue("device_status_memory_auto_restart")
end,
callback = function()
G_reader_settings:flipNilOrFalse("device_status_memory_auto_restart")
ok_text = _("Set threshold"),
title_text = _("Low battery threshold"),
callback = function(battery_spin)
G_reader_settings:saveSetting("low_battery_threshold", battery_spin.value)
powerd:setDissmisBatteryStatus(false)
end
}
UIManager:show(battery_spin)
end,
})
end
},
},
}
end
-- `checkLowBatteryLevel` and `checkHighMemoryUsage` are each supposed to start one second past the top of the minute,
-- as some other periodic activities do (e.g. footer). This means that the processor is woken up less often from standby.
function ReaderDeviceStatus:startBatteryChecker(sync)
if G_reader_settings:isTrue("device_status_battery_alarm") then
self.checkLowBatteryLevel(sync)
function ReaderDeviceStatus:startBatteryChecker()
if G_reader_settings:nilOrTrue("battery_alarm") and self.checkLowBattery then
self.checkLowBattery()
end
end
function ReaderDeviceStatus:stopBatteryChecker()
if self.checkLowBatteryLevel then
UIManager:unschedule(self.checkLowBatteryLevel)
end
end
-- `checkLowBatteryLevel` and `checkHighMemoryUsage` are each supposed to start one second past the top of the minute,
-- as some other periodic activities do (e.g. footer). This means that the processor is woken up less often from standby.
function ReaderDeviceStatus:startMemoryChecker(sync)
if G_reader_settings:isTrue("device_status_memory_alarm") then
self.checkHighMemoryUsage(sync)
end
end
function ReaderDeviceStatus:stopMemoryChecker()
if self.checkHighMemoryUsage then
UIManager:unschedule(self.checkHighMemoryUsage)
if self.checkLowBattery then
UIManager:unschedule(self.checkLowBattery)
end
end
function ReaderDeviceStatus:onResume()
self:startBatteryChecker(true)
self:startMemoryChecker(true)
self:startBatteryChecker()
end
function ReaderDeviceStatus:onSuspend()
self:stopBatteryChecker()
self:stopMemoryChecker()
end
function ReaderDeviceStatus:onCloseWidget()
self:stopBatteryChecker()
self:stopMemoryChecker()
end
return ReaderDeviceStatus

File diff suppressed because it is too large Load Diff

@ -1,14 +1,12 @@
local BD = require("ui/bidi")
local Device = require("device")
local Geom = require("ui/geometry")
local IconWidget = require("ui/widget/iconwidget")
local ImageWidget = require("ui/widget/imagewidget")
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,11 +15,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_max_size = math.ceil( math.min(Screen:getWidth(), Screen:getHeight()) / 32)
self.dogear_size = nil
self.dogear_y_offset = 0
self.top_pad = nil
self:setupDogear()
self:resetLayout()
end
@ -35,36 +30,37 @@ function ReaderDogear:setupDogear(new_dogear_size)
if self[1] then
self[1]:free()
end
self.top_pad = VerticalSpan:new{width = self.dogear_y_offset}
self.vgroup = VerticalGroup:new{
self.top_pad,
IconWidget:new{
icon = "dogear.alpha",
self[1] = RightContainer:new{
dimen = Geom:new{w = Screen:getWidth(), h = self.dogear_size},
ImageWidget:new{
file = "resources/icons/dogear.png",
rotation_angle = BD.mirroredUILayout() and 90 or 0,
width = self.dogear_size,
height = self.dogear_size,
alpha = true, -- Keep the alpha layer intact
}
}
self[1] = RightContainer:new{
dimen = Geom:new{w = Screen:getWidth(), h = self.dogear_y_offset + self.dogear_size},
self.vgroup
}
end
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
@ -74,42 +70,10 @@ function ReaderDogear:onSetPageMargins(margins)
-- top & right margins and be sure no text is hidden by the icon
-- (the provided margins are not scaled, so do as ReaderTypeset)
local margin = Screen:scaleBySize(math.max(margin_top, margin_right))
local new_dogear_size = math.min(self.dogear_max_size, math.max(self.dogear_min_size, margin))
local new_dogear_size = math.min(self.dogear_max_size, margin)
self:setupDogear(new_dogear_size)
end
function ReaderDogear:updateDogearOffset()
if not self.ui.rolling then
return
end
self.dogear_y_offset = 0
if self.view.view_mode == "page" then
self.dogear_y_offset = self.ui.document:getHeaderHeight()
end
-- Update components heights and positionnings
if self[1] then
self[1].dimen.h = self.dogear_y_offset + self.dogear_size
self.top_pad.width = self.dogear_y_offset
self.vgroup:resetLayout()
end
end
function ReaderDogear:onReaderReady()
self:updateDogearOffset()
end
function ReaderDogear:onDocumentRerendered()
-- Catching the top status bar toggling with :onSetStatusLine()
-- would be too early. But "DocumentRerendered" is sent after
-- it has been applied
self:updateDogearOffset()
end
function ReaderDogear:onChangeViewMode()
-- No top status bar when switching between page and scroll mode
self:updateDogearOffset()
end
function ReaderDogear:resetLayout()
local new_screen_width = Screen:getWidth()
if new_screen_width == self._last_screen_width then return end

@ -1,47 +1,20 @@
local Geom = require("ui/geometry")
local IconWidget = require("ui/widget/iconwidget")
local ImageWidget = require("ui/widget/imagewidget")
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{
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,
local widget = ImageWidget:new{
file = "resources/icons/appbar.book.open.png",
}
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 +27,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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -1,26 +1,30 @@
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 SkimToWidget = require("apps/reader/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{
goto_menu_title = _("Go to"),
skim_menu_title = _("Skim document"),
}
function ReaderGoto:init()
self.ui.menu:registerToMainMenu(self)
end
function ReaderGoto:addToMainMenu(menu_items)
-- insert goto command to main reader menu
menu_items.go_to = {
text = _("Go to page"),
text = self.goto_menu_title,
callback = function()
self:onShowGotoDialog()
end,
}
menu_items.skim_to = {
text = _("Skim document"),
text = self.skim_menu_title,
callback = function()
self:onShowSkimtoDialog()
end,
@ -28,7 +32,25 @@ function ReaderGoto:addToMainMenu(menu_items)
end
function ReaderGoto:onShowGotoDialog()
local curr_page = self.ui:getCurrentPage()
local dialog_title, goto_btn, curr_page
if self.document.info.has_pages then
dialog_title = _("Go to Page")
goto_btn = {
is_enter_default = true,
text = _("Page"),
callback = function() self:gotoPage() end,
}
curr_page = self.ui.paging.current_page
else
dialog_title = _("Go to Location")
goto_btn = {
is_enter_default = true,
text = _("Location"),
callback = function() self:gotoPage() end,
}
-- only CreDocument has this method
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),
@ -37,47 +59,36 @@ function ReaderGoto:onShowGotoDialog()
else
input_hint = T("@%1 (1 - %2)", curr_page, self.document:getPageCount())
end
input_hint = input_hint .. string.format(" %.2f%%", curr_page / self.document:getPageCount() * 100)
self.goto_dialog = InputDialog:new{
title = _("Enter page number or percentage"),
title = dialog_title,
input_hint = input_hint,
description = self.document:hasHiddenFlows() and
_([[
x for an absolute page number
[x] for a page number in the main (linear) flow
[x]y for a page number in the non-linear fragment y]])
or nil,
buttons = {
{
{
text = _("Skim"),
text = _("Cancel"),
enabled = true,
callback = function()
self:close()
self:onShowSkimtoDialog()
end,
},
{
text = _("Go to %"),
callback = function()
self:gotoPercent()
end,
}
},
{
{
text = _("Cancel"),
id = "close",
text = _("Skim mode"),
enabled = true,
callback = function()
self:close()
self.skimto = SkimToWidget:new{
document = self.document,
ui = self.ui,
callback_switch_to_goto = function()
UIManager:close(self.skimto)
self:onShowGotoDialog()
end,
}
UIManager:show(self.skimto)
end,
},
{
text = _("Go to page"),
is_enter_default = true,
callback = function()
self:gotoPage()
end,
}
goto_btn,
},
},
input_type = "number",
@ -123,86 +134,22 @@ function ReaderGoto:gotoPage()
end
end
self:close()
elseif self.ui.document:hasHiddenFlows() then
-- if there are hidden flows, we accept the syntax [x]y
-- for page number x in flow number y (y defaults to 0 if not present)
local flow
number, flow = string.match(page_number, "^ *%[(%d+)%](%d*) *$")
flow = tonumber(flow) or 0
number = tonumber(number)
if number then
if self.ui.document.flows[flow] ~= nil then
if number < 1 or number > self.ui.document:getTotalPagesInFlow(flow) then
return
end
local page = 0
-- in flow 0 (linear), we count pages skipping non-linear flows,
-- in a non-linear flow the target page is immediate
if flow == 0 then
for i=1, number do
page = self.ui.document:getNextPage(page)
end
else
page = self.ui.document:getFirstPageInFlow(flow) + number - 1
end
if page > 0 then
self.ui:handleEvent(Event:new("GotoPage", page))
self:close()
end
end
end
end
end
function ReaderGoto:gotoPercent()
local number = self.goto_dialog:getInputValue()
if number then
self.ui.link:addCurrentLocationToStack()
self.ui:handleEvent(Event:new("GotoPercent", number))
self:close()
end
end
function ReaderGoto:onGoToBeginning()
local new_page = self.ui.document:getNextPage(0)
if new_page then
self.ui.link:addCurrentLocationToStack()
self.ui:handleEvent(Event:new("GotoPage", new_page))
end
self.ui.link:addCurrentLocationToStack()
self.ui:handleEvent(Event:new("GotoPage", 1))
return true
end
function ReaderGoto:onGoToEnd()
local new_page = self.ui.document:getPrevPage(0)
if new_page then
local endpage = self.document:getPageCount()
if endpage then
self.ui.link:addCurrentLocationToStack()
self.ui:handleEvent(Event:new("GotoPage", new_page))
self.ui:handleEvent(Event:new("GotoPage", endpage))
end
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

@ -1,9 +1,7 @@
local EventListener = require("ui/widget/eventlistener")
local Event = require("ui/event")
local ReaderZooming = require("apps/reader/modules/readerzooming")
local UIManager = require("ui/uimanager")
local ReaderKoptListener = EventListener:extend{}
local ReaderKoptListener = EventListener:new{}
function ReaderKoptListener:setZoomMode(zoom_mode)
if self.document.configurable.text_wrap == 1 then
@ -16,18 +14,17 @@ end
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
self.normal_zoom_mode = normal_zoom_mode
self:setZoomMode(normal_zoom_mode)
self.ui:handleEvent(Event:new("GammaUpdate", self.document.configurable.contrast))
self.normal_zoom_mode = config:readSetting("normal_zoom_mode") or
G_reader_settings:readSetting("zoom_mode") or "page"
self:setZoomMode(self.normal_zoom_mode)
self.document.configurable.contrast = config:readSetting("kopt_contrast") or
G_reader_settings:readSetting("kopt_contrast") or 1.0
self.ui:handleEvent(Event:new("GammaUpdate", 1/self.document.configurable.contrast))
-- 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
self.document.configurable.word_spacing = -0.2
end
self.ui:handleEvent(Event:new("DitheringUpdate"))
end
function ReaderKoptListener:onSaveSettings()
@ -62,20 +59,10 @@ 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")
return true
end
return ReaderKoptListener

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()
@ -26,22 +26,22 @@ function ReaderMenu:init()
},
-- items in top menu
navi = {
icon = "appbar.navigation",
icon = "resources/icons/appbar.page.corner.bookmark.png",
},
typeset = {
icon = "appbar.typeset",
icon = "resources/icons/appbar.page.text.png",
},
setting = {
icon = "appbar.settings",
icon = "resources/icons/appbar.settings.png",
},
tools = {
icon = "appbar.tools",
icon = "resources/icons/appbar.tools.png",
},
search = {
icon = "appbar.search",
icon = "resources/icons/appbar.magnify.browse.png",
},
filemanager = {
icon = "appbar.filebrowser",
icon = "resources/icons/appbar.cabinet.files.png",
remember = false,
callback = function()
self:onTapCloseMenu()
@ -50,56 +50,43 @@ function ReaderMenu:init()
end,
},
main = {
icon = "appbar.menu",
icon = "resources/icons/menu-icon.png",
}
}
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
self.activation_menu = G_reader_settings:readSetting("activate_menu")
if self.activation_menu == nil then
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",
@ -114,18 +101,6 @@ function ReaderMenu:initGesListener()
},
handler = function(ges) return self:onTapShowMenu(ges) end,
},
{
id = "readermenu_ext_tap",
ges = "tap",
screen_zone = {
ratio_x = DTAP_ZONE_MENU_EXT.x, ratio_y = DTAP_ZONE_MENU_EXT.y,
ratio_w = DTAP_ZONE_MENU_EXT.w, ratio_h = DTAP_ZONE_MENU_EXT.h,
},
overrides = {
"readermenu_tap",
},
handler = function(ges) return self:onTapShowMenu(ges) end,
},
{
id = "readermenu_swipe",
ges = "swipe",
@ -139,18 +114,6 @@ function ReaderMenu:initGesListener()
},
handler = function(ges) return self:onSwipeShowMenu(ges) end,
},
{
id = "readermenu_ext_swipe",
ges = "swipe",
screen_zone = {
ratio_x = DTAP_ZONE_MENU_EXT.x, ratio_y = DTAP_ZONE_MENU_EXT.y,
ratio_w = DTAP_ZONE_MENU_EXT.w, ratio_h = DTAP_ZONE_MENU_EXT.h,
},
overrides = {
"readermenu_swipe",
},
handler = function(ges) return self:onSwipeShowMenu(ges) end,
},
{
id = "readermenu_pan",
ges = "pan",
@ -164,23 +127,9 @@ function ReaderMenu:initGesListener()
},
handler = function(ges) return self:onSwipeShowMenu(ges) end,
},
{
id = "readermenu_ext_pan",
ges = "pan",
screen_zone = {
ratio_x = DTAP_ZONE_MENU_EXT.x, ratio_y = DTAP_ZONE_MENU_EXT.y,
ratio_w = DTAP_ZONE_MENU_EXT.w, ratio_h = DTAP_ZONE_MENU_EXT.h,
},
overrides = {
"readermenu_pan",
},
handler = function(ges) return self:onSwipeShowMenu(ges) end,
},
})
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)
@ -189,63 +138,11 @@ function ReaderMenu:setUpdateItemTable()
end
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,
})
end,
},
},
}
self.menu_items.page_overlap = require("ui/elements/page_overlap")
-- settings tab
-- 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
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")
end
-- insert DjVu render mode submenu just before the last entry (show advanced)
-- this is a bit of a hack
if self.ui.document.is_djvu then
@ -254,23 +151,19 @@ 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")
return self.ui and self.ui.doc_settings and self.ui.doc_settings:readSetting("exclude_screensaver") == true
end,
callback = function()
if Screensaver:isExcluded() then
self.ui.doc_settings:makeFalse("exclude_screensaver")
if Screensaver:excluded() then
self.ui.doc_settings:saveSetting("exclude_screensaver", false)
else
self.ui.doc_settings:makeTrue("exclude_screensaver")
self.ui.doc_settings:saveSetting("exclude_screensaver", true)
end
self.ui:saveSettings()
end,
@ -285,7 +178,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
@ -300,9 +193,29 @@ function ReaderMenu:setUpdateItemTable()
for id, common_setting in pairs(dofile("frontend/ui/elements/common_info_menu_table.lua")) do
self.menu_items[id] = common_setting
end
-- insert common exit for reader
for id, common_setting in pairs(dofile("frontend/ui/elements/common_exit_menu_table.lua")) do
self.menu_items[id] = common_setting
self.menu_items.exit_menu = {
text = _("Exit"),
hold_callback = function()
self:exitOrRestart()
end,
}
self.menu_items.exit = {
text = _("Exit"),
callback = function()
self:exitOrRestart()
end,
}
self.menu_items.restart_koreader = {
text = _("Restart KOReader"),
callback = function()
self:exitOrRestart(function() UIManager:restartKOReader() end)
end,
}
if not Device:canRestart() then
self.menu_items.exit_menu = self.menu_items.exit
self.menu_items.exit = nil
self.menu_items.restart_koreader = nil
end
self.menu_items.open_previous_document = {
@ -346,36 +259,8 @@ 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)
function ReaderMenu:exitOrRestart(callback)
if self.menu_container then self:onTapCloseMenu() end
-- Only restart sets a callback, which suits us just fine for this check ;)
if callback and not force and not Device:isStartupScriptUpToDate() then
UIManager:show(ConfirmBox:new{
text = _("KOReader's startup script has been updated. You'll need to completely exit KOReader to finalize the update."),
ok_text = _("Restart anyway"),
ok_callback = function()
self:exitOrRestart(callback, true)
end,
})
return
end
UIManager:nextTick(function()
self.ui:onClose()
if callback ~= nil then
@ -418,7 +303,6 @@ function ReaderMenu:onShowMenu(tab_index)
end
local menu_container = CenterContainer:new{
covers_header = true,
ignore = "height",
dimen = Screen:getSize(),
}
@ -442,8 +326,8 @@ function ReaderMenu:onShowMenu(tab_index)
}
end
main_menu.close_callback = function()
self:onCloseReaderMenu()
main_menu.close_callback = function ()
self.ui:handleEvent(Event:new("CloseReaderMenu"))
end
main_menu.touch_menu_callback = function ()
@ -458,24 +342,12 @@ 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
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()
self.last_tab_index = self.menu_container[1].last_index
self:onSaveSettings()
UIManager:close(self.menu_container)
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()
return true
end
function ReaderMenu:onCloseDocument()
@ -498,10 +370,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,8 +386,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("HandledAsSwipe")) -- cancel any pan scroll made
self.ui:handleEvent(Event:new("ShowMenu", self:_getTabIndexFromLocation(ges)))
return true
end
end
@ -525,21 +396,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 +414,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,24 +6,25 @@ 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)
label_color = Blitbuffer.COLOR_BLACK,
show_page_labels = nil,
use_page_labels = nil,
_mirroredUI = BD.mirroredUILayout(),
}
function ReaderPageMap:init()
@ -87,18 +88,18 @@ 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])
if config:has("pagemap_show_page_labels") then
self.show_page_labels = config:isTrue("pagemap_show_page_labels")
else
self.show_page_labels = config:readSetting("pagemap_show_page_labels")
if self.show_page_labels == nil then
self.show_page_labels = G_reader_settings:nilOrTrue("pagemap_show_page_labels")
end
if config:has("pagemap_use_page_labels") then
self.use_page_labels = config:isTrue("pagemap_use_page_labels")
else
self.use_page_labels = config:readSetting("pagemap_use_page_labels")
if self.use_page_labels == nil then
self.use_page_labels = G_reader_settings:isTrue("pagemap_use_page_labels")
end
end
@ -132,20 +133,15 @@ function ReaderPageMap:updateVisibleLabels()
end
self.container:clear()
local page_labels = self.ui.document:getPageMapVisiblePageLabels()
local footer_height = ((self.view.footer_visible and not self.view.footer.settings.reclaim_height) and 1 or 0) * self.view.footer:getHeight()
local footer_height = (self.view.footer_visible and 1 or 0) * self.view.footer:getHeight()
local max_y = Screen:getHeight() - footer_height
local last_label_bottom_y = 0
local on_second_page = false
for _, page in ipairs(page_labels) do
local in_left_margin = BD.mirroredUILayout()
local in_left_margin = self._mirroredUI
if self.ui.document:getVisiblePageCount() > 1 then
-- Pages in 2-page mode are not mirrored, so we'll
-- have to handle any mirroring tweak ourselves
in_left_margin = page.screen_page == 1
if not on_second_page and page.screen_page == 2 then
on_second_page = true
last_label_bottom_y = 0 -- reset this
end
end
local max_label_width = in_left_margin and self.max_left_label_width or self.max_right_label_width
if max_label_width < self.min_label_width then
@ -196,26 +192,12 @@ ReaderPageMap.onSetStatusLine = ReaderPageMap.updateVisibleLabels
function ReaderPageMap:onShowPageList()
-- build up item_table
local cur_page = self.ui.document:getCurrentPage()
local cur_page_idx = 0
local page_list = self.ui.document:getPageMap()
for k, v in ipairs(page_list) do
v.text = v.label
v.mandatory = v.page
if v.page <= cur_page then
cur_page_idx = k
end
end
if cur_page_idx > 0 then
-- Have Menu jump to the current page and show it in bold
page_list.current = cur_page_idx
end
-- We use the per-page and font-size settings set for the ToC
local items_per_page = G_reader_settings:readSetting("toc_items_per_page") or 14
local items_font_size = G_reader_settings:readSetting("toc_items_font_size") or Menu.getItemFontSize(items_per_page)
local items_with_dots = G_reader_settings:nilOrTrue("toc_items_with_dots")
local pl_menu = Menu:new{
title = _("Reference page numbers list"),
item_table = page_list,
@ -223,12 +205,10 @@ function ReaderPageMap:onShowPageList()
is_popout = false,
width = Screen:getWidth(),
height = Screen:getHeight(),
items_per_page = items_per_page,
items_font_size = items_font_size,
cface = Font:getFace("x_smallinfofont"),
perpage = G_reader_settings:readSetting("items_per_page") or 14,
line_color = require("ffi/blitbuffer").COLOR_WHITE,
single_line = true,
align_baselines = true,
with_dots = items_with_dots,
on_close_ges = {
GestureRange:new{
ges = "two_finger_swipe",
@ -280,11 +260,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)
@ -361,14 +338,14 @@ function ReaderPageMap:addToMainMenu(menu_items)
return use_page_labels and _("Renderer") or _("Renderer (★)")
end,
choice1_callback = function()
G_reader_settings:makeFalse("pagemap_use_page_labels")
G_reader_settings:saveSetting("pagemap_use_page_labels", false)
if touchmenu_instance then touchmenu_instance:updateItems() end
end,
choice2_text_func = function()
return use_page_labels and _("Reference (★)") or _("Reference")
end,
choice2_callback = function()
G_reader_settings:makeTrue("pagemap_use_page_labels")
G_reader_settings:saveSetting("pagemap_use_page_labels", true)
if touchmenu_instance then touchmenu_instance:updateItems() end
end,
})
@ -394,14 +371,14 @@ function ReaderPageMap:addToMainMenu(menu_items)
return show_page_labels and _("Hide") or _("Hide (★)")
end,
choice1_callback = function()
G_reader_settings:makeFalse("pagemap_show_page_labels")
G_reader_settings:saveSetting("pagemap_show_page_labels", false)
if touchmenu_instance then touchmenu_instance:updateItems() end
end,
choice2_text_func = function()
return show_page_labels and _("Show (★)") or _("Show")
end,
choice2_callback = function()
G_reader_settings:makeTrue("pagemap_show_page_labels")
G_reader_settings:saveSetting("pagemap_show_page_labels", true)
if touchmenu_instance then touchmenu_instance:updateItems() end
end,
})
@ -409,12 +386,13 @@ 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)
local SpinWidget = require("ui/widget/spinwidget")
local spin_w = SpinWidget:new{
width = math.floor(Screen:getWidth() * 0.6),
value = self.label_font_size,
value_min = 8,
value_max = 20,

File diff suppressed because it is too large Load Diff

@ -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,418 +0,0 @@
local Device = require("device")
local Event = require("ui/event")
local UIManager = require("ui/uimanager")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local logger = require("logger")
local time = require("ui/time")
local _ = require("gettext")
local C_ = _.pgettext
local T = require("ffi/util").template
local Screen = Device.screen
-- This module exposes Scrolling settings, and additionnally
-- handles inertial scrolling on non-eInk devices.
local SCROLL_METHOD_CLASSIC = "classic"
local SCROLL_METHOD_TURBO = "turbo"
local SCROLL_METHOD_ON_RELEASE = "on_release"
local ReaderScrolling = WidgetContainer:extend{
-- Available scrolling methods (make them available to other reader modules)
SCROLL_METHOD_CLASSIC = SCROLL_METHOD_CLASSIC,
SCROLL_METHOD_TURBO = SCROLL_METHOD_TURBO,
SCROLL_METHOD_ON_RELEASE = SCROLL_METHOD_ON_RELEASE,
scroll_method = SCROLL_METHOD_CLASSIC,
scroll_activation_delay_ms = 0, -- 0 ms
inertial_scroll = false,
pan_rate = 30, -- default 30 ops, will be adjusted in readerui
scroll_friction = 0.2, -- the lower, the sooner inertial scrolling stops
-- go at ending scrolling soon when we reach steps smaller than this
end_scroll_dist = Screen:scaleBySize(10),
-- no inertial scrolling if 300ms pause without any movement before release
pause_before_release_cancel_duration = time.ms(300),
-- Callbacks to be updated by readerrolling or readerpaging
_do_scroll_callback = function(distance) return false end,
_scroll_done_callback = function() end,
_inertial_scroll_supported = false,
_inertial_scroll_enabled = false,
_inertial_scroll_interval = 1 / 30,
_inertial_scroll_action_scheduled = false,
_just_reschedule = false,
_last_manual_scroll_dy = 0,
_velocity = 0,
}
function ReaderScrolling:init()
if not Device:isTouchDevice() then
-- No scroll support, no menu
return
end
-- The different scrolling methods are handled directly by readerpaging/readerrolling
self.scroll_method = G_reader_settings:readSetting("scroll_method")
-- Keep inertial scrolling available on the emulator (which advertizes itself as eInk)
if not Device:hasEinkScreen() or Device:isEmulator() then
self._inertial_scroll_supported = true
end
if self._inertial_scroll_supported then
self.inertial_scroll = G_reader_settings:nilOrTrue("inertial_scroll")
self._inertial_scroll_interval = 1 / self.pan_rate
-- Set this so we don't have to check for nil, and in case
-- we miss a first touch event.
-- We can keep it obsolete, which will result in a long
-- duration and a small/zero velocity that won't hurt.
self._last_manual_scroll_timev = 0
self:_setupAction()
end
self.ui.menu:registerToMainMenu(self)
end
function ReaderScrolling:getDefaultScrollActivationDelay_ms()
if (self.ui.gestures and self.ui.gestures.multiswipes_enabled)
or G_reader_settings:readSetting("activate_menu") ~= "tap" then
-- If swipes to show menu or multiswipes are enabled, higher default
-- scroll activation delay to avoid scrolling and restoring when
-- doing swipes
return 500 -- 500ms
end
-- Otherwise, no need for any delay
return 0
end
function ReaderScrolling:addToMainMenu(menu_items)
menu_items.scrolling = {
text = _("Scrolling"),
enabled_func = function()
-- Make it only enabled when in continuous/scroll mode
-- (different setting in self.view whether rolling or paging document)
if self.view and (self.view.page_scroll or self.view.view_mode == "scroll") then
return true
end
return false
end,
sub_item_table = {
{
text = _("Classic scrolling"),
help_text = _([[Classic scrolling will move the document with your finger.]]),
checked_func = function()
return self.scroll_method == self.SCROLL_METHOD_CLASSIC
end,
callback = function()
if self.scroll_method ~= self.SCROLL_METHOD_CLASSIC then
self.scroll_method = self.SCROLL_METHOD_CLASSIC
self:applyScrollSettings()
end
end,
},
{
text = _("Turbo scrolling"),
help_text = _([[
Turbo scrolling will scroll the document, at each step, by the distance from your initial finger position (rather than by the distance from your previous finger position).
It allows for faster scrolling without the need to lift and reposition your finger.]]),
checked_func = function()
return self.scroll_method == self.SCROLL_METHOD_TURBO
end,
callback = function()
if self.scroll_method ~= self.SCROLL_METHOD_TURBO then
self.scroll_method = self.SCROLL_METHOD_TURBO
self:applyScrollSettings()
end
end,
},
{
text = _("On-release scrolling"),
help_text = _([[
On-release scrolling will scroll the document by the panned distance only on finger up.
This is interesting on eInk if you only pan to better adjust page vertical position.]]),
checked_func = function()
return self.scroll_method == self.SCROLL_METHOD_ON_RELEASE
end,
callback = function()
if self.scroll_method ~= self.SCROLL_METHOD_ON_RELEASE then
self.scroll_method = self.SCROLL_METHOD_ON_RELEASE
self:applyScrollSettings()
end
end,
separator = true,
},
{
text_func = function()
return T(_("Activation delay: %1 ms"), self.scroll_activation_delay_ms)
end,
keep_menu_open = true,
callback = function(touchmenu_instance)
local scroll_activation_delay_default_ms = self:getDefaultScrollActivationDelay_ms()
local SpinWidget = require("ui/widget/spinwidget")
local widget = SpinWidget:new{
title_text = _("Scroll activation delay"),
info_text = T(_([[
A delay can be used to avoid scrolling when swipes or multiswipes are intended.
The delay value is in milliseconds and can range from 0 to 2000 (2 seconds).
Default value: %1 ms]]), scroll_activation_delay_default_ms),
width = math.floor(Screen:getWidth() * 0.75),
value = self.scroll_activation_delay_ms,
value_min = 0,
value_max = 2000,
value_step = 100,
value_hold_step = 500,
unit = C_("Time", "ms"),
ok_text = _("Set delay"),
default_value = scroll_activation_delay_default_ms,
callback = function(spin)
self.scroll_activation_delay_ms = spin.value
self:applyScrollSettings()
if touchmenu_instance then touchmenu_instance:updateItems() end
end
}
UIManager:show(widget)
end,
},
}
}
if self._inertial_scroll_supported then
-- Add it before "Activation delay" to keep checkboxes together
table.insert(menu_items.scrolling.sub_item_table, 4, {
text = _("Allow inertial scrolling"),
enabled_func = function()
return self.scroll_method == self.SCROLL_METHOD_CLASSIC
end,
checked_func = function()
return self.scroll_method == self.SCROLL_METHOD_CLASSIC and self.inertial_scroll
end,
callback = function()
self.inertial_scroll = not self.inertial_scroll
self:applyScrollSettings()
end,
})
end
end
function ReaderScrolling:onReaderReady()
-- We don't know if the gestures plugin is loaded in :init(), but we know it here
self.scroll_activation_delay_ms = G_reader_settings:readSetting("scroll_activation_delay")
or self:getDefaultScrollActivationDelay_ms()
self:applyScrollSettings()
end
function ReaderScrolling:applyScrollSettings()
G_reader_settings:saveSetting("scroll_method", self.scroll_method)
G_reader_settings:saveSetting("inertial_scroll", self.inertial_scroll)
if self.scroll_activation_delay_ms == self:getDefaultScrollActivationDelay_ms() then
G_reader_settings:delSetting("scroll_activation_delay")
else
G_reader_settings:saveSetting("scroll_activation_delay", self.scroll_activation_delay_ms)
end
if self.scroll_method == self.SCROLL_METHOD_CLASSIC then
self._inertial_scroll_enabled = self.inertial_scroll
else
self._inertial_scroll_enabled = false
end
self:setupTouchZones()
self.ui:handleEvent(Event:new("ScrollSettingsUpdated", self.scroll_method,
self._inertial_scroll_enabled, self.scroll_activation_delay_ms))
end
function ReaderScrolling:setupTouchZones()
local zones = {
{
id = "inertial_scrolling_touch",
ges = "touch",
screen_zone = {
ratio_x = 0, ratio_y = 0, ratio_w = 1, ratio_h = 1,
},
handler = function(ges)
-- A touch might set the start of the first pan event,
-- that we need to compute its duration
self._last_manual_scroll_timev = ges.time
-- If we are scrolling, a touch cancels it.
-- We want its release (which will trigger a tap) to not change pages.
-- This also allows a pan following this touch to skip any scroll
-- activation delay
self._cancelled_by_touch = self._inertial_scroll_action
and self._inertial_scroll_action(false)
or false
end,
},
{
id = "inertial_scrolling_tap",
ges = "tap",
screen_zone = {
ratio_x = 0, ratio_y = 0, ratio_w = 1, ratio_h = 1,
},
overrides = {
"tap_forward",
"tap_backward",
"readermenu_tap",
"readermenu_ext_tap",
"readerconfigmenu_tap",
"readerconfigmenu_ext_tap",
"readerfooter_tap",
"readerhighlight_tap",
"tap_link",
},
handler = function()
-- Ignore tap if cancelled by its initial touch
if self._cancelled_by_touch then
self._cancelled_by_touch = false
return true
end
-- Otherwise, let it be handled by other tap handlers
end,
},
}
if self._inertial_scroll_enabled then
self.ui:registerTouchZones(zones)
else
self.ui:unRegisterTouchZones(zones)
end
end
function ReaderScrolling:isInertialScrollingEnabled()
return self._inertial_scroll_enabled
end
function ReaderScrolling:setInertialScrollCallbacks(do_scroll_callback, scroll_done_callback)
self._do_scroll_callback = do_scroll_callback
self._scroll_done_callback = scroll_done_callback
end
function ReaderScrolling:startInertialScroll()
if not self._inertial_scroll_enabled then
return false
end
return self._inertial_scroll_action(true)
end
function ReaderScrolling:cancelInertialScroll()
if not self._inertial_scroll_enabled then
return
end
return self._inertial_scroll_action(false)
end
function ReaderScrolling:cancelledByTouch()
return self._cancelled_by_touch
end
function ReaderScrolling:accountManualScroll(dy, timev)
if not self._inertial_scroll_enabled then
return
end
self._last_manual_scroll_dy = dy
self._last_manual_scroll_duration = timev - self._last_manual_scroll_timev
self._last_manual_scroll_timev = timev
end
function ReaderScrolling:_setupAction()
self._inertial_scroll_action = function(action)
-- action can be:
-- - true: stop any previous ongoing inertial scroll, then start a new one
-- (returns true if we started one)
-- - false: just stop any previous ongoing inertial scroll
-- (returns true if we did cancel one)
if action ~= nil then
local cancelled = false
if self._inertial_scroll_action_scheduled then
UIManager:unschedule(self._inertial_scroll_action)
self._inertial_scroll_action_scheduled = false
cancelled = true
self._scroll_done_callback()
logger.dbg("inertial scrolling cancelled")
end
if action == false then
self._last_manual_scroll_dy = 0
return cancelled
end
-- Initiate inertial scrolling (action=true), unless we should not
if UIManager:getTime() - self._last_manual_scroll_timev >= self.pause_before_release_cancel_duration then
-- but not if no finger move for 0.3s before finger up
self._last_manual_scroll_dy = 0
return false
end
if self._last_manual_scroll_duration == 0 or self._last_manual_scroll_dy == 0 then
return false
end
-- Initial velocity is the one of the last pan scroll given to accountManualScroll()
local delay_us = time.to_us(self._last_manual_scroll_duration)
if delay_us < 1 then delay_us = 1 end -- safety check
self._velocity = self._last_manual_scroll_dy * time.s(1 / delay_us)
self._last_manual_scroll_dy = 0
self._inertial_scroll_action_scheduled = true
-- We'll keep re-scheduling this same action, which will do
-- alternatively thanks to the _just_reschedule flag:
-- * either, in _inertial_scroll_interval, do a scroll
-- * or, then, at next tick, reschedule 1)
-- This is needed as the first one will cause a repaint that
-- may take more than _inertial_scroll_interval, which if we
-- didn't do that could be run before we process any input,
-- not allowing us to interrupt this inertial scrolling.
self._just_reschedule = false
UIManager:scheduleIn(self._inertial_scroll_interval, self._inertial_scroll_action)
-- self._stats_scroll_iterations = 0
-- self._stats_scroll_distance = 0
logger.dbg("inertial scrolling started")
return true
end
if not self._inertial_scroll_action_scheduled then
-- Safety check, shouldn't happen
return
end
if not self.ui.document then
-- might happen if scheduled and run after document is closed
return
end
if self._just_reschedule then
-- just re-schedule this, so a real scrolling is done after the delay
self._just_reschedule = false
UIManager:scheduleIn(self._inertial_scroll_interval, self._inertial_scroll_action)
return
end
-- Decrease velocity at each step
self._velocity = self._velocity * 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)
end
-- self._stats_scroll_iterations = self._stats_scroll_iterations + 1
-- self._stats_scroll_distance = self._stats_scroll_distance + dist
logger.dbg("inertial scrolling by", dist)
local did_scroll = self._do_scroll_callback(dist)
if did_scroll and math.abs(dist) > 2 then
-- Schedule at next tick the real re-scheduling
self._just_reschedule = true
UIManager:nextTick(self._inertial_scroll_action)
return
end
-- We're done
self._inertial_scroll_action_scheduled = false
self._scroll_done_callback()
logger.dbg("inertial scrolling ended")
--[[
local Notification = require("ui/widget/notification")
UIManager:show(Notification:new{
text = string.format("%d iterations, %d px scrolled",
self._stats_scroll_iterations, self._stats_scroll_distance),
})
]]--
end
end
return ReaderScrolling

@ -1,42 +1,14 @@
local BD = require("ui/bidi")
local ButtonDialog = require("ui/widget/buttondialog")
local CheckButton = require("ui/widget/checkbutton")
local Device = require("device")
local InfoMessage = require("ui/widget/infomessage")
local InputDialog = require("ui/widget/inputdialog")
local Menu = require("ui/widget/menu")
local Notification = require("ui/widget/notification")
local SpinWidget = require("ui/widget/spinwidget")
local TextBoxWidget = require("ui/widget/textboxwidget")
local InputContainer = require("ui/widget/container/inputcontainer")
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
-- For a regex like [a-z\. ] many many hits are found, maybe the number of chars on a few pages.
-- We don't try to catch them all as this is a reader and not a computer science playground. ;)
-- So if some regex gets more than max_hits a notification will be shown.
-- Increasing max_hits will slow down search for nasty regex. There is no slowdown for friendly
-- regexs like `Max|Moritz` for `One|Two|Three`
-- 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,
-- internal: whether we expect results on previous pages
-- (can be different from self.direction, if, from a page in the
-- middle of a book, we search forward from start of book)
@ -47,162 +19,13 @@ function ReaderSearch:init()
self.ui.menu:registerToMainMenu(self)
end
local help_text = _([[
Regular expressions allow you to search for a matching pattern in a text. The simplest pattern is a simple sequence of characters, such as `James Bond`. There are many different varieties of regular expressions, but we support the ECMAScript syntax. The basics will be explained below.
If you want to search for all occurrences of 'Mister Moore', 'Sir Moore' or 'Alfons Moore' but not for 'Lady Moore'.
Enter 'Mister Moore|Sir Moore|Alfons Moore'.
If your search contains a special character from ^$.*+?()[]{}|\/ you have to put a \ before that character.
Examples:
Words containing 'os' -> '[^ ]+os[^ ]+'
Any single character '.' -> 'r.nge'
Any characters '.*' -> 'J.*s'
Numbers -> '[0-9]+'
Character range -> '[a-f]'
Not a space -> '[^ ]'
A word -> '[^ ]*[^ ]'
Last word in a sentence -> '[^ ]*\.'
Complex expressions may lead to an extremely long search time, in which case not all matches will be shown.]])
local SRELL_ERROR_CODES = {}
SRELL_ERROR_CODES[102] = _("Wrong escape '\\'")
SRELL_ERROR_CODES[103] = _("Back reference does not exist.")
SRELL_ERROR_CODES[104] = _("Mismatching brackets '[]'")
SRELL_ERROR_CODES[105] = _("Mismatched parens '()'")
SRELL_ERROR_CODES[106] = _("Mismatched brace '{}'")
SRELL_ERROR_CODES[107] = _("Invalid Range in '{}'")
SRELL_ERROR_CODES[108] = _("Invalid character range")
SRELL_ERROR_CODES[110] = _("No preceding expression in repetition.")
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)
self.last_search_text = search_text
local regex_error
if text then -- from highlight dialog
self.use_regex = false
self.case_insensitive = true
else -- from input dialog
-- search_text comes from our keyboard, and may contain multiple diacritics ordered
-- in any order: we'd rather have them normalized, and expect the book content to
-- be proper and normalized text.
search_text = Utf8Proc.normalize_NFC(search_text)
self.use_regex = self.check_button_regex.checked
self.case_insensitive = not self.check_button_case.checked
regex_error = self.use_regex and self.ui.document:checkRegex(search_text)
end
if self.use_regex and regex_error ~= 0 then
logger.dbg("ReaderSearch: regex error", regex_error, SRELL_ERROR_CODES[regex_error])
local error_message
if SRELL_ERROR_CODES[regex_error] then
error_message = T(_("Invalid regular expression:\n%1"), SRELL_ERROR_CODES[regex_error])
else
error_message = _("Invalid regular expression.")
end
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
end
end
function ReaderSearch:onShowFulltextSearchInput()
@ -211,88 +34,45 @@ function ReaderSearch:onShowFulltextSearchInput()
if BD.mirroredUILayout() then
backward_text, forward_text = forward_text, backward_text
end
self.input_dialog = InputDialog:new{
self:onInput{
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"),
type = "text",
buttons = {
{
{
text = _("Cancel"),
id = "close",
callback = function()
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()
self:closeInputDialog()
end,
},
{
text = backward_text,
callback = function()
self:searchCallback(1)
self:onShowSearchDialog(self.input_dialog:getInputText(), 1)
self:closeInputDialog()
end,
},
{
text = forward_text,
is_enter_default = true,
callback = function()
self:searchCallback(0)
self:onShowSearchDialog(self.input_dialog:getInputText(), 0)
self:closeInputDialog()
end,
},
},
},
}
self.check_button_case = CheckButton:new{
text = _("Case sensitive"),
checked = not self.case_insensitive,
parent = self.input_dialog,
}
self.input_dialog:addWidget(self.check_button_case)
self.check_button_regex = CheckButton:new{
text = _("Regular expression (long-press for help)"),
checked = self.use_regex,
parent = self.input_dialog,
hold_callback = function()
UIManager:show(InfoMessage:new{
text = help_text,
width = Screen:getWidth() * 0.9,
})
end,
}
if self.ui.rolling then
self.input_dialog:addWidget(self.check_button_regex)
end
UIManager:show(self.input_dialog)
self.input_dialog:onShowKeyboard()
end
function ReaderSearch:onShowSearchDialog(text, direction, regex, case_insensitive)
function ReaderSearch:onShowSearchDialog(text, direction)
local neglect_current_location = false
local current_page
local function isSlowRegex(pattern)
if pattern:find("%[") or pattern:find("%*") or pattern:find("%?") or pattern:find("%.") then
return true
end
return false
end
local do_search = function(search_func, search_term, param)
local do_search = function(search_func, _text, param)
return function()
local no_results = true -- for notification
local res = search_func(self, search_term, param, regex, case_insensitive)
local res = search_func(self, _text, param)
if res then
if self.ui.paging then
if not current_page then -- initial search
current_page = self.ui.paging.current_page
end
no_results = false
if self.ui.document.info.has_pages then
self.ui.link:onGotoLink({page = res.page - 1}, neglect_current_location)
self.view.highlight.temp[res.page] = res
else
@ -356,39 +136,11 @@ function ReaderSearch:onShowSearchDialog(text, direction, regex, case_insensitiv
end
end
if valid_link then
no_results = false
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
end
if no_results then
local notification_text
if self._expect_back_results then
notification_text = _("No results on preceding pages")
else
notification_text = _("No results on following pages")
end
UIManager:show(Notification:new{
text = notification_text,
})
-- Don't add result pages to location ("Go back") stack
neglect_current_location = true
end
end
end
@ -401,56 +153,25 @@ function ReaderSearch:onShowSearchDialog(text, direction, regex, case_insensitiv
-- Keep the LTR order of |< and >|:
from_start_text, from_end_text = BD.ltr(from_end_text), BD.ltr(from_start_text)
end
self.wait_button = ButtonDialog:new{
buttons = {{{ text = "" }}},
}
local function search(func, pattern, param)
if regex and isSlowRegex(pattern) then
return function()
self.wait_button.alpha = 0.75
self.wait_button.movable:setMovedOffset(self.search_dialog.movable:getMovedOffset())
UIManager:show(self.wait_button)
UIManager:tickAfterNext(function()
do_search(func, pattern, param)()
UIManager:close(self.wait_button)
end)
end
else
return do_search(func, pattern, param)
end
end
self.search_dialog = ButtonDialog:new{
-- alpha = 0.7,
buttons = {
{
{
text = from_start_text,
vsync = true,
callback = search(self.searchFromStart, text, nil),
callback = do_search(self.searchFromStart, text),
},
{
text = backward_text,
vsync = true,
callback = search(self.searchNext, text, 1),
},
{
icon = "appbar.search",
icon_width = Screen:scaleBySize(DGENERIC_ICON_SIZE * 0.8),
icon_height = Screen:scaleBySize(DGENERIC_ICON_SIZE * 0.8),
callback = function()
self.search_dialog:onClose()
self:onShowFulltextSearchInput()
end,
callback = do_search(self.searchNext, text, 1),
},
{
text = forward_text,
vsync = true,
callback = search(self.searchNext, text, 0),
callback = do_search(self.searchNext, text, 0),
},
{
text = from_end_text,
vsync = true,
callback = search(self.searchFromEnd, text, nil),
callback = do_search(self.searchFromEnd, text),
},
}
},
@ -460,187 +181,45 @@ function ReaderSearch:onShowSearchDialog(text, direction, regex, case_insensitiv
UIManager:setDirty(self.dialog, "ui")
end,
}
if regex and isSlowRegex(text) then
self.wait_button.alpha = nil
-- initial position: center of the screen
UIManager:show(self.wait_button)
UIManager:tickAfterNext(function()
do_search(self.searchFromCurrent, text, direction)()
UIManager:close(self.wait_button)
UIManager:show(self.search_dialog)
--- @todo regional
UIManager:setDirty(self.dialog, "partial")
end)
else
do_search(self.searchFromCurrent, text, direction)()
UIManager:show(self.search_dialog)
--- @todo regional
UIManager:setDirty(self.dialog, "partial")
end
do_search(self.searchFromCurrent, text, direction)()
UIManager:show(self.search_dialog)
--- @todo regional
UIManager:setDirty(self.dialog, "partial")
return true
end
-- if regex == true, use regular expression in pattern
-- if case == true or nil, the search is case insensitive
function ReaderSearch:search(pattern, origin, regex, case_insensitive)
function ReaderSearch:search(pattern, origin)
logger.dbg("search pattern", pattern)
if pattern == nil or pattern == '' then return end
local direction = self.direction
local case = self.case_insensitive
local page = self.view.state.page
if case_insensitive == nil then
case_insensitive = true
end
Device:setIgnoreInput(true)
local retval, words_found = self.ui.document:findText(pattern, origin, direction, case_insensitive, page, regex, self.max_hits)
Device:setIgnoreInput(false)
self:showErrorNotification(words_found, regex, self.max_hits)
return retval
end
function ReaderSearch:showErrorNotification(words_found, regex, max_hits)
regex = regex or self.use_regex
max_hits = max_hits or self.findall_max_hits
local regex_retval = regex and self.ui.document:getAndClearRegexSearchError()
if regex and regex_retval ~= 0 then
local error_message
if SRELL_ERROR_CODES[regex_retval] then
error_message = SRELL_ERROR_CODES[regex_retval]
else
error_message = _("Unspecified error")
end
UIManager:show(Notification:new{
text = error_message,
timeout = false,
})
elseif words_found and words_found >= max_hits then
UIManager:show(Notification:new{
text =_("Too many hits"),
timeout = 4,
})
end
return self.ui.document:findText(pattern, origin, direction, case, page)
end
function ReaderSearch:searchFromStart(pattern, _, regex, case_insensitive)
function ReaderSearch:searchFromStart(pattern)
self.direction = 0
self._expect_back_results = true
return self:search(pattern, -1, regex, case_insensitive)
return self:search(pattern, -1)
end
function ReaderSearch:searchFromEnd(pattern, _, regex, case_insensitive)
function ReaderSearch:searchFromEnd(pattern)
self.direction = 1
self._expect_back_results = false
return self:search(pattern, -1, regex, case_insensitive)
return self:search(pattern, -1)
end
function ReaderSearch:searchFromCurrent(pattern, direction, regex, case_insensitive)
function ReaderSearch:searchFromCurrent(pattern, direction)
self.direction = direction
self._expect_back_results = direction == 1
return self:search(pattern, 0, regex, case_insensitive)
return self:search(pattern, 0)
end
-- ignore current page and search next occurrence
function ReaderSearch:searchNext(pattern, direction, regex, case_insensitive)
function ReaderSearch:searchNext(pattern, direction)
self.direction = direction
self._expect_back_results = direction == 1
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)
return self:search(pattern, 1)
end
return ReaderSearch

@ -1,23 +1,30 @@
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 +42,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 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 +89,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 +106,45 @@ 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{
name = "end_document",
choose_action = ButtonDialogTitle:new{
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()
self:openNextFile(self.document.file)
UIManager:close(info)
self:onOpenNextDocumentInFolder()
else
UIManager:show(InfoMessage:new{
text = _("Could not open next file. Sort by last read date does not support this feature."),
@ -138,27 +153,18 @@ function ReaderStatus:onEndOfBook()
elseif settings == "goto_beginning" then
self.ui:handleEvent(Event:new("GoToBeginning"))
elseif settings == "file_browser" then
-- Ditto
UIManager:nextTick(function()
self:openFileBrowser()
end)
self:openFileBrowser()
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
-- Ditto
UIManager:nextTick(function()
local before_show_callback = function() self:openFileBrowser() end
self:onShowBookStatus(before_show_callback)
end)
local before_show_callback = function() self:openFileBrowser() end
self:onShowBookStatus(before_show_callback)
elseif settings == "delete_file" then
-- Ditto
UIManager:nextTick(function()
self:deleteFile()
end)
self:deleteFile(self.document.file, true)
end
end
@ -168,17 +174,18 @@ function ReaderStatus:openFileBrowser()
if not FileManager.instance then
self.ui:showFileManager()
end
self.document = nil
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,26 +193,36 @@ 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()
end
local function post_delete_callback()
local path = util.splitFilePathName(self.document.file)
FileManager:showFiles(path)
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
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
FileManager:showFiles()
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,
view = self.view,
}
if before_show_callback then
before_show_callback()
@ -215,19 +232,29 @@ 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 and self.settings.data.summary.status then
local current_status = self.settings.data.summary.status
if current_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 = { {"Back"}, doc = "cancel" }
}
end
local content = VerticalGroup:new{
TextBoxWidget:new{
@ -91,7 +91,7 @@ function TweakInfoWidget:init()
f:close()
end
end
self.css_text = util.trim(css)
self.css_text = css:gsub("^%s+", ""):gsub("%s+$", "")
self.css_frame = FrameContainer:new{
bordersize = Size.border.thin,
padding = Size.padding.large,
@ -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
@ -211,6 +194,7 @@ function TweakInfoWidget:onTap(arg, ges)
Device.input.setClipboardText("\n"..self.css_text.."\n")
UIManager:show(Notification:new{
text = _("CSS text copied to clipboard"),
timeout = 2
})
return true
elseif ges.pos:notIntersectWith(self.movable.dimen) then
@ -243,13 +227,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)
@ -285,82 +268,6 @@ function ReaderStyleTweak:nbTweaksEnabled(sub_item_table)
return nb_enabled, nb_found
end
function ReaderStyleTweak:resolveConflictsBeforeEnabling(id, conflicts_with)
-- conflicts_with may be a string, an array or hash table of ids, or a function:
-- make it a function for us here
local conflicts_with_type = type(conflicts_with)
local conflicts_with_func
if conflicts_with_type == "function" then
conflicts_with_func = conflicts_with
elseif conflicts_with_type == "string" then
conflicts_with_func = function(otid) return otid == conflicts_with end
elseif conflicts_with_type == "table" then
conflicts_with_func = function(otid) return conflicts_with[otid] ~= nil or util.arrayContains(conflicts_with, otid) end
else
conflicts_with_func = function(otid) return false end
end
local to_remove = {}
for other_id, other_enabled in pairs(self.doc_tweaks) do
-- We also reset the provided "id" for a complete cleanup,
-- it is expected the caller will re-enable it
if other_enabled and (other_id == id or conflicts_with_func(other_id)) then
table.insert(to_remove, other_id)
end
end
for _, other_id in ipairs(to_remove) do
self.doc_tweaks[other_id] = nil
end
-- global_tweaks may also contain some conflicting ids: we need to make them false
-- in doc_tweaks to have them disabled (but we keep them in global_tweaks)
local to_make_false = {}
for other_id, other_enabled in pairs(self.global_tweaks) do
-- (We shouldn't be called if the provided "id" is already enabled
-- in global_tweaks. So we don't check for that here.)
if other_enabled and conflicts_with_func(other_id) then
table.insert(to_make_false, other_id)
end
end
for _, other_id in ipairs(to_make_false) do
self.doc_tweaks[other_id] = false
end
end
function ReaderStyleTweak:resolveConflictsBeforeMakingDefault(id, conflicts_with)
local conflicts_with_type = type(conflicts_with)
local conflicts_with_func
if conflicts_with_type == "function" then
conflicts_with_func = conflicts_with
elseif conflicts_with_type == "string" then
conflicts_with_func = function(otid) return otid == conflicts_with end
elseif conflicts_with_type == "table" then
conflicts_with_func = function(otid) return conflicts_with[otid] ~= nil or util.arrayContains(conflicts_with, otid) end
else
conflicts_with_func = function(otid) return false end
end
local to_remove = {}
for other_id, other_enabled in pairs(self.global_tweaks) do
-- We also reset the provided "id" for a complete cleanup,
-- it is expected the caller will re-enable it
if other_id == id or conflicts_with_func(other_id) then
table.insert(to_remove, other_id)
end
end
for _, other_id in ipairs(to_remove) do
self.global_tweaks[other_id] = nil
end
-- Also remove the provided "id" and any conflicting one from doc_tweaks (where
-- they may be false and prevent this new default to apply to current book)
to_remove = {}
for other_id, other_enabled in pairs(self.doc_tweaks) do
if other_id == id or conflicts_with_func(other_id) then
table.insert(to_remove, other_id)
end
end
for _, other_id in ipairs(to_remove) do
self.doc_tweaks[other_id] = nil
end
end
-- Called by ReaderTypeset, returns the already built string
function ReaderStyleTweak:getCssText()
return self.css_text
@ -402,7 +309,7 @@ function ReaderStyleTweak:updateCssText(apply)
-- re-reading it, but this will allow a user to experiment
-- wihout having to restart KOReader
end
css = util.trim(css)
css = css:gsub("^%s+", ""):gsub("%s+$", "")
table.insert(css_snippets, css)
end
if self.book_style_tweak and self.book_style_tweak_enabled then
@ -421,7 +328,7 @@ function ReaderStyleTweak:updateCssText(apply)
end
function ReaderStyleTweak:onReadSettings(config)
self.enabled = config:nilOrTrue("style_tweaks_enabled")
self.enabled = not (config:readSetting("style_tweaks_enabled") == false)
self.doc_tweaks = config:readSetting("style_tweaks") or {}
-- Default globally enabled style tweaks (for new installations)
-- are defined in css_tweaks.lua
@ -436,27 +343,16 @@ function ReaderStyleTweak:onSaveSettings()
if self.enabled then
self.ui.doc_settings:delSetting("style_tweaks_enabled")
else
self.ui.doc_settings:makeFalse("style_tweaks_enabled")
self.ui.doc_settings:saveSetting("style_tweaks_enabled", false)
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 = {}
@ -464,7 +360,7 @@ function ReaderStyleTweak:init()
-- enabled tweaks / none (without the need to disable each of
-- them)
table.insert(self.tweaks_table, {
text = _("Enable style tweaks (long-press for help)"),
text = _("Enable style tweaks (hold for info)"),
checked_func = function() return self.enabled end,
callback = function()
self.enabled = not self.enabled
@ -522,14 +418,12 @@ You can enable individual tweaks on this book with a tap, or view more details a
tweak_id = item.id,
enabled_func = is_enabled,
checked_func = function() return self:isTweakEnabled(item.id) end,
-- text = item.title or "### undefined tweak title ###",
text_func = function()
local title = item.title or "### undefined tweak title ###"
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)
@ -539,48 +433,29 @@ You can enable individual tweaks on this book with a tap, or view more details a
toggle_global_default_callback = function()
if self.global_tweaks[item.id] then
self.global_tweaks[item.id] = nil
if self.doc_tweaks[item.id] == false then
self.doc_tweaks[item.id] = nil
end
else
if item.conflicts_with and item.global_conflicts_with ~= false then
-- For hold/makeDefault/global_tweaks, the tweak may provide 'global_conflicts_with':
-- if 'false': no conflict checks
-- if nil or 'true', use item.conflicts_with
-- otherwise, use it instead of item.conflicts_with
if item.global_conflicts_with ~= true and item.global_conflicts_with ~= nil then
self:resolveConflictsBeforeMakingDefault(item.id, item.global_conflicts_with)
else
self:resolveConflictsBeforeMakingDefault(item.id, item.conflicts_with)
end
-- Remove all references in doc_tweak
self:resolveConflictsBeforeEnabling(item.id, item.conflicts_with)
self.doc_tweaks[item.id] = nil
end
self.global_tweaks[item.id] = true
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
self.doc_tweaks[item.id] = true
end
self:updateCssText(true) -- apply it immediately
end,
separator = item.separator,
})
@ -673,7 +548,7 @@ You can enable individual tweaks on this book with a tap, or view more details a
local book_tweak_item = {
text_func = function()
if self.book_style_tweak then
return _("Book-specific tweak (long-press to edit)")
return _("Book-specific tweak (hold to edit)")
else
return _("Book-specific tweak")
end
@ -697,7 +572,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,68 +588,17 @@ 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 {
font-size: 0.8rem !important;
text-align: justify !important;
margin: 0 !important;
-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;
}
]]
local BOOK_TWEAK_INPUT_HINT = T([[
@ -783,126 +606,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 +658,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 +676,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 +696,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
@ -1178,13 +777,15 @@ function ReaderStyleTweak:editBookTweak(touchmenu_instance)
if not editor.save_callback_called then
UIManager:show(Notification:new{
text = NOT_MODIFIED_MSG,
timeout = 2,
})
-- This has to be the same message above and below: when
-- discarding, we can't prevent these 2 notifications from
-- 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)

@ -1,542 +0,0 @@
local Blitbuffer = require("ffi/blitbuffer")
local Cache = require("cache")
local Device = require("device")
local Geom = require("ui/geometry")
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")
local util = require("util")
local _ = require("gettext")
-- This ReaderThumbnail module provides a service for generating thumbnails
-- of book pages.
-- 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{}
function ReaderThumbnail:init()
if not Device:isTouchDevice() then
-- The BookMap and PageBrowser widgets depend too much on gestures,
-- making them work with keys would be hard and very limited, so
-- just don't make them available.
return
end
self.ui.menu:registerToMainMenu(self)
-- Use LuaJIT fast buffer.encode()/decode() when serializing BlitBuffer
-- for exchange between subprocess and parent.
self.codec = Persist.getCodec("luajit")
self:setupColor()
self.thumbnails_requests = {}
self.current_target_size_tag = nil
-- Ensure no multiple executions, and nextTick() the scheduleIn()
-- so we get a chance to process events in-between refreshes and
-- this can be interrupted (otherwise, something scheduleIn(0.1),
-- if a screen refresh is then done and taking longer than 0.1s,
-- would be executed immediately, without emptying any input event).
local schedule_step = 0
self._ensureTileGeneration_action = function(restart)
if restart then
UIManager:unschedule(self._ensureTileGeneration_action)
schedule_step = 0
end
if schedule_step == 0 then
schedule_step = 1
UIManager:nextTick(self._ensureTileGeneration_action)
elseif schedule_step == 1 then
schedule_step = 2
UIManager:scheduleIn(0.1, self._ensureTileGeneration_action)
else
schedule_step = 0
self:ensureTileGeneration()
end
end
end
function ReaderThumbnail:addToMainMenu(menu_items)
menu_items.book_map = {
text = _("Book map"),
callback = function()
self:onShowBookMap()
end,
-- Show the alternative overview mode (which is just a restricted
-- variation of the main book map) with long-press (let's avoid
-- adding another item in the crowded first menu).
hold_callback = function()
self:onShowBookMap(true)
end,
}
menu_items.page_browser = {
text = _("Page browser"),
callback = function()
self:onShowPageBrowser()
end,
}
end
function ReaderThumbnail:onShowBookMap(overview_mode)
local BookMapWidget = require("ui/widget/bookmapwidget")
UIManager:show(BookMapWidget:new{
ui = self.ui,
overview_mode = overview_mode,
})
return true
end
function ReaderThumbnail:onShowPageBrowser()
local PageBrowserWidget = require("ui/widget/pagebrowserwidget")
UIManager:show(PageBrowserWidget:new{
ui = self.ui,
})
return true
end
-- This is made a module local so we can keep track of pids
-- to collect across multiple Reader instantiations
local pids_to_collect = {}
function ReaderThumbnail:collectPids()
if #pids_to_collect == 0 then
return false
end
for i=#pids_to_collect, 1, -1 do
if ffiutil.isSubProcessDone(pids_to_collect[i]) then
table.remove(pids_to_collect, i)
end
end
return #pids_to_collect > 0
end
function ReaderThumbnail:setupColor()
self.bb_type = self.ui.document.render_color and self.ui.document.color_bb_type or Blitbuffer.TYPE_BB8
end
function ReaderThumbnail:setupCache()
if not self.tile_cache then
-- We want to allow browsing at least N pages worth of thumbnails
-- without cache trashing. A little more than N pages (because inter
-- thumbnail margins) will fit in N * screen size.
-- With N=5, this should use from 5 to 15 Mb on a classic eInk device.
local N = 5
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))
self.tile_cache = Cache:new{
size = max_bytes,
avg_itemsize = avg_itemsize, -- will make slots=500
enable_eviction_cb = true,
}
end
end
function ReaderThumbnail:logCacheSize()
logger.info(string.format("Thumbnails cache: %d/%d (%s/%s)",
self.tile_cache.cache.used_slots(),
self.tile_cache.slots,
util.getFriendlySize(self.tile_cache.cache.used_size()),
util.getFriendlySize(self.tile_cache.size)))
end
function ReaderThumbnail:resetCache()
if self.tile_cache then
self.tile_cache:clear()
self.tile_cache = nil
end
end
function ReaderThumbnail:removeFromCache(hash_subs, remove_only_non_matching)
-- Remove from cache all tiles matching any hash from hash_subs.
-- IF only_non_matching=true, keep those matching and remove all others.
if not self.tile_cache then
return
end
if type(hash_subs) ~= "table" then
hash_subs = { hash_subs }
end
local nb_removed, size_removed = 0, 0
local to_remove = {}
for thash, tile in self.tile_cache.cache:pairs() do
local remove = remove_only_non_matching
for _, h in ipairs(hash_subs) do
if thash:find(h, 1, true) then -- plain text match (no pattern needed)
remove = not remove
break
end
end
if remove then
to_remove[thash] = true
nb_removed = nb_removed + 1
size_removed = size_removed + tile.size
end
end
for thash, _ in pairs(to_remove) do
self.tile_cache.cache:delete(thash)
logger.dbg("removed cached thumbnail", thash)
end
return nb_removed, size_removed
end
function ReaderThumbnail:resetCachedPagesForBookmarks(annotations)
-- Multiple bookmarks may be provided
local start_page, end_page
for i = 1, #annotations do
local bm = annotations[i]
if self.ui.rolling then
-- Look at all properties that may be xpointers
for _, k in ipairs({"page", "pos0", "pos1"}) do
if bm[k] and type(bm[k]) == "string" then
local p = self.ui.document:getPageFromXPointer(bm[k])
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
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
end
end
end
end
if start_page and end_page then
local hash_subs_to_remove = {}
for p=start_page, end_page do
table.insert(hash_subs_to_remove, string.format("p%d-", p))
end
self:removeFromCache(hash_subs_to_remove)
end
end
function ReaderThumbnail:tidyCache()
if self.current_target_size_tag then
-- Remove all thumbnails generated for an older target size
self:removeFromCache("-"..self.current_target_size_tag, true)
end
end
function ReaderThumbnail:cancelPageThumbnailRequests(batch_id)
if batch_id then
self.thumbnails_requests[batch_id] = nil
else
self.thumbnails_requests = {}
end
if self.req_in_progress and (not batch_id or self.req_in_progress.batch_id == batch_id) then
-- Kill any reference to the module cancelling it
self.req_in_progress.when_generated_callback = nil
end
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
-- Cached: call callback and we're done.
when_generated_callback(tile, batch_id, false)
return false -- not delayed
end
if not self.thumbnails_requests[batch_id] then
self.thumbnails_requests[batch_id] = {}
end
table.insert(self.thumbnails_requests[batch_id], {
batch_id = batch_id,
hash = hash,
page = page,
width = width,
height = height,
when_generated_callback = when_generated_callback,
})
-- Start tile generation, avoid multiple ones
self._ensureTileGeneration_action(true)
return true -- delayed
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
if self.req_in_progress then
local pid_still_to_collect
still_in_progress, pid_still_to_collect = self:checkTileGeneration(self.req_in_progress)
if pid_still_to_collect then
has_pids_still_to_collect = true
end
end
if not still_in_progress then
self.req_in_progress = nil
while true do
local req_id, requests = next(self.thumbnails_requests)
if not req_id then -- no more requests
break
end
local req = table.remove(requests, 1)
if #requests == 0 then
self.thumbnails_requests[req_id] = nil
end
if req.when_generated_callback then -- not cancelled since queued
-- It might have been generated and cached by a previous batch
local tile = self.tile_cache and self.tile_cache:check(req.hash)
if tile then
req.when_generated_callback(tile, req.batch_id, true)
else
if self:startTileGeneration(req) then
self.req_in_progress = req
break
else
-- Failure starting it: let requester know in case it cares, and forget it
req.when_generated_callback(nil, req.batch_id, true)
end
end
end
end
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
function ReaderThumbnail:startTileGeneration(request)
local pid, parent_read_fd = ffiutil.runInSubProcess(function(pid, child_write_fd)
-- Get page image as if drawn on the screen
local bb = self:_getPageImage(request.page)
-- Scale it to fit in the requested size
local scale_factor = math.min(request.width / bb:getWidth(), request.height / bb:getHeight())
local target_w = math.floor(bb:getWidth() * scale_factor)
local target_h = math.floor(bb:getHeight() * scale_factor)
-- local time = require("ui/time")
-- local start_time = time.now()
local tile = TileCacheItem:new{
bb = RenderImage:scaleBlitBuffer(bb, target_w, target_h, true),
pageno = request.page,
}
tile.size = tonumber(tile.bb.stride) * tile.bb.h
-- logger.info("tile size", tile.bb.w, tile.bb.h, "=>", tile.size)
-- logger.info(string.format(" scaling took %.3f seconds, %d bpp", time.to_s(time.since(start_time)), tile.bb:getBpp()))
-- bb:free() -- no need to spend time freeing, we're dying soon anyway!
ffiutil.writeToFD(child_write_fd, self.codec.serialize(tile:totable()), true)
end, true) -- with_pipe = true
if pid then
-- Store these in the request object itself
request.pid = pid
request.parent_read_fd = parent_read_fd
return true
end
logger.warn("PageBrowserWidget thumbnail start failure:", parent_read_fd)
return false
end
function ReaderThumbnail:checkTileGeneration(request)
local pid, parent_read_fd = request.pid, request.parent_read_fd
local stuff_to_read = ffiutil.getNonBlockingReadSize(parent_read_fd) ~= 0
local subprocess_done = ffiutil.isSubProcessDone(pid)
logger.dbg("subprocess_done:", subprocess_done, " stuff_to_read:", stuff_to_read)
if stuff_to_read then
-- local time = require("ui/time")
-- local start_time = time.now()
local result, err = self.codec.deserialize(ffiutil.readAllFromFD(parent_read_fd))
if result then
local tile = TileCacheItem:new{}
tile:fromtable(result)
if self.tile_cache then
self.tile_cache:insert(request.hash, tile)
end
if request.when_generated_callback then -- not cancelled
request.when_generated_callback(tile, request.batch_id, true)
end
else
logger.warn("PageBrowserWidget thumbnail deserialize() failed:", err)
if request.when_generated_callback then -- not cancelled
request.when_generated_callback(nil, request.batch_id, true)
end
end
-- logger.info(string.format(" parsing result from subprocess took %.3f seconds", time.to_s(time.since(start_time))))
if not subprocess_done then
table.insert(pids_to_collect, pid)
return false, true
end
return false
elseif subprocess_done then
-- subprocess_done: process exited with no output
ffiutil.readAllFromFD(parent_read_fd) -- close our fd
return false
end
logger.dbg("process not yet done, will check again soon")
return true
end
function ReaderThumbnail:_getPageImage(page)
-- This is run in a subprocess: we can tweak all document settings
-- to get an adequate image of the page.
-- No need to worry about the final state of things: this subprocess
-- will die just after drawing the page, and all will be forgotten,
-- without impact on the parent process.
-- Be sure to limit our impact on the disk-saved book state
self.ui.saveSettings = function() end -- Be sure nothing is flushed
self.ui.statistics = nil -- Don't update statistics for pages we visit
-- By default, our target page size is the current screen size
local target_w, target_h = Screen:getWidth(), Screen:getHeight()
-- This was all mostly chosen by experimenting.
-- Be sure to call the innermost methods enough to get what we want, and
-- not upper event handlers that may trigger other unneeded events and stuff.
-- Especially, be sure to not trigger any paint on the screen buffer, or
-- any processing of input events.
-- No need to worry about UIManager:scheduleIn() or :nextTick(), as
-- we will die before the callback gets a chance to be run.
-- Common to ReaderRolling and ReaderPaging
self.ui.view.footer_visible = false -- We want no footer on page image
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.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.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
self.ui.pagemap:onPageUpdate(page) -- Update pagemap labels for this page
end
if self.ui.paging then
-- With PDF/DJVU/Pics, we will show the native page (no reflow, no crop, no zoom
-- to columns...). This makes thumbnail generation faster, and will allow the user
-- to get an overview of the book native pages to better decide which option will
-- be best to use for the book.
-- We also want to get a thumbnail with the aspect ratio of the native page
-- (so we don't get a native landscape page smallish and centered with blank above
-- and below in a portrait thumbnail, if the screen is in portrait mode).
self.ui.view.hinting = false -- Disable hinting
self.ui.view.page_scroll = false -- Get out of scroll mode
self.ui.view.flipping_visible = false -- No page flipping icon
self.ui.document.configurable.text_wrap = false -- Get out of reflow mode
self.ui.document.configurable.trim_page = 3 -- Page crop: none
-- self.ui.document.configurable.trim_page = 1 -- Page crop: auto (very slower)
self.ui.document.configurable.auto_straighten = 0 -- No auto straighten
-- We can let dewatermark if the user has enabled it, it helps
-- limiting annoying eInk refreshes of light gray areas
-- self.ui.document.configurable.page_opt = 0 -- No dewatermark
-- We won't touch the contrast (to try making text less gray), as it applies on
-- images that could get too dark.
-- Get native page dimensions, and update our target bb dimensions so it gets the
-- same aspect ratio (we don't use native dimensions as is, as they may get huge)
local dimen = self.ui.document:getPageDimensions(page, 1, 0)
local scale_factor = math.min(target_w / dimen.w, target_h / dimen.h)
target_w = math.floor(dimen.w * scale_factor)
target_h = math.floor(dimen.h * scale_factor)
dimen = Geom:new{ w=target_w, h=target_h }
-- logger.info("getPageImage", page, dimen, "=>", target_w, target_h, scale_factor)
-- This seems to do it all well:
-- local Event = require("ui/event")
-- self.ui:handleEvent(Event:new("SetDimensions", dimen))
-- self.ui.view.dogear[1].dimen.w = dimen.w -- (hack... its code uses the Screen width)
-- self.ui:handleEvent(Event:new("PageUpdate", page))
-- self.ui:handleEvent(Event:new("SetZoomMode", "page"))
-- Trying to do as little as needed, knowing the internals:
self.ui.view:onSetDimensions(dimen)
self.ui.view:onBBoxUpdate(nil) -- drop any bbox, draw native page
self.ui.view.state.page = page
self.ui.view.state.zoom = scale_factor
self.ui.view.state.rotation = 0
self.ui.view:recalculate()
self.ui.view.dogear[1].dimen.w = dimen.w -- (hack... its code uses the Screen width)
self.ui.bookmark:onPageUpdate(page) -- Update dogear state for this page
end
-- Draw the page on a new BB with the targetted size
local bb = Blitbuffer.new(target_w, target_h, self.bb_type)
self.ui.view:paintTo(bb, 0, 0)
if self.ui.rolling then
-- Crop out the top alt status bar if enabled
local header_height = self.ui.document:getHeaderHeight()
if header_height > 0 then
bb = bb:viewport(0, header_height, bb.w, bb.h - header_height)
end
end
return bb
end
function ReaderThumbnail:onCloseDocument()
self:cancelPageThumbnailRequests()
if self.tile_cache then
self:logCacheSize()
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()
self:setupColor()
self:resetCache()
end
-- CRE: emitted after a re-rendering
ReaderThumbnail.onDocumentRerendered = ReaderThumbnail.resetCache
ReaderThumbnail.onDocumentPartiallyRerendered = ReaderThumbnail.resetCache
-- Emitted When adding/removing/updating bookmarks and highlights
ReaderThumbnail.onAnnotationsModified = ReaderThumbnail.resetCachedPagesForBookmarks
return ReaderThumbnail

File diff suppressed because it is too large Load Diff

@ -2,21 +2,19 @@ local BD = require("ui/bidi")
local ConfirmBox = require("ui/widget/confirmbox")
local Event = require("ui/event")
local InfoMessage = require("ui/widget/infomessage")
local Math = require("optmath")
local Notification = require("ui/widget/notification")
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 Math = require("optmath")
local lfs = require("libs/libkoreader-lfs")
local optionsutil = require("ui/data/optionsutil")
local _ = require("gettext")
local C_ = _.pgettext
local Screen = require("device").screen
local T = require("ffi/util").template
local ReaderTypeset = WidgetContainer:extend{
-- @translators This is style in the sense meant by CSS (cascading style sheets), relating to the layout and presentation of the document. See <https://en.wikipedia.org/wiki/CSS> for more information.
css_menu_title = C_("CSS", "Style"),
local ReaderTypeset = InputContainer:new{
css_menu_title = _("Style"),
css = nil,
internal_css = true,
unscaled_margins = nil,
}
@ -25,132 +23,141 @@ function ReaderTypeset:init()
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
self.css = config:readSetting("css") 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)
self.embedded_fonts = config:readSetting("embedded_fonts")
if self.embedded_fonts == nil then
-- 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)
self.embedded_css = config:readSetting("embedded_css")
if self.embedded_css == nil then
-- default to enable embedded CSS
-- 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_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.
-- For new books, use 'web' mode below in BLOCK_RENDERING_FLAGS
if config:has("copt_block_rendering_mode") then
self.block_rendering_mode = config:readSetting("copt_block_rendering_mode")
else
if config:has("last_xpointer") and not config:has("docsettings_reset_done") then
local block_rendering_default_mode = 3
self.block_rendering_mode = config:readSetting("copt_block_rendering_mode")
if not self.block_rendering_mode then
if config:readSetting("last_xpointer") then
-- We have a last_xpointer: this book was previously opened
self.block_rendering_mode = 0
else
self.block_rendering_mode = G_reader_settings:readSetting("copt_block_rendering_mode")
or 3 -- default to 'web' mode
or block_rendering_default_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
-- default to disable TXT formatting as it does more harm than good (the setting is not in UI)
self.txt_preformatted = config:readSetting("txt_preformatted")
or G_reader_settings:readSetting("txt_preformatted")
or 1
self.ui.document:setTxtPreFormatted(self.txt_preformatted)
-- default to disable smooth scaling
self.ui.document:setImageScaling(self.configurable.smooth_scaling == 1)
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 floating punctuation
-- the floating punctuation should not be boolean value for the following
-- expression otherwise a false value will never be returned but numerical
-- values will survive this expression
self.floating_punctuation = config:readSetting("floating_punctuation") or
G_reader_settings:readSetting("floating_punctuation") or 0
self:toggleFloatingPunctuation(self.floating_punctuation)
-- 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:toggleTxtPreFormatted(self.txt_preformatted)
-- default to disable smooth scaling for now.
self.smooth_scaling = config:readSetting("smooth_scaling")
if self.smooth_scaling == nil then
local global = G_reader_settings:readSetting("copt_smooth_scaling")
self.smooth_scaling = (global == nil or global == 0) and 0 or 1
end
self:toggleImageScaling(self.smooth_scaling)
-- default to automagic nightmode-friendly handling of images
self.ui.document:setNightmodeImages(self.configurable.nightmode_images == 1)
self.nightmode_images = config:readSetting("nightmode_images")
if self.nightmode_images == nil then
local global = G_reader_settings:readSetting("copt_nightmode_images")
self.nightmode_images = (global == nil or global == 1) and 1 or 0
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("floating_punctuation", self.floating_punctuation)
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
if toggle then
self.configurable.embedded_css = 1
text = _("Enabled embedded styles.")
else
self.configurable.embedded_css = 0
text = _("Disabled embedded styles.")
end
self.ui.document:setEmbeddedStyleSheet(self.configurable.embedded_css)
self.ui:handleEvent(Event:new("UpdatePos"))
Notification:notify(text)
self:toggleEmbeddedStyleSheet(toggle)
return true
end
function ReaderTypeset:onToggleEmbeddedFonts(toggle)
local text
if toggle then
self.configurable.embedded_fonts = 1
text = _("Enabled embedded fonts.")
else
self.configurable.embedded_fonts = 0
text = _("Disabled embedded fonts.")
end
self.ui.document:setEmbeddedFonts(self.configurable.embedded_fonts)
self.ui:handleEvent(Event:new("UpdatePos"))
Notification:notify(text)
self:toggleEmbeddedFonts(toggle)
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)
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)
return true
end
@ -170,18 +177,22 @@ local OBSOLETED_CSS = {
"txt.css",
}
function ReaderTypeset:onSetRenderDPI(dpi)
self: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 +200,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 +207,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 +218,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 +234,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 +245,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 +284,6 @@ function ReaderTypeset:setStyleSheet(new_css)
end
end
-- Not used
function ReaderTypeset:setEmbededStyleSheetOnly()
if self.css ~= nil then
-- clear applied css
@ -328,6 +294,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,70 +389,125 @@ 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:toggleFloatingPunctuation(toggle)
-- for some reason the toggle value read from history files may stay boolean
-- and there seems no more elegant way to convert boolean values to numbers
if toggle == true then
toggle = 1
elseif toggle == false then
toggle = 0
end
self.ui.document:setFloatingPunctuation(toggle)
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 = {
text = self.css_menu_title,
sub_item_table = self:genStyleSheetMenu(),
}
menu_items.floating_punctuation = {
-- @translators See https://en.wikipedia.org/wiki/Hanging_punctuation
text = _("Hanging punctuation"),
checked_func = function() return self.floating_punctuation == 1 end,
callback = function()
self.floating_punctuation = self.floating_punctuation == 1 and 0 or 1
self:toggleFloatingPunctuation(self.floating_punctuation)
end,
hold_callback = function() self:makeDefaultFloatingPunctuation() end,
}
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:makeDefaultFloatingPunctuation()
local floating_punctuation = G_reader_settings:isTrue("floating_punctuation")
UIManager:show(MultiConfirmBox:new{
text = floating_punctuation and _("Would you like to enable or disable hanging punctuation by default?\n\nThe current default (★) is enabled.")
or _("Would you like to enable or disable hanging punctuation by default?\n\nThe current default (★) is disabled."),
choice1_text_func = function()
return floating_punctuation and _("Disable") or _("Disable (★)")
end,
choice1_callback = function()
G_reader_settings:saveSetting("floating_punctuation", false)
end,
choice2_text_func = function()
return floating_punctuation and _("Enable (★)") or _("Enable")
end,
choice2_callback = function()
G_reader_settings:saveSetting("floating_punctuation", true)
end,
})
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,
})
end
function ReaderTypeset:onSetPageHorizMargins(h_margins, when_applied_callback)
function ReaderTypeset:onSetPageHorizMargins(h_margins, refresh_callback)
self.unscaled_margins = { h_margins[1], self.unscaled_margins[2], h_margins[2], self.unscaled_margins[4] }
self.ui:handleEvent(Event:new("SetPageMargins", self.unscaled_margins, when_applied_callback))
self.ui:handleEvent(Event:new("SetPageMargins", self.unscaled_margins, refresh_callback))
end
function ReaderTypeset:onSetPageTopMargin(t_margin, when_applied_callback)
function ReaderTypeset:onSetPageTopMargin(t_margin, refresh_callback)
self.unscaled_margins = { self.unscaled_margins[1], t_margin, self.unscaled_margins[3], self.unscaled_margins[4] }
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))
self.ui:handleEvent(Event:new("SetPageMargins", self.unscaled_margins, refresh_callback))
end
function ReaderTypeset:onSetPageBottomMargin(b_margin, when_applied_callback)
function ReaderTypeset:onSetPageBottomMargin(b_margin, refresh_callback)
self.unscaled_margins = { self.unscaled_margins[1], self.unscaled_margins[2], self.unscaled_margins[3], b_margin }
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
end
self.ui:handleEvent(Event:new("SetPageMargins", self.unscaled_margins, when_applied_callback))
end
function ReaderTypeset:onSetPageTopAndBottomMargin(t_b_margins, when_applied_callback)
local t_margin, b_margin = t_b_margins[1], t_b_margins[2]
self.unscaled_margins = { self.unscaled_margins[1], t_margin, self.unscaled_margins[3], b_margin }
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.t_page_margin = b_margin
end
self.ui:handleEvent(Event:new("SetPageMargins", self.unscaled_margins, when_applied_callback))
self.ui:handleEvent(Event:new("SetPageMargins", self.unscaled_margins, refresh_callback))
end
function ReaderTypeset:onSyncPageTopBottomMargins(toggle, when_applied_callback)
function ReaderTypeset:onSyncPageTopBottomMargins(toggle, refresh_callback)
self.sync_t_b_page_margins = not self.sync_t_b_page_margins
if self.sync_t_b_page_margins then
-- Adjust current top and bottom margins if needed
@ -474,19 +519,19 @@ 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
self.ui:handleEvent(Event:new("SetPageMargins", self.unscaled_margins, refresh_callback))
refresh_callback = nil
end
end
if when_applied_callback then
when_applied_callback()
if refresh_callback then
refresh_callback()
end
end
function ReaderTypeset:onSetPageMargins(margins, when_applied_callback)
function ReaderTypeset:onSetPageMargins(margins, refresh_callback)
local left = Screen:scaleBySize(margins[1])
local top = Screen:scaleBySize(margins[2])
local right = Screen:scaleBySize(margins[3])
@ -498,22 +543,19 @@ function ReaderTypeset:onSetPageMargins(margins, when_applied_callback)
end
self.ui.document:setPageMargins(left, top, right, bottom)
self.ui:handleEvent(Event:new("UpdatePos"))
if when_applied_callback then
-- Provided when hide_on_apply, and ConfigDialog temporarily hidden:
-- show an InfoMessage with the unscaled & scaled values,
-- and call when_applied_callback on dismiss
if refresh_callback then
-- Show a toast on set, with the unscaled & scaled values
UIManager:show(InfoMessage:new{
text = T(_([[
Margins set to:
left: %1 (%2px)
right: %3 (%4px)
top: %5 (%6px)
bottom: %7 (%8px)
horizontal: %1 (%2px)
top: %3 (%4px)
bottom: %5 (%6px)
Tap to dismiss.]]),
margins[1], left, margins[3], right, margins[2], top, margins[4], bottom),
dismiss_callback = when_applied_callback,
margins[1], left, margins[2], top, margins[4], bottom),
dismiss_callback = refresh_callback,
})
end
end

@ -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,19 +12,40 @@ local C_ = _.pgettext
local T = require("ffi/util").template
local Screen = Device.screen
local ReaderTypography = WidgetContainer:extend{}
-- This is used to migrate old hyph settings, and to show the currently
-- used hyph dict language in the hyphenation menu.
-- It will be completed with info from the LANGUAGES table below.
-- NOTE: Actual migration is handled in ui/data/onetime_migration,
-- which is why this hash is public.
ReaderTypography.HYPH_DICT_NAME_TO_LANG_NAME_TAG = {
-- Mostly for migrating hyph settings, and to know the dict
-- left and right hyph min values (2/2 when not specified)
local HYPH_DICT_NAME_TO_LANG_NAME_TAG = {
["@none"] = { "@none", "en" },
["@softhyphens"] = { "@softhyphens", "en" },
["@algorithm"] = { "@algorithm", "en" },
-- Old filenames with typos, before they were renamed
["Bulgarian.pattern"] = { _("Bulgarian"), "bg" },
["Catalan.pattern"] = { _("Catalan"), "ca" },
["Czech.pattern"] = { _("Czech"), "cs" },
["Danish.pattern"] = { _("Danish"), "da" },
["Dutch.pattern"] = { _("Dutch"), "nl" },
["English_GB.pattern"] = { _("English (UK)"), "en-GB" },
["English_US.pattern"] = { _("English (US)"), "en-US" },
["Finnish.pattern"] = { _("Finnish"), "fi" },
["French.pattern"] = { _("French"), "fr", 2, 1 },
["Galician.pattern"] = { _("Galician"), "gl" },
["German.pattern"] = { _("German"), "de" },
["Greek.pattern"] = { _("Greek"), "el" },
["Hungarian.pattern"] = { _("Hungarian"), "hu" },
["Icelandic.pattern"] = { _("Icelandic"), "is" },
["Irish.pattern"] = { _("Irish"), "ga" },
["Italian.pattern"] = { _("Italian"), "it" },
["Norwegian.pattern"] = { _("Norwegian"), "no" },
["Polish.pattern"] = { _("Polish"), "pl" },
["Portuguese.pattern"] = { _("Portuguese"), "pt" },
["Roman.pattern"] = { _("Romanian"), "ro" },
["Russian_EnGB.pattern"] = { _("Russian + English (UK)"), "ru-GB" },
["Russian_EnUS.pattern"] = { _("Russian + English (US)"), "ru-US" },
["Russian.pattern"] = { _("Russian"), "ru" },
["Slovak.pattern"] = { _("Slovak"), "sk" },
["Slovenian.pattern"] = { _("Slovenian"), "sl" },
["Spanish.pattern"] = { _("Spanish"), "es" },
["Swedish.pattern"] = { _("Swedish"), "sv" },
["Turkish.pattern"] = { _("Turkish"), "tr" },
["Ukrain.pattern"] = { _("Ukrainian"), "uk" },
}
@ -36,86 +56,59 @@ ReaderTypography.HYPH_DICT_NAME_TO_LANG_NAME_TAG = {
-- H = language specific hyphenation dictionary
-- b = language specific line breaking rules
-- B = language specific additional line breaking tweaks
-- The "hyphenation file name" field is used to
-- update HYPH_DICT_NAME_TO_LANG_NAME_TAG. If multiple
-- languages were to use the same hyphenation pattern,
-- just set it for one language, whose name will be
-- used in the Hyphenation sub-menu.
-- Update them when language tweaks and features are added to crengine/src/textlang.cpp
local LANGUAGES = {
-- lang-tag aliases features menu title hyphenation file name
{ "hy", {"arm", "hye", "hyw"}, "H ", _("Armenian"), "Armenian.pattern" },
{ "eu", {}, "H ", _("Basque"), "Basque.pattern" },
{ "bg", {"bul"}, "H ", _("Bulgarian"), "Bulgarian.pattern" },
{ "ca", {"cat"}, "H ", _("Catalan"), "Catalan.pattern" },
-- lang-tag aliases features menu title
{ "bg", {"bul"}, "H ", _("Bulgarian") },
{ "ca", {"cat"}, "H ", _("Catalan") },
{ "zh-CN", {"zh", "zh-Hans"}, " b ", _("Chinese (Simplified)") },
{ "zh-TW", {"zh-Hant"}, " b ", _("Chinese (Traditional)") },
{ "hr", {}, "H ", _("Croatian"), "Croatian.pattern" },
{ "cs", {"ces"}, "HB ", _("Czech"), "Czech.pattern" },
{ "da", {"dan"}, "H ", _("Danish"), "Danish.pattern" },
{ "nl", {"nld"}, "H ", _("Dutch"), "Dutch.pattern" },
{ "en-GB", {}, "Hb ", _("English (UK)"), "English_GB.pattern" },
{ "en-US", {"en", "eng"}, "Hb ", _("English (US)"), "English_US.pattern" },
{ "eo", {"epo"}, "H ", _("Esperanto"), "Esperanto.pattern" },
{ "et", {"est"}, "H ", _("Estonian"), "Estonian.pattern" },
{ "fi", {"fin"}, "H ", _("Finnish"), "Finnish.pattern" },
{ "fr", {"fra", "fre"}, "Hb ", _("French"), "French.pattern" },
{ "fur", {}, "H ", _("Friulian"), "Friulian.pattern" },
{ "gl", {"glg"}, "H ", _("Galician"), "Galician.pattern" },
{ "ka", {}, "H ", _("Georgian"), "Georgian.pattern" },
{ "de", {"deu"}, "Hb ", _("German"), "German.pattern" },
{ "el", {"ell"}, "H ", _("Greek"), "Greek.pattern" },
{ "hu", {"hun"}, "H ", _("Hungarian"), "Hungarian.pattern" },
{ "is", {"isl"}, "H ", _("Icelandic"), "Icelandic.pattern" },
{ "ga", {"gle"}, "H ", _("Irish"), "Irish.pattern" },
{ "it", {"ita"}, "H ", _("Italian"), "Italian.pattern" },
{ "cs", {"ces"}, "HB ", _("Czech") },
{ "da", {"dan"}, "H ", _("Danish") },
{ "nl", {"nld"}, "H ", _("Dutch") },
{ "en-GB", {}, "Hb ", _("English (UK)") },
{ "en-US", {"en", "eng"}, "Hb ", _("English (US)") },
{ "fi", {"fin"}, "H ", _("Finnish") },
{ "fr", {"fra", "fre"}, "Hb ", _("French") },
{ "gl", {"glg"}, "H ", _("Galician") },
{ "de", {"deu"}, "Hb ", _("German") },
{ "el", {"ell"}, "H ", _("Greek") },
{ "hu", {"hun"}, "H ", _("Hungarian") },
{ "is", {"isl"}, "H ", _("Icelandic") },
{ "ga", {"gle"}, "H ", _("Irish") },
{ "it", {"ita"}, "H ", _("Italian") },
{ "ja", {}, " ", _("Japanese") },
{ "ko", {}, " ", _("Korean") },
{ "la", {"lat"}, "H ", _("Latin"), "Latin.pattern" },
{ "la-lit", {"lat-lit"}, "H ", _("Latin (liturgical)"), "Latin_liturgical.pattern" },
{ "lv", {"lav"}, "H ", _("Latvian"), "Latvian.pattern" },
{ "lt", {"lit"}, "H ", _("Lithuanian"), "Lithuanian.pattern" },
{ "mk", {""}, "H ", _("Macedonian"), "Macedonian.pattern" },
{ "no", {"nor"}, "H ", _("Norwegian"), "Norwegian.pattern" },
{ "oc", {"oci"}, "H ", _("Occitan"), "Occitan.pattern" },
{ "pl", {"pol"}, "HB ", _("Polish"), "Polish.pattern" },
{ "pms", {}, "H ", _("Piedmontese"), "Piedmontese.pattern" },
{ "pt", {"por"}, "HB ", _("Portuguese"), "Portuguese.pattern" },
{ "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" },
{ "sr", {"srp"}, "HB ", _("Serbian"), "Serbian.pattern" },
{ "sk", {"slk"}, "HB ", _("Slovak"), "Slovak.pattern" },
{ "sl", {"slv"}, "H ", _("Slovenian"), "Slovenian.pattern" },
{ "es", {"spa"}, "Hb ", _("Spanish"), "Spanish.pattern" },
{ "sv", {"swe"}, "H ", _("Swedish"), "Swedish.pattern" },
{ "tr", {"tur"}, "H ", _("Turkish"), "Turkish.pattern" },
{ "uk", {"ukr"}, "H ", _("Ukrainian"), "Ukrainian.pattern" },
{ "cy", {"cym"}, "H ", _("Welsh"), "Welsh.pattern" },
{ "zu", {"zul"}, "H ", _("Zulu"), "Zulu.pattern" },
{ "no", {"nor"}, "H ", _("Norwegian") },
{ "pl", {"pol"}, "HB ", _("Polish") },
{ "pt", {"por"}, "HB ", _("Portuguese") },
{ "ro", {"ron"}, "H ", _("Romanian") },
{ "ru-GB", {}, "Hb ", _("Russian + English (UK)") },
{ "ru-US", {}, "Hb ", _("Russian + English (US)") },
{ "ru", {"rus"}, "Hb ", _("Russian") },
{ "sk", {"slk"}, "HB ", _("Slovak") },
{ "sl", {"slv"}, "H ", _("Slovenian") },
{ "es", {"spa"}, "Hb ", _("Spanish") },
{ "sv", {"swe"}, "H ", _("Swedish") },
{ "tr", {"tur"}, "H ", _("Turkish") },
{ "uk", {"ukr"}, "H ", _("Ukrainian") }
}
ReaderTypography.DEFAULT_LANG_TAG = "en-US" -- English_US.pattern is loaded by default in crengine
local DEFAULT_LANG_TAG = "en-US" -- English_US.pattern is loaded by default in crengine
local LANG_TAG_TO_LANG_NAME = {}
local LANG_ALIAS_TO_LANG_TAG = {}
for __, v in ipairs(LANGUAGES) do
local lang_tag, lang_aliases, lang_features, lang_name, hyph_filename = unpack(v) -- luacheck: no unused
local lang_tag, lang_aliases, lang_features, lang_name = unpack(v) -- luacheck: no unused
LANG_TAG_TO_LANG_NAME[lang_tag] = lang_name
if lang_aliases and #lang_aliases > 0 then
for ___, alias in ipairs(lang_aliases) do
LANG_ALIAS_TO_LANG_TAG[alias] = lang_tag
end
end
if hyph_filename then
ReaderTypography.HYPH_DICT_NAME_TO_LANG_NAME_TAG[hyph_filename] = { lang_name, lang_tag }
end
end
-- Make lang aliases available to other modules (can be used by Translator)
ReaderTypography.LANG_ALIAS_TO_LANG_TAG = LANG_ALIAS_TO_LANG_TAG
local ReaderTypography = InputContainer:new{}
function ReaderTypography:init()
self.menu_table = {}
@ -127,11 +120,48 @@ function ReaderTypography:init()
self.hyph_trust_soft_hyphens = false
self.hyph_soft_hyphens_only = false
self.hyph_force_algorithmic = false
self.floating_punctuation = 0
-- Migrate old readerhyphenation settings (but keep them in case one
-- go back to a previous version)
if not G_reader_settings:readSetting("text_lang_default") and not G_reader_settings:readSetting("text_lang_fallback") then
local g_text_lang_set = false
local hyph_alg_default = G_reader_settings:readSetting("hyph_alg_default")
if hyph_alg_default then
local dict_info = HYPH_DICT_NAME_TO_LANG_NAME_TAG[hyph_alg_default]
if dict_info then
G_reader_settings:saveSetting("text_lang_default", dict_info[2])
g_text_lang_set = true
-- Tweak the other settings if the default hyph algo happens
-- to be one of these:
if hyph_alg_default == "@none" then
G_reader_settings:saveSetting("hyphenation", false)
elseif hyph_alg_default == "@softhyphens" then
G_reader_settings:saveSetting("hyph_soft_hyphens_only", true)
elseif hyph_alg_default == "@algorithm" then
G_reader_settings:saveSetting("hyph_force_algorithmic", true)
end
end
end
local hyph_alg_fallback = G_reader_settings:readSetting("hyph_alg_fallback")
if not g_text_lang_set and hyph_alg_fallback then
local dict_info = HYPH_DICT_NAME_TO_LANG_NAME_TAG[hyph_alg_fallback]
if dict_info then
G_reader_settings:saveSetting("text_lang_fallback", dict_info[2])
g_text_lang_set = true
-- We can't really tweak other settings if the hyph algo fallback
-- happens to be @none, @softhyphens, @algortihm...
end
end
if not g_text_lang_set then
-- If nothing migrated, set the fallback to DEFAULT_LANG_TAG,
-- as we'll always have one of text_lang_default/_fallback set.
G_reader_settings:saveSetting("text_lang_fallback", DEFAULT_LANG_TAG)
end
end
local info_text = _([[
Some languages have specific typographic rules: these include hyphenation, line breaking rules, and language specific glyph variants.
KOReader will choose one according to the language tag from the book's metadata, but you can select another one.
KOReader will chose one according to the language tag from the book's metadata, but you can select another one.
You can also set a default language or a fallback one with a long-press.
Features available per language are marked with:
@ -152,7 +182,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 +226,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,
@ -235,15 +265,11 @@ When the book's language tag is not among our presets, no specific features will
return text
end,
callback = function()
-- We use an InfoMessage because the text might be too long for a Notification.
-- Use a small timeout (but long enough to read) as this might be bothering.
UIManager:show(InfoMessage:new{
text = T(_("Changed language for typography rules to %1."), BD.wrap(lang_name)),
timeout = 2,
})
self.text_lang_tag = lang_tag
self.ui.document:setTextMainLang(lang_tag)
self.ui:handleEvent(Event:new("TypographyLanguageChanged"))
self.ui:handleEvent(Event:new("UpdatePos"))
end,
hold_callback = function(touchmenu_instance)
@ -299,13 +325,13 @@ When the book's language tag is not among our presets, no specific features will
return text_lang_embedded_langs and _("Ignore") or _("Ignore (★)")
end,
choice1_callback = function()
G_reader_settings:makeFalse("text_lang_embedded_langs")
G_reader_settings:saveSetting("text_lang_embedded_langs", false)
end,
choice2_text_func = function()
return text_lang_embedded_langs and _("Respect (★)") or _("Respect")
end,
choice2_callback = function()
G_reader_settings:makeTrue("text_lang_embedded_langs")
G_reader_settings:saveSetting("text_lang_embedded_langs", true)
end,
})
end,
@ -332,13 +358,13 @@ When the book's language tag is not among our presets, no specific features will
return hyphenation and _("Disable") or _("Disable (★)")
end,
choice1_callback = function()
G_reader_settings:makeFalse("hyphenation")
G_reader_settings:saveSetting("hyphenation", false)
end,
choice2_text_func = function()
return hyphenation and _("Enable (★)") or _("Enable")
end,
choice2_callback = function()
G_reader_settings:makeTrue("hyphenation")
G_reader_settings:saveSetting("hyphenation", true)
end,
})
end,
@ -350,10 +376,10 @@ When the book's language tag is not among our presets, no specific features will
text_func = function()
-- Note: with our callback, we either get hyph_left_hyphen_min and
-- hyph_right_hyphen_min both nil, or both defined.
if G_reader_settings:has("hyph_left_hyphen_min") or
G_reader_settings:has("hyph_right_hyphen_min") then
if G_reader_settings:readSetting("hyph_left_hyphen_min") or
G_reader_settings:readSetting("hyph_right_hyphen_min") then
-- @translators to RTL language translators: %1/left is the min length of the start of a hyphenated word, %2/right is the min length of the end of a hyphenated word (note that there is yet no support for hyphenation with RTL languages, so this will mostly apply to LTR documents)
return T(_("Left/right minimal sizes: %1 / %2"),
return T(_("Left/right minimal sizes: %1 - %2"),
G_reader_settings:readSetting("hyph_left_hyphen_min"),
G_reader_settings:readSetting("hyph_right_hyphen_min"))
end
@ -361,8 +387,15 @@ 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
-- We will show the defaults for the current main language hyph dict
local alg_left_hyphen_min = 2
local alg_right_hyphen_min = 2
local hyph_alg = cre.getSelectedHyphDict()
local hyph_dict_info = HYPH_DICT_NAME_TO_LANG_NAME_TAG[hyph_alg]
if hyph_dict_info then
alg_left_hyphen_min = hyph_dict_info[3] or 2
alg_right_hyphen_min = hyph_dict_info[4] or 2
end
local hyph_limits_widget = DoubleSpinWidget:new{
-- Min (1) and max (10) values are enforced by crengine
-- Note that when hitting "Use language defaults", we show the default
@ -379,8 +412,9 @@ When the book's language tag is not among our presets, no specific features will
right_default = alg_right_hyphen_min,
-- let room on the widget sides so we can see
-- the hyphenation changes happening
width_factor = 0.6,
default_text = T(_("Language defaults: %1 / %2"), alg_left_hyphen_min, alg_right_hyphen_min),
width = math.floor(Screen:getWidth() * 0.6),
default_values = true,
default_text = _("Use language defaults"),
title_text = _("Hyphenation limits"),
info_text = _([[
Set minimum length before hyphenation occurs.
@ -388,9 +422,6 @@ These settings will apply to all books with any hyphenation dictionary.
'Use language defaults' resets them.]]),
keep_shown_on_apply = true,
callback = function(left_hyphen_min, right_hyphen_min)
if left_hyphen_min == alg_left_hyphen_min and right_hyphen_min == alg_right_hyphen_min then
left_hyphen_min, right_hyphen_min = nil, nil -- don't store default values
end
G_reader_settings:saveSetting("hyph_left_hyphen_min", left_hyphen_min)
G_reader_settings:saveSetting("hyph_right_hyphen_min", right_hyphen_min)
self.ui.document:setHyphLeftHyphenMin(G_reader_settings:readSetting("hyph_left_hyphen_min") or 0)
@ -421,13 +452,13 @@ These settings will apply to all books with any hyphenation dictionary.
return hyph_trust_soft_hyphens and _("Disable") or _("Disable (★)")
end,
choice1_callback = function()
G_reader_settings:makeFalse("hyph_trust_soft_hyphens")
G_reader_settings:saveSetting("hyph_trust_soft_hyphens", false)
end,
choice2_text_func = function()
return hyph_trust_soft_hyphens and _("Enable (★)") or _("Enable")
end,
choice2_callback = function()
G_reader_settings:makeTrue("hyph_trust_soft_hyphens")
G_reader_settings:saveSetting("hyph_trust_soft_hyphens", true)
end,
})
end,
@ -437,8 +468,8 @@ These settings will apply to all books with any hyphenation dictionary.
enabled_func = function()
return self.hyphenation and not self.hyph_soft_hyphens_only
end,
separator = true,
})
table.insert(hyphenation_submenu, self.ui.userhyph:getMenuEntry())
table.insert(hyphenation_submenu, {
text_func = function()
-- Show the current language default hyph dict (ie: English_US for zh)
@ -478,13 +509,13 @@ These settings will apply to all books with any hyphenation dictionary.
return hyph_force_algorithmic and _("Disable") or _("Disable (★)")
end,
choice1_callback = function()
G_reader_settings:makeFalse("hyph_force_algorithmic")
G_reader_settings:saveSetting("hyph_force_algorithmic", false)
end,
choice2_text_func = function()
return hyph_force_algorithmic and _("Enable (★)") or _("Enable")
end,
choice2_callback = function()
G_reader_settings:makeTrue("hyph_force_algorithmic")
G_reader_settings:saveSetting("hyph_force_algorithmic", true)
end,
})
end,
@ -498,7 +529,7 @@ These settings will apply to all books with any hyphenation dictionary.
end,
})
table.insert(hyphenation_submenu, {
text = _("Soft hyphens only"),
text = _("Soft-hyphens only"),
callback = function()
self.hyph_soft_hyphens_only = not self.hyph_soft_hyphens_only
self.hyph_force_algorithmic = false
@ -515,13 +546,13 @@ These settings will apply to all books with any hyphenation dictionary.
return hyph_soft_hyphens_only and _("Disable") or _("Disable (★)")
end,
choice1_callback = function()
G_reader_settings:makeFalse("hyph_soft_hyphens_only")
G_reader_settings:saveSetting("hyph_soft_hyphens_only", false)
end,
choice2_text_func = function()
return hyph_soft_hyphens_only and _("Enable (★)") or _("Enable")
end,
choice2_callback = function()
G_reader_settings:makeTrue("hyph_soft_hyphens_only")
G_reader_settings:saveSetting("hyph_soft_hyphens_only", true)
end,
})
end,
@ -552,17 +583,6 @@ These settings will apply to all books with any hyphenation dictionary.
sub_item_table = hyphenation_submenu,
})
table.insert(self.menu_table, {
-- @translators See https://en.wikipedia.org/wiki/Hanging_punctuation
text = _("Hanging punctuation"),
checked_func = function() return self.floating_punctuation == 1 end,
callback = function()
self.floating_punctuation = self.floating_punctuation == 1 and 0 or 1
self:onToggleFloatingPunctuation(self.floating_punctuation)
end,
hold_callback = function() self:makeDefaultFloatingPunctuation() end,
})
self.ui.menu:registerToMainMenu(self)
end
@ -578,42 +598,9 @@ function ReaderTypography:addToMainMenu(menu_items)
}
end
function ReaderTypography:onToggleFloatingPunctuation(toggle)
-- for some reason the toggle value read from history files may stay boolean
-- and there seems no more elegant way to convert boolean values to numbers
if toggle == true then
toggle = 1
elseif toggle == false then
toggle = 0
end
self.ui.document:setFloatingPunctuation(toggle)
self.ui:handleEvent(Event:new("UpdatePos"))
end
function ReaderTypography:makeDefaultFloatingPunctuation()
local floating_punctuation = G_reader_settings:isTrue("floating_punctuation")
UIManager:show(MultiConfirmBox:new{
text = floating_punctuation and _("Would you like to enable or disable hanging punctuation by default?\n\nThe current default (★) is enabled.")
or _("Would you like to enable or disable hanging punctuation by default?\n\nThe current default (★) is disabled."),
choice1_text_func = function()
return floating_punctuation and _("Disable") or _("Disable (★)")
end,
choice1_callback = function()
G_reader_settings:makeFalse("floating_punctuation")
end,
choice2_text_func = function()
return floating_punctuation and _("Enable (★)") or _("Enable")
end,
choice2_callback = function()
G_reader_settings:makeTrue("floating_punctuation")
end,
})
end
function ReaderTypography:getCurrentDefaultHyphDictLanguage()
local hyph_dict_name = self.ui.document:getTextMainLangDefaultHyphDictionary()
local dict_info = self.HYPH_DICT_NAME_TO_LANG_NAME_TAG[hyph_dict_name]
local dict_info = HYPH_DICT_NAME_TO_LANG_NAME_TAG[hyph_dict_name]
if dict_info then
hyph_dict_name = dict_info[1]
else -- shouldn't happen
@ -675,59 +662,54 @@ end
-- in book settings, no default lang, and book has some language defined.
function ReaderTypography:onReadSettings(config)
-- Migrate old readerhyphenation setting, if one was set
if config:hasNot("text_lang") and config:has("hyph_alg") then
if not config:readSetting("text_lang") and config:readSetting("hyph_alg") then
local hyph_alg = config:readSetting("hyph_alg")
local dict_info = self.HYPH_DICT_NAME_TO_LANG_NAME_TAG[hyph_alg]
local dict_info = HYPH_DICT_NAME_TO_LANG_NAME_TAG[hyph_alg]
if dict_info then
config:saveSetting("text_lang", dict_info[2])
-- Set the other settings if the default hyph algo happens
-- to be one of these:
if hyph_alg == "@none" then
config:makeFalse("hyphenation")
config:saveSetting("hyphenation", false)
elseif hyph_alg == "@softhyphens" then
config:makeTrue("hyph_soft_hyphens_only")
config:saveSetting("hyph_soft_hyphens_only", true)
elseif hyph_alg == "@algorithm" then
config:makeTrue("hyph_force_algorithmic")
config:saveSetting("hyph_force_algorithmic", true)
end
end
end
-- Enable text lang tags attributes by default
if config:has("text_lang_embedded_langs") then
self.text_lang_embedded_langs = config:isTrue("text_lang_embedded_langs")
else
self.text_lang_embedded_langs = config:readSetting("text_lang_embedded_langs")
if self.text_lang_embedded_langs == nil then
self.text_lang_embedded_langs = G_reader_settings:nilOrTrue("text_lang_embedded_langs")
end
self.ui.document:setTextEmbeddedLangs(self.text_lang_embedded_langs)
-- Enable hyphenation by default
if config:has("hyphenation") then
self.hyphenation = config:isTrue("hyphenation")
else
self.hyphenation = config:readSetting("hyphenation")
if self.hyphenation == nil then
self.hyphenation = G_reader_settings:nilOrTrue("hyphenation")
end
self.ui.document:setTextHyphenation(self.hyphenation)
-- Checking for soft-hyphens adds a bit of overhead, so have it disabled by default
if config:has("hyph_trust_soft_hyphens") then
self.hyph_trust_soft_hyphens = config:isTrue("hyph_trust_soft_hyphens")
else
self.hyph_trust_soft_hyphens = config:readSetting("hyph_trust_soft_hyphens")
if self.hyph_trust_soft_hyphens == nil then
self.hyph_trust_soft_hyphens = G_reader_settings:isTrue("hyph_trust_soft_hyphens")
end
self.ui.document:setTrustSoftHyphens(self.hyph_trust_soft_hyphens)
-- Alternative hyphenation method (available with all dicts) to use soft hyphens only
if config:has("hyph_soft_hyphens_only") then
self.hyph_soft_hyphens_only = config:isTrue("hyph_soft_hyphens_only")
else
self.hyph_soft_hyphens_only = config:readSetting("hyph_soft_hyphens_only")
if self.hyph_soft_hyphens_only == nil then
self.hyph_soft_hyphens_only = G_reader_settings:isTrue("hyph_soft_hyphens_only")
end
self.ui.document:setTextHyphenationSoftHyphensOnly(self.hyph_soft_hyphens_only)
-- Alternative hyphenation method (available with all dicts) to use algorithmic hyphenation
if config:has("hyph_force_algorithmic") then
self.hyph_force_algorithmic = config:isTrue("hyph_force_algorithmic")
else
self.hyph_force_algorithmic = config:readSetting("hyph_force_algorithmic")
if self.hyph_force_algorithmic == nil then
self.hyph_force_algorithmic = G_reader_settings:isTrue("hyph_force_algorithmic")
end
self.ui.document:setTextHyphenationForceAlgorithmic(self.hyph_force_algorithmic)
@ -736,64 +718,55 @@ function ReaderTypography:onReadSettings(config)
self.ui.document:setHyphLeftHyphenMin(G_reader_settings:readSetting("hyph_left_hyphen_min") or 0)
self.ui.document:setHyphRightHyphenMin(G_reader_settings:readSetting("hyph_right_hyphen_min") or 0)
-- Default to disable hanging/floating punctuation
-- (Stored as 0/1 in docsetting for historical reasons, but as true/false
-- in global settings.)
if config:has("floating_punctuation") then
self.floating_punctuation = config:readSetting("floating_punctuation")
else
self.floating_punctuation = G_reader_settings:isTrue("floating_punctuation") and 1 or 0
end
self:onToggleFloatingPunctuation(self.floating_punctuation)
-- Decide and set the text main lang tag according to settings
if config:has("text_lang") then
self.allow_doc_lang_tag_override = false
-- Use the one manually set for this document
self.text_lang_tag = config:readSetting("text_lang")
self.allow_doc_lang_tag_override = false
-- Use the one manually set for this document
self.text_lang_tag = config:readSetting("text_lang")
if self.text_lang_tag then
logger.dbg("Typography lang: using", self.text_lang_tag, "from doc settings")
elseif G_reader_settings:has("text_lang_default") then
self.allow_doc_lang_tag_override = false
-- Use the one manually set as default (with Hold)
self.text_lang_tag = G_reader_settings:readSetting("text_lang_default")
self.ui.document:setTextMainLang(self.text_lang_tag)
return
end
-- Use the one manually set as default (with Hold)
self.text_lang_tag = G_reader_settings:readSetting("text_lang_default")
if self.text_lang_tag then
logger.dbg("Typography lang: using default ", self.text_lang_tag)
elseif G_reader_settings:has("text_lang_fallback") then
-- Document language will be allowed to override the one we set from now on
self.allow_doc_lang_tag_override = true
-- Use the one manually set as fallback (with Hold)
self.text_lang_tag = G_reader_settings:readSetting("text_lang_fallback")
self.ui.document:setTextMainLang(self.text_lang_tag)
return
end
-- Document language will be allowed to override the one we set from now on
self.allow_doc_lang_tag_override = true
-- Use the one manually set as fallback (with Hold)
self.text_lang_tag = G_reader_settings:readSetting("text_lang_fallback")
if self.text_lang_tag then
logger.dbg("Typography lang: using fallback ", self.text_lang_tag, ", might be overriden by doc language")
else
self.allow_doc_lang_tag_override = true
-- None decided, use default (shouldn't be reached)
self.text_lang_tag = self.DEFAULT_LANG_TAG
logger.dbg("Typography lang: no lang set, using", self.text_lang_tag)
self.ui.document:setTextMainLang(self.text_lang_tag)
return
end
-- None decided, use default (shouldn't be reached)
self.text_lang_tag = DEFAULT_LANG_TAG
logger.dbg("Typography lang: no lang set, using", self.text_lang_tag)
self.ui.document:setTextMainLang(self.text_lang_tag)
self.ui:handleEvent(Event:new("TypographyLanguageChanged"))
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
-- Add a menu item to language sub-menu, whether the lang is known or not, so the
-- user can see it and switch from and back to it easily
table.insert(self.language_submenu, 1, {
text = T(_("Book language: %1"), self.book_lang_tag or _("N/A")),
text = T(_("Book language: %1"), self.book_lang_tag or _("n/a")),
callback = function()
UIManager:show(InfoMessage:new{
text = T(_("Changed language for typography rules to book language: %1."), BD.wrap(self.book_lang_tag)),
timeout = 2,
})
self.text_lang_tag = self.book_lang_tag
self.ui.doc_settings:saveSetting("text_lang", self.text_lang_tag)
self.ui.document:setTextMainLang(self.text_lang_tag)
self.ui:handleEvent(Event:new("TypographyLanguageChanged"))
self.ui:handleEvent(Event:new("UpdatePos"))
end,
enabled_func = function()
@ -823,7 +796,6 @@ function ReaderTypography:onPreRenderDocument(config)
end
self.text_lang_tag = self.book_lang_tag
self.ui.document:setTextMainLang(self.text_lang_tag)
self.ui:handleEvent(Event:new("TypographyLanguageChanged"))
end
end
@ -834,7 +806,6 @@ function ReaderTypography:onSaveSettings()
self.ui.doc_settings:saveSetting("hyph_trust_soft_hyphens", self.hyph_trust_soft_hyphens)
self.ui.doc_settings:saveSetting("hyph_soft_hyphens_only", self.hyph_soft_hyphens_only)
self.ui.doc_settings:saveSetting("hyph_force_algorithmic", self.hyph_force_algorithmic)
self.ui.doc_settings:saveSetting("floating_punctuation", self.floating_punctuation)
end
return ReaderTypography

@ -1,328 +0,0 @@
local DataStorage = require("datastorage")
local Event = require("ui/event")
local FFIUtil = require("ffi/util")
local InfoMessage = require("ui/widget/infomessage")
local InputDialog = require("ui/widget/inputdialog")
local UIManager = require("ui/uimanager")
local Utf8Proc = require("ffi/utf8proc")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local lfs = require("libs/libkoreader-lfs")
local logger = require("logger")
local _ = require("gettext")
local T = require("ffi/util").template
-- if sometime in the future crengine is updated to use normalized utf8 for hypenation
-- this variable can be set to `true`. (see discussion in : https://github.com/koreader/crengine/pull/466),
-- and some `if NORM then` branches can be simplified.
local NORM = false
local ReaderUserHyph = WidgetContainer:extend{
-- return values from setUserHyphenationDict (crengine's UserHyphDict::init())
USER_DICT_RELOAD = 0,
USER_DICT_NOCHANGE = 1,
USER_DICT_MALFORMED = 2,
USER_DICT_ERROR_NOT_SORTED = 3,
}
-- returns path to the user dictionary
function ReaderUserHyph:getDictionaryPath()
return FFIUtil.joinPath(DataStorage:getSettingsDir(),
"user-" .. tostring(self.ui.document:getTextMainLangDefaultHyphDictionary():gsub(".pattern$", "")) .. ".hyph")
end
-- Load the user dictionary suitable for the actual language
-- 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)
-- this should only happen, if a user edits a dictionary by hand or the user messed
-- with the dictionary file by hand. -> Warning and disable.
if ret == self.USER_DICT_ERROR_NOT_SORTED then
if no_scrubbing then
UIManager:show(InfoMessage:new{
text = T(_("The user dictionary\n%1\nis not alphabetically sorted.\n\nIt will be disabled now."), name),
})
logger.warn("UserHyph: Dictionary " .. name .. " is not sorted alphabetically.")
G_reader_settings:makeFalse("hyph_user_dict")
else
self:scrubDictionary()
self:loadDictionary(name, reload, true)
end
elseif ret == self.USER_DICT_MALFORMED then
UIManager:show(InfoMessage:new{
text = T(_("The user dictionary\n%1\nhas corrupted entries.\n\nOnly valid entries will be used."), name),
})
logger.warn("UserHyph: Dictionary " .. name .. " has corrupted entries.")
end
else
logger.dbg("UserHyph: reset user hyphenation dict")
cre.setUserHyphenationDict("", true) -- clear crengine user hyph dict
end
end
-- Reload on change of the hyphenation language
function ReaderUserHyph:onTypographyLanguageChanged()
self:loadUserDictionary()
end
-- Reload on "ChangedUserDictionary" event,
-- doesn't load dictionary if filesize and filename haven't changed
-- if reload==true reload
function ReaderUserHyph:loadUserDictionary(reload)
self:loadDictionary(self:isAvailable() and self:getDictionaryPath() or "", reload and true or false)
self.ui:handleEvent(Event:new("UpdatePos"))
end
-- Functions to use with the UI
function ReaderUserHyph:isAvailable()
return G_reader_settings:isTrue("hyph_user_dict") and self:_enabled()
end
function ReaderUserHyph:_enabled()
return self.ui.typography.hyphenation
end
-- add Menu entry
function ReaderUserHyph:getMenuEntry()
return {
text = _("Custom hyphenation rules"),
help_text = _("The hyphenation of a word can be changed from its default by long pressing for 3 seconds and selecting 'Hyphenate'."),
callback = function()
local hyph_user_dict = not G_reader_settings:isTrue("hyph_user_dict")
G_reader_settings:saveSetting("hyph_user_dict", hyph_user_dict)
self:loadUserDictionary() -- not needed to force a reload here
end,
checked_func = function()
return self:isAvailable()
end,
enabled_func = function()
return self:_enabled()
end,
separator = true,
}
end
-- Helper functions for dictionary entries-------------------------------------------
-- checks if suggestion is well formated
function ReaderUserHyph:checkHyphenation(suggestion, word)
if suggestion:find("%-%-") then
return false -- two or more consecutive '-'
end
suggestion = suggestion:gsub("-","")
if Utf8Proc.lowercase(suggestion, NORM) == Utf8Proc.lowercase(word, NORM) then
return true -- characters match (case insensitive)
end
return false
end
function ReaderUserHyph:updateDictionary(word, hyphenation)
if not word then
logger.err("UserHyph: called without arguments")
end
local dict_file = self:getDictionaryPath()
local new_dict_file = dict_file .. ".new"
local new_dict = io.open(new_dict_file, "w")
if not new_dict then
logger.err("UserHyph: could not open " .. new_dict_file)
return
end
if NORM then
word = Utf8Proc.normalize_NFC(word)
end
local word_lower = Utf8Proc.lowercase(word, NORM)
local line
local dict = io.open(dict_file, "r")
if dict then
line = dict:read()
if NORM then
line = line and Utf8Proc.normalize_NFC(line)
end
--search entry
while line and Utf8Proc.lowercase(line:sub(1, line:find(";") - 1), NORM) < word_lower do
new_dict:write(line .. "\n")
line = dict:read()
if NORM then
line = line and Utf8Proc.normalize_NFC(line)
end
end
-- last word = nil if EOF, else last_word=word if found in file, else last_word is word after the new entry
if line then
local last_word = Utf8Proc.lowercase(line:sub(1, line:find(";") - 1), NORM)
if last_word == word_lower then
line = nil -- word found
end
else
line = nil -- EOF
end
end
-- write new entry
if hyphenation and hyphenation ~= "" then
new_dict:write(string.format("%s;%s\n", word, hyphenation))
end
-- write old entry if there was one
if line then
new_dict:write(line .. "\n")
end
if dict then
repeat
line = dict:read()
if NORM then
line = line and Utf8Proc.normalize_NFC(line)
end
if line then
new_dict:write(line .. "\n")
end
until (not line)
dict:close()
os.remove(dict_file)
end
new_dict:close()
os.rename(new_dict_file, dict_file)
self:loadUserDictionary(true) -- dictionary has changed, force a reload here
end
-- This is called when the file is badly sorted or has double entries (which should only happen
-- if a user has edited the hyphenation file by hand).
function ReaderUserHyph:scrubDictionary()
logger.dbg("UserHyph: scrubbing and sorting user hyphenation dict")
local dict_file = self:getDictionaryPath()
local dict = io.open(dict_file, "r")
if not dict then
return
end
local dict_entries = {}
local line = dict:read()
if NORM then
line = line and Utf8Proc.normalize_NFC(line)
end
while line do
table.insert(dict_entries, line)
line = dict:read()
if NORM then
line = line and Utf8Proc.normalize_NFC(line)
end
end
dict:close()
if #dict_entries == 1 then
return
end
table.sort(dict_entries, function(a,b) return Utf8Proc.lowercase(a, NORM) < Utf8Proc.lowercase(b, NORM) end)
-- remove double entries
local later_key = Utf8Proc.lowercase(dict_entries[#dict_entries]:gsub(";.*$",""), NORM)
for i = #dict_entries-1, 1, -1 do
local former_key = Utf8Proc.lowercase(dict_entries[i]:gsub(";.*$",""), NORM)
if later_key == former_key then
logger.dbg("UserHyph: remove double entry", dict_entries[i])
table.remove(dict_entries, i)
end
later_key = former_key
end
local new_dict_file = dict_file .. ".new"
local new_dict = io.open(new_dict_file, "w")
if not new_dict then
logger.err("UserHyph: could not open " .. new_dict_file)
return
end
for i = 1, #dict_entries do
new_dict:write(dict_entries[i], "\n")
end
new_dict:close()
os.remove(dict_file)
os.rename(new_dict_file, dict_file)
end
function ReaderUserHyph:modifyUserEntry(word)
if word:find("[ ,;-%.]") then return end -- no button if more than one word
if not self.ui.document then return end
if NORM then
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),
-- so we use crengine to trimm that.
word = suggested_hyphenation:gsub("-","")
local input_dialog
input_dialog = InputDialog:new{
title = T(_("Hyphenate: %1"), word),
description = _("Add hyphenation positions with hyphens ('-') or spaces (' ')."),
input = suggested_hyphenation,
old_hyph_lowercase = Utf8Proc.lowercase(suggested_hyphenation, NORM),
input_type = "string",
buttons = {
{
{
text = _("Cancel"),
id = "close",
callback = function()
UIManager:close(input_dialog)
end,
},
{
text = _("Remove"),
callback = function()
UIManager:close(input_dialog)
self:updateDictionary(word)
end,
},
{
text = _("Save"),
is_enter_default = true,
callback = function()
local new_suggestion = input_dialog:getInputText()
new_suggestion = new_suggestion:gsub(" ","-") -- replace spaces with hyphens
new_suggestion = new_suggestion:gsub("^-","") -- remove leading hypenations
new_suggestion = new_suggestion:gsub("-$","") -- remove trailing hypenations
if self:checkHyphenation(new_suggestion, word) then
-- don't save if no changes
if Utf8Proc.lowercase(new_suggestion, NORM) ~= input_dialog.old_hyph_lowercase then
self:updateDictionary(word, new_suggestion)
end
UIManager:close(input_dialog)
else
UIManager:show(InfoMessage:new{
text = _("Invalid hyphenation!"),
})
end
end,
},
},
},
}
UIManager:show(input_dialog)
input_dialog:onShowKeyboard()
end
return ReaderUserHyph

File diff suppressed because it is too large Load Diff

@ -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")
@ -24,27 +24,26 @@ local wikipedia_history = nil
local ReaderWikipedia = ReaderDictionary:extend{
-- identify itself
is_wiki = true,
wiki_languages = {},
disable_history = G_reader_settings:isTrue("wikipedia_disable_history"),
}
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
function ReaderWikipedia:lookupInput()
self.input_dialog = InputDialog:new{
title = _("Enter a word or phrase to look up"),
title = _("Enter words to look up on Wikipedia"),
input = "",
input_type = "text",
buttons = {
{
{
text = _("Cancel"),
id = "close",
callback = function()
UIManager:close(self.input_dialog)
end,
@ -53,10 +52,8 @@ function ReaderWikipedia:lookupInput()
text = _("Search Wikipedia"),
is_enter_default = true,
callback = function()
if self.input_dialog:getInputText() == "" then return end
UIManager:close(self.input_dialog)
-- Trust that input text does not need any cleaning (allows querying for "-suffix")
self:onLookupWikipedia(self.input_dialog:getInputText(), true)
self:onLookupWikipedia(self.input_dialog:getInputText())
end,
},
}
@ -101,29 +98,16 @@ function ReaderWikipedia:addToMainMenu(menu_items)
os.date("%Y-%m-%d %H:%M:%S", value.time),
text,
callback = function()
-- Word had been cleaned before being added to history
self:onLookupWikipedia(value.word, true, nil, value.page, value.lang)
self:onLookupWikipedia(value.word, nil, value.page, value.lang)
end
})
end
UIManager:show(KeyValuePage:new{
title = _("Wikipedia history"),
value_overflow_align = "right",
kv_pairs = kv_pairs,
})
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 = {
@ -165,7 +149,6 @@ function ReaderWikipedia:addToMainMenu(menu_items)
{
{
text = _("Cancel"),
id = "close",
callback = function()
UIManager:close(wikilang_input)
end,
@ -181,71 +164,123 @@ 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"),
text = _("Set Wikipedia 'Save as EPUB' directory"),
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")
if not default_dir then default_dir = G_reader_settings:readSetting("home_dir") end
if not default_dir then default_dir = require("apps/filemanager/filemanagerutil").getDefaultDir() end
local dialog
dialog = ButtonDialogTitle:new{
title = T(_("Current Wikipedia 'Save as EPUB' directory:\n\n%1\n"), BD.dirpath(default_dir)),
buttons = {
{
{
text = _("Keep this directory"),
callback = function()
UIManager:close(dialog)
end,
},
},
{
{
text = _("Select another directory"),
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")
if not dir then dir = G_reader_settings:readSetting("home_dir") end
if not dir then dir = require("apps/filemanager/filemanagerutil").getDefaultDir() end
if not dir then dir = "/" end
-- 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' directory 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 select an existing directory, or use a default directory named "Wikipedia" in your reader's home directory.
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' directory set to:\n%1"), BD.dirpath(wikipedia_dir)),
})
end,
cancel_text = _("Select directory"),
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
text = _("Save Wikipedia EPUB in current book folder"),
text = _("Save Wikipedia EPUB in current book directory"),
checked_func = function()
return G_reader_settings:isTrue("wikipedia_save_in_book_dir")
end,
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,
},
{
@ -260,18 +295,14 @@ You can choose an existing folder, or use a default folder named "Wikipedia" in
},
{
text = _("Clean Wikipedia history"),
enabled_func = function()
return wikipedia_history:has("wikipedia_history")
end,
keep_menu_open = true,
callback = function(touchmenu_instance)
callback = function()
UIManager:show(ConfirmBox:new{
text = _("Clean Wikipedia history?"),
ok_text = _("Clean"),
ok_callback = function()
-- empty data table to replace current one
wikipedia_history:reset{}
touchmenu_instance:updateItems()
end,
})
end,
@ -309,8 +340,7 @@ function ReaderWikipedia:initLanguages(word)
-- Fill self.wiki_languages with languages to propose
local wikipedia_languages = G_reader_settings:readSetting("wikipedia_languages")
if type(wikipedia_languages) == "table" and #wikipedia_languages > 0 then
-- use this setting, no need to guess: we reference the setting table, so
-- any update to it will have it saved in settings
-- use this setting, no need to guess
self.wiki_languages = wikipedia_languages
else
-- guess some languages
@ -329,7 +359,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
@ -345,37 +375,35 @@ function ReaderWikipedia:initLanguages(word)
end
end
function ReaderWikipedia:onLookupWikipedia(word, is_sane, box, get_fullpage, forced_lang)
function ReaderWikipedia:onLookupWikipedia(word, box, get_fullpage, forced_lang)
-- Wrapped through Trapper, as we may be using Trapper:dismissableRunInSubprocess() in it
Trapper:wrap(function()
self:lookupWikipedia(word, is_sane, box, get_fullpage, forced_lang)
self:lookupWikipedia(word, box, get_fullpage, forced_lang)
end)
return true
end
function ReaderWikipedia:lookupWikipedia(word, is_sane, box, get_fullpage, forced_lang)
if NetworkMgr:willRerunWhenOnline(function() self:lookupWikipedia(word, is_sane, box, get_fullpage, forced_lang) end) then
-- Not online yet, nothing more to do here, NetworkMgr will forward the callback and run it once connected!
function ReaderWikipedia:lookupWikipedia(word, box, get_fullpage, forced_lang)
if not NetworkMgr:isOnline() then
NetworkMgr:promptWifiOn()
return
end
-- word is the text to query. If get_fullpage is true, it is the
-- exact wikipedia page title we want the full page of.
self:initLanguages(word)
local lang
if forced_lang then
-- use provided lang (from readerlink when noticing that an external link is a wikipedia url,
-- of from Wikipedia lookup history, or when switching to next language in DictQuickLookup)
-- use provided lang (from readerlink when noticing that an external link is a wikipedia url)
lang = forced_lang
else
-- use first lang from self.wiki_languages
-- use first lang from self.wiki_languages, which may have been rotated by DictQuickLookup
lang = self.wiki_languages[1]
end
logger.dbg("lookup word:", word, box, get_fullpage)
-- no need to clean word if get_fullpage, as it is the exact wikipetia page title
if word and not get_fullpage then
-- escape quotes and other funny characters in word
word = self:cleanSelection(word, is_sane)
word = self:cleanSelection(word)
-- no need to lower() word with wikipedia search
end
logger.dbg("stripped word:", word)
@ -385,7 +413,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(),
@ -404,7 +438,7 @@ function ReaderWikipedia:lookupWikipedia(word, is_sane, box, get_fullpage, force
else
self.lookup_msg = T(_("Searching Wikipedia %2 for:\n%1"), "%1", lang:upper())
req_failure_text = _("Failed searching Wikipedia.")
no_result_text = _("No results.")
no_result_text = _("No Wikipedia articles matching search term.")
end
self:showLookupInfo(display_word)
@ -440,7 +474,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
@ -453,7 +487,7 @@ function ReaderWikipedia:lookupWikipedia(word, is_sane, box, get_fullpage, force
dict = T(_("Wikipedia %1"), lang:upper()),
word = page.title,
definition = definition,
is_wiki_fullpage = get_fullpage,
is_fullpage = get_fullpage,
lang = lang,
rtl_lang = Wikipedia:isWikipediaLanguageRTL(lang),
images = page.images,
@ -465,7 +499,7 @@ function ReaderWikipedia:lookupWikipedia(word, is_sane, box, get_fullpage, force
-- dummy results
local definition
if lookup_cancelled then
definition = _("Wikipedia request interrupted.")
definition = _("Wikipedia request canceled.")
elseif ok then
definition = no_result_text
else
@ -477,78 +511,25 @@ function ReaderWikipedia:lookupWikipedia(word, is_sane, box, get_fullpage, force
dict = T(_("Wikipedia %1"), lang:upper()),
word = word,
definition = definition,
is_wiki_fullpage = get_fullpage,
is_fullpage = get_fullpage,
lang = lang,
}
}
-- Also put this as a k/v into the results array: if we end up with this
-- after lang rotation, DictQuickLookup will not update this lang rotation.
results.no_result = true
logger.dbg("dummy result table:", word, results)
end
self:showDict(word, results, box)
end
function ReaderWikipedia:getWikiLanguages(first_lang)
-- Always return a copy of ours
local wiki_languages = {unpack(self.wiki_languages)}
local is_first_lang = first_lang == wiki_languages[1]
if not is_first_lang then
-- return a wiki_languages with requested lang at first
if util.arrayContains(wiki_languages, first_lang) then
-- first_lang in the list: rotate until it is first
while wiki_languages[1] ~= first_lang do
table.insert(wiki_languages, table.remove(wiki_languages, 1))
end
else
-- first_lang not in the list: add it first
table.insert(wiki_languages, 1, first_lang)
end
end
local update_wiki_languages_on_close = false
if DictQuickLookup.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
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)
-- update the main ReaderWikipedia.wiki_languages. That is, queries
-- from Wikipedia url links for another language, or from Wikipedia
-- lookup history with other languages (and any lang rotation made
-- 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
-- Another upper Wikipedia result: only this one may update it
update_wiki_languages_on_close = false
break
end
end
end
end
return wiki_languages, update_wiki_languages_on_close
end
function ReaderWikipedia:onUpdateWikiLanguages(wiki_languages)
-- Update our self.wiki_languages in-place
while table.remove(self.wiki_languages) do end
for _, lang in ipairs(wiki_languages) do
table.insert(self.wiki_languages, lang)
end
end
-- override onSaveSettings in ReaderDictionary
function ReaderWikipedia:onSaveSettings()
end
function ReaderWikipedia:onShowWikipediaLookup()
local connect_callback = function()
if NetworkMgr:isOnline() then
self:lookupInput()
else
NetworkMgr:promptWifiOn()
end
NetworkMgr:runWhenOnline(connect_callback)
return true
end

@ -1,11 +1,11 @@
local Cache = require("cache")
local ConfirmBox = require("ui/widget/confirmbox")
local Device = require("device")
local DocCache = require("document/doccache")
local Event = require("ui/event")
local Geom = require("ui/geometry")
local GestureRange = require("ui/gesturerange")
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 logger = require("logger")
local _ = require("gettext")
@ -13,258 +13,79 @@ 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
"page",
"pagewidth",
"pageheight",
"content",
"contentwidth",
"contentheight",
"columns",
"rows",
"manual",
},
zoom_mode_label = { -- const
page = _("page") .. " - " .. _("full"),
pagewidth = _("page") .. " - " .. _("width"),
pageheight = _("page") .. " - " .. _("height"),
content = _("content") .. " - " .. _("full"),
contentwidth = _("content") .. " - " .. _("width"),
contentheight = _("content") .. " - " .. _("height"),
columns = _("columns"),
rows = _("rows"),
manual = _("manual"),
},
zoom_genus_to_mode = { -- const
[4] = "page",
[3] = "content",
[2] = "columns",
[1] = "rows",
[0] = "manual",
},
zoom_mode_to_genus = { -- const
page = 4,
content = 3,
columns = 2,
rows = 1,
manual = 0,
},
zoom_type_to_mode = { -- const
[2] = "",
[1] = "width",
[0] = "height",
},
zoom_mode_to_type = { -- const
[""] = 2,
width = 1,
height = 0,
},
-- default to nil so we can trigger ZoomModeUpdate events on start up
zoom_mode = nil,
DEFAULT_ZOOM_MODE = "pagewidth",
-- for pan mode: fit to width/zoom_factor,
-- with overlap of zoom_overlap_h % (horizontally)
-- and zoom_overlap_v % (vertically).
kopt_zoom_factor = 1.5,
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."),
content = _("Zoom to fit content works best with page view."),
columns = _("Zoom to fit columns works best with page view."),
},
}
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",
ZoomToFitColumn = {
{ "Shift", "C" },
doc = "zoom to fit column",
event = "SetZoomMode", args = "colu"
},
}
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
zoom_mode = self.DEFAULT_ZOOM_MODE
end
-- Quick'n dirty zoom_mode to genus/type conversion...
local zgenus, ztype = zoom_mode:match("^(page)(%l*)$")
if not zgenus then
zgenus, ztype = zoom_mode:match("^(content)(%l*)$")
end
if not zgenus then
zgenus = zoom_mode
end
if not ztype then
ztype = ""
end
local zoom_mode_genus = self.zoom_mode_to_genus[zgenus]
local zoom_mode_type = self.zoom_mode_to_type[ztype]
return zoom_mode_genus, zoom_mode_type
end
function ReaderZooming:combo_to_mode(zoom_mode_genus, zoom_mode_type)
local default_genus, default_type = self:mode_to_combo(self.DEFAULT_ZOOM_MODE)
if not zoom_mode_genus then
zoom_mode_genus = default_genus
end
if not zoom_mode_type then
zoom_mode_type = default_type
end
local zoom_genus = self.zoom_genus_to_mode[zoom_mode_genus]
local zoom_type = self.zoom_type_to_mode[zoom_mode_type]
local zoom_mode
if zoom_genus == "page" or zoom_genus == "content" then
zoom_mode = zoom_genus .. zoom_type
else
zoom_mode = zoom_genus
end
return zoom_mode
end
-- Update the genus/type Configurables given a specific zoom_mode...
function ReaderZooming:_updateConfigurable(zoom_mode)
-- We may need to poke at the Configurable directly, because ReaderConfig is instantiated before us,
-- so simply updating the DocSetting doesn't cut it...
-- Technically ought to be conditional,
-- because this is an optional engine feature (only if self.document.info.configurable is true).
-- But the rest of the code (as well as most other modules) assumes this is supported on all paged engines (it is).
local configurable = self.document.configurable
local zoom_mode_genus, zoom_mode_type = self:mode_to_combo(zoom_mode)
-- Configurable keys aren't prefixed, unlike the actual settings...
configurable.zoom_mode_genus = zoom_mode_genus
configurable.zoom_mode_type = zoom_mode_type
return zoom_mode_genus, zoom_mode_type
self.ui.menu:registerToMainMenu(self)
end
function ReaderZooming:onReadSettings(config)
-- If we have a composite zoom_mode stored, use that
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
-- 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)
config:saveSetting("kopt_zoom_mode_genus", zoom_mode_genus)
config:saveSetting("kopt_zoom_mode_type", zoom_mode_type)
else
-- 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)
-- Validate it
zoom_mode = self.zoom_mode_label[zoom_mode] and zoom_mode or self.DEFAULT_ZOOM_MODE
end
-- Import legacy zoom_factor settings
if config:has("zoom_factor") and config:hasNot("kopt_zoom_factor") then
config:saveSetting("kopt_zoom_factor", config:readSetting("zoom_factor"))
self.document.configurable.zoom_factor = config:readSetting("kopt_zoom_factor")
config:delSetting("zoom_factor")
elseif config:has("zoom_factor") and config:has("kopt_zoom_factor") then
config:delSetting("zoom_factor")
end
-- Don't stomp on normal_zoom_mode in ReaderKoptListener if we're reflowed...
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
local zoom_mode = config:readSetting("zoom_mode") or
G_reader_settings:readSetting("zoom_mode") or
self.DEFAULT_ZOOM_MODE
self:setZoomMode(zoom_mode, true) -- avoid informative message on load
end
function ReaderZooming:onSaveSettings()
@ -340,114 +161,14 @@ function ReaderZooming:onZoom(direction)
return true
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 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]
local zoom_mode_type = self.zoom_type_to_mode[config.zoom_mode_type]
settings.zoom_overlap_h = config.zoom_overlap_h
settings.zoom_overlap_v = config.zoom_overlap_v
if btn == "set_zoom_overlap_h" then
self:_zoomPanChange(_("Set horizontal overlap"), "zoom_overlap_h")
settings.zoom_overlap_h = self.zoom_overlap_h
elseif btn == "set_zoom_overlap_v" then
self:_zoomPanChange(_("Set vertical overlap"), "zoom_overlap_v")
settings.zoom_overlap_v = self.zoom_overlap_v
end
local zoom_mode
if zoom_mode_genus == "page" or zoom_mode_genus == "content" then
zoom_mode = zoom_mode_genus .. zoom_mode_type
else
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
settings.zoom_mode = zoom_mode
if settings.right_to_left then
if settings.zoom_bottom_to_top then
config.writing_direction = 2
else
config.writing_direction = 1
end
else
config.writing_direction = 0
end
settings.right_to_left = nil
if zoom_mode == "columns" or zoom_mode == "rows" then
if btn ~= "columns" and btn ~= "rows" then
self.ui:handleEvent(Event:new("SetZoomPan", settings, true))
config.zoom_factor = self:setNumberOf(
zoom_mode,
zoom_range_number,
zoom_mode == "columns" and settings.zoom_overlap_h or settings.zoom_overlap_v
)
settings.kopt_zoom_factor = config.zoom_factor
end
elseif zoom_mode == "manual" then
if btn == "manual" then
config.zoom_factor = self:getNumberOf("columns")
settings.kopt_zoom_factor = config.zoom_factor
-- We *want* a redraw the first time we swap to manual mode (like any other mode swap)
self.ui:handleEvent(Event:new("SetZoomPan", settings))
else
self:setNumberOf("columns", zoom_factor)
-- No redraw here, because setNumberOf already took care of it
self.ui:handleEvent(Event:new("SetZoomPan", settings, true))
end
end
self.ui:handleEvent(Event:new("SetZoomMode", zoom_mode))
if btn == "columns" or btn == "rows" then
config.zoom_range_number = self:getNumberOf(
zoom_mode,
btn == "columns" and settings.zoom_overlap_h or settings.zoom_overlap_v
)
end
if when_applied_callback then
-- Provided when hide_on_apply, and ConfigDialog temporarily hidden:
-- show an InfoMessage with the values, and call when_applied_callback on dismiss
UIManager:show(InfoMessage:new{
text = T(_([[Zoom set to:
mode: %1
number of columns: %2
number of rows: %4
horizontal overlap: %3 %
vertical overlap: %5 %
zoom factor: %6]]),
self.zoom_mode_label[zoom_mode],
("%.2f"):format(self:getNumberOf("columns", settings.zoom_overlap_h)),
settings.zoom_overlap_h,
("%.2f"):format(self:getNumberOf("rows", settings.zoom_overlap_v)),
settings.zoom_overlap_v,
("%.2f"):format(config.zoom_factor)),
dismiss_callback = when_applied_callback,
})
end
end
function ReaderZooming:onSetZoomMode(new_mode)
self.view.zoom_mode = new_mode
if self.zoom_mode ~= new_mode then
logger.info("setting zoom mode to", new_mode)
self.ui:handleEvent(Event:new("ZoomModeUpdate", new_mode))
self.zoom_mode = new_mode
self:_updateConfigurable(new_mode)
self:setZoom()
if new_mode == "manual" then
self.ui:handleEvent(Event:new("SetScrollMode", false))
else
self.ui:handleEvent(Event:new("InitScrollPageStates", new_mode))
end
self.ui:handleEvent(Event:new("InitScrollPageStates", new_mode))
end
end
@ -520,9 +241,12 @@ end
function ReaderZooming:getZoom(pageno)
-- check if we're in bbox mode and work on bbox if that's the case
local zoom
local zoom = nil
local page_size = self.ui.document:getNativePageDimensions(pageno)
if not (self.zoom_mode and self.zoom_mode:match("^page") or self.ui.document.configurable.trim_page == 3) then
if self.zoom_mode == "content"
or self.zoom_mode == "contentwidth"
or self.zoom_mode == "contentheight"
or self.zoom_mode == "column" then
local ubbox_dimen = self.ui.document:getUsedBBoxDimensions(pageno, 1)
-- if bbox is larger than the native page dimension render the full page
-- See discussion in koreader/koreader#970.
@ -539,7 +263,7 @@ function ReaderZooming:getZoom(pageno)
-- calculate zoom value:
local zoom_w = self.dimen.w
local zoom_h = self.dimen.h
if self.ui.view.footer_visible and not self.ui.view.footer.settings.reclaim_height then
if self.ui.view.footer_visible then
zoom_h = zoom_h - self.ui.view.footer:getHeight()
end
if self.rotation % 180 == 0 then
@ -559,19 +283,16 @@ function ReaderZooming:getZoom(pageno)
end
elseif self.zoom_mode == "contentwidth" or self.zoom_mode == "pagewidth" then
zoom = zoom_w
elseif self.zoom_mode == "column" then
zoom = zoom_w * 2
elseif self.zoom_mode == "contentheight" or self.zoom_mode == "pageheight" then
zoom = zoom_h
elseif self.zoom_mode == "free" then
zoom = self.zoom
else
local zoom_factor = self.ui.doc_settings:readSetting("kopt_zoom_factor")
or G_reader_settings:readSetting("kopt_zoom_factor")
or self.kopt_zoom_factor
zoom = zoom_w * zoom_factor
end
if zoom and zoom > 10 and not DocCache:willAccept(zoom * (self.dimen.w * self.dimen.h + 512)) then
if zoom and zoom > 10 and not Cache:willAccept(zoom * (self.dimen.w * self.dimen.h + 64)) then
logger.dbg("zoom too large, adjusting")
while not DocCache:willAccept(zoom * (self.dimen.w * self.dimen.h + 512)) do
while not Cache:willAccept(zoom * (self.dimen.w * self.dimen.h + 64)) do
if zoom > 100 then
zoom = zoom - 50
elseif zoom > 10 then
@ -588,7 +309,7 @@ function ReaderZooming:getZoom(pageno)
if zoom < 0 then return 0 end
end
end
return zoom, zoom_w, zoom_h
return zoom
end
function ReaderZooming:getRegionalZoomCenter(pageno, pos)
@ -623,123 +344,68 @@ function ReaderZooming:genSetZoomModeCallBack(mode)
end
end
function ReaderZooming:setZoomMode(mode, no_warning, is_reflowed)
if not no_warning and self.ui.view.page_scroll then
local message
if self.paged_modes[mode] then
message = T(_([[
function ReaderZooming:setZoomMode(mode, no_warning)
if not no_warning and self.ui.view.page_scroll and self.paged_modes[mode] then
UIManager:show(InfoMessage:new{
text = T(_([[
%1
In combination with continuous view (scroll mode), this can cause unexpected vertical shifts when turning pages.]]),
self.paged_modes[mode])
elseif self.zoom_mode == "manual" then
message = _([[
Manual zoom works best with page view.
Please enable page view instead of continuous view (scroll mode).]])
end
if message then
UIManager:show(InfoMessage:new{text = message, timeout = 5})
end
In combination with continuous view (scroll mode), this can cause unexpected vertical shifts when turning pages.]]), self.paged_modes[mode]),
timeout = 5,
})
end
-- Dirty hack to prevent ReaderKoptListener from stomping on normal_zoom_mode...
self.ui:handleEvent(Event:new("SetZoomMode", mode, is_reflowed and "koptlistener"))
self.ui:handleEvent(Event:new("SetZoomMode", mode))
self.ui:handleEvent(Event:new("InitScrollPageStates"))
end
local function _getOverlapFactorForNum(n, overlap)
-- Auxiliary function to "distribute" an overlap between tiles
overlap = overlap * (n - 1) / n
return (100 / (100 - overlap))
end
function ReaderZooming:getNumberOf(what, overlap)
-- Number of columns (if what ~= "rows") or rows (if what == "rows")
local zoom, zoom_w, zoom_h = self:getZoom(self.current_page)
local zoom_factor = zoom / (what == "rows" and zoom_h or zoom_w)
if overlap then
overlap = (what == "rows" and self.zoom_overlap_v or self.zoom_overlap_h)
zoom_factor = (overlap - 100 * zoom_factor) / (overlap - 100) -- Thanks Xcas for this one...
end
return zoom_factor
end
function ReaderZooming:setNumberOf(what, num, overlap)
-- Sets number of columns (if what ~= "rows") or rows (if what == "rows")
local _, zoom_w, zoom_h = self:getZoom(self.current_page)
local overlap_factor = overlap and _getOverlapFactorForNum(num, overlap) or 1
local zoom_factor = num / overlap_factor
if what == "rows" then
zoom_factor = zoom_factor * zoom_h / zoom_w
end
self.ui:handleEvent(Event:new("SetZoomPan", {kopt_zoom_factor = zoom_factor}))
return zoom_factor
end
function ReaderZooming:_zoomFactorChange(title_text, direction, precision)
local zoom_factor, overlap = self:getNumberOf(direction)
UIManager:show(SpinWidget:new{
value = zoom_factor,
value_min = 0.1,
value_max = 10,
value_step = 0.1,
value_hold_step = 1,
precision = "%.1f",
ok_text = title_text,
title_text = title_text,
callback = function(spin)
zoom_factor = spin.value
self:setNumberOf(direction, zoom_factor, overlap)
end
})
end
function ReaderZooming:_zoomPanChange(text, setting)
UIManager:show(SpinWidget:new{
value = self[setting],
value_min = 0,
value_max = 90,
value_step = 1,
value_hold_step = 10,
ok_text = _("Set"),
title_text = text,
callback = function(spin)
self.ui:handleEvent(Event:new("SetZoomPan", {[setting] = spin.value}))
function ReaderZooming:addToMainMenu(menu_items)
if self.ui.document.info.has_pages then
local function getZoomModeMenuItem(text, mode, separator)
return {
text_func = function()
local default_zoom_mode = G_reader_settings:readSetting("zoom_mode") or self.DEFAULT_ZOOM_MODE
return text .. (mode == default_zoom_mode and "" or "")
end,
checked_func = function()
return self.zoom_mode == mode
end,
callback = self:genSetZoomModeCallBack(mode),
hold_callback = function(touchmenu_instance)
self:makeDefault(mode, touchmenu_instance)
end,
separator = separator,
}
end
})
end
function ReaderZooming:onZoomFactorChange()
self:_zoomFactorChange(_("Set Zoom factor"), false, "%.1f")
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
-- Configurable keys aren't prefixed...
local configurable_key = k:gsub("^kopt_", "")
if self.ui.document.configurable[configurable_key] then
self.ui.document.configurable[configurable_key] = v
end
end
if not no_redraw then
self.ui:handleEvent(Event:new("RedrawCurrentPage"))
menu_items.switch_zoom_mode = {
text = _("Switch zoom mode"),
enabled_func = function()
return self.ui.document.configurable.text_wrap ~= 1
end,
sub_item_table = {
getZoomModeMenuItem(_("Zoom to fit content width"), "contentwidth"),
getZoomModeMenuItem(_("Zoom to fit content height"), "contentheight", true),
getZoomModeMenuItem(_("Zoom to fit page width"), "pagewidth"),
getZoomModeMenuItem(_("Zoom to fit page height"), "pageheight", true),
getZoomModeMenuItem(_("Zoom to fit column"), "column"),
getZoomModeMenuItem(_("Zoom to fit content"), "content"),
getZoomModeMenuItem(_("Zoom to fit page"), "page"),
}
}
end
end
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
function ReaderZooming:makeDefault(zoom_mode, touchmenu_instance)
UIManager:show(ConfirmBox:new{
text = T(
_("Set default zoom mode to %1?"),
zoom_mode
),
ok_callback = function()
G_reader_settings:saveSetting("zoom_mode", zoom_mode)
if touchmenu_instance then touchmenu_instance:updateItems() end
end,
})
end
return ReaderZooming

@ -5,10 +5,10 @@ It works using data gathered from a document interface.
]]--
local BD = require("ui/bidi")
local Cache = require("cache")
local ConfirmBox = require("ui/widget/confirmbox")
local Device = require("device")
local DeviceListener = require("device/devicelistener")
local DocCache = require("document/doccache")
local DocSettings = require("docsettings")
local DocumentRegistry = require("document/documentregistry")
local Event = require("ui/event")
@ -20,12 +20,8 @@ local FileManagerShortcuts = require("apps/filemanager/filemanagershortcuts")
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")
@ -34,11 +30,10 @@ local ReaderCropping = require("apps/reader/modules/readercropping")
local ReaderDeviceStatus = require("apps/reader/modules/readerdevicestatus")
local ReaderDictionary = require("apps/reader/modules/readerdictionary")
local ReaderFont = require("apps/reader/modules/readerfont")
local ReaderGesture = require("apps/reader/modules/readergesture")
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")
local ReaderKoptListener = require("apps/reader/modules/readerkoptlistener")
local ReaderLink = require("apps/reader/modules/readerlink")
local ReaderMenu = require("apps/reader/modules/readermenu")
@ -50,31 +45,25 @@ local ReaderRolling = require("apps/reader/modules/readerrolling")
local ReaderSearch = require("apps/reader/modules/readersearch")
local ReaderStatus = require("apps/reader/modules/readerstatus")
local ReaderStyleTweak = require("apps/reader/modules/readerstyletweak")
local ReaderThumbnail = require("apps/reader/modules/readerthumbnail")
local ReaderToc = require("apps/reader/modules/readertoc")
local ReaderTypeset = require("apps/reader/modules/readertypeset")
local ReaderTypography = require("apps/reader/modules/readertypography")
local ReaderUserHyph = require("apps/reader/modules/readeruserhyph")
local ReaderView = require("apps/reader/modules/readerview")
local ReaderWikipedia = require("apps/reader/modules/readerwikipedia")
local ReaderZooming = require("apps/reader/modules/readerzooming")
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 T = ffiUtil.template
local Screen = require("device").screen
local T = require("ffi/util").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,40 +75,29 @@ local ReaderUI = InputContainer:extend{
password = nil,
postInitCallback = nil,
postReaderReadyCallback = nil,
postReaderCallback = nil,
}
function ReaderUI:registerModule(name, ui_module, always_active)
if name then
self[name] = ui_module
ui_module.name = "reader" .. name
end
table.insert(self, ui_module)
if always_active then
-- to get events even when hidden
table.insert(self.active_widgets, ui_module)
end
if name then self[name] = ui_module end
ui_module.name = "reader" .. name
table.insert(always_active and self.active_widgets or self, ui_module)
end
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 +107,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 +146,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 +158,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{
@ -199,10 +166,6 @@ function ReaderUI:init()
ui = self,
document = self.document,
})
self:registerModule("languagesupport", LanguageSupport:new{
ui = self,
document = self.document,
})
-- dictionary
self:registerModule("dictionary", ReaderDictionary:new{
dialog = self.dialog,
@ -225,7 +188,7 @@ function ReaderUI:init()
ui = self
}, true)
-- device status controller
self:registerModule("devicestatus", ReaderDeviceStatus:new{
self:registerModule("battery", ReaderDeviceStatus:new{
ui = self,
})
-- configurable controller
@ -255,15 +218,13 @@ function ReaderUI:init()
document = self.document,
})
end
-- activity indicator for when some settings take time to take effect (Kindle under KPV)
if not ReaderActivityIndicator:isStub() then
self:registerModule("activityindicator", ReaderActivityIndicator:new{
dialog = self.dialog,
view = self.view,
ui = self,
document = self.document,
})
end
-- activity indicator when some configurations take long take to affect
self:registerModule("activityindicator", ReaderActivityIndicator:new{
dialog = self.dialog,
view = self.view,
ui = self,
document = self.document,
})
end
-- for page specific controller
if self.document.info.has_pages then
@ -310,22 +271,15 @@ function ReaderUI:init()
end
-- make sure we render document first before calling any callback
self:registerPostInitCallback(function()
local start_time = time.now()
if not self.document:loadDocument() then
self:dealWithLoadDocumentFailure()
end
logger.dbg(string.format(" loading took %.3f seconds", time.to_s(time.since(start_time))))
-- used to read additional settings after the document has been
-- loaded (but not rendered yet)
self:handleEvent(Event:new("PreRenderDocument", self.doc_settings))
start_time = time.now()
self.document:render()
logger.dbg(string.format(" rendering took %.3f seconds", time.to_s(time.since(start_time))))
-- Uncomment to output the built DOM (for debugging)
-- logger.dbg(self.document:getHTMLFromXPointer(".0", 0x6830))
end)
-- styletweak controller (must be before typeset controller)
self:registerModule("styletweak", ReaderStyleTweak:new{
@ -335,20 +289,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
})
-- user hyphenation (must be registered before typography)
self:registerModule("userhyph", ReaderUserHyph:new{
dialog = self.dialog,
view = self.view,
ui = self
@ -361,7 +307,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,
@ -374,14 +319,7 @@ function ReaderUI:init()
ui = self
})
end
self.disable_double_tap = G_reader_settings:nilOrTrue("disable_double_tap")
-- scrolling (scroll settings + inertial scrolling)
self:registerModule("scrolling", ReaderScrolling:new{
pan_rate = pan_rate,
dialog = self.dialog,
ui = self,
view = self.view,
})
self.disable_double_tap = G_reader_settings:readSetting("disable_double_tap") ~= false
-- back location stack
self:registerModule("back", ReaderBack:new{
ui = self,
@ -399,11 +337,6 @@ function ReaderUI:init()
document = self.document,
view = self.view,
})
-- thumbnails service (book map, page browser)
self:registerModule("thumbnail", ReaderThumbnail:new{
ui = self,
document = self.document,
})
-- file searcher
self:registerModule("filesearcher", FileManagerFileSearcher:new{
dialog = self.dialog,
@ -436,12 +369,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(
@ -454,10 +381,27 @@ function ReaderUI:init()
})
if ok then
self:registerModule(plugin_module.name, plugin_or_err)
logger.dbg("RD loaded plugin", plugin_module.name,
logger.info("RD loaded plugin", plugin_module.name,
"at", plugin_module.path)
end
end
if Device:isTouchDevice() then
-- gesture manager
self:registerModule("gesture", ReaderGesture:new {
document = self.document,
view = self.view,
ui = self,
})
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.
@ -470,76 +414,27 @@ 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
Device:setIgnoreInput(false) -- Allow processing of events (on Android).
Input:inhibitInputUntil(0.2)
self.postReaderCallback = nil
-- print("Ordered registered gestures:")
-- for _, tzone in ipairs(self._ordered_touch_zones) do
-- print(" "..tzone.def.id)
-- end
if ReaderUI.instance == nil then
logger.dbg("Spinning up new ReaderUI instance", tostring(self))
else
-- Should never happen, given what we did in (do)showReader...
logger.err("ReaderUI instance mismatch! Opened", tostring(self), "while we still have an existing instance:", tostring(ReaderUI.instance), debug.traceback())
end
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)
@ -568,8 +463,8 @@ function ReaderUI:showFileManager(file)
local last_dir, last_file
if file then
last_dir = util.splitFilePathName(file)
last_file = file
last_dir, last_file = util.splitFilePathName(file)
last_dir = last_dir:match("(.*)/")
else
last_dir, last_file = self:getLastDirFile(true)
end
@ -580,71 +475,65 @@ function ReaderUI:showFileManager(file)
end
end
function ReaderUI:onShowingReader()
-- Allows us to optimize out a few useless refreshes in various CloseWidgets handlers...
self.tearing_down = true
self.dithered = nil
-- Don't enforce a "full" refresh, leave that decision to the next widget we'll *show*.
self:onClose(false)
end
-- Same as above, except we don't close it yet. Useful for plugins that need to close custom Menus before calling showReader.
function ReaderUI:onSetupShowReader()
self.tearing_down = true
self.dithered = nil
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
end
-- We can now signal the existing ReaderUI/FileManager instances that it's time to go bye-bye...
UIManager:broadcastEvent(Event:new("ShowingReader"))
-- prevent crash due to incompatible bookmarks
--- @todo Split bookmarks from metadata and do per-engine in conversion.
provider = provider or DocumentRegistry:getProvider(file)
if provider.provider then
self:showReaderCoroutine(file, provider, seamless)
local doc_settings = DocSettings:open(file)
local bookmarks = doc_settings:readSetting("bookmarks") or {}
if #bookmarks >= 1 and
((provider.provider == "crengine" and type(bookmarks[1].page) == "number") or
(provider.provider == "mupdf" and type(bookmarks[1].page) == "string")) then
UIManager:show(ConfirmBox:new{
text = T(_("The document '%1' with bookmarks or highlights was previously opened with a different engine. To prevent issues, bookmarks need to be deleted before continuing."),
BD.filepath(file)),
ok_text = _("Delete"),
ok_callback = function()
doc_settings:delSetting("bookmarks")
doc_settings:close()
self:showReaderCoroutine(file, provider)
end,
cancel_callback = function() self:showFileManager() end,
})
else
self:showReaderCoroutine(file, provider)
end
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,15 +542,12 @@ function ReaderUI:showReaderCoroutine(file, provider, seamless)
end)
end
function ReaderUI:doShowReader(file, provider, seamless)
if seamless then
UIManager:avoidFlashOnNextRepaint()
end
local _running_instance = nil
function ReaderUI:doShowReader(file, provider)
logger.info("opening file", file)
-- Only keep a single instance running
if ReaderUI.instance then
logger.warn("ReaderUI instance mismatch! Tried to spin up a new instance, while we still have an existing one:", tostring(ReaderUI.instance))
ReaderUI.instance:onClose()
-- keep only one instance running
if _running_instance then
_running_instance:onClose()
end
local document = DocumentRegistry:openDocument(file, provider)
if not document then
@ -683,25 +569,32 @@ 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
-- This is mostly for the few callers that bypass the coroutine shenanigans and call doShowReader directly,
-- instead of showReader...
-- Otherwise, showReader will have taken care of that *before* instantiating a new RD,
-- in order to ensure a sane ordering of plugins teardown -> instantiation.
UIManager:show(reader)
_running_instance = reader
local FileManager = require("apps/filemanager/filemanager")
if FileManager.instance then
FileManager.instance:onClose()
end
end
UIManager:show(reader, seamless and "ui" or "full")
function ReaderUI:_getRunningInstance()
return _running_instance
end
function ReaderUI:unlockDocumentWithPassword(document, try_again)
@ -713,7 +606,7 @@ function ReaderUI:unlockDocumentWithPassword(document, try_again)
{
{
text = _("Cancel"),
id = "close",
enabled = true,
callback = function()
self:closeDialog()
coroutine.resume(self._coroutine)
@ -721,6 +614,7 @@ function ReaderUI:unlockDocumentWithPassword(document, try_again)
},
{
text = _("OK"),
enabled = true,
callback = function()
local success = self:onVerifyPassword(document)
self:closeDialog()
@ -760,12 +654,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 +674,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,
@ -803,8 +693,6 @@ end
function ReaderUI:onClose(full_refresh)
logger.dbg("closing reader")
PluginLoader:finalize()
Device:notifyBookState(nil, nil)
if full_refresh == nil then
full_refresh = true
end
@ -814,23 +702,15 @@ function ReaderUI:onClose(full_refresh)
self:saveSettings()
end
if self.document ~= nil then
require("readhistory"):updateLastBookTime(self.tearing_down)
-- Serialize the most recently displayed page for later launch
DocCache:serialize(self.document.file)
logger.dbg("closing document")
self:notifyCloseDocument()
end
UIManager:close(self.dialog, full_refresh and "full")
end
function ReaderUI:onCloseWidget()
if ReaderUI.instance == self then
logger.dbg("Tearing down ReaderUI", tostring(self))
else
logger.warn("ReaderUI instance mismatch! Closed", tostring(self), "while the active one is supposed to be", tostring(ReaderUI.instance))
-- serialize last used items for later launch
Cache:serialize()
if _running_instance == self then
_running_instance = nil
end
ReaderUI.instance = nil
self._coroutine = nil
end
function ReaderUI:dealWithLoadDocumentFailure()
@ -839,6 +719,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 +733,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,43 +745,26 @@ 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
-- Mimic onShowingReader's refresh optimizations
self.tearing_down = true
self.dithered = nil
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
-- allow caller to do stuff between close an re-open
after_close_callback(file, provider)
end
self:showReader(file, provider, seamless)
self:showReader(file, provider)
end
function ReaderUI:switchDocument(new_file)
if not new_file then return end
-- Mimic onShowingReader's refresh optimizations
self.tearing_down = true
self.dithered = nil
self:handleEvent(Event:new("CloseReaderMenu"))
self:handleEvent(Event:new("CloseConfigMenu"))
self.highlight:onClose() -- close highlight dialog if any
self:onClose(false)
self:showReader(new_file)
end
@ -909,7 +773,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

@ -0,0 +1,451 @@
local BD = require("ui/bidi")
local Blitbuffer = require("ffi/blitbuffer")
local Button = require("ui/widget/button")
local CenterContainer = require("ui/widget/container/centercontainer")
local CloseButton = require("ui/widget/closebutton")
local Device = require("device")
local Event = require("ui/event")
local Font = require("ui/font")
local FrameContainer = require("ui/widget/container/framecontainer")
local Geom = require("ui/geometry")
local GestureRange = require("ui/gesturerange")
local HorizontalGroup = require("ui/widget/horizontalgroup")
local HorizontalSpan = require("ui/widget/horizontalspan")
local InputContainer = require("ui/widget/container/inputcontainer")
local LineWidget = require("ui/widget/linewidget")
local Math = require("optmath")
local MovableContainer = require("ui/widget/container/movablecontainer")
local OverlapGroup = require("ui/widget/overlapgroup")
local ProgressWidget = require("ui/widget/progresswidget")
local Size = require("ui/size")
local TextWidget = require("ui/widget/textwidget")
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 _ = require("gettext")
local Screen = Device.screen
local SkimToWidget = InputContainer:new{
title_face = Font:getFace("x_smalltfont"),
width = nil,
height = nil,
}
function SkimToWidget:init()
self.medium_font_face = Font:getFace("ffont")
self.screen_width = Screen:getWidth()
self.screen_height = Screen:getHeight()
self.span = math.ceil(self.screen_height * 0.01)
self.width = math.floor(self.screen_width * 0.95)
self.button_bordersize = Size.border.button
-- the buttons need some kind of separation but maybe I should just implement
-- margin_left and margin_right…
self.button_margin = self.button_bordersize
self.button_width = math.floor(self.screen_width * 0.16) - (2*self.button_margin)
if Device:hasKeys() then
self.key_events = {
Close = { {"Back"}, doc = "close skimto page" }
}
end
if Device:isTouchDevice() then
self.ges_events = {
TapProgress = {
GestureRange:new{
ges = "tap",
range = Geom:new{
x = 0, y = 0,
w = self.screen_width,
h = self.screen_height,
}
},
},
}
end
local dialog_title = _("Skim")
self.curr_page = self.ui:getCurrentPage()
self.page_count = self.document:getPageCount()
local curr_page_display = tostring(self.curr_page)
if self.ui.pagemap and self.ui.pagemap:wantsPageLabels() then
curr_page_display = self.ui.pagemap:getCurrentPageLabel(true)
end
local ticks_candidates = {}
if self.ui.toc then
local max_level = self.ui.toc:getMaxDepth()
for i = 0, -max_level, -1 do
local ticks = self.ui.toc:getTocTicks(i)
table.insert(ticks_candidates, ticks)
end
-- find the finest toc ticks by sorting out the largest one
table.sort(ticks_candidates, function(a, b) return #a > #b end)
end
if #ticks_candidates > 0 then
self.ticks_candidates = ticks_candidates[1]
end
local skimto_title = FrameContainer:new{
padding = Size.padding.default,
margin = Size.margin.title,
bordersize = 0,
TextWidget:new{
text = dialog_title,
face = self.title_face,
bold = true,
max_width = math.floor(self.screen_width * 0.95),
},
}
self.progress_bar = ProgressWidget:new{
width = math.floor(self.screen_width * 0.9),
height = Size.item.height_big,
percentage = self.curr_page / self.page_count,
ticks = self.ticks_candidates,
tick_width = Size.line.medium,
last = self.page_count,
}
self.skimto_progress = FrameContainer:new{
padding = Size.padding.button,
margin = Size.margin.small,
bordersize = 0,
self.progress_bar,
}
local skimto_line = LineWidget:new{
dimen = Geom:new{
w = self.width,
h = Size.line.thick,
}
}
local skimto_bar = OverlapGroup:new{
dimen = {
w = self.width,
h = skimto_title:getSize().h
},
skimto_title,
CloseButton:new{ window = self, padding_top = Size.margin.title, },
}
local button_minus = Button:new{
text = "-1",
bordersize = self.button_bordersize,
margin = self.button_margin,
radius = 0,
enabled = true,
width = self.button_width,
show_parent = self,
callback = function()
self:goToPage(self.curr_page - 1)
end,
}
local button_minus_ten = Button:new{
text = "-10",
bordersize = self.button_bordersize,
margin = self.button_margin,
radius = 0,
enabled = true,
width = self.button_width,
show_parent = self,
callback = function()
self:goToPage(self.curr_page - 10)
end,
}
local button_plus = Button:new{
text = "+1",
bordersize = self.button_bordersize,
margin = self.button_margin,
radius = 0,
enabled = true,
width = self.button_width,
show_parent = self,
callback = function()
self:goToPage(self.curr_page + 1)
end,
}
local button_plus_ten = Button:new{
text = "+10",
bordersize = self.button_bordersize,
margin = self.button_margin,
radius = 0,
enabled = true,
width = self.button_width,
show_parent = self,
callback = function()
self:goToPage(self.curr_page + 10)
end,
}
self.current_page_text = Button:new{
text = curr_page_display,
bordersize = 0,
margin = self.button_margin,
radius = 0,
padding = 0,
enabled = true,
width = math.floor(self.screen_width * 0.2) - (2*self.button_margin),
show_parent = self,
callback = function()
self.callback_switch_to_goto()
end,
}
local chapter_next_text = "▷│"
local chapter_prev_text = "│◁"
local bookmark_next_text = "☆▷"
local bookmark_prev_text = "◁☆"
if BD.mirroredUILayout() then
chapter_next_text, chapter_prev_text = chapter_prev_text, chapter_next_text
bookmark_next_text, bookmark_prev_text = bookmark_prev_text, bookmark_next_text
end
local button_chapter_next = Button:new{
text = chapter_next_text,
bordersize = self.button_bordersize,
margin = self.button_margin,
radius = 0,
enabled = true,
width = self.button_width,
show_parent = self,
callback = function()
local page = self:getNextChapter(self.curr_page)
if page and page >=1 and page <= self.page_count then
self:goToPage(page)
end
end,
hold_callback = function()
self:goToPage(self.page_count)
end,
}
local button_chapter_prev = Button:new{
text = chapter_prev_text,
bordersize = self.button_bordersize,
margin = self.button_margin,
radius = 0,
enabled = true,
width = self.button_width,
show_parent = self,
callback = function()
local page = self:getPrevChapter(self.curr_page)
if page and page >=1 and page <= self.page_count then
self:goToPage(page)
end
end,
hold_callback = function()
self:goToPage(1)
end,
}
local button_bookmark_next = Button:new{
text = bookmark_next_text,
bordersize = self.button_bordersize,
margin = self.button_margin,
radius = 0,
enabled = true,
width = self.button_width,
show_parent = self,
callback = function()
self:goToByEvent("GotoNextBookmarkFromPage")
end,
hold_callback = function()
local page = self.ui.bookmark:getLastBookmarkedPageFromPage(self.ui:getCurrentPage())
self:goToBookmark(page)
end,
}
local button_bookmark_prev = Button:new{
text = bookmark_prev_text,
bordersize = self.button_bordersize,
margin = self.button_margin,
radius = 0,
enabled = true,
width = self.button_width,
show_parent = self,
callback = function()
self:goToByEvent("GotoPreviousBookmarkFromPage")
end,
hold_callback = function()
local page = self.ui.bookmark:getFirstBookmarkedPageFromPage(self.ui:getCurrentPage())
self:goToBookmark(page)
end,
}
local horizontal_span_up = HorizontalSpan:new{ width = math.floor(self.screen_width * 0.2) }
local button_table_up = HorizontalGroup:new{
align = "center",
button_chapter_prev,
button_bookmark_prev,
horizontal_span_up,
button_bookmark_next,
button_chapter_next,
}
local vertical_group_up = VerticalGroup:new{ align = "center" }
local padding_span_up = VerticalSpan:new{ width = math.ceil(self.screen_height * 0.015) }
table.insert(vertical_group_up, padding_span_up)
table.insert(vertical_group_up, button_table_up)
table.insert(vertical_group_up, padding_span_up)
local button_table_down = HorizontalGroup:new{
align = "center",
button_minus,
button_minus_ten,
self.current_page_text,
button_plus_ten,
button_plus,
}
local vertical_group_down = VerticalGroup:new{ align = "center" }
local padding_span = VerticalSpan:new{ width = math.ceil(self.screen_height * 0.015) }
table.insert(vertical_group_down, padding_span)
table.insert(vertical_group_down, button_table_down)
table.insert(vertical_group_down, padding_span)
self.skimto_frame = FrameContainer:new{
radius = Size.radius.window,
bordersize = Size.border.window,
padding = 0,
margin = 0,
background = Blitbuffer.COLOR_WHITE,
VerticalGroup:new{
align = "center",
skimto_bar,
skimto_line,
vertical_group_up,
CenterContainer:new{
dimen = Geom:new{
w = skimto_line:getSize().w,
h = self.skimto_progress:getSize().h,
},
self.skimto_progress,
},
vertical_group_down,
}
}
self[1] = WidgetContainer:new{
align = "center",
dimen =Geom:new{
x = 0, y = 0,
w = self.screen_width,
h = self.screen_height,
},
MovableContainer:new{
-- alpha = 0.8,
self.skimto_frame,
}
}
end
function SkimToWidget:update()
if self.curr_page <= 0 then
self.curr_page = 1
end
if self.curr_page > self.page_count then
self.curr_page = self.page_count
end
self.progress_bar.percentage = self.curr_page / self.page_count
local curr_page_display = tostring(self.curr_page)
if self.ui.pagemap and self.ui.pagemap:wantsPageLabels() then
curr_page_display = self.ui.pagemap:getCurrentPageLabel(true)
end
self.current_page_text:setText(curr_page_display, self.current_page_text.width)
end
function SkimToWidget:addOriginToLocationStack(add_current)
-- Only add the page from which we launched the SkimToWidget
-- to the location stack, unless add_current = true
if not self.orig_page_added_to_stack or add_current then
self.ui.link:addCurrentLocationToStack()
self.orig_page_added_to_stack = true
end
end
function SkimToWidget:getNextChapter(cur_pageno)
local next_chapter = nil
for i = 1, #self.ticks_candidates do
if self.ticks_candidates[i] > cur_pageno then
next_chapter = self.ticks_candidates[i]
break
end
end
return next_chapter
end
function SkimToWidget:getPrevChapter(cur_pageno)
local previous_chapter = nil
for i = 1, #self.ticks_candidates do
if self.ticks_candidates[i] >= cur_pageno then
break
end
previous_chapter = self.ticks_candidates[i]
end
return previous_chapter
end
function SkimToWidget:onCloseWidget()
UIManager:setDirty(nil, function()
return "ui", self.skimto_frame.dimen
end)
return true
end
function SkimToWidget:onShow()
UIManager:setDirty(self, function()
return "ui", self.skimto_frame.dimen
end)
return true
end
function SkimToWidget:goToPage(page)
self.curr_page = page
self:addOriginToLocationStack()
self.ui:handleEvent(Event:new("GotoPage", self.curr_page))
self:update()
end
function SkimToWidget:goToBookmark(page)
if page then
self:addOriginToLocationStack()
self.ui.bookmark:gotoBookmark(page)
self.curr_page = self.ui:getCurrentPage()
self:update()
end
end
function SkimToWidget:goToByEvent(event_name)
if event_name then
self:addOriginToLocationStack()
self.ui:handleEvent(Event:new(event_name, false))
-- add_current_location_to_stack=false, as we handled it here
self.curr_page = self.ui:getCurrentPage()
self:update()
end
end
function SkimToWidget:onAnyKeyPressed()
UIManager:close(self)
return true
end
function SkimToWidget:onTapProgress(arg, ges_ev)
if ges_ev.pos:intersectWith(self.progress_bar.dimen) then
local perc = self.progress_bar:getPercentageFromPosition(ges_ev.pos)
if not perc then
return true
end
local page = Math.round(perc * self.page_count)
self:addOriginToLocationStack()
self.ui:handleEvent(Event:new("GotoPage", page ))
self.curr_page = page
self:update()
elseif not ges_ev.pos:intersectWith(self.skimto_frame.dimen) then
-- close if tap outside
self:onClose()
end
-- otherwise, do nothing (it's easy missing taping a button)
return true
end
function SkimToWidget:onClose()
UIManager:close(self)
return true
end
return SkimToWidget

@ -1,72 +1,51 @@
--[[
A LRU cache, based on https://github.com/starius/lua-lru
A global LRU cache
]]--
local DataStorage = require("datastorage")
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 md5 = require("ffi/MD5")
local Cache = {
-- Cache configuration:
-- Max storage space, in bytes...
size = nil,
-- ...Average item size, used to compute the amount of slots in the LRU.
avg_itemsize = nil,
-- Or, simply set the number of slots, with no storage space limitation.
-- c.f., GlyphCache, CatalogCache
slots = nil,
-- Should LRU call the object's onFree method on eviction? Implies using CacheItem instead of plain tables/objects.
-- c.f., DocCache
enable_eviction_cb = false,
-- Generally, only DocCache uses this
disk_cache = false,
cache_path = nil,
}
function Cache:new(o)
o = o or {}
setmetatable(o, self)
self.__index = self
if o.init then o:init() end
return o
local CanvasContext = require("document/canvascontext")
if CanvasContext.should_restrict_JIT then
require("jit").off(true, true)
end
function Cache:init()
if self.slots then
-- Caller doesn't care about storage space, just slot count
self.cache = lru.new(self.slots, nil, self.enable_eviction_cb)
else
-- Compute the amount of slots in the LRU based on the max size & the average item size
self.slots = math.ceil(self.size / self.avg_itemsize)
self.cache = lru.new(self.slots, self.size, self.enable_eviction_cb)
end
if self.disk_cache then
self.cached = self:_getDiskCache()
else
-- No need to go through our own check or even get methods if there's no disk cache, hit lru directly
self.check = self.cache.get
local function calcFreeMem()
local meminfo = io.open("/proc/meminfo", "r")
local freemem = 0
if meminfo then
for line in meminfo:lines() do
local free, buffer, cached, n
free, n = line:gsub("^MemFree:%s-(%d+) kB", "%1")
if n ~= 0 then freemem = freemem + tonumber(free)*1024 end
buffer, n = line:gsub("^Buffers:%s-(%d+) kB", "%1")
if n ~= 0 then freemem = freemem + tonumber(buffer)*1024 end
cached, n = line:gsub("^Cached:%s-(%d+) kB", "%1")
if n ~= 0 then freemem = freemem + tonumber(cached)*1024 end
end
meminfo:close()
end
return freemem
end
if not self.enable_eviction_cb or not self.size then
-- We won't be using CacheItem here, so we can pass the size manually if necessary.
-- e.g., insert's signature is now (key, value, [size]), instead of relying on CacheItem's size field.
self.insert = self.cache.set
-- With debug info (c.f., below)
--self.insert = self.set
end
local function calcCacheMemSize()
local min = DGLOBAL_CACHE_SIZE_MINIMUM
local max = DGLOBAL_CACHE_SIZE_MAXIMUM
local calc = calcFreeMem()*(DGLOBAL_CACHE_FREE_PROPORTION or 0)
return math.min(max, math.max(min, calc))
end
local cache_path = DataStorage:getDataDir() .. "/cache/"
--[[
-- return a snapshot of disk cached items for subsequent check
--]]
function Cache:_getDiskCache()
local function getDiskCache()
local cached = {}
for key_md5 in lfs.dir(self.cache_path) do
local file = self.cache_path .. key_md5
for key_md5 in lfs.dir(cache_path) do
local file = cache_path..key_md5
if lfs.attributes(file, "mode") == "file" then
cached[key_md5] = file
end
@ -74,46 +53,83 @@ function Cache:_getDiskCache()
return cached
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
logger.warn("Too much memory would be claimed by caching", key)
return
end
local Cache = {
-- cache configuration:
max_memsize = calcCacheMemSize(),
-- cache state:
current_memsize = 0,
-- associative cache
cache = {},
-- this will hold the LRU order of the cache
cache_order = {},
-- disk Cache snapshot
cached = getDiskCache(),
}
self.cache:set(key, object, object.size)
function Cache:new(o)
o = o or {}
setmetatable(o, self)
self.__index = self
return o
end
-- Accounting debugging
--self:_insertion_stats(key, object.size)
-- internal: remove reference in cache_order list
function Cache:_unref(key)
for i = #self.cache_order, 1, -1 do
if self.cache_order[i] == key then
table.remove(self.cache_order, i)
end
end
end
--[[
function Cache:set(key, object, size)
self.cache:set(key, object, size)
-- internal: free cache item
function Cache:_free(key)
if not self.cache[key] then return end
self.current_memsize = self.current_memsize - self.cache[key].size
self.cache[key]:onFree()
self.cache[key] = nil
end
-- Accounting debugging
self:_insertion_stats(key, size)
-- drop an item named via key from the cache
function Cache:drop(key)
self:_unref(key)
self:_free(key)
end
function Cache:_insertion_stats(key, size)
print(string.format("Cache %s (%d/%d) [%.2f/%.2f @ ~%db] inserted %db key: %s",
self,
self.cache:used_slots(), self.slots,
self.cache:used_size() / 1024 / 1024, (self.size or 0) / 1024 / 1024, self.cache:used_size() / self.cache:used_slots(),
size or 0, key))
function Cache:insert(key, object)
-- make sure that one key only exists once: delete existing
self:drop(key)
-- guarantee that we have enough memory in cache
if (object.size > self.max_memsize) then
logger.warn("too much memory claimed for", key)
return
end
-- delete objects that least recently used
-- (they are at the end of the cache_order array)
while self.current_memsize + object.size > self.max_memsize do
local removed_key = table.remove(self.cache_order)
self:_free(removed_key)
end
-- insert new object in front of the LRU order
table.insert(self.cache_order, 1, key)
self.cache[key] = object
self.current_memsize = self.current_memsize + object.size
end
--]]
--[[
-- check for cache item by key
-- check for cache item for key
-- if ItemClass is given, disk cache is also checked.
--]]
function Cache:check(key, ItemClass)
local value = self.cache:get(key)
if value then
return value
if self.cache[key] then
if self.cache_order[1] ~= key then
-- put key in front of the LRU list
self:_unref(key)
table.insert(self.cache_order, 1, key)
end
return self.cache[key]
elseif ItemClass then
local cached = self.cached[md5(key)]
local cached = self.cached[md5.sum(key)]
if cached then
local item = ItemClass:new{}
local ok, msg = pcall(item.load, item, cached)
@ -121,74 +137,64 @@ function Cache:check(key, ItemClass)
self:insert(key, item)
return item
else
logger.warn("Failed to load on-disk cache:", msg)
--- It's apparently unusable, purge it and refresh the snapshot.
os.remove(cached)
self:refreshSnapshot()
logger.warn("discard cache", msg)
end
end
end
end
-- Shortcut when disk_cache is disabled
function Cache:get(key)
return self.cache:get(key)
end
function Cache:willAccept(size)
-- We only allow a single object to fill 75% of the cache
return size*4 < self.size*3
end
-- Blank the cache
function Cache:clear()
self.cache:clear()
-- we only allow single objects to fill 75% of the cache
if size*4 < self.max_memsize*3 then
return true
end
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()
-- Nonsensical values? (!Linux), skip this.
if memtotal == nil then
return
function Cache:serialize()
-- calculate 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
-- If less that 20% of the total RAM is free, drop half the Cache...
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)))
self.cache:chop()
-- And finish by forcing a GC sweep now...
collectgarbage()
collectgarbage()
table.sort(sorted_caches, function(v1,v2) return v1.time > v2.time end)
-- only serialize the most recently used cache
local cache_size = 0
for _, key in ipairs(self.cache_order) do
local cache_item = self.cache[key]
-- only dump cache item that requests serialization explicitly
if cache_item.persistent and cache_item.dump then
local cache_full_path = cache_path..md5.sum(key)
local cache_file_exists = lfs.attributes(cache_full_path)
if cache_file_exists then break end
logger.dbg("dump cache item", key)
cache_size = cache_item:dump(cache_full_path) or 0
if cache_size > 0 then break end
end
end
end
-- Refresh the disk snapshot (mainly used by ui/data/onetime_migration)
function Cache:refreshSnapshot()
if not self.disk_cache then
return
-- set disk cache the same limit as memory cache
while cached_size + cache_size - self.max_memsize > 0 do
-- discard the least recently used cache
local discarded = table.remove(sorted_caches)
cached_size = cached_size - lfs.attributes(discarded.file, "size")
os.remove(discarded.file)
end
self.cached = self:_getDiskCache()
-- disk cache may have changes so need to refresh disk cache snapshot
self.cached = getDiskCache()
end
-- Evict the disk cache (ditto)
function Cache:clearDiskCache()
if not self.disk_cache then
return
end
for _, file in pairs(self.cached) do
os.remove(file)
-- blank the cache
function Cache:clear()
for k, _ in pairs(self.cache) do
self.cache[k]:onFree()
end
self:refreshSnapshot()
self.cache = {}
self.cache_order = {}
self.current_memsize = 0
end
return Cache

@ -3,25 +3,16 @@ Inheritable abstraction for cache items
--]]
local CacheItem = {
size = 128, -- some reasonable default for a small table.
size = 64, -- some reasonable default for simple Lua values / small tables
}
--- NOTE: As far as size estimations go, the assumption is that a key, value pair should roughly take two words,
--- 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).
-- c.f., TileCacheItem
function CacheItem:onFree()
end

@ -1,5 +1,3 @@
local ffiUtil = require("ffi/util")
local Configurable = {}
function Configurable:new(o)
@ -10,7 +8,7 @@ function Configurable:new(o)
end
function Configurable:reset()
for key, value in pairs(self) do
for key,value in pairs(self) do
local value_type = type(value)
if value_type == "number" or value_type == "string" then
self[key] = nil
@ -18,32 +16,37 @@ function Configurable:reset()
end
end
function Configurable:hash(list)
for key, value in ffiUtil.orderedPairs(self) do
function Configurable:hash(sep)
local hash = ""
for key,value in pairs(self) do
local value_type = type(value)
if value_type == "number" or value_type == "string" then
table.insert(list, value)
hash = hash..sep..value
end
end
return hash
end
function Configurable:loadDefaults(config_options)
-- reset configurable before loading new options
self:reset()
local prefix = config_options.prefix.."_"
for i=1, #config_options do
for i=1,#config_options do
local options = config_options[i].options
for j=1,#options do
local key = options[j].name
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
function Configurable:loadSettings(settings, prefix)
for key, value in pairs(self) do
for key,value in pairs(self) do
local value_type = type(value)
if value_type == "number" or value_type == "string"
or value_type == "table" then
@ -56,7 +59,7 @@ function Configurable:loadSettings(settings, prefix)
end
function Configurable:saveSettings(settings, prefix)
for key, value in pairs(self) do
for key,value in pairs(self) do
local value_type = type(value)
if value_type == "number" or value_type == "string"
or value_type == "table" then

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

@ -1,101 +1,99 @@
--[[--
This module provides development-only asserts and other debug guards.
Instead of a regular Lua @{assert}(), use @{dbg.dassert}() which can be toggled at runtime.
dbg.dassert(important_variable ~= nil)
For checking whether the input given to a function is sane, you can use @{dbg.guard}().
dbg:guard(NickelConf.frontLightLevel, "set",
function(new_intensity)
assert(type(new_intensity) == "number",
"Wrong brightness value type (expected number)!")
assert(new_intensity >= 0 and new_intensity <= 100,
"Wrong brightness value given!")
end)
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.
function Dbg:turnOn()
if self.is_on == true then return end
self.is_on = true
logger:setLevel(logger.levels.dbg)
Dbg_mt.__call = function(_, ...) return LvDEBUG(...) end
--- Pass a guard function to detect bad input values.
Dbg_mt.__call = function(dbg, ...) LvDEBUG(math.huge, ...) end
Dbg.guard = function(_, mod, method, pre_guard, post_guard)
local old_method = mod[method]
mod[method] = function(...)
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}().
Dbg.dassert = function(check, msg)
assert(check, msg)
return check
end
--- @todo close ev.log fd for children
-- create or clear ev log file
self.ev_log = io.open("ev.log", "w")
end
--- Turn off debug mode.
-- This should only be used in tests and at the user's request.
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
io.close(self.ev_log)
self.ev_log = nil
end
end
--- Turn on verbose mode.
-- This should only be used in tests and at the user's request.
function Dbg:setVerbose(verbose)
self.is_verbose = verbose
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(...)
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,60 +1,52 @@
local isAndroid, _ = pcall(require, "android")
local lfs = require("libs/libkoreader-lfs")
local util = require("ffi/util")
local function probeDevice()
if util.isSDL() then
return require("device/sdl/device")
end
if isAndroid then
util.noSDL()
return require("device/android/device")
end
local kindle_test_stat = lfs.attributes("/proc/usid")
if kindle_test_stat then
util.noSDL()
local kindle_sn = io.open("/proc/usid", "r")
if kindle_sn then
kindle_sn:close()
return require("device/kindle/device")
end
local kobo_test_stat = lfs.attributes("/bin/kobo_config.sh")
if kobo_test_stat then
util.noSDL()
local kg_test_stat = lfs.attributes("/bin/kobo_config.sh")
if kg_test_stat then
return require("device/kobo/device")
end
local pbook_test_stat = lfs.attributes("/ebrmain")
if pbook_test_stat then
util.noSDL()
return require("device/pocketbook/device")
end
local remarkable_test_stat = lfs.attributes("/usr/bin/xochitl")
if remarkable_test_stat then
util.noSDL()
return require("device/remarkable/device")
end
local sony_prstux_test_stat = lfs.attributes("/etc/PRSTUX")
if sony_prstux_test_stat then
util.noSDL()
return require("device/sony-prstux/device")
end
local cervantes_test_stat = lfs.attributes("/usr/bin/ntxinfo")
if cervantes_test_stat then
util.noSDL()
return require("device/cervantes/device")
end
-- add new ports here:
--
-- if --[[ implement a proper test instead --]] false then
-- util.noSDL()
-- return require("device/newport/device")
-- end
if util.isSDL() then
return require("device/sdl/device")
end
error("Could not find hardware abstraction for this platform. If you are trying to run the emulator, please ensure SDL is installed.")
end

@ -1,27 +1,27 @@
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")
local _ = require("gettext")
local T = FFIUtil.template
local T = require("ffi/util").template
local function yes() return true end
local function no() return false end
local function canUpdateApk()
-- disable updates on fdroid builds, since they manage their own repo.
return (android.prop.flavor ~= "fdroid")
end
local function getCodename()
local api = android.app.activity.sdkVersion
local codename = ""
if api > 30 then
codename = "S"
elseif api == 30 then
if api > 29 then
codename = "R"
elseif api == 29 then
codename = "Q"
@ -46,104 +46,97 @@ local function getCodename()
return codename
end
-- thirdparty app support
local external = require("device/thirdparty"):new{
dicts = {
{ "Aard2", "Aard2", false, "itkach.aard2", "aard2" },
{ "Alpus", "Alpus", false, "com.ngcomputing.fora.android", "search" },
{ "ColorDict", "ColorDict", false, "com.socialnmobile.colordict", "send" },
{ "Eudic", "Eudic", false, "com.eusoft.eudic", "send" },
{ "EudicPlay", "Eudic (Google Play)", false, "com.qianyan.eudic", "send" },
{ "Fora", "Fora Dict", false, "com.ngc.fora", "search" },
{ "ForaPro", "Fora Dict Pro", false, "com.ngc.fora.android", "search" },
{ "GoldenFree", "GoldenDict Free", false, "mobi.goldendict.android.free", "send" },
{ "GoldenPro", "GoldenDict Pro", false, "mobi.goldendict.android", "send" },
{ "Kiwix", "Kiwix", false, "org.kiwix.kiwixmobile", "text" },
{ "LookUp", "Look Up", false, "gaurav.lookup", "send" },
{ "LookUpPro", "Look Up Pro", false, "gaurav.lookuppro", "send" },
{ "Mdict", "Mdict", false, "cn.mdict", "send" },
{ "QuickDic", "QuickDic", false, "de.reimardoeffinger.quickdic", "quickdic" },
},
check = function(self, app)
return android.isPackageEnabled(app)
end,
}
local EXTERNAL_DICTS_AVAILABILITY_CHECKED = false
local EXTERNAL_DICTS = require("device/android/dictionaries")
local external_dict_when_back_callback = nil
local function getExternalDicts()
if not EXTERNAL_DICTS_AVAILABILITY_CHECKED then
EXTERNAL_DICTS_AVAILABILITY_CHECKED = true
for i, v in ipairs(EXTERNAL_DICTS) do
local package = v[4]
if android.isPackageEnabled(package) then
v[3] = true
end
end
end
return EXTERNAL_DICTS
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,
hasFrontlight = android.hasLights,
hasNaturalLight = android.isWarmthDevice,
hasColorScreen = function() return not android.isEink() end,
hasFrontlight = yes,
hasLightLevelFallback = yes,
canRestart = no,
canSuspend = no,
firmware_rev = android.app.activity.sdkVersion,
home_dir = android.getExternalStoragePath(),
display_dpi = android.lib.AConfiguration_getDensity(android.app.config),
isHapticFeedbackEnabled = yes,
isDefaultFullscreen = function() return android.app.activity.sdkVersion >= 19 end,
hasClipboard = yes,
hasOTAUpdates = android.ota.isEnabled,
hasOTARunning = function() return android.ota.isRunning end,
hasFastWifiStatusQuery = yes,
hasSystemFonts = yes,
hasOTAUpdates = canUpdateApk,
canOpenLink = yes,
openLink = function(self, link)
if not link or type(link) ~= "string" then return end
return android.openLink(link)
return android.openLink(link) == 0
end,
canImportFiles = function() return android.app.activity.sdkVersion >= 19 end,
hasExternalSD = function() return android.getExternalSdPath() end,
importFile = function(path) android.importFile(path) end,
isValidPath = function(path) return android.isPathInsideSandbox(path) end,
canShareText = yes,
doShareText = function(self, text, reason, title, mimetype)
android.sendText(text, reason, title, mimetype)
end,
doShareText = function(text) android.sendText(text) end,
canExternalDictLookup = yes,
getExternalDictLookupList = function() return external.dicts end,
getExternalDictLookupList = getExternalDicts,
doExternalDictLookup = function (self, text, method, callback)
external.when_back_callback = callback
local _, app, action = external:checkMethod("dict", method)
if action then
android.dictLookup(text, app, action)
external_dict_when_back_callback = callback
local package, action = nil
for i, v in ipairs(getExternalDicts()) do
if v[1] == method then
package = v[4]
action = v[5]
break
end
end
android.dictLookup(text, package, action)
end,
--[[
Disable jit on some modules on android to make koreader on Android more stable.
The strategy here is that we only use precious mcode memory (jitting)
on deep loops like the several blitting methods in blitbuffer.lua and
the pixel-copying methods in mupdf.lua. So that a small amount of mcode
memory (64KB) allocated when koreader is launched in the android.lua
is enough for the program and it won't need to jit other parts of lua
code and thus won't allocate mcode memory any more which by our
observation will be harder and harder as we run koreader.
]]--
should_restrict_JIT = true,
}
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 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"))
elseif ev.code == C.APP_CMD_DESTROY then
UIManager:quit()
return "SaveState"
elseif ev.code == C.APP_CMD_GAINED_FOCUS
or ev.code == C.APP_CMD_INIT_WINDOW
or ev.code == C.APP_CMD_WINDOW_REDRAW_NEEDED then
this.device.screen:_updateWindow()
elseif ev.code == C.APP_CMD_LOST_FOCUS
or ev.code == C.APP_CMD_TERM_WINDOW then
this.device.input:resetState()
elseif ev.code == C.APP_CMD_CONFIG_CHANGED then
-- orientation and size changes
if android.screen.width ~= android.getScreenWidth()
@ -151,72 +144,46 @@ function Device:init()
this.device.screen:resize()
local new_size = this.device.screen:getSize()
logger.info("Resizing screen to", new_size)
local FileManager = require("apps/filemanager/filemanager")
local Event = require("ui/event")
UIManager:broadcastEvent(Event:new("SetDimensions", new_size))
UIManager:broadcastEvent(Event:new("ScreenResize", new_size))
UIManager:broadcastEvent(Event:new("RedrawCurrentPage"))
if FileManager.instance then
FileManager.instance:reinit(FileManager.instance.path,
FileManager.instance.focused_file)
end
end
-- to-do: keyboard connected, disconnected
elseif ev.code == C.APP_CMD_RESUME then
if not android.prop.brokenLifecycle then
UIManager:broadcastEvent(Event:new("Resume"))
end
if external.when_back_callback then
external.when_back_callback()
external.when_back_callback = nil
EXTERNAL_DICTS_AVAILABILITY_CHECKED = false
if external_dict_when_back_callback then
external_dict_when_back_callback()
external_dict_when_back_callback = nil
end
if android.ota.isPending then
UIManager:scheduleIn(0.1, self.install)
local new_file = android.getIntent()
if new_file ~= nil and lfs.attributes(new_file, "mode") == "file" then
-- we cannot blit to a window here since we have no focus yet.
local InfoMessage = require("ui/widget/infomessage")
local BD = require("ui/bidi")
UIManager:scheduleIn(0.1, function()
UIManager:show(InfoMessage:new{
text = T(_("Opening file '%1'."), BD.filepath(new_file)),
timeout = 0.0,
})
end)
UIManager:scheduleIn(0.2, function()
require("apps/reader/readerui"):doShowReader(new_file)
end)
else
local new_file = android.getIntent()
if new_file ~= nil and lfs.attributes(new_file, "mode") == "file" then
-- we cannot blit to a window here since we have no focus yet.
local InfoMessage = require("ui/widget/infomessage")
local BD = require("ui/bidi")
UIManager:scheduleIn(0.1, function()
UIManager:show(InfoMessage:new{
text = T(_("Opening file '%1'."), BD.filepath(new_file)),
timeout = 0.0,
})
end)
UIManager:scheduleIn(0.2, function()
require("apps/reader/readerui"):doShowReader(new_file)
-- check if we're resuming from importing content.
local content_path = android.getLastImportedPath()
if content_path ~= nil then
local FileManager = require("apps/filemanager/filemanager")
UIManager:scheduleIn(0.5, function()
if FileManager.instance then
FileManager.instance:onRefresh()
else
FileManager:showFiles(content_path)
end
end)
else
-- check if we're resuming from importing content.
local content_path = android.getLastImportedPath()
if content_path ~= nil then
local FileManager = require("apps/filemanager/filemanager")
UIManager:scheduleIn(0.5, function()
if FileManager.instance then
FileManager.instance:onRefresh()
else
FileManager:showFiles(content_path)
end
end)
end
end
end
elseif ev.code == C.APP_CMD_PAUSE then
if not android.prop.brokenLifecycle then
UIManager:broadcastEvent(Event:new("RequestSuspend"))
end
elseif ev.code == C.AEVENT_POWER_CONNECTED then
UIManager:broadcastEvent(Event:new("Charging"))
elseif ev.code == C.AEVENT_POWER_DISCONNECTED then
UIManager:broadcastEvent(Event:new("NotCharging"))
elseif ev.code == C.AEVENT_DOWNLOAD_COMPLETE then
android.ota.isRunning = false
if android.isResumed() then
self:install()
else
android.ota.isPending = true
end
end
end,
hasClipboardText = function()
@ -250,9 +217,8 @@ function Device:init()
local timeout = G_reader_settings:readSetting("android_screen_timeout")
if timeout then
if timeout == C.AKEEP_SCREEN_ON_ENABLED
or timeout > C.AKEEP_SCREEN_ON_DISABLED
and android.settings.hasPermission("settings")
then
or (timeout > C.AKEEP_SCREEN_ON_DISABLED
and android.settings.canWrite()) then
android.timeout.set(timeout)
end
end
@ -278,11 +244,13 @@ function Device:init()
android.setBackButtonIgnored(true)
end
Generic.init(self)
end
-- check if we enable a custom light level for this activity
local last_value = G_reader_settings:readSetting("fl_last_level")
if type(last_value) == "number" and last_value >= 0 then
Device:setScreenBrightness(last_value)
end
function Device:UIManagerReady(uimgr)
UIManager = uimgr
Generic.init(self)
end
function Device:initNetworkManager(NetworkMgr)
@ -297,13 +265,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)
@ -311,7 +278,6 @@ function Device:performHapticFeedback(type)
end
function Device:setIgnoreInput(enable)
logger.dbg("android.setIgnoreInput", enable)
android.setIgnoreInput(enable)
end
@ -336,93 +302,35 @@ 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
-- fullscreen
-- to-do: implement fullscreen toggle in API19+
local function canToggleFullscreen()
local api = android.app.activity.sdkVersion
return api < 19, api
end
-- toggle fullscreen API 19+
function Device:_toggleFullscreenImmersive()
logger.dbg("ignoring fullscreen toggle, reason: always in immersive mode")
end
-- toggle fullscreen API 17-18
function Device:_toggleFullscreenLegacy()
local width = android.getScreenWidth()
local height = android.getScreenHeight()
-- NOTE: Since we don't do HW rotation here, this should always match width
local available_width = android.getScreenAvailableWidth()
local available_height = android.getScreenAvailableHeight()
local is_fullscreen = android.isFullscreen()
android.setFullscreen(not is_fullscreen)
G_reader_settings:saveSetting("disable_android_fullscreen", is_fullscreen)
self.fullscreen = android.isFullscreen()
if self.fullscreen then
self:setViewport(0, 0, width, height)
else
self:setViewport(0, 0, available_width, available_height)
end
end
-- toggle fullscreen API 14-16
function Device:_toggleStatusBarVisibility()
local is_fullscreen = android.isFullscreen()
android.setFullscreen(not is_fullscreen)
logger.dbg(string.format("requesting fullscreen: %s", not is_fullscreen))
local width = android.getScreenWidth()
local height = android.getScreenHeight()
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
end
local viewport = Geom:new{x=0, y=statusbar_height, w=width, h=new_height}
logger.info(string.format("Switching viewport to new geometry [x=%d,y=%d,w=%d,h=%d]",
0, statusbar_height, width, new_height))
self.screen:setViewport(viewport)
if is_fullscreen and self.viewport and self.viewport.y ~= 0 then
self.input:registerEventAdjustHook(
self.input.adjustTouchTranslate,
{x = 0 - self.viewport.x, y = 0 - self.viewport.y}
)
end
self.fullscreen = is_fullscreen
end
function Device:isAlwaysFullscreen()
return not canToggleFullscreen()
function Device:setScreenBrightness(level)
android.setScreenBrightness(level)
end
function Device:toggleFullscreen()
local is_fullscreen = android.isFullscreen()
logger.dbg(string.format("requesting fullscreen: %s", not is_fullscreen))
local dummy, api = canToggleFullscreen()
local api = android.app.activity.sdkVersion
if api >= 19 then
self:_toggleFullscreenImmersive()
elseif api >= 16 then
self:_toggleFullscreenLegacy()
logger.dbg("ignoring fullscreen toggle, reason: always in immersive mode")
elseif api < 19 and api >= 17 then
local width = android.getScreenWidth()
local height = android.getScreenHeight()
local available_height = android.getScreenAvailableHeight()
local is_fullscreen = android.isFullscreen()
android.setFullscreen(not is_fullscreen)
G_reader_settings:saveSetting("disable_android_fullscreen", is_fullscreen)
is_fullscreen = android.isFullscreen()
if is_fullscreen then
self:setViewport(0, 0, width, height)
else
self:setViewport(0, 0, width, available_height)
end
else
self:_toggleStatusBarVisibility()
logger.dbg("ignoring fullscreen toggle, reason: legacy api " .. api)
end
end
@ -430,8 +338,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,127 +359,20 @@ 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()
function Device:epdTest()
android.einkTest()
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
function Device:canExecuteScript(file)
local file_ext = string.lower(util.getFileNameSuffix(file))
if android.prop.flavor ~= "fdroid" and file_ext == "sh" then
return true
end
end
function Device:isValidPath(path)
-- the fast check
if android.isPathInsideSandbox(path) then
if android.prop.flavor ~= "fdroid" and file_ext == "sh" then
return true
end
-- the thorough check
local real_ext_storage = FFIUtil.realpath(android.getExternalStoragePath())
local real_path = FFIUtil.realpath(path)
if real_path then
return real_path:sub(1, #real_ext_storage) == real_ext_storage
else
return false
end
end
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).
UIManager:nextTick(function() self:_showLightDialog() end)
end
function Device:_showLightDialog()
local title = android.isEink() and _("Frontlight settings") or _("Light settings")
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
UIManager:broadcastEvent(Event:new("FrontlightStateChanged"))
elseif action == C.ALIGHTS_DIALOG_CANCEL then
logger.dbg("Dialog Cancel, brightness: " .. self.powerd.fl_intensity)
self.powerd:setIntensityHW(self.powerd.fl_intensity)
if android.isWarmthDevice() then
logger.dbg("Dialog Cancel, warmth: " .. self.powerd.fl_warmth)
self.powerd:setWarmth(self.powerd.fl_warmth)
end
end
end
function Device:untar(archive, extract_to)
return android.untar(archive, extract_to)
end
function Device:download(link, name, ok_text)
local ConfirmBox = require("ui/widget/confirmbox")
local InfoMessage = require("ui/widget/infomessage")
local ok = android.download(link, name)
if ok == C.ADOWNLOAD_EXISTS then
self:install()
elseif ok == C.ADOWNLOAD_OK then
android.ota.isRunning = true
UIManager:show(InfoMessage:new{
text = ok_text,
timeout = 3,
})
elseif ok == C.ADOWNLOAD_FAILED then
UIManager:show(ConfirmBox:new{
text = _("Your device seems to be unable to download packages.\nRetry using the browser?"),
ok_text = _("Retry"),
ok_callback = function() self:openLink(link) end,
})
end
end
function Device:install()
local ConfirmBox = require("ui/widget/confirmbox")
UIManager:show(ConfirmBox:new{
text = _("Update is ready. Install it now?"),
ok_text = _("Install"),
ok_callback = function()
UIManager:broadcastEvent(Event:new("FlushSettings"))
UIManager:tickAfterNext(function()
android.ota.install()
android.ota.isPending = false
end)
end,
})
end
-- todo: Wouldn't we like an android.deviceIdentifier() method, so we can use better default paths?
function Device:getDefaultCoverPath()
if android.prop.product == "ntx_6sl" then -- Tolino HD4 and other
return android.getExternalStoragePath() .. "/suspend_others.jpg"
else
return android.getExternalStoragePath() .. "/cover.jpg"
end
end
android.LOGI(string.format("Android %s - %s (API %d) - flavor: %s",

@ -0,0 +1,20 @@
local user_path = require("datastorage"):getDataDir() .. "/dictionaries.lua"
local ok, dicts = pcall(dofile, user_path)
if ok then
return dicts
else
return {
-- tested dictionary applications
{ "Aard2", "Aard2", false, "itkach.aard2", "aard2" },
{ "Alpus", "Alpus", false, "com.ngcomputing.fora.android", "search" },
{ "ColorDict", "ColorDict", false, "com.socialnmobile.colordict", "colordict" },
{ "Eudic", "Eudic", false, "com.eusoft.eudic", "send" },
{ "Fora", "Fora Dict", false, "com.ngc.fora", "search" },
{ "GoldenFree", "GoldenDict Free", false, "mobi.goldendict.android.free", "send" },
{ "GoldenPro", "GoldenDict Pro", false, "mobi.goldendict.android", "send" },
{ "Kiwix", "Kiwix", false, "org.kiwix.kiwixmobile", "text" },
{ "Mdict", "Mdict", false, "cn.mdict", "send" },
{ "QuickDic", "QuickDic", false, "de.reimardoeffinger.quickdic", "quickdic" },
}
end

@ -2,47 +2,16 @@ local BasePowerD = require("device/generic/powerd")
local _, android = pcall(require, "android")
local AndroidPowerD = BasePowerD:new{
fl_min = 0,
fl_max = 100,
fl_min = 0, fl_max = 25,
fl_intensity = 10,
}
function AndroidPowerD:frontlightIntensityHW()
return math.floor(android.getScreenBrightness() / self.bright_diff * self.fl_max)
return math.floor(android.getScreenBrightness() / 255 * self.fl_max)
end
function AndroidPowerD:setIntensityHW(intensity)
-- If the frontlight switch was off, turn it on.
android.enableFrontlightSwitch()
self.fl_intensity = intensity
android.setScreenBrightness(math.floor(intensity * self.bright_diff / self.fl_max))
self:_decideFrontlightState()
end
function AndroidPowerD:init()
local min_bright = android.getScreenMinBrightness()
self.bright_diff = android.getScreenMaxBrightness() - min_bright
-- if necessary scale fl_min:
-- do not use fl_min==0 if getScreenMinBrightness!=0,
-- because intenstiy==0 would mean to use system intensity
if min_bright ~= self.fl_min then
self.fl_min = math.ceil(min_bright * self.bright_diff / self.fl_max)
end
if self.device:hasNaturalLight() then
self.fl_warmth_min = android.getScreenMinWarmth()
self.fl_warmth_max = android.getScreenMaxWarmth()
self.warm_diff = self.fl_warmth_max - self.fl_warmth_min
end
end
function AndroidPowerD:setWarmthHW(warmth)
android.setScreenWarmth(warmth)
end
function AndroidPowerD:frontlightWarmthHW()
return android.getScreenWarmth() * self.warm_diff
android.setScreenBrightness(math.floor(255 * intensity / self.fl_max))
end
function AndroidPowerD:getCapacityHW()
@ -53,30 +22,4 @@ function AndroidPowerD:isChargingHW()
return android.isCharging()
end
function AndroidPowerD:turnOffFrontlightHW()
if not self:isFrontlightOnHW() then
return
end
android.setScreenBrightness(self.fl_min)
if android.hasStandaloneWarmth() then
android.setScreenWarmth(self.fl_warmth_min)
end
end
function AndroidPowerD:turnOnFrontlightHW(done_callback)
if self:isFrontlightOn() and self:isFrontlightOnHW() then
return
end
-- on devices with a software frontlight switch (e.g Tolinos), enable it
android.enableFrontlightSwitch()
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
end
return AndroidPowerD

@ -1,4 +1,5 @@
local Generic = require("device/generic/device")
local TimeVal = require("ui/timeval")
local logger = require("logger")
local function yes() return true end
@ -7,11 +8,38 @@ local function no() return false end
local function getProductId()
local ntxinfo_pcb = io.popen("/usr/bin/ntxinfo /dev/mmcblk0 | grep pcb | cut -d ':' -f2", "r")
if not ntxinfo_pcb then return 0 end
local product_id = ntxinfo_pcb:read("*number") or 0
local product_id = tonumber(ntxinfo_pcb:read()) or 0
ntxinfo_pcb:close()
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 wifi module is loaded.
if not file then return 0 end
-- 0 means not connected, 1 connected
local out = file:read("*all")
file:close()
-- strip NaN from file read (ie: line endings, error messages)
local carrier
if type(out) ~= "number" then
carrier = tonumber(out)
else
carrier = out
end
-- finally return if we're connected or not
if type(carrier) == "number" then
return carrier
else
return 0
end
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 +48,7 @@ local function isMassStorageSupported()
return true
end
local Cervantes = Generic:extend{
local Cervantes = Generic:new{
model = "Cervantes",
isCervantes = yes,
isAlwaysPortrait = yes,
@ -28,15 +56,12 @@ local Cervantes = Generic:extend{
touch_legacy = true, -- SingleTouch input events
touch_switch_xy = true,
touch_mirrored_x = true,
touch_probe_ev_epoch_time = true,
hasOTAUpdates = yes,
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 +78,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,
@ -93,21 +118,68 @@ local Cervantes4 = Cervantes:extend{
}
-- input events
local probeEvEpochTime
-- this function will update itself after the first touch event
probeEvEpochTime = function(self, ev)
local now = TimeVal:now()
-- This check should work as long as main UI loop is not blocked for more
-- than 10 minute before handling the first touch event.
if ev.time.sec <= now.sec - 600 then
-- time is seconds since boot, force it to epoch
probeEvEpochTime = function(_, _ev)
_ev.time = TimeVal:now()
end
ev.time = now
else
-- time is already epoch time, no need to do anything
probeEvEpochTime = function(_, _) end
end
end
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
if self.touch_probe_ev_epoch_time then
self.input:registerEventAdjustHook(function(_, ev)
probeEvEpochTime(_, ev)
end)
end
if self.touch_legacy then
self.input.handleTouchEv = self.input.handleTouchEvLegacy
end
end
-- Make sure the C BB cannot be used on devices with unsafe HW inversion, as otherwise NightMode would be ineffective.
function Cervantes:blacklistCBB()
local ffi = require("ffi")
local dummy = require("ffi/posix_h")
local C = ffi.C
-- NOTE: canUseCBB is never no on Cervantes ;).
if not self:canUseCBB() or not self:canHWInvert() then
logger.info("Blacklisting the C BB on this device")
if ffi.os == "Windows" then
C._putenv("KO_NO_CBB=true")
else
C.setenv("KO_NO_CBB", "true", 1)
end
-- Enforce the global setting, too, so the Dev menu is accurate...
G_reader_settings:saveSetting("dev_no_c_blitter", true)
end
end
function Cervantes:init()
self.screen = require("ffi/framebuffer_mxcfb"):new{device = self, debug = logger.dbg, is_always_portrait = self.isAlwaysPortrait()}
-- Blacklist the C BB before the first BB require...
self:blacklistCBB()
self.screen = require("ffi/framebuffer_mxcfb"):new{device = self, debug = logger.dbg}
-- Automagically set this so we never have to remember to do it manually ;p
if self:hasNaturalLight() and self.frontlight_settings and self.frontlight_settings.frontlight_mixer then
@ -152,20 +224,17 @@ end
-- wireless
function Cervantes:initNetworkManager(NetworkMgr)
function NetworkMgr:turnOffWifi(complete_callback)
logger.info("Cervantes: disabling Wi-Fi")
logger.info("Cervantes: disabling WiFi")
self:releaseIP()
os.execute("./disable-wifi.sh")
if complete_callback then
complete_callback()
end
end
function NetworkMgr:turnOnWifi(complete_callback, interactive)
logger.info("Cervantes: enabling Wi-Fi")
function NetworkMgr:turnOnWifi(complete_callback)
logger.info("Cervantes: enabling WiFi")
os.execute("./enable-wifi.sh")
return self:reconnectOrShowNetworkMenu(complete_callback, interactive)
end
function NetworkMgr:getNetworkInterfaceName()
return "eth0"
self:reconnectOrShowNetworkMenu(complete_callback)
end
NetworkMgr:setWirelessBackend("wpa_supplicant", {ctrl_interface = "/var/run/wpa_supplicant/eth0"})
function NetworkMgr:obtainIP()
@ -177,8 +246,32 @@ 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: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 +282,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 ------------

@ -1,10 +1,14 @@
local BasePowerD = require("device/generic/powerd")
local SysfsLight = require ("device/sysfs_light")
local PluginShare = require("pluginshare")
local battery_sysfs = "/sys/devices/platform/pmic_battery.1/power_supply/mc13892_bat/"
local CervantesPowerD = BasePowerD:new{
fl = nil,
fl_warmth = nil,
auto_warmth = false,
max_warmth_hour = 23,
fl_min = 0,
fl_max = 100,
@ -14,16 +18,17 @@ local CervantesPowerD = BasePowerD:new{
status_file = battery_sysfs .. 'status'
}
-- We can't read back the current state from the OS or hardware.
-- Use the last value stored in KOReader settings instead.
function CervantesPowerD:frontlightWarmthHW()
return G_reader_settings:readSetting("frontlight_warmth") or 0
end
function CervantesPowerD:_syncLightOnStart()
-- We can't read value from the OS or hardware.
-- Use last values stored in koreader settings.
local new_intensity = G_reader_settings:readSetting("frontlight_intensity") or nil
local is_frontlight_on = G_reader_settings:readSetting("is_frontlight_on") or nil
local new_warmth, auto_warmth = nil
if self.fl_warmth ~= nil then
new_warmth = G_reader_settings:readSetting("frontlight_warmth") or nil
auto_warmth = G_reader_settings:readSetting("frontlight_auto_warmth") or nil
end
if new_intensity ~= nil then
self.hw_intensity = new_intensity
@ -33,7 +38,17 @@ function CervantesPowerD:_syncLightOnStart()
self.initial_is_fl_on = is_frontlight_on
end
self.fl_warmth = self:frontlightWarmthHW()
local max_warmth_hour =
G_reader_settings:readSetting("frontlight_max_warmth_hour")
if max_warmth_hour then
self.max_warmth_hour = max_warmth_hour
end
if auto_warmth then
self.auto_warmth = true
self:calculateAutoWarmth()
elseif new_warmth ~= nil then
self.fl_warmth = new_warmth
end
if self.initial_is_fl_on == false and self.hw_intensity == 0 then
self.hw_intensity = 1
@ -46,18 +61,16 @@ function CervantesPowerD:init()
-- not be called)
self.hw_intensity = 20
self.initial_is_fl_on = true
self.autowarmth_job_running = false
if self.device:hasFrontlight() then
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")
@ -67,6 +80,7 @@ function CervantesPowerD:init()
end
end
self.fl = SysfsLight:new(self.device.frontlight_settings)
self.fl_warmth = 0
self:_syncLightOnStart()
else
local kobolight = require("ffi/kobolight")
@ -87,11 +101,15 @@ function CervantesPowerD:saveSettings()
local cur_intensity = self.fl_intensity
local cur_is_fl_on = self.is_fl_on
local cur_warmth = self.fl_warmth
local cur_auto_warmth = self.auto_warmth
local cur_max_warmth_hour = self.max_warmth_hour
-- Save intensity to koreader settings
G_reader_settings:saveSetting("frontlight_intensity", cur_intensity)
G_reader_settings:saveSetting("is_frontlight_on", cur_is_fl_on)
if cur_warmth ~= nil then
G_reader_settings:saveSetting("frontlight_warmth", cur_warmth)
G_reader_settings:saveSetting("frontlight_auto_warmth", cur_auto_warmth)
G_reader_settings:saveSetting("frontlight_max_warmth_hour", cur_max_warmth_hour)
end
end
end
@ -121,9 +139,47 @@ function CervantesPowerD:setIntensityHW(intensity)
self:_decideFrontlightState()
end
function CervantesPowerD:setWarmthHW(warmth)
function CervantesPowerD:setWarmth(warmth)
if self.fl == nil then return end
self.fl:setWarmth(warmth)
if not warmth and self.auto_warmth then
self:calculateAutoWarmth()
end
self.fl_warmth = warmth or self.fl_warmth
self.fl:setWarmth(self.fl_warmth)
end
function CervantesPowerD:calculateAutoWarmth()
local current_time = os.date("%H") + os.date("%M")/60
local max_hour = self.max_warmth_hour
local diff_time = max_hour - current_time
if diff_time < 0 then
diff_time = diff_time + 24
end
if diff_time < 12 then
-- We are before bedtime. Use a slower progression over 5h.
self.fl_warmth = math.max(20 * (5 - diff_time), 0)
elseif diff_time > 22 then
-- Keep warmth at maximum for two hours after bedtime.
self.fl_warmth = 100
else
-- Between 2-4h after bedtime, return to zero.
self.fl_warmth = math.max(100 - 50 * (22 - diff_time), 0)
end
self.fl_warmth = math.floor(self.fl_warmth + 0.5)
-- Enable background job for setting Warmth, if not already done.
if not self.autowarmth_job_running then
table.insert(PluginShare.backgroundJobs, {
when = 180,
repeated = true,
executable = function()
if self.auto_warmth then
self:setWarmth()
end
end,
})
self.autowarmth_job_running = true
end
end
function CervantesPowerD:getCapacityHW()
@ -131,33 +187,26 @@ function CervantesPowerD:getCapacityHW()
end
function CervantesPowerD:isChargingHW()
return self:read_str_file(self.status_file) == "Charging"
return self:read_str_file(self.status_file) == "Charging\n"
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)
if self.fl == nil then return end
-- just re-set it to self.hw_intensity that we haven't change on Suspend
if self.fl_warmth == nil then
self.fl:setBrightness(self.hw_intensity)
else
if self.auto_warmth then
self:calculateAutoWarmth()
end
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,15 +9,13 @@ local bit = require("bit")
local _ = require("gettext")
local T = require("ffi/util").template
local DeviceListener = EventListener:extend{}
local DeviceListener = InputContainer:new{
steps_fl = { 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, },
}
function DeviceListener:onToggleNightMode()
local night_mode = G_reader_settings:isTrue("night_mode")
Screen:toggleNightMode()
-- Make sure CRe will bypass the call cache
if self.ui and self.ui.document and self.ui.document.provider == "crengine" then
self.ui.document:resetCallCache()
end
UIManager:setDirty("all", "full")
UIManager:ToggleNightMode(not night_mode)
G_reader_settings:saveSetting("night_mode", not night_mode)
@ -29,6 +28,10 @@ function DeviceListener:onSetNightMode(night_mode_on)
end
end
local function lightFrontlight()
return Device:hasLightLevelFallback() and G_reader_settings:nilOrTrue("light_fallback")
end
function DeviceListener:onShowIntensity()
if not Device:hasFrontlight() then return true end
local powerd = Device:getPowerDevice()
@ -38,84 +41,85 @@ function DeviceListener:onShowIntensity()
else
new_text = T(_("Frontlight intensity set to %1."), powerd:frontlightIntensity())
end
Notification:notify(new_text)
UIManager:show(Notification:new{
text = new_text,
timeout = 1,
})
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
UIManager:show(Notification:new{
text = T(_("Warmth set to %1."), powerd.fl_warmth),
timeout = 1.0,
})
end
return true
end
-- frontlight controller
if Device:hasFrontlight() then
local function calculateGestureDelta(ges, direction, min, max)
-- direction +1 - increase frontlight
-- direction -1 - decrease frontlight
function DeviceListener:onChangeFlIntensity(ges, direction)
local powerd = Device:getPowerDevice()
local delta_int
--received gesture
if type(ges) == "table" then
local gesture_multiplier
if ges.ges == "two_finger_swipe" or ges.ges == "swipe" then
gesture_multiplier = 0.8
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
if powerd.fl_intensity == nil then return false end
local steps_tbl = {}
local scale = (powerd.fl_max - powerd.fl_min) / 2 / 10.6
for i = 1, #self.steps_fl, 1 do
steps_tbl[i] = math.ceil(self.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
end
local new_intensity = powerd.fl_intensity + direction * delta_int
-- 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 new_intensity = powerd:frontlightIntensity() + delta
-- when new_intensity <= 0, toggle light off
self:onSetFlIntensity(new_intensity)
self:onShowIntensity()
return true
end
function DeviceListener:onSetFlIntensity(new_intensity)
local powerd = Device:getPowerDevice()
if new_intensity == nil then return true end
-- when new_intensity <=0, toggle light off
if new_intensity <= 0 then
powerd:turnOffFrontlight()
else
powerd:setIntensity(new_intensity)
end
self:onShowIntensity()
return true
end
@ -132,30 +136,79 @@ 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
-- when using frontlight system settings
if lightFrontlight() then
UIManager:show(Notification:new{
text = _("Frontlight controlled by system settings."),
timeout = 2.5,
})
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
if powerd.auto_warmth then
UIManager:show(Notification:new{
text = _("Warmth is handled automatically."),
timeout = 1.0,
})
return true
end
local delta_int
--received gesture
if type(ges) == "table" then
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
scale_multiplier = 1
end
-- 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)
if ges.direction == "south" or ges.direction == "north" then
gestureScale = Screen:getHeight() * scale_multiplier
elseif ges.direction == "west" or ges.direction == "east" then
gestureScale = Screen:getWidth() * scale_multiplier
else
local width = Screen:getWidth()
local height = Screen:getHeight()
-- diagonal
gestureScale = math.sqrt(width * width + height * height) * scale_multiplier
end
self:onSetFlWarmth(warmth)
self:onShowWarmth()
return true
end
local steps_tbl = {}
local scale = (powerd.fl_max - powerd.fl_min) / 2 / 10.6
for i = 1, #self.steps_fl, 1 do
steps_tbl[i] = math.ceil(self.steps_fl[i] * scale)
end
function DeviceListener:onSetFlWarmth(warmth)
local powerd = Device:getPowerDevice()
if ges.distance == nil then
ges.distance = 1
end
local step = math.ceil(#steps_tbl * ges.distance / gestureScale)
delta_int = steps_tbl[step] or steps_tbl[#steps_tbl]
else
-- received amount to change
delta_int = ges
end
if direction ~= -1 and direction ~= 1 then
-- set default value (increase frontlight)
direction = 1
end
local warmth = powerd.fl_warmth + direction * delta_int
if warmth > 100 then
warmth = 100
elseif warmth < 0 then
warmth = 0
end
powerd:setWarmth(warmth)
self:onShowWarmth()
return true
end
@ -168,30 +221,39 @@ if Device:hasFrontlight() then
end
function DeviceListener:onToggleFrontlight()
-- when using frontlight system settings
if lightFrontlight() then
UIManager:show(Notification:new{
text = _("Frontlight controlled by system settings."),
timeout = 2.5,
})
return true
end
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
UIManager:show(Notification:new{
text = new_text,
timeout = 1.0,
})
return true
end
function DeviceListener:onShowFlDialog()
Device:showLightDialog()
local FrontLightWidget = require("ui/widget/frontlightwidget")
UIManager:show(FrontLightWidget:new{
use_system_fl = Device:hasLightLevelFallback()
})
end
end
if Device:hasGSensor() then
if Device:canToggleGSensor() then
function DeviceListener:onToggleGSensor()
G_reader_settings:flipNilOrFalse("input_ignore_gsensor")
Device:toggleGSensor(not G_reader_settings:isTrue("input_ignore_gsensor"))
@ -201,168 +263,59 @@ 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,
timeout = 1.0,
})
return true
end
end
if not Device:isAlwaysFullscreen() then
function DeviceListener:onToggleFullscreen()
Device:toggleFullscreen()
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)
self.ui:handleEvent(Event:new("SetRotationMode", arg))
return true
end
function DeviceListener:onInvertRotation()
-- Invert is always rota + 2, w/ wraparound
local arg = bit.band(Screen:getRotationMode() + 2, 3)
function DeviceListener:onToggleRotation()
local arg = bit.band((Screen:getRotationMode() + 1), 3)
self.ui:handleEvent(Event:new("SetRotationMode", arg))
return true
end
function DeviceListener:onSwapRotation()
local rota = Screen:getRotationMode()
-- Portrait is always even, Landscape is always odd. For each of 'em, Landscape = Portrait + 1.
-- As such...
local arg
if bit.band(rota, 1) == 0 then
-- If Portrait, Landscape is +1
arg = bit.band(rota + 1, 3)
else
-- If Landscape, Portrait is -1
arg = bit.band(rota - 1, 3)
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
self.ui:handleEvent(Event:new("SetRotationMode", arg))
return true
end
function DeviceListener:onSetRefreshRates(day, night)
UIManager:setRefreshRate(day, night)
end
function DeviceListener:onSetBothRefreshRates(rate)
UIManager:setRefreshRate(rate, rate)
end
function DeviceListener:onSetDayRefreshRate(day)
UIManager:setRefreshRate(day, nil)
end
function DeviceListener:onSetNightRefreshRate(night)
UIManager:setRefreshRate(nil, night)
end
function DeviceListener:onSetFlashOnChapterBoundaries(toggle)
if toggle == true then
G_reader_settings:makeTrue("refresh_on_chapter_boundaries")
else
G_reader_settings:delSetting("refresh_on_chapter_boundaries")
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
end
function DeviceListener:onToggleFlashOnChapterBoundaries()
G_reader_settings:flipNilOrFalse("refresh_on_chapter_boundaries")
end
function DeviceListener:onSetNoFlashOnSecondChapterPage(toggle)
if toggle == true then
G_reader_settings:makeTrue("no_refresh_on_second_chapter_page")
else
G_reader_settings:delSetting("no_refresh_on_second_chapter_page")
end
end
function DeviceListener:onToggleNoFlashOnSecondChapterPage()
G_reader_settings:flipNilOrFalse("no_refresh_on_second_chapter_page")
end
function DeviceListener:onSetFlashOnPagesWithImages(toggle)
if toggle == true then
G_reader_settings:delSetting("refresh_on_pages_with_images")
else
G_reader_settings:makeFalse("refresh_on_pages_with_images")
end
end
function DeviceListener:onToggleFlashOnPagesWithImages()
G_reader_settings:flipNilOrTrue("refresh_on_pages_with_images")
end
function DeviceListener:onSwapPageTurnButtons()
G_reader_settings:flipNilOrFalse("input_invert_page_turn_keys")
Device:invertButtons()
end
function DeviceListener:onToggleKeyRepeat(toggle)
if toggle == true then
G_reader_settings:makeFalse("input_no_key_repeat")
elseif toggle == false then
G_reader_settings:makeTrue("input_no_key_repeat")
else
G_reader_settings:flipNilOrFalse("input_no_key_repeat")
end
Device:toggleKeyRepeat(G_reader_settings:nilOrFalse("input_no_key_repeat"))
end
function DeviceListener:onRequestUSBMS()
local MassStorage = require("ui/elements/mass_storage")
-- It already takes care of the canToggleMassStorage cap check for us
-- NOTE: Never request confirmation, it's sorted right next to exit, restart & friends in Dispatcher,
-- and they don't either...
MassStorage:start(false)
end
function DeviceListener:onRestart()
self.ui.menu:exitOrRestart(function() UIManager:restartKOReader() end)
end
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:onFullRefresh()
if self.ui and self.ui.view then
self.ui:handleEvent(Event:new("UpdateFooter", self.ui.view.footer_visible))
end
UIManager:setDirty(nil, "full")
function DeviceListener:onRestart()
self.ui.menu:exitOrRestart(function() UIManager:restartKOReader() end)
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)
function DeviceListener:onFullRefresh()
self.ui:handleEvent(Event:new("UpdateFooter"))
UIManager:setDirty("all", "full")
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,

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

Loading…
Cancel
Save