Compare commits

..

No commits in common. 'master' and 'v2024.03.1' have entirely different histories.

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

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

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

@ -7,6 +7,8 @@ ANSI_RED="\033[31;1m"
# shellcheck disable=SC2034 # shellcheck disable=SC2034
ANSI_GREEN="\033[32;1m" ANSI_GREEN="\033[32;1m"
ANSI_RESET="\033[0m" ANSI_RESET="\033[0m"
# shellcheck disable=SC2034
ANSI_CLEAR="\033[0K"
travis_retry() { travis_retry() {
local result=0 local result=0
@ -31,3 +33,43 @@ travis_retry() {
set -e set -e
return ${result} return ${result}
} }
retry_cmd() {
local result=0
local count=1
set +e
retry_cnt=$1
shift 1
while [ ${count} -le "${retry_cnt}" ]; do
[ ${result} -ne 0 ] && {
echo -e "\n${ANSI_RED}The command \"$*\" failed. Retrying, ${count} of ${retry_cnt}${ANSI_RESET}\n" >&2
}
"$@"
result=$?
[ ${result} -eq 0 ] && break
count=$((count + 1))
sleep 1
done
[ ${count} -gt "${retry_cnt}" ] && {
echo -e "\n${ANSI_RED}The command \"$*\" failed ${retry_cnt} times.${ANSI_RESET}\n" >&2
}
set -e
return ${result}
}
# export CI_BUILD_DIR=${TRAVIS_BUILD_DIR}
# use eval to get fully expanded path
eval CI_BUILD_DIR="${CIRCLE_WORKING_DIRECTORY}"
export CI_BUILD_DIR
test -e "${HOME}/bin" || mkdir "${HOME}/bin"
export PATH=${PWD}/bin:${HOME}/bin:${PATH}
export PATH=${PATH}:${CI_BUILD_DIR}/install/bin
if [ -f "${CI_BUILD_DIR}/install/bin/luarocks" ]; then
# add local rocks to $PATH
eval "$(luarocks path --bin)"
fi

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

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

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

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

@ -4,11 +4,9 @@ CI_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=/dev/null # shellcheck source=/dev/null
source "${CI_DIR}/common.sh" 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 # 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' ' ')" BUSTED_SPEC_FILE="$(circleci tests glob "spec/front/unit/*_spec.lua" | circleci tests split --split-by=timings --timings-type=filename | tr '\n' ' ')"
} && popd || exit } && popd || exit
make testfront BUSTED_SPEC_FILE="${BUSTED_SPEC_FILE}" make testfront BUSTED_SPEC_FILE="${BUSTED_SPEC_FILE}"
# vim: sw=4

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

@ -28,7 +28,8 @@ 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. 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. 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, go to `Top menu → Hamburger menu → Help → Report a bug` and tap `Enable verbose logging`. Restart as requested, then repeat the steps for your issue. 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: If you instead opt to inline it, please do so behind a spoiler tag:
<details> <details>

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

1
.gitmodules vendored

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

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

@ -1 +1 @@
Subproject commit 171222f22686fb11eb611fa25ed91cb132a79b76 Subproject commit 8f5f38d732bba170abdae5df015f9f4b475fac6e

@ -17,7 +17,7 @@ function DataStorage:getDataDir()
local package_name = app_id:match("^(.-)_") local package_name = app_id:match("^(.-)_")
-- confined ubuntu app has write access to this dir -- confined ubuntu app has write access to this dir
data_dir = string.format("%s/%s", os.getenv("XDG_DATA_HOME"), package_name) data_dir = string.format("%s/%s", os.getenv("XDG_DATA_HOME"), package_name)
elseif os.getenv("APPIMAGE") or os.getenv("FLATPAK") or os.getenv("KO_MULTIUSER") then elseif os.getenv("APPIMAGE") or os.getenv("KO_MULTIUSER") then
if os.getenv("XDG_CONFIG_HOME") then if os.getenv("XDG_CONFIG_HOME") then
data_dir = string.format("%s/%s", os.getenv("XDG_CONFIG_HOME"), "koreader") data_dir = string.format("%s/%s", os.getenv("XDG_CONFIG_HOME"), "koreader")
if lfs.attributes(os.getenv("XDG_CONFIG_HOME"), "mode") ~= "directory" then if lfs.attributes(os.getenv("XDG_CONFIG_HOME"), "mode") ~= "directory" then

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

@ -17,10 +17,10 @@ Each target has its own architecture and you'll need to setup a proper cross-com
A compatible version of the Android NDK and SDK will be downloaded automatically by `./kodev release android` if no NDK or SDK is provided in environment variables. For that purpose you can use: 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 #### for embedded linux devices

@ -53,7 +53,7 @@ function WebDav:uploadFile(url, address, username, password, local_path, callbac
local path = WebDavApi:getJoinedPath(address, url) local path = WebDavApi:getJoinedPath(address, url)
path = WebDavApi:getJoinedPath(path, ffiutil.basename(local_path)) path = WebDavApi:getJoinedPath(path, ffiutil.basename(local_path))
local code_response = WebDavApi:uploadFile(path, username, password, local_path) local code_response = WebDavApi:uploadFile(path, username, password, local_path)
if type(code_response) == "number" and code_response >= 200 and code_response < 300 then if code_response >= 200 and code_response < 300 then
UIManager:show(InfoMessage:new{ UIManager:show(InfoMessage:new{
text = T(_("File uploaded:\n%1"), BD.filepath(address)), text = T(_("File uploaded:\n%1"), BD.filepath(address)),
}) })
@ -67,7 +67,7 @@ function WebDav:uploadFile(url, address, username, password, local_path, callbac
end end
function WebDav:createFolder(url, address, username, password, folder_name, callback_close) 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) local code_response = WebDavApi:createFolder(address .. WebDavApi:urlEncode(url .. "/" .. folder_name), username, password, folder_name)
if code_response == 201 then if code_response == 201 then
if callback_close then if callback_close then
callback_close() callback_close()

@ -12,23 +12,44 @@ local lfs = require("libs/libkoreader-lfs")
local WebDavApi = { local WebDavApi = {
} }
-- Trim leading & trailing slashes from string `s` (based on util.trim) function WebDavApi:getJoinedPath( address, path )
function WebDavApi.trim_slashes(s) local path_encoded = self:urlEncode( path ) or ""
local from = s:match"^/*()" local address_strip = address:sub(-1) == "/" and address:sub(1, -2) or address
return from > #s and "" or s:match(".*[^/]", from) local path_strip = path_encoded:sub(1, 1) == "/" and path_encoded:sub(2) or path_encoded
return address_strip .. "/" .. path_strip
end end
-- Trim trailing slashes from string `s` (based on util.rtrim) function WebDavApi:isCurrentDirectory( current_item, address, path )
function WebDavApi.rtrim_slashes(s) local is_home, is_parent
local n = #s local home_path
while n > 0 and s:find("^/", n) do -- find first occurence of / after http(s)://
n = n - 1 local start = string.find( address, "/", 9 )
if not start then
home_path = "/"
else
home_path = string.sub( address, start )
end
local item
if string.sub( current_item, -1 ) == "/" then
item = string.sub( current_item, 1, -2 )
else
item = current_item
end
if item == home_path then
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
end end
return s:sub(1, n) return is_home or is_parent
end end
-- Variant of util.urlEncode that doesn't encode the / -- version of urlEncode that doesn't encode the /
function WebDavApi.urlEncode(url_data) function WebDavApi:urlEncode(url_data)
local char_to_hex = function(c) local char_to_hex = function(c)
return string.format("%%%02X", string.byte(c)) return string.format("%%%02X", string.byte(c))
end end
@ -39,30 +60,28 @@ function WebDavApi.urlEncode(url_data)
return url_data return url_data
end end
-- Append path to address with a slash separator, trimming any unwanted slashes in the process.
function WebDavApi:getJoinedPath(address, path)
local path_encoded = self.urlEncode(path) or ""
-- Strip leading & trailing slashes from `path`
local sane_path = self.trim_slashes(path_encoded)
-- Strip trailing slashes from `address` for now
local sane_address = self.rtrim_slashes(address)
-- Join our final URL
return sane_address .. "/" .. sane_path
end
function WebDavApi:listFolder(address, user, pass, folder_path, folder_mode) function WebDavApi:listFolder(address, user, pass, folder_path, folder_mode)
local path = folder_path or "" local path = self:urlEncode( folder_path )
local webdav_list = {} local webdav_list = {}
local webdav_file = {} local webdav_file = {}
-- Strip leading & trailing slashes from `path` local has_trailing_slash = false
path = self.trim_slashes(path) local has_leading_slash = false
-- Strip trailing slashes from `address` for now if string.sub( address, -1 ) == "/" then has_trailing_slash = true end
address = self.rtrim_slashes(address) if path == nil or path == "/" then
-- Join our final URL, which *must* have a trailing / (it's a URL) path = ""
-- This is where we deviate from getJoinedPath ;). elseif string.sub( path, 1, 1 ) == "/" then
local webdav_url = address .. "/" .. self.urlEncode(path) if has_trailing_slash then
if webdav_url:sub(-1) ~= "/" then -- too many slashes, remove one
path = string.sub( path, 2 )
end
has_leading_slash = true
end
if not has_trailing_slash and not has_leading_slash then
address = address .. "/"
end
local webdav_url = address .. path
if string.sub(webdav_url, -1) ~= "/" then
webdav_url = webdav_url .. "/" webdav_url = webdav_url .. "/"
end end
@ -103,18 +122,24 @@ function WebDavApi:listFolder(address, user, pass, folder_path, folder_mode)
--logger.dbg("WebDav catalog item=", item) --logger.dbg("WebDav catalog item=", item)
-- <d:href> is the path and filename of the entry. -- <d:href> is the path and filename of the entry.
local item_fullpath = item:match("<[^:]*:href[^>]*>(.*)</[^:]*:href>") local item_fullpath = item:match("<[^:]*:href[^>]*>(.*)</[^:]*:href>")
local item_name = FFIUtil.basename(util.htmlEntitiesToUtf8(util.urlDecode(item_fullpath))) if string.sub( item_fullpath, -1 ) == "/" then
local is_current_dir = item_name == string.sub(folder_path, -#item_name) item_fullpath = string.sub( item_fullpath, 1, -2 )
end
local is_current_dir = self:isCurrentDirectory( util.urlDecode(item_fullpath), address, folder_path )
local item_name = util.urlDecode( FFIUtil.basename( item_fullpath ) )
item_name = util.htmlEntitiesToUtf8(item_name)
local is_not_collection = item:find("<[^:]*:resourcetype/>") or local is_not_collection = item:find("<[^:]*:resourcetype/>") or
item:find("<[^:]*:resourcetype></[^:]*:resourcetype>") item:find("<[^:]*:resourcetype></[^:]*:resourcetype>")
local item_path = path .. "/" .. item_name
local item_path = (path == "" and has_trailing_slash) and item_name or path .. "/" .. item_name
if item:find("<[^:]*:collection[^<]*/>") then if item:find("<[^:]*:collection[^<]*/>") then
item_name = item_name .. "/" item_name = item_name .. "/"
if not is_current_dir then if not is_current_dir then
table.insert(webdav_list, { table.insert(webdav_list, {
text = item_name, text = item_name,
url = item_path, url = util.urlDecode( item_path ),
type = "folder", type = "folder",
}) })
end end
@ -122,7 +147,7 @@ function WebDavApi:listFolder(address, user, pass, folder_path, folder_mode)
or G_reader_settings:isTrue("show_unsupported")) then or G_reader_settings:isTrue("show_unsupported")) then
table.insert(webdav_file, { table.insert(webdav_file, {
text = item_name, text = item_name,
url = item_path, url = util.urlDecode( item_path ),
type = "file", type = "file",
}) })
end end
@ -188,7 +213,7 @@ function WebDavApi:uploadFile(file_url, user, pass, local_path, etag)
} }
}) })
socketutil:reset_timeout() socketutil:reset_timeout()
if type(code) ~= "number" or code < 200 or code > 299 then if code < 200 or code > 299 then
logger.warn("WebDavApi: upload failure:", status or code or "network unreachable") logger.warn("WebDavApi: upload failure:", status or code or "network unreachable")
end end
return code return code

@ -23,7 +23,6 @@ local InputDialog = require("ui/widget/inputdialog")
local LanguageSupport = require("languagesupport") local LanguageSupport = require("languagesupport")
local Menu = require("ui/widget/menu") local Menu = require("ui/widget/menu")
local MultiConfirmBox = require("ui/widget/multiconfirmbox") local MultiConfirmBox = require("ui/widget/multiconfirmbox")
local NetworkListener = require("ui/network/networklistener")
local PluginLoader = require("pluginloader") local PluginLoader = require("pluginloader")
local ReadCollection = require("readcollection") local ReadCollection = require("readcollection")
local ReaderDeviceStatus = require("apps/reader/modules/readerdevicestatus") local ReaderDeviceStatus = require("apps/reader/modules/readerdevicestatus")
@ -125,7 +124,6 @@ end
function FileManager:setupLayout() function FileManager:setupLayout()
self.show_parent = self.show_parent or self self.show_parent = self.show_parent or self
self.title_bar = TitleBar:new{ self.title_bar = TitleBar:new{
show_parent = self.show_parent,
fullscreen = "true", fullscreen = "true",
align = "center", align = "center",
title = self.title, title = self.title,
@ -138,7 +136,7 @@ function FileManager:setupLayout()
left_icon_size_ratio = 1, left_icon_size_ratio = 1,
left_icon_tap_callback = function() self:goHome() end, left_icon_tap_callback = function() self:goHome() end,
left_icon_hold_callback = function() self:onShowFolderMenu() end, left_icon_hold_callback = function() self:onShowFolderMenu() end,
right_icon = self.selected_files and "check" or "plus", right_icon = "plus",
right_icon_size_ratio = 1, right_icon_size_ratio = 1,
right_icon_tap_callback = function() self:onShowPlusMenu() end, right_icon_tap_callback = function() self:onShowPlusMenu() end,
right_icon_hold_callback = false, -- propagate long-press to dispatcher right_icon_hold_callback = false, -- propagate long-press to dispatcher
@ -167,31 +165,37 @@ function FileManager:setupLayout()
local file_manager = self local file_manager = self
function file_chooser:onPathChanged(path) function file_chooser:onPathChanged(path) -- luacheck: ignore
file_manager:updateTitleBarPath(path) file_manager:updateTitleBarPath(path)
return true return true
end end
function file_chooser:onFileSelect(item) function file_chooser:onFileSelect(item) -- luacheck: ignore
if file_manager.selected_files then -- toggle selection local file = item.path
item.dim = not item.dim and true or nil if file_manager.select_mode then
file_manager.selected_files[item.path] = item.dim if file_manager.selected_files[file] then
file_manager.selected_files[file] = nil
item.dim = nil
else
file_manager.selected_files[file] = true
item.dim = true
end
self:updateItems() self:updateItems()
else else
file_manager:openFile(item.path) file_manager:openFile(file)
end end
return true return true
end end
function file_chooser:onFileHold(item) function file_chooser:onFileHold(item)
if file_manager.selected_files then if file_manager.select_mode then
file_manager:tapPlus() file_manager:tapPlus()
else else
self:showFileDialog(item) self:showFileDialog(item)
end end
end end
function file_chooser:showFileDialog(item) function file_chooser:showFileDialog(item) -- luacheck: ignore
local file = item.path local file = item.path
local is_file = item.is_file local is_file = item.is_file
local is_not_parent_folder = not item.is_go_up local is_not_parent_folder = not item.is_go_up
@ -229,7 +233,7 @@ function FileManager:setupLayout()
text = _("Select"), text = _("Select"),
callback = function() callback = function()
UIManager:close(self.file_dialog) UIManager:close(self.file_dialog)
file_manager:onToggleSelectMode() file_manager:onToggleSelectMode(true) -- no full screen refresh
if is_file then if is_file then
file_manager.selected_files[file] = true file_manager.selected_files[file] = true
item.dim = true item.dim = true
@ -288,7 +292,7 @@ function FileManager:setupLayout()
table.insert(buttons, {}) -- separator table.insert(buttons, {}) -- separator
table.insert(buttons, { table.insert(buttons, {
filemanagerutil.genResetSettingsButton(doc_settings_or_file, close_dialog_refresh_callback), filemanagerutil.genResetSettingsButton(doc_settings_or_file, close_dialog_refresh_callback),
file_manager.collections:genAddToCollectionButton(file, close_dialog_callback, refresh_callback), filemanagerutil.genAddRemoveFavoritesButton(file, close_dialog_callback),
}) })
end end
table.insert(buttons, { table.insert(buttons, {
@ -375,9 +379,6 @@ function FileManager:registerKeyEvents()
self.key_events.Home = { { "Home" } } self.key_events.Home = { { "Home" } }
-- Override the menu.lua way of handling the back key -- Override the menu.lua way of handling the back key
self.file_chooser.key_events.Back = { { Device.input.group.Back } } self.file_chooser.key_events.Back = { { Device.input.group.Back } }
if Device:hasScreenKB() then
self.key_events.ToggleWifi = { { "ScreenKB", "Home" } }
end
if not Device:hasFewKeys() then if not Device:hasFewKeys() then
-- Also remove the handler assigned to the "Back" key by menu.lua -- Also remove the handler assigned to the "Back" key by menu.lua
self.file_chooser.key_events.Close = nil self.file_chooser.key_events.Close = nil
@ -422,7 +423,6 @@ function FileManager:init()
self:registerModule("wikipedia", ReaderWikipedia:new{ ui = self }) self:registerModule("wikipedia", ReaderWikipedia:new{ ui = self })
self:registerModule("devicestatus", ReaderDeviceStatus:new{ ui = self }) self:registerModule("devicestatus", ReaderDeviceStatus:new{ ui = self })
self:registerModule("devicelistener", DeviceListener:new{ ui = self }) self:registerModule("devicelistener", DeviceListener:new{ ui = self })
self:registerModule("networklistener", NetworkListener:new{ ui = self })
-- koreader plugins -- koreader plugins
for _, plugin_module in ipairs(PluginLoader:loadPlugins()) do for _, plugin_module in ipairs(PluginLoader:loadPlugins()) do
@ -438,6 +438,11 @@ function FileManager:init()
end end
end end
if Device:hasWifiToggle() then
local NetworkListener = require("ui/network/networklistener")
table.insert(self, NetworkListener:new{ ui = self })
end
self:initGesListener() self:initGesListener()
self:handleEvent(Event:new("SetDimensions", self.dimen)) self:handleEvent(Event:new("SetDimensions", self.dimen))
@ -490,21 +495,14 @@ function FileManager:onShowPlusMenu()
return true return true
end end
function FileManager:onToggleSelectMode(do_refresh) function FileManager:onToggleSelectMode(no_refresh)
logger.dbg("toggle select mode") logger.dbg("toggle select mode")
if self.selected_files then self.select_mode = not self.select_mode
self.selected_files = nil self.selected_files = self.select_mode and {} or nil
self.title_bar:setRightIcon("plus") self.title_bar:setRightIcon(self.select_mode and "check" or "plus")
if do_refresh then if not no_refresh then
self.file_chooser:refreshPath() self:onRefresh()
else
self.file_chooser:selectAllFilesInFolder(false) -- undim
end
else
self.selected_files = {}
self.title_bar:setRightIcon("check")
end end
return true
end end
function FileManager:tapPlus() function FileManager:tapPlus()
@ -516,18 +514,12 @@ function FileManager:tapPlus()
end end
local title, buttons local title, buttons
if self.selected_files then if self.select_mode then
local function toggle_select_mode_callback()
self:onToggleSelectMode(true)
end
local select_count = util.tableSize(self.selected_files) local select_count = util.tableSize(self.selected_files)
local actions_enabled = select_count > 0 local actions_enabled = select_count > 0
title = actions_enabled and T(N_("1 file selected", "%1 files selected", select_count), select_count) title = actions_enabled and T(N_("1 file selected", "%1 files selected", select_count), select_count)
or _("No files selected") or _("No files selected")
buttons = { buttons = {
{
self.collections:genAddToCollectionButton(self.selected_files, close_dialog_callback, toggle_select_mode_callback),
},
{ {
{ {
text = _("Show selected files list"), text = _("Show selected files list"),
@ -626,7 +618,7 @@ function FileManager:tapPlus()
text = _("Select files"), text = _("Select files"),
callback = function() callback = function()
UIManager:close(self.file_dialog) UIManager:close(self.file_dialog)
self:onToggleSelectMode() self:onToggleSelectMode(true) -- no full screen refresh
end, end,
}, },
}, },
@ -724,7 +716,7 @@ function FileManager:tapPlus()
title = title, title = title,
title_align = "center", title_align = "center",
buttons = buttons, buttons = buttons,
select_mode = self.selected_files and true or nil, -- for coverbrowser select_mode = self.select_mode, -- for coverbrowser
} }
UIManager:show(self.file_dialog) UIManager:show(self.file_dialog)
end end
@ -733,7 +725,7 @@ function FileManager:reinit(path, focused_file)
UIManager:flushSettings() UIManager:flushSettings()
self.dimen = Screen:getSize() self.dimen = Screen:getSize()
-- backup the root path and path items -- backup the root path and path items
self.root_path = BaseUtil.realpath(path or self.file_chooser.path) self.root_path = path or self.file_chooser.path
local path_items_backup = {} local path_items_backup = {}
for k, v in pairs(self.file_chooser.path_items) do for k, v in pairs(self.file_chooser.path_items) do
path_items_backup[k] = v path_items_backup[k] = v
@ -748,6 +740,9 @@ function FileManager:reinit(path, focused_file)
-- looks unnecessary (cheap with classic mode, less cheap with -- looks unnecessary (cheap with classic mode, less cheap with
-- CoverBrowser plugin's cover image renderings) -- CoverBrowser plugin's cover image renderings)
-- self:onRefresh() -- self:onRefresh()
if self.select_mode then
self.title_bar:setRightIcon("check")
end
end end
function FileManager:getCurrentDir() function FileManager:getCurrentDir()
@ -856,10 +851,6 @@ function FileManager:pasteFileFromClipboard(file)
local dest_path = BaseUtil.realpath(file or self.file_chooser.path) local dest_path = BaseUtil.realpath(file or self.file_chooser.path)
dest_path = isFile(dest_path) and dest_path:match("(.*/)") or dest_path dest_path = isFile(dest_path) and dest_path:match("(.*/)") or dest_path
local dest_file = BaseUtil.joinPath(dest_path, orig_name) local dest_file = BaseUtil.joinPath(dest_path, orig_name)
if orig_file == dest_file or orig_file == dest_path then -- do not paste to itself
self.clipboard = nil
return
end
local is_file = isFile(orig_file) local is_file = isFile(orig_file)
local function doPaste() local function doPaste()
@ -951,24 +942,20 @@ function FileManager:pasteSelectedFiles(overwrite)
for orig_file in pairs(self.selected_files) do for orig_file in pairs(self.selected_files) do
local orig_name = BaseUtil.basename(orig_file) local orig_name = BaseUtil.basename(orig_file)
local dest_file = BaseUtil.joinPath(dest_path, orig_name) local dest_file = BaseUtil.joinPath(dest_path, orig_name)
if BaseUtil.realpath(orig_file) == dest_file then -- do not paste to itself local ok
self.selected_files[orig_file] = nil local dest_mode = lfs.attributes(dest_file, "mode")
else if not dest_mode or (dest_mode == "file" and overwrite) then
local ok if self.cutfile then
local dest_mode = lfs.attributes(dest_file, "mode") ok = self:moveFile(orig_file, dest_path)
if not dest_mode or (dest_mode == "file" and overwrite) then else
if self.cutfile then ok = self:copyRecursive(orig_file, dest_path)
ok = self:moveFile(orig_file, dest_path)
else
ok = self:copyRecursive(orig_file, dest_path)
end
end
if ok then
DocSettings.updateLocation(orig_file, dest_file, not self.cutfile)
ok_files[orig_file] = true
self.selected_files[orig_file] = nil
end end
end end
if ok then
DocSettings.updateLocation(orig_file, dest_file, not self.cutfile)
ok_files[orig_file] = true
self.selected_files[orig_file] = nil
end
end end
local skipped_nb = util.tableSize(self.selected_files) local skipped_nb = util.tableSize(self.selected_files)
if util.tableSize(ok_files) > 0 then if util.tableSize(ok_files) > 0 then
@ -988,7 +975,7 @@ function FileManager:pasteSelectedFiles(overwrite)
icon = "notice-warning", icon = "notice-warning",
}) })
else else
self:onToggleSelectMode(true) self:onToggleSelectMode()
end end
end end
@ -1116,7 +1103,7 @@ function FileManager:deleteSelectedFiles()
icon = "notice-warning", icon = "notice-warning",
}) })
else else
self:onToggleSelectMode(true) self:onToggleSelectMode()
end end
end end
@ -1207,7 +1194,7 @@ function FileManager:renameFile(file, basename, is_file)
end end
--- @note: This is the *only* safe way to instantiate a new FileManager instance! --- @note: This is the *only* safe way to instantiate a new FileManager instance!
function FileManager:showFiles(path, focused_file, selected_files) function FileManager:showFiles(path, focused_file)
-- Warn about and close any pre-existing FM instances first... -- Warn about and close any pre-existing FM instances first...
if FileManager.instance then if FileManager.instance then
logger.warn("FileManager instance mismatch! Tried to spin up a new instance, while we still have an existing one:", tostring(FileManager.instance)) logger.warn("FileManager instance mismatch! Tried to spin up a new instance, while we still have an existing one:", tostring(FileManager.instance))
@ -1215,7 +1202,7 @@ function FileManager:showFiles(path, focused_file, selected_files)
FileManager.instance:onClose() FileManager.instance:onClose()
end end
path = BaseUtil.realpath(path or G_reader_settings:readSetting("lastdir") or filemanagerutil.getDefaultDir()) path = path or G_reader_settings:readSetting("lastdir") or filemanagerutil.getDefaultDir()
G_reader_settings:saveSetting("lastdir", path) G_reader_settings:saveSetting("lastdir", path)
self:setRotationMode() self:setRotationMode()
local file_manager = FileManager:new{ local file_manager = FileManager:new{
@ -1223,7 +1210,6 @@ function FileManager:showFiles(path, focused_file, selected_files)
covers_fullscreen = true, -- hint for UIManager:_repaint() covers_fullscreen = true, -- hint for UIManager:_repaint()
root_path = path, root_path = path,
focused_file = focused_file, focused_file = focused_file,
selected_files = selected_files,
} }
UIManager:show(file_manager) UIManager:show(file_manager)
end end
@ -1348,7 +1334,6 @@ function FileManager:showSelectedFilesList()
item_table = selected_files, item_table = selected_files,
is_borderless = true, is_borderless = true,
is_popout = false, is_popout = false,
title_bar_fm_style = true,
truncate_left = true, truncate_left = true,
onMenuSelect = function(_, item) onMenuSelect = function(_, item)
UIManager:close(menu) UIManager:close(menu)
@ -1457,7 +1442,6 @@ function FileManager:showOpenWithDialog(file)
table.insert(buttons, { table.insert(buttons, {
{ {
text = _("Cancel"), text = _("Cancel"),
id = "close",
callback = function() callback = function()
UIManager:close(dialog) UIManager:close(dialog)
end, end,
@ -1526,67 +1510,4 @@ function FileManager:openFile(file, provider, doc_caller_callback, aux_caller_ca
end end
end end
-- Dispatcher helpers
function FileManager.getDisplayModeActions()
local action_names, action_texts = { "classic" }, { _("Classic (filename only)") }
local ui = FileManager.instance or require("apps/reader/readerui").instance
if ui.coverbrowser then
for _, v in ipairs(ui.coverbrowser.modes) do
local action_text, action_name = unpack(v)
if action_name then -- skip Classic
table.insert(action_names, action_name)
table.insert(action_texts, action_text)
end
end
end
return action_names, action_texts
end
function FileManager:onSetDisplayMode(mode)
if self.coverbrowser then
mode = mode ~= "classic" and mode or nil
self.coverbrowser:setDisplayMode(mode)
end
return true
end
function FileManager.getSortByActions()
local collates = {}
for k, v in pairs(FileChooser.collates) do
table.insert(collates, {
name = k,
text = v.text,
menu_order = v.menu_order,
})
end
table.sort(collates, function(a, b) return a.menu_order < b.menu_order end)
local action_names, action_texts = {}, {}
for _, v in ipairs(collates) do
table.insert(action_names, v.name)
table.insert(action_texts, v.text)
end
return action_names, action_texts
end
function FileManager:onSetSortBy(mode)
G_reader_settings:saveSetting("collate", mode)
self.file_chooser:clearSortingCache()
self.file_chooser:refreshPath()
return true
end
function FileManager:onSetReverseSorting(toggle)
G_reader_settings:saveSetting("reverse_collate", toggle or nil)
self.file_chooser:refreshPath()
return true
end
function FileManager:onSetMixedSorting(toggle)
G_reader_settings:saveSetting("collate_mixed", toggle or nil)
self.file_chooser:refreshPath()
return true
end
return FileManager return FileManager

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

@ -97,9 +97,14 @@ function FileSearcher:onShowFileSearch(search_string)
end end
function FileSearcher:doSearch() function FileSearcher:doSearch()
local results
local dirs, files = self:getList() local dirs, files = self:getList()
-- If we have a FileChooser instance, use it, to be able to make use of its natsort cache -- If we have a FileChooser instance, use it, to be able to make use of its natsort cache
local results = (self.ui.file_chooser or FileChooser):genItemTable(dirs, files) if self.ui.file_chooser then
results = self.ui.file_chooser:genItemTable(dirs, files)
else
results = FileChooser:genItemTable(dirs, files)
end
if #results > 0 then if #results > 0 then
self:showSearchResults(results) self:showSearchResults(results)
else else
@ -216,51 +221,31 @@ end
function FileSearcher:showSearchResults(results) function FileSearcher:showSearchResults(results)
self.search_menu = Menu:new{ self.search_menu = Menu:new{
title = T(_("Search results (%1)"), #results),
subtitle = T(_("Query: %1"), self.search_string), subtitle = T(_("Query: %1"), self.search_string),
item_table = results,
ui = self.ui,
covers_fullscreen = true, -- hint for UIManager:_repaint() covers_fullscreen = true, -- hint for UIManager:_repaint()
is_borderless = true, is_borderless = true,
is_popout = false, is_popout = false,
title_bar_fm_style = true, title_bar_fm_style = true,
title_bar_left_icon = "appbar.menu",
onLeftButtonTap = function() self:setSelectMode() end,
onMenuSelect = self.onMenuSelect, onMenuSelect = self.onMenuSelect,
onMenuHold = self.onMenuHold, onMenuHold = self.onMenuHold,
handle_hold_on_hold_release = true, handle_hold_on_hold_release = true,
ui = self.ui,
_manager = self,
} }
self.search_menu.close_callback = function() self.search_menu.close_callback = function()
self.selected_files = nil
UIManager:close(self.search_menu) UIManager:close(self.search_menu)
if self.ui.file_chooser then if self.ui.file_chooser then
self.ui.file_chooser:refreshPath() self.ui.file_chooser:refreshPath()
end end
end end
self:updateMenu(results)
UIManager:show(self.search_menu) UIManager:show(self.search_menu)
if self.no_metadata_count ~= 0 then if self.no_metadata_count ~= 0 then
self:showSearchResultsMessage() self:showSearchResultsMessage()
end end
end end
function FileSearcher:updateMenu(item_table)
item_table = item_table or self.search_menu.item_table
self.search_menu:switchItemTable(T(_("Search results (%1)"), #item_table), item_table, -1)
end
function FileSearcher:onMenuSelect(item) function FileSearcher:onMenuSelect(item)
if self._manager.selected_files then
if item.is_file then
item.dim = not item.dim and true or nil
self._manager.selected_files[item.path] = item.dim
self._manager:updateMenu()
end
else
self._manager:showFileDialog(item)
end
end
function FileSearcher:showFileDialog(item)
local file = item.path local file = item.path
local bookinfo, dialog local bookinfo, dialog
local function close_dialog_callback() local function close_dialog_callback()
@ -268,11 +253,7 @@ function FileSearcher:showFileDialog(item)
end end
local function close_dialog_menu_callback() local function close_dialog_menu_callback()
UIManager:close(dialog) UIManager:close(dialog)
self.search_menu.close_callback() self.close_callback()
end
local function update_item_callback()
item.mandatory = FileChooser:getMenuItemMandatory(item, FileChooser:getCollate())
self:updateMenu()
end end
local buttons = {} local buttons = {}
if item.is_file then if item.is_file then
@ -284,7 +265,7 @@ function FileSearcher:showFileDialog(item)
table.insert(buttons, {}) -- separator table.insert(buttons, {}) -- separator
table.insert(buttons, { table.insert(buttons, {
filemanagerutil.genResetSettingsButton(file, close_dialog_callback, is_currently_opened), filemanagerutil.genResetSettingsButton(file, close_dialog_callback, is_currently_opened),
self.ui.collections:genAddToCollectionButton(file, close_dialog_callback, update_item_callback), filemanagerutil.genAddRemoveFavoritesButton(file, close_dialog_callback),
}) })
end end
table.insert(buttons, { table.insert(buttons, {
@ -294,8 +275,13 @@ function FileSearcher:showFileDialog(item)
callback = function() callback = function()
local function post_delete_callback() local function post_delete_callback()
UIManager:close(dialog) UIManager:close(dialog)
table.remove(self.search_menu.item_table, item.idx) for i, menu_item in ipairs(self.item_table) do
self:updateMenu() if menu_item.path == file then
table.remove(self.item_table, i)
break
end
self:switchItemTable(T(_("Search results (%1)"), #self.item_table), self.item_table)
end
end end
local FileManager = require("apps/filemanager/filemanager") local FileManager = require("apps/filemanager/filemanager")
FileManager:showDeleteFileDialog(file, post_delete_callback) FileManager:showDeleteFileDialog(file, post_delete_callback)
@ -310,9 +296,9 @@ function FileSearcher:showFileDialog(item)
text = _("Open"), text = _("Open"),
enabled = DocumentRegistry:hasProvider(file, nil, true), -- allow auxiliary providers enabled = DocumentRegistry:hasProvider(file, nil, true), -- allow auxiliary providers
callback = function() callback = function()
close_dialog_menu_callback() close_dialog_callback()
local FileManager = require("apps/filemanager/filemanager") local FileManager = require("apps/filemanager/filemanager")
FileManager.openFile(self.ui, file) FileManager.openFile(self.ui, file, nil, self.close_callback)
end, end,
}, },
}) })
@ -333,12 +319,10 @@ function FileSearcher:showFileDialog(item)
end end
function FileSearcher:onMenuHold(item) function FileSearcher:onMenuHold(item)
if self._manager.selected_files then return true end
if item.is_file then if item.is_file then
if DocumentRegistry:hasProvider(item.path, nil, true) then if DocumentRegistry:hasProvider(item.path, nil, true) then
self.close_callback()
local FileManager = require("apps/filemanager/filemanager") local FileManager = require("apps/filemanager/filemanager")
FileManager.openFile(self.ui, item.path) FileManager.openFile(self.ui, item.path, nil, self.close_callback)
end end
else else
self.close_callback() self.close_callback()
@ -353,92 +337,4 @@ function FileSearcher:onMenuHold(item)
return true return true
end end
function FileSearcher:setSelectMode()
if self.selected_files then
self:showSelectModeDialog()
else
self.selected_files = {}
self.search_menu:setTitleBarLeftIcon("check")
end
end
function FileSearcher:showSelectModeDialog()
local item_table = self.search_menu.item_table
local select_count = util.tableSize(self.selected_files)
local actions_enabled = select_count > 0
local title = actions_enabled and T(N_("1 file selected", "%1 files selected", select_count), select_count)
or _("No files selected")
local select_dialog
local buttons = {
{
{
text = _("Deselect all"),
enabled = actions_enabled,
callback = function()
UIManager:close(select_dialog)
for file in pairs (self.selected_files) do
self.selected_files[file] = nil
end
for _, item in ipairs(item_table) do
item.dim = nil
end
self:updateMenu()
end,
},
{
text = _("Select all"),
callback = function()
UIManager:close(select_dialog)
for _, item in ipairs(item_table) do
if item.is_file then
item.dim = true
self.selected_files[item.path] = true
end
end
self:updateMenu()
end,
},
},
{
{
text = _("Exit select mode"),
callback = function()
UIManager:close(select_dialog)
self.selected_files = nil
self.search_menu:setTitleBarLeftIcon("appbar.menu")
if actions_enabled then
for _, item in ipairs(item_table) do
item.dim = nil
end
end
self:updateMenu()
end,
},
{
text = _("Select in file browser"),
enabled = actions_enabled,
callback = function()
UIManager:close(select_dialog)
local selected_files = self.selected_files
self.search_menu.close_callback()
if self.ui.file_chooser then
self.ui.selected_files = selected_files
self.ui.title_bar:setRightIcon("check")
self.ui.file_chooser:refreshPath()
else -- called from Reader
self.ui:onClose()
self.ui:showFileManager(self.path .. "/", selected_files)
end
end,
},
},
}
select_dialog = ButtonDialog:new{
title = title,
title_align = "center",
buttons = buttons,
}
UIManager:show(select_dialog)
end
return FileSearcher return FileSearcher

@ -6,7 +6,6 @@ local DocSettings = require("docsettings")
local FileManagerBookInfo = require("apps/filemanager/filemanagerbookinfo") local FileManagerBookInfo = require("apps/filemanager/filemanagerbookinfo")
local InputDialog = require("ui/widget/inputdialog") local InputDialog = require("ui/widget/inputdialog")
local Menu = require("ui/widget/menu") local Menu = require("ui/widget/menu")
local ReadCollection = require("readcollection")
local UIManager = require("ui/uimanager") local UIManager = require("ui/uimanager")
local WidgetContainer = require("ui/widget/container/widgetcontainer") local WidgetContainer = require("ui/widget/container/widgetcontainer")
local Screen = require("device").screen local Screen = require("device").screen
@ -70,14 +69,8 @@ function FileManagerHistory:updateItemTable()
local item_table = {} local item_table = {}
for _, v in ipairs(require("readhistory").hist) do for _, v in ipairs(require("readhistory").hist) do
if self:isItemMatch(v) then if self:isItemMatch(v) then
local item = util.tableDeepCopy(v) v.mandatory_dim = (self.is_frozen and v.status == "complete") and true or nil
if item.select_enabled and ReadCollection:isFileInCollections(item.file) then table.insert(item_table, v)
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 end
if self.statuses_fetched then if self.statuses_fetched then
self.count[v.status] = self.count[v.status] + 1 self.count[v.status] = self.count[v.status] + 1
@ -86,8 +79,6 @@ function FileManagerHistory:updateItemTable()
local subtitle = "" local subtitle = ""
if self.search_string then if self.search_string then
subtitle = T(_("Search results (%1)"), #item_table) 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 elseif self.filter ~= "all" then
subtitle = T(_("Status: %1 (%2)"), filter_text[self.filter]:lower(), #item_table) subtitle = T(_("Status: %1 (%2)"), filter_text[self.filter]:lower(), #item_table)
end end
@ -110,13 +101,6 @@ function FileManagerHistory:isItemMatch(item)
end end
end 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 return self.filter == "all" or item.status == self.filter
end end
@ -156,9 +140,6 @@ function FileManagerHistory:onMenuHold(item)
self._manager:updateItemTable() self._manager:updateItemTable()
self._manager.files_updated = true -- sidecar folder may be created/deleted self._manager.files_updated = true -- sidecar folder may be created/deleted
end end
local function update_callback()
self._manager:updateItemTable()
end
local is_currently_opened = file == (self.ui.document and self.ui.document.file) local is_currently_opened = file == (self.ui.document and self.ui.document.file)
local buttons = {} local buttons = {}
@ -187,7 +168,7 @@ function FileManagerHistory:onMenuHold(item)
end end
table.insert(buttons, { table.insert(buttons, {
filemanagerutil.genResetSettingsButton(doc_settings_or_file, close_dialog_update_callback, is_currently_opened), 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), filemanagerutil.genAddRemoveFavoritesButton(file, close_dialog_callback, item.dim),
}) })
table.insert(buttons, { table.insert(buttons, {
{ {
@ -270,7 +251,6 @@ function FileManagerHistory:onShowHist(search_info)
self.case_sensitive = search_info.case_sensitive self.case_sensitive = search_info.case_sensitive
else else
self.search_string = nil self.search_string = nil
self.selected_colections = nil
end end
self.filter = G_reader_settings:readSetting("history_filter", "all") self.filter = G_reader_settings:readSetting("history_filter", "all")
self.is_frozen = G_reader_settings:isTrue("history_freeze_finished_books") self.is_frozen = G_reader_settings:isTrue("history_freeze_finished_books")
@ -290,7 +270,7 @@ function FileManagerHistory:onShowHist(search_info)
self.hist_menu = nil self.hist_menu = nil
G_reader_settings:saveSetting("history_filter", self.filter) G_reader_settings:saveSetting("history_filter", self.filter)
end end
UIManager:show(self.hist_menu, "flashui") UIManager:show(self.hist_menu)
return true return true
end end
@ -309,7 +289,6 @@ function FileManagerHistory:showHistDialog()
self.filter = filter self.filter = filter
if filter == "all" then -- reset all filters if filter == "all" then -- reset all filters
self.search_string = nil self.search_string = nil
self.selected_colections = nil
end end
self:updateItemTable() self:updateItemTable()
end, end,
@ -325,19 +304,6 @@ function FileManagerHistory:showHistDialog()
genFilterButton("abandoned"), genFilterButton("abandoned"),
genFilterButton("complete"), 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, { table.insert(buttons, {
{ {
text = _("Search in filename and book metadata"), text = _("Search in filename and book metadata"),

@ -61,9 +61,6 @@ end
function FileManagerMenu:registerKeyEvents() function FileManagerMenu:registerKeyEvents()
if Device:hasKeys() then if Device:hasKeys() then
self.key_events.ShowMenu = { { "Menu" } } self.key_events.ShowMenu = { { "Menu" } }
if Device:hasScreenKB() then
self.key_events.OpenLastDoc = { { "ScreenKB", "Back" } }
end
end end
end end
@ -175,8 +172,7 @@ function FileManagerMenu:setUpdateItemTable()
sub_item_table = { sub_item_table = {
{ {
text_func = function() text_func = function()
return T(_("Items per page: %1"), return T(_("Items per page: %1"), G_reader_settings:readSetting("items_per_page"))
G_reader_settings:readSetting("items_per_page") or FileChooser.items_per_page_default)
end, end,
help_text = _([[This sets the number of items per page in: help_text = _([[This sets the number of items per page in:
- File browser, history and favorites in 'classic' display mode - File browser, history and favorites in 'classic' display mode
@ -184,8 +180,8 @@ function FileManagerMenu:setUpdateItemTable()
- File and folder selection - File and folder selection
- Calibre and OPDS browsers/search results]]), - Calibre and OPDS browsers/search results]]),
callback = function(touchmenu_instance) callback = function(touchmenu_instance)
local current_value = G_reader_settings:readSetting("items_per_page")
local default_value = FileChooser.items_per_page_default local default_value = FileChooser.items_per_page_default
local current_value = G_reader_settings:readSetting("items_per_page") or default_value
local widget = SpinWidget:new{ local widget = SpinWidget:new{
title_text = _("Items per page"), title_text = _("Items per page"),
value = current_value, value = current_value,
@ -208,8 +204,7 @@ function FileManagerMenu:setUpdateItemTable()
end, end,
callback = function(touchmenu_instance) callback = function(touchmenu_instance)
local current_value = FileChooser.font_size local current_value = FileChooser.font_size
local default_value = FileChooser.getItemFontSize(G_reader_settings:readSetting("items_per_page") local default_value = FileChooser.getItemFontSize(G_reader_settings:readSetting("items_per_page"))
or FileChooser.items_per_page_default)
local widget = SpinWidget:new{ local widget = SpinWidget:new{
title_text = _("Item font size"), title_text = _("Item font size"),
value = current_value, value = current_value,
@ -457,7 +452,7 @@ To:
if Device:supportsScreensaver() then if Device:supportsScreensaver() then
self.menu_items.screensaver = { self.menu_items.screensaver = {
text = _("Sleep screen"), text = _("Screensaver"),
sub_item_table = require("ui/elements/screensaver_menu"), sub_item_table = require("ui/elements/screensaver_menu"),
} }
end end
@ -629,30 +624,6 @@ To:
end, end,
}) })
end end
if Device:isKobo() and Device:hasColorScreen() then
table.insert(self.menu_items.developer_options.sub_item_table, {
-- We default to a flag (G2) that slightly boosts saturation,
-- but it *is* a destructive process, so we want to allow disabling it.
-- @translators CFA is a technical term for the technology behind eInk's color panels. It stands for Color Film/Filter Array, leave the abbreviation alone ;).
text = _("Disable CFA post-processing"),
checked_func = function()
return G_reader_settings:isTrue("no_cfa_post_processing")
end,
callback = function()
G_reader_settings:flipNilOrFalse("no_cfa_post_processing")
UIManager:askForRestart()
end,
})
end
table.insert(self.menu_items.developer_options.sub_item_table, {
text = _("Anti-alias rounded corners"),
checked_func = function()
return G_reader_settings:nilOrTrue("anti_alias_ui")
end,
callback = function()
G_reader_settings:flipNilOrTrue("anti_alias_ui")
end,
})
--- @note: Currently, only Kobo implements this quirk --- @note: Currently, only Kobo implements this quirk
if Device:hasEinkScreen() and Device:isKobo() then if Device:hasEinkScreen() and Device:isKobo() then
table.insert(self.menu_items.developer_options.sub_item_table, { table.insert(self.menu_items.developer_options.sub_item_table, {

@ -73,18 +73,11 @@ end
-- Purge doc settings except kept -- Purge doc settings except kept
function filemanagerutil.resetDocumentSettings(file) function filemanagerutil.resetDocumentSettings(file)
local settings_to_keep = { local settings_to_keep = {
annotations = true,
annotations_paging = true,
annotations_rolling = true,
bookmarks = true, bookmarks = true,
bookmarks_paging = true,
bookmarks_rolling = true,
bookmarks_sorted_20220106 = true, bookmarks_sorted_20220106 = true,
bookmarks_version = true, bookmarks_version = true,
cre_dom_version = true, cre_dom_version = true,
highlight = true, highlight = true,
highlight_paging = true,
highlight_rolling = true,
highlights_imported = true, highlights_imported = true,
last_page = true, last_page = true,
last_xpointer = true, last_xpointer = true,
@ -241,6 +234,25 @@ function filemanagerutil.genResetSettingsButton(doc_settings_or_file, caller_cal
} }
end end
function filemanagerutil.genAddRemoveFavoritesButton(file, caller_callback, button_disabled)
local ReadCollection = require("readcollection")
local has_file = ReadCollection:hasFile(file)
return {
text_func = function()
return has_file and _("Remove from favorites") or _("Add to favorites")
end,
enabled = not button_disabled,
callback = function()
caller_callback()
if has_file then
ReadCollection:removeItem(file)
else
ReadCollection:addItem(file)
end
end,
}
end
function filemanagerutil.genShowFolderButton(file, caller_callback, button_disabled) function filemanagerutil.genShowFolderButton(file, caller_callback, button_disabled)
return { return {
text = _("Show folder"), text = _("Show folder"),

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

File diff suppressed because it is too large Load Diff

@ -41,11 +41,6 @@ function ReaderCoptListener:onReadSettings(config)
self.document._document:setIntProperty("window.status.battery.percent", self.battery_percent) self.document._document:setIntProperty("window.status.battery.percent", self.battery_percent)
self.document._document:setIntProperty("window.status.pos.percent", self.reading_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() self:onTimeFormatChanged()
-- Enable or disable crengine header status line (note that for crengine, 0=header enabled, 1=header disabled) -- Enable or disable crengine header status line (note that for crengine, 0=header enabled, 1=header disabled)
@ -81,113 +76,6 @@ function ReaderCoptListener:onReaderReady()
end end
end end
function ReaderCoptListener:updatePageInfoOverride(pageno)
pageno = pageno or self.ui.view.footer.pageno
if not (self.document.configurable.status_line == 0 and self.view.view_mode == "page" and self.page_info_override) then
return
end
-- There are a few cases where we may not be updated on change, at least:
-- - when toggling ReaderPageMap's "Use reference page numbers"
-- - when changing footer's nb of digits after decimal point
-- but we will update on next page turn. Let's not bother.
local page_pre = ""
local page_number = pageno
local page_sep = " / "
local page_count = self.ui.document:getPageCount()
local page_post = ""
local percentage = page_number / page_count
local percentage_pre = ""
local percentage_post = ""
-- Let's use the same setting for nb of digits after decimal point as configured for the footer
local percentage_digits = self.ui.view.footer.settings.progress_pct_format
local percentage_fmt = "%." .. percentage_digits .. "f%%"
-- We want the same output as with ReaderFooter's page_progress() and percentage()
-- but here each item (page number, page counte, percentage) is individually toggable,
-- so try to get something that make sense when not all are enabled
if self.ui.pagemap and self.ui.pagemap:wantsPageLabels() then
-- These become strings here
page_number = self.ui.pagemap:getCurrentPageLabel(true)
page_count = self.ui.pagemap:getLastPageLabel(true)
elseif self.ui.document:hasHiddenFlows() then
local flow = self.ui.document:getPageFlow(pageno)
page_number = tostring(self.ui.document:getPageNumberInFlow(pageno))
page_count = tostring(self.ui.document:getTotalPagesInFlow(flow))
percentage = page_number / page_count
if flow == 0 then
page_sep = " // "
else
page_pre = "["
page_post = "]"..tostring(flow)
percentage_pre = "["
percentage_post = "]"
end
end
local page_info = ""
if self.page_number == 1 or self.page_count == 1 then
page_info = page_info .. page_pre
if self.page_number == 1 then
page_info = page_info .. page_number
if self.page_count == 1 then
page_info = page_info .. page_sep
end
end
if self.page_count == 1 then
page_info = page_info .. page_count
end
page_info = page_info .. page_post
if self.reading_percent == 1 then
page_info = page_info .. " " -- (double space as done by crengine's own drawing)
end
end
if self.reading_percent == 1 then
page_info = page_info .. percentage_pre .. percentage_fmt:format(percentage*100) .. percentage_post
end
if self.battery == 1 and self.battery_percent == 1 then -- append battery percentage
local batt_pre = "["
local batt_post = "]"
local batt_val = nil
if Device:hasBattery() then
local powerd = Device:getPowerDevice()
local batt_lvl = powerd:getCapacity()
if Device:hasAuxBattery() and powerd:isAuxBatteryConnected() then
local aux_batt_lvl = powerd:getAuxCapacity()
if powerd:isAuxCharging() then
batt_pre = "[\u{21AF}"
end
-- Sum both batteries for the actual text
batt_lvl = batt_lvl + aux_batt_lvl
else
if powerd:isCharging() then
batt_pre = "[\u{21AF}"
end
end
batt_val = string.format("%2d%%", batt_lvl)
end
if batt_val then
local battery_info = " " .. batt_pre .. batt_val .. batt_post
-- ^--- (double space as done by crengine's own drawing)
page_info = page_info .. battery_info
end
end
self.document:setPageInfoOverride(page_info)
end
function ReaderCoptListener:onPageUpdate(pageno)
self:updatePageInfoOverride(pageno)
end
function ReaderCoptListener:onPosUpdate(pos, pageno)
self:updatePageInfoOverride(pageno)
end
function ReaderCoptListener:onBookMetadataChanged(prop_updated) function ReaderCoptListener:onBookMetadataChanged(prop_updated)
-- custom metadata support for alt status bar and cre synthetic cover -- custom metadata support for alt status bar and cre synthetic cover
local prop_key = prop_updated and prop_updated.metadata_key_updated local prop_key = prop_updated and prop_updated.metadata_key_updated
@ -303,12 +191,6 @@ ReaderCoptListener.onSuspend = ReaderCoptListener.unscheduleHeaderRefresh
function ReaderCoptListener:setAndSave(setting, property, value) function ReaderCoptListener:setAndSave(setting, property, value)
self.document._document:setIntProperty(property, value) self.document._document:setIntProperty(property, value)
G_reader_settings:saveSetting(setting, value) G_reader_settings:saveSetting(setting, value)
self.page_info_override = self.page_number == 1 or self.page_count == 1 or self.reading_percent == 1
if self.page_info_override then
self:updatePageInfoOverride()
else
self.document:setPageInfoOverride("") -- Don't forget to restore CRE default behaviour.
end
-- Have crengine redraw it (even if hidden by the menu at this time) -- Have crengine redraw it (even if hidden by the menu at this time)
self.ui.rolling:updateBatteryState() self.ui.rolling:updateBatteryState()
self:updateHeader() self:updateHeader()
@ -329,7 +211,7 @@ function ReaderCoptListener:getAltStatusBarMenu()
separator = true, separator = true,
sub_item_table = { sub_item_table = {
{ {
text = _("About alt status bar"), text = _("About alternate status bar"),
keep_menu_open = true, keep_menu_open = true,
callback = function() callback = function()
UIManager:show(InfoMessage:new{ UIManager:show(InfoMessage:new{
@ -412,18 +294,15 @@ function ReaderCoptListener:getAltStatusBarMenu()
}, },
{ {
text_func = function() text_func = function()
local status = _("Battery status") local status = _("off")
if self.battery == 1 then if self.battery == 1 then
if self.battery_percent == 1 then if self.battery_percent == 1 then
status = _("Battery status: percentage") status = _("percentage")
else else
status = _("Battery status: icon") status = _("icon")
end end
end end
return status return T(_("Battery status: %1"), status)
end,
checked_func = function()
return self.battery == 1
end, end,
sub_item_table = { sub_item_table = {
{ {

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

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

@ -1,4 +1,5 @@
local BD = require("ui/bidi") local BD = require("ui/bidi")
local CenterContainer = require("ui/widget/container/centercontainer")
local ConfirmBox = require("ui/widget/confirmbox") local ConfirmBox = require("ui/widget/confirmbox")
local Device = require("device") local Device = require("device")
local Event = require("ui/event") local Event = require("ui/event")
@ -7,6 +8,7 @@ local FontList = require("fontlist")
local InfoMessage = require("ui/widget/infomessage") local InfoMessage = require("ui/widget/infomessage")
local Input = Device.input local Input = Device.input
local InputContainer = require("ui/widget/container/inputcontainer") local InputContainer = require("ui/widget/container/inputcontainer")
local Menu = require("ui/widget/menu")
local MultiConfirmBox = require("ui/widget/multiconfirmbox") local MultiConfirmBox = require("ui/widget/multiconfirmbox")
local Notification = require("ui/widget/notification") local Notification = require("ui/widget/notification")
local Screen = require("device").screen local Screen = require("device").screen
@ -140,19 +142,20 @@ function ReaderFont:onGesture() end
function ReaderFont:registerKeyEvents() function ReaderFont:registerKeyEvents()
if Device:hasKeyboard() then if Device:hasKeyboard() then
if not (Device:hasScreenKB() or Device:hasSymKey()) then -- add shortcut for keyboard
-- add shortcut for keyboard self.key_events = {
self.key_events.IncreaseSize = { ShowFontMenu = { { "F" } },
IncreaseSize = {
{ "Shift", Input.group.PgFwd }, { "Shift", Input.group.PgFwd },
event = "ChangeSize", event = "ChangeSize",
args = 0.5 args = 0.5
} },
self.key_events.DecreaseSize = { DecreaseSize = {
{ "Shift", Input.group.PgBack }, { "Shift", Input.group.PgBack },
event = "ChangeSize", event = "ChangeSize",
args = -0.5 args = -0.5
} },
end }
end end
end end
@ -193,6 +196,34 @@ function ReaderFont:onReadSettings(config)
end) end)
end end
function ReaderFont:onShowFontMenu()
-- build menu widget
local main_menu = Menu:new{
title = self.font_menu_title,
item_table = self.face_table,
width = Screen:getWidth() - 100,
height = math.floor(Screen:getHeight() * 0.5),
single_line = true,
items_per_page = 8,
items_font_size = Menu.getItemFontSize(8),
}
-- build container
local menu_container = CenterContainer:new{
dimen = Screen:getSize(),
main_menu,
}
main_menu.close_callback = function()
UIManager:close(menu_container)
end
-- show menu
main_menu.show_parent = menu_container
UIManager:show(menu_container)
return true
end
--[[ --[[
UpdatePos event is used to tell ReaderRolling to update pos. UpdatePos event is used to tell ReaderRolling to update pos.
--]] --]]

File diff suppressed because it is too large Load Diff

@ -41,7 +41,6 @@ function ReaderGoto:onShowGotoDialog()
self.goto_dialog = InputDialog:new{ self.goto_dialog = InputDialog:new{
title = _("Enter page number or percentage"), title = _("Enter page number or percentage"),
input_hint = input_hint, input_hint = input_hint,
input_type = "number",
description = self.document:hasHiddenFlows() and description = self.document:hasHiddenFlows() and
_([[ _([[
x for an absolute page number x for an absolute page number
@ -81,6 +80,7 @@ x for an absolute page number
} }
}, },
}, },
input_type = "number",
} }
UIManager:show(self.goto_dialog) UIManager:show(self.goto_dialog)
self.goto_dialog:onShowKeyboard() self.goto_dialog:onShowKeyboard()
@ -88,6 +88,7 @@ end
function ReaderGoto:onShowSkimtoDialog() function ReaderGoto:onShowSkimtoDialog()
self.skimto = SkimToWidget:new{ self.skimto = SkimToWidget:new{
document = self.document,
ui = self.ui, ui = self.ui,
callback_switch_to_goto = function() callback_switch_to_goto = function()
UIManager:close(self.skimto) UIManager:close(self.skimto)
@ -180,28 +181,4 @@ function ReaderGoto:onGoToEnd()
return true return true
end 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 return ReaderGoto

@ -1,5 +1,4 @@
local ConfirmBox = require("ui/widget/confirmbox") local ConfirmBox = require("ui/widget/confirmbox")
local Device = require("device")
local Event = require("ui/event") local Event = require("ui/event")
local InfoMessage = require("ui/widget/infomessage") local InfoMessage = require("ui/widget/infomessage")
local InputDialog = require("ui/widget/inputdialog") local InputDialog = require("ui/widget/inputdialog")
@ -120,10 +119,6 @@ function ReaderHandMade:onToggleHandmadeFlows()
end end
function ReaderHandMade:addToMainMenu(menu_items) 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 = { menu_items.handmade_toc = {
text = _("Custom table of contents") .. " " .. self.custom_toc_symbol, text = _("Custom table of contents") .. " " .. self.custom_toc_symbol,
checked_func = function() return self.toc_enabled end, checked_func = function() return self.toc_enabled end,

File diff suppressed because it is too large Load Diff

@ -3,7 +3,7 @@ ReaderLink is an abstraction for document-specific link interfaces.
]] ]]
local BD = require("ui/bidi") local BD = require("ui/bidi")
local ButtonDialog = require("ui/widget/buttondialog") local ButtonDialogTitle = require("ui/widget/buttondialogtitle")
local ConfirmBox = require("ui/widget/confirmbox") local ConfirmBox = require("ui/widget/confirmbox")
local Device = require("device") local Device = require("device")
local DocumentRegistry = require("document/documentregistry") local DocumentRegistry = require("document/documentregistry")
@ -67,7 +67,6 @@ local ReaderLink = InputContainer:extend{
location_stack = nil, -- table, per-instance location_stack = nil, -- table, per-instance
forward_location_stack = nil, -- table, per-instance forward_location_stack = nil, -- table, per-instance
_external_link_buttons = nil, _external_link_buttons = nil,
supported_external_schemes = nil,
} }
function ReaderLink:init() function ReaderLink:init()
@ -119,7 +118,7 @@ function ReaderLink:init()
end) end)
if G_reader_settings:isTrue("opening_page_location_stack") then if G_reader_settings:isTrue("opening_page_location_stack") then
-- Add location at book opening to stack -- Add location at book opening to stack
self.ui:registerPostReaderReadyCallback(function() self.ui:registerPostReadyCallback(function()
self:addCurrentLocationToStack() self:addCurrentLocationToStack()
end) end)
end end
@ -137,16 +136,12 @@ function ReaderLink:init()
-- delegate gesture listener to readerui, NOP our own -- delegate gesture listener to readerui, NOP our own
self.ges_events = nil self.ges_events = nil
-- Set always supported external link schemes
self.supported_external_schemes = {"http", "https"}
-- Set up buttons for alternative external link handling methods -- Set up buttons for alternative external link handling methods
self._external_link_buttons = {} self._external_link_buttons = {}
self._external_link_buttons["10_copy"] = function(this, link_url) self._external_link_buttons["10_copy"] = function(this, link_url)
return { return {
text = _("Copy"), text = _("Copy"),
callback = function() callback = function()
Device.input.setClipboardText(link_url)
UIManager:close(this.external_link_dialog) UIManager:close(this.external_link_dialog)
end, end,
} }
@ -229,31 +224,10 @@ function ReaderLink:init()
end end
end end
-- Register URL scheme. The external link dialog will be brought up when a URL
-- with a registered scheme is followed; this also applies to schemeless
-- (including relative) URLs if the empty scheme ("") is registered,
-- overriding the default behaviour of treating these as filepaths.
-- Registering the "file" scheme also overrides its default handling.
-- Registered schemes are reset on each initialisation of ReaderLink.
function ReaderLink:registerScheme(scheme)
table.insert(self.supported_external_schemes, scheme)
end
function ReaderLink:onGesture() end function ReaderLink:onGesture() end
function ReaderLink:registerKeyEvents() function ReaderLink:registerKeyEvents()
if Device:hasScreenKB() or Device:hasSymKey() then if Device:hasKeys() then
self.key_events.GotoSelectedPageLink = { { "Press" }, event = "GotoSelectedPageLink" }
if Device:hasKeyboard() then
self.key_events.AddCurrentLocationToStackNonTouch = { { "Shift", "Press" } }
self.key_events.SelectNextPageLink = { { "Shift", "LPgFwd" }, event = "SelectNextPageLink" }
self.key_events.SelectPrevPageLink = { { "Shift", "LPgBack" }, event = "SelectPrevPageLink" }
else
self.key_events.AddCurrentLocationToStackNonTouch = { { "ScreenKB", "Press" } }
self.key_events.SelectNextPageLink = { { "ScreenKB", "LPgFwd" }, event = "SelectNextPageLink" }
self.key_events.SelectPrevPageLink = { { "ScreenKB", "LPgBack" }, event = "SelectPrevPageLink" }
end
elseif Device:hasKeys() then
self.key_events = { self.key_events = {
SelectNextPageLink = { SelectNextPageLink = {
{ "Tab" }, { "Tab" },
@ -261,6 +235,7 @@ function ReaderLink:registerKeyEvents()
}, },
SelectPrevPageLink = { SelectPrevPageLink = {
{ "Shift", "Tab" }, { "Shift", "Tab" },
{ "Sym", "Tab" }, -- Shift or Sym + Tab
event = "SelectPrevPageLink", event = "SelectPrevPageLink",
}, },
GotoSelectedPageLink = { GotoSelectedPageLink = {
@ -727,13 +702,6 @@ function ReaderLink:onAddCurrentLocationToStack(show_notification)
if show_notification then if show_notification then
Notification:notify(_("Current location added to history.")) Notification:notify(_("Current location added to history."))
end end
return true
end
function ReaderLink:onAddCurrentLocationToStackNonTouch()
self:addCurrentLocationToStack()
Notification:notify(_("Current location added to history."), Notification.SOURCE_ALWAYS_SHOW)
return true
end end
-- Remember current location so we can go back to it -- Remember current location so we can go back to it
@ -743,10 +711,6 @@ function ReaderLink:addCurrentLocationToStack(loc)
table.insert(self.location_stack, location) table.insert(self.location_stack, location)
end end
function ReaderLink:popFromLocationStack()
return table.remove(self.location_stack)
end
function ReaderLink:onClearLocationStack(show_notification) function ReaderLink:onClearLocationStack(show_notification)
self.location_stack = {} self.location_stack = {}
self:onClearForwardLocationStack() self:onClearForwardLocationStack()
@ -843,9 +807,8 @@ function ReaderLink:onGotoLink(link, neglect_current_location, allow_footnote_po
end end
logger.dbg("ReaderLink:onGotoLink: External link:", link_url) logger.dbg("ReaderLink:onGotoLink: External link:", link_url)
local scheme = link_url:match("^(%w[%w+%-.]*):") or "" local is_http_link = link_url:find("^https?://") ~= nil
local is_supported_external_link = util.arrayContains(self.supported_external_schemes, scheme:lower()) if is_http_link and self:onGoToExternalLink(link_url) then
if is_supported_external_link and self:onGoToExternalLink(link_url) then
return true return true
end end
@ -894,7 +857,7 @@ end
function ReaderLink:onGoToExternalLink(link_url) function ReaderLink:onGoToExternalLink(link_url)
local buttons, title = self:getButtonsForExternalLinkDialog(link_url) local buttons, title = self:getButtonsForExternalLinkDialog(link_url)
self.external_link_dialog = ButtonDialog:new{ self.external_link_dialog = ButtonDialogTitle:new{
title = title, title = title,
buttons = buttons, buttons = buttons,
} }

@ -254,7 +254,7 @@ function ReaderMenu:setUpdateItemTable()
if Device:supportsScreensaver() then if Device:supportsScreensaver() then
local ss_book_settings = { local ss_book_settings = {
text = _("Do not show this book cover on sleep screen"), text = _("Exclude this book's content and cover from screensaver"),
enabled_func = function() enabled_func = function()
if self.ui and self.ui.document then if self.ui and self.ui.document then
local screensaverType = G_reader_settings:readSetting("screensaver_type") local screensaverType = G_reader_settings:readSetting("screensaver_type")
@ -285,7 +285,7 @@ function ReaderMenu:setUpdateItemTable()
end end
table.insert(screensaver_sub_item_table, ss_book_settings) table.insert(screensaver_sub_item_table, ss_book_settings)
self.menu_items.screensaver = { self.menu_items.screensaver = {
text = _("Sleep screen"), text = _("Screensaver"),
sub_item_table = screensaver_sub_item_table, sub_item_table = screensaver_sub_item_table,
} }
end end

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

@ -51,28 +51,7 @@ end
function ReaderPaging:onGesture() end function ReaderPaging:onGesture() end
function ReaderPaging:registerKeyEvents() function ReaderPaging:registerKeyEvents()
if Device:hasDPad() and Device:useDPadAsActionKeys() then if Device:hasKeys() then
self.key_events.GotoNextPos = {
{ { "RPgFwd", "LPgFwd" } },
event = "GotoPosRel",
args = 1,
}
self.key_events.GotoPrevPos = {
{ { "RPgBack", "LPgBack" } },
event = "GotoPosRel",
args = -1,
}
self.key_events.GotoNextChapter = {
{ "Right" },
event = "GotoNextChapter",
args = 1,
}
self.key_events.GotoPrevChapter = {
{ "Left" },
event = "GotoPrevChapter",
args = -1,
}
elseif Device:hasKeys() then
self.key_events.GotoNextPage = { self.key_events.GotoNextPage = {
{ { "RPgFwd", "LPgFwd", not Device:hasFewKeys() and "Right" } }, { { "RPgFwd", "LPgFwd", not Device:hasFewKeys() and "Right" } },
event = "GotoViewRel", event = "GotoViewRel",
@ -359,37 +338,6 @@ function ReaderPaging:bookmarkFlipping(flipping_page, flipping_ges)
UIManager:setDirty(self.view.dialog, "partial") UIManager:setDirty(self.view.dialog, "partial")
end end
function ReaderPaging:enterSkimMode()
if self.view.document.configurable.text_wrap ~= 0 or self.view.page_scroll or self.view.zoom_mode ~= "page" then
self.skim_backup = {
text_wrap = self.view.document.configurable.text_wrap,
page_scroll = self.view.page_scroll,
zoom_mode = self.view.zoom_mode,
current_page = self.current_page,
location = self:getBookLocation(),
}
self.view.document.configurable.text_wrap = 0
self.view.page_scroll = false
self.ui.zooming:onSetZoomMode("page")
self.ui.zooming:onReZoom()
end
end
function ReaderPaging:exitSkimMode()
if self.skim_backup then
self.view.document.configurable.text_wrap = self.skim_backup.text_wrap
self.view.page_scroll = self.skim_backup.page_scroll
self.ui.zooming:onSetZoomMode(self.skim_backup.zoom_mode)
self.ui.zooming:onReZoom()
if self.current_page == self.skim_backup.current_page then
-- if SkimToWidget is closed on the start page, restore exact location
self.current_page = 0 -- do not emit extra PageUpdate event
self:onRestoreBookLocation(self.skim_backup.location)
end
self.skim_backup = nil
end
end
function ReaderPaging:onScrollSettingsUpdated(scroll_method, inertial_scroll_enabled, scroll_activation_delay_ms) function ReaderPaging:onScrollSettingsUpdated(scroll_method, inertial_scroll_enabled, scroll_activation_delay_ms)
self.scroll_method = scroll_method self.scroll_method = scroll_method
self.scroll_activation_delay = time.ms(scroll_activation_delay_ms) self.scroll_activation_delay = time.ms(scroll_activation_delay_ms)

@ -100,7 +100,7 @@ function ReaderRolling:init()
self.valid_cache_rendering_hash = self.ui.document:getDocumentRenderingHash(false) self.valid_cache_rendering_hash = self.ui.document:getDocumentRenderingHash(false)
end end
end) end)
table.insert(self.ui.postReaderReadyCallback, function() table.insert(self.ui.postReaderCallback, function()
self:updatePos() self:updatePos()
-- Disable crengine internal history, with required redraw -- Disable crengine internal history, with required redraw
self.ui.document:enableInternalHistory(false) self.ui.document:enableInternalHistory(false)
@ -116,30 +116,7 @@ end
function ReaderRolling:onGesture() end function ReaderRolling:onGesture() end
function ReaderRolling:registerKeyEvents() function ReaderRolling:registerKeyEvents()
if Device:hasScreenKB() or Device:hasSymKey() then if Device:hasKeys() then
self.key_events.GotoNextView = {
{ { "RPgFwd", "LPgFwd" } },
event = "GotoViewRel",
args = 1,
}
self.key_events.GotoPrevView = {
{ { "RPgBack", "LPgBack" } },
event = "GotoViewRel",
args = -1,
}
if Device:hasKeyboard() then
self.key_events.MoveUp = {
{ "Shift", "RPgBack" },
event = "Panning",
args = {0, -1},
}
self.key_events.MoveDown = {
{ "Shift", "RPgFwd" },
event = "Panning",
args = {0, 1},
}
end
elseif Device:hasKeys() then
self.key_events.GotoNextView = { self.key_events.GotoNextView = {
{ { "RPgFwd", "LPgFwd", "Right" } }, { { "RPgFwd", "LPgFwd", "Right" } },
event = "GotoViewRel", event = "GotoViewRel",
@ -151,18 +128,7 @@ function ReaderRolling:registerKeyEvents()
args = -1, args = -1,
} }
end end
if Device:hasDPad() and Device:useDPadAsActionKeys() then if Device:hasDPad() then
self.key_events.GotoNextChapter = {
{ "Right" },
event = "GotoNextChapter",
args = 1,
}
self.key_events.GotoPrevChapter = {
{ "Left" },
event = "GotoPrevChapter",
args = -1,
}
elseif Device:hasDPad() then
self.key_events.MoveUp = { self.key_events.MoveUp = {
{ "Up" }, { "Up" },
event = "Panning", event = "Panning",
@ -174,18 +140,6 @@ function ReaderRolling:registerKeyEvents()
args = {0, 1}, args = {0, 1},
} }
end end
if Device:hasScreenKB() then
self.key_events.MoveUp = {
{ "ScreenKB", "RPgBack" },
event = "Panning",
args = {0, -1},
}
self.key_events.MoveDown = {
{ "ScreenKB", "RPgFwd" },
event = "Panning",
args = {0, 1},
}
end
if Device:hasKeyboard() then if Device:hasKeyboard() then
self.key_events.GotoFirst = { self.key_events.GotoFirst = {
{ "1" }, { "1" },
@ -280,7 +234,7 @@ function ReaderRolling:onReadSettings(config)
-- And check if we can migrate to a newest DOM version after -- And check if we can migrate to a newest DOM version after
-- the book is loaded (unless the user told us not to). -- the book is loaded (unless the user told us not to).
if config:nilOrFalse("cre_keep_old_dom_version") then if config:nilOrFalse("cre_keep_old_dom_version") then
self.ui:registerPostReaderReadyCallback(function() self.ui:registerPostReadyCallback(function()
self:checkXPointersAndProposeDOMVersionUpgrade() self:checkXPointersAndProposeDOMVersionUpgrade()
end) end)
end end
@ -1075,9 +1029,9 @@ function ReaderRolling:onUpdatePos(force)
if self.batched_update_count > 0 then if self.batched_update_count > 0 then
return return
end end
if self.ui.postReaderReadyCallback ~= nil then -- ReaderUI:init() not yet done if self.ui.postReaderCallback ~= nil then -- ReaderUI:init() not yet done
-- Don't schedule any updatePos as long as ReaderUI:init() is -- Don't schedule any updatePos as long as ReaderUI:init() is
-- not finished (one will be called in the ui.postReaderReadyCallback -- not finished (one will be called in the ui.postReaderCallback
-- we have set above) to avoid multiple refreshes. -- we have set above) to avoid multiple refreshes.
return true return true
end end
@ -1175,7 +1129,7 @@ function ReaderRolling:onRedrawCurrentView()
end end
function ReaderRolling:onSetDimensions(dimen) function ReaderRolling:onSetDimensions(dimen)
if self.ui.postReaderReadyCallback ~= nil then if self.ui.postReaderCallback ~= nil then
-- ReaderUI:init() not yet done: just set document dimensions -- ReaderUI:init() not yet done: just set document dimensions
self.ui.document:setViewDimen(Screen:getSize()) self.ui.document:setViewDimen(Screen:getSize())
-- (what's done in the following else is done elsewhere by -- (what's done in the following else is done elsewhere by
@ -1491,12 +1445,25 @@ function ReaderRolling:checkXPointersAndProposeDOMVersionUpgrade()
local applyFuncToXPointersSlots = function(func) local applyFuncToXPointersSlots = function(func)
-- Last position -- Last position
func(self, "xpointer", "last position in book") func(self, "xpointer", "last position in book")
-- Annotations -- Bookmarks
if self.ui.annotation and self.ui.annotation.annotations and #self.ui.annotation.annotations > 0 then if self.ui.bookmark and self.ui.bookmark.bookmarks and #self.ui.bookmark.bookmarks > 0 then
local slots = { "page", "pos0", "pos1" } local slots = { "page", "pos0", "pos1" }
for _, item in ipairs(self.ui.annotation.annotations) do for _, bookmark in ipairs(self.ui.bookmark.bookmarks) do
for _, slot in ipairs(slots) do for _, slot in ipairs(slots) do
func(item, slot, item.text or "annotation") func(bookmark, slot, bookmark.notes or "bookmark")
end
end
end
-- Highlights
if self.view.highlight and self.view.highlight.saved then
local slots = { "pos0", "pos1" }
for page, items in pairs(self.view.highlight.saved) do
if items and #items > 0 then
for _, highlight in ipairs(items) do
for _, slot in ipairs(slots) do
func(highlight, slot, highlight.text or "highlight")
end
end
end end
end end
end end
@ -1542,9 +1509,6 @@ function ReaderRolling:checkXPointersAndProposeDOMVersionUpgrade()
local new_xp = normalized_xpointers[xp] local new_xp = normalized_xpointers[xp]
if new_xp then if new_xp then
obj[slot] = new_xp obj[slot] = new_xp
if slot == "page" then
self.ui.annotation:updateItemByXPointer(obj)
end
else else
-- Let lost/not-found XPointer be. There is a small chance that -- Let lost/not-found XPointer be. There is a small chance that
-- it will be found (it it was made before the boxing code moved -- it will be found (it it was made before the boxing code moved

@ -83,16 +83,6 @@ function ReaderSearch:addToMainMenu(menu_items)
menu_items.fulltext_search_settings = { menu_items.fulltext_search_settings = {
text = _("Fulltext search settings"), text = _("Fulltext search settings"),
sub_item_table = { 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() text_func = function()
return T(_("Words in context: %1"), self.findall_nb_context_words) return T(_("Words in context: %1"), self.findall_nb_context_words)
@ -152,36 +142,19 @@ function ReaderSearch:addToMainMenu(menu_items)
} }
end end
function ReaderSearch:searchText(text) -- from highlight dialog -- if reverse ~= 0 search backwards
if G_reader_settings:isTrue("fulltext_search_find_all") then function ReaderSearch:searchCallback(reverse)
self.ui.highlight:clear() local search_text = self.input_dialog:getInputText()
self:searchCallback(nil, text) if search_text == "" then return end
else -- search_text comes from our keyboard, and may contain multiple diacritics ordered
self:searchCallback(0, text) -- forward -- in any order: we'd rather have them normalized, and expect the book content to
end -- be proper and normalized text.
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.ui.doc_settings:saveSetting("fulltext_search_last_search_text", search_text)
self.last_search_text = search_text self.last_search_text = search_text -- if shown again, show it as it has been inputted
search_text = Utf8Proc.normalize_NFC(search_text)
local regex_error self.use_regex = self.check_button_regex.checked
if text then -- from highlight dialog self.case_insensitive = not self.check_button_case.checked
self.use_regex = false local regex_error = self.use_regex and self.ui.document:checkRegex(search_text)
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 if self.use_regex and regex_error ~= 0 then
logger.dbg("ReaderSearch: regex error", regex_error, SRELL_ERROR_CODES[regex_error]) logger.dbg("ReaderSearch: regex error", regex_error, SRELL_ERROR_CODES[regex_error])
local error_message local error_message
@ -194,7 +167,6 @@ function ReaderSearch:searchCallback(reverse, text)
else else
UIManager:close(self.input_dialog) UIManager:close(self.input_dialog)
if reverse then if reverse then
self.last_search_hash = nil
self:onShowSearchDialog(search_text, reverse, self.use_regex, self.case_insensitive) self:onShowSearchDialog(search_text, reverse, self.use_regex, self.case_insensitive)
else else
local Trapper = require("ui/trapper") local Trapper = require("ui/trapper")
@ -289,9 +261,6 @@ function ReaderSearch:onShowSearchDialog(text, direction, regex, case_insensitiv
local res = search_func(self, search_term, param, regex, case_insensitive) local res = search_func(self, search_term, param, regex, case_insensitive)
if res then if res then
if self.ui.paging then if self.ui.paging then
if not current_page then -- initial search
current_page = self.ui.paging.current_page
end
no_results = false no_results = false
self.ui.link:onGotoLink({page = res.page - 1}, neglect_current_location) self.ui.link:onGotoLink({page = res.page - 1}, neglect_current_location)
self.view.highlight.temp[res.page] = res self.view.highlight.temp[res.page] = res
@ -360,24 +329,8 @@ function ReaderSearch:onShowSearchDialog(text, direction, regex, case_insensitiv
self.ui.link:onGotoLink({xpointer=valid_link}, neglect_current_location) self.ui.link:onGotoLink({xpointer=valid_link}, neglect_current_location)
end end
end end
if not neglect_current_location then -- Don't add result pages to location ("Go back") stack
-- Initial search: onGotoLink() has added the current page to the location stack, neglect_current_location = true
-- 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 end
if no_results then if no_results then
local notification_text local notification_text
@ -439,6 +392,7 @@ function ReaderSearch:onShowSearchDialog(text, direction, regex, case_insensitiv
icon_height = Screen:scaleBySize(DGENERIC_ICON_SIZE * 0.8), icon_height = Screen:scaleBySize(DGENERIC_ICON_SIZE * 0.8),
callback = function() callback = function()
self.search_dialog:onClose() self.search_dialog:onClose()
self.last_search_text = text
self:onShowFulltextSearchInput() self:onShowFulltextSearchInput()
end, end,
}, },
@ -546,7 +500,7 @@ function ReaderSearch:searchNext(pattern, direction, regex, case_insensitive)
end end
function ReaderSearch:findAllText(search_text) function ReaderSearch:findAllText(search_text)
local last_search_hash = (self.last_search_text or "") .. tostring(self.case_insensitive) .. tostring(self.use_regex) local last_search_hash = self.last_search_text .. tostring(self.case_insensitive) .. tostring(self.use_regex)
local not_cached = self.last_search_hash ~= last_search_hash local not_cached = self.last_search_hash ~= last_search_hash
if not_cached then if not_cached then
local Trapper = require("ui/trapper") local Trapper = require("ui/trapper")
@ -571,7 +525,7 @@ function ReaderSearch:findAllText(search_text)
end end
function ReaderSearch:onShowFindAllResults(not_cached) function ReaderSearch:onShowFindAllResults(not_cached)
if not self.last_search_hash or (not not_cached and self.findall_results == nil) then if not not_cached and self.findall_results == nil then
-- no cached results, show input dialog -- no cached results, show input dialog
self:onShowFulltextSearchInput() self:onShowFulltextSearchInput()
return return
@ -592,15 +546,9 @@ function ReaderSearch:onShowFindAllResults(not_cached)
local text = TextBoxWidget.PTF_BOLD_START .. word .. TextBoxWidget.PTF_BOLD_END local text = TextBoxWidget.PTF_BOLD_START .. word .. TextBoxWidget.PTF_BOLD_END
-- append context before and after the word -- append context before and after the word
if item.prev_text then if item.prev_text then
if not item.prev_text:find("%s$") then
text = " " .. text
end
text = item.prev_text .. text text = item.prev_text .. text
end end
if item.next_text then if item.next_text then
if not item.next_text:find("^[%s%p]") then
text = text .. " "
end
text = text .. item.next_text text = text .. item.next_text
end end
text = TextBoxWidget.PTF_HEADER .. text -- enable handling of our bold tags text = TextBoxWidget.PTF_HEADER .. text -- enable handling of our bold tags

@ -219,7 +219,6 @@ end
-- Otherwise we change status from reading/abandoned to complete or from complete to reading. -- Otherwise we change status from reading/abandoned to complete or from complete to reading.
function ReaderStatus:onMarkBook(mark_read) function ReaderStatus:onMarkBook(mark_read)
self.summary.status = (not mark_read and self.summary.status == "complete") and "reading" or "complete" 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 -- 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:saveSetting("summary", self.summary)
self.settings:flush() self.settings:flush()

@ -1,6 +1,5 @@
local BD = require("ui/bidi") local BD = require("ui/bidi")
local Blitbuffer = require("ffi/blitbuffer") local Blitbuffer = require("ffi/blitbuffer")
local ButtonDialog = require("ui/widget/buttondialog")
local ButtonTable = require("ui/widget/buttontable") local ButtonTable = require("ui/widget/buttontable")
local CenterContainer = require("ui/widget/container/centercontainer") local CenterContainer = require("ui/widget/container/centercontainer")
local CssTweaks = require("ui/data/css_tweaks") local CssTweaks = require("ui/data/css_tweaks")
@ -162,6 +161,7 @@ function TweakInfoWidget:init()
FrameContainer:new{ FrameContainer:new{
background = Blitbuffer.COLOR_WHITE, background = Blitbuffer.COLOR_WHITE,
radius = Size.radius.window, radius = Size.radius.window,
margin = Size.margin.default,
padding = Size.padding.default, padding = Size.padding.default,
padding_bottom = 0, -- no padding below buttontable padding_bottom = 0, -- no padding below buttontable
VerticalGroup:new{ VerticalGroup:new{
@ -631,7 +631,7 @@ You can enable individual tweaks on this book with a tap, or view more details a
local mode = lfs.attributes(dir.."/"..f, "mode") local mode = lfs.attributes(dir.."/"..f, "mode")
if mode == "directory" then if mode == "directory" then
table.insert(dir_list, f) table.insert(dir_list, f)
elseif mode == "file" and string.match(f, "%.css$") and not util.stringStartsWith(f, "._") then elseif mode == "file" and string.match(f, "%.css$") then
table.insert(file_list, f) table.insert(file_list, f)
end end
end end
@ -783,123 +783,13 @@ local BOOK_TWEAK_INPUT_HINT = T([[
%2]], _("You can add CSS snippets which will be applied only to this book."), BOOK_TWEAK_SAMPLE_CSS) %2]], _("You can add CSS snippets which will be applied only to this book."), BOOK_TWEAK_SAMPLE_CSS)
local CSS_SUGGESTIONS = { local CSS_SUGGESTIONS = {
{ _("Long-press for info ⓘ"), _([[ { "-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.")},
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. { "-cr-hint: non-linear-combining;", _("Can be set on some specific DocFragments (ie. DocFragment[id*=16]) to ignore them in the linear pages flow.")},
{ "-cr-hint: toc-level1;", _("When set on an element, its text can be used to build the alternative table of contents.")},
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. { "display: run-in !important;", _("When set on a block element, this element content will be inlined with the next block element.")},
{ "font-size: 1rem !important;", _("1rem will enforce your main font size.")},
Long-press on any item in this popup to get more information on what it does and what it can help solving. { "hyphens: none !important;", _("Disables hyphenation inside the targeted elements.")},
{ "text-indent: 1.2em !important;", _("1.2em is our default text indentation.")},
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) function ReaderStyleTweak:editBookTweak(touchmenu_instance)
@ -979,89 +869,19 @@ function ReaderStyleTweak:editBookTweak(touchmenu_instance)
local suggestions_popup_widget local suggestions_popup_widget
local buttons = {} local buttons = {}
for _, suggestion in ipairs(CSS_SUGGESTIONS) do 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, {{ table.insert(buttons, {{
text = text, text = suggestion[1],
id = title,
align = "left", align = "left",
callback = function() callback = function()
if is_info_only then UIManager:close(suggestions_popup_widget)
-- No CSS bit to insert, show description also on tap editor._input_widget:addChars(suggestion[1])
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, end,
hold_callback = description and function() hold_callback = suggestion[2] and function()
UIManager:show(InfoMessage:new{ text = description }) UIManager:show(InfoMessage:new{ text = suggestion[2] })
end or nil end or nil
}}) }})
end end
local ButtonDialog = require("ui/widget/buttondialog")
suggestions_popup_widget = ButtonDialog:new{ suggestions_popup_widget = ButtonDialog:new{
modal = true, -- needed when keyboard is shown modal = true, -- needed when keyboard is shown
width = math.floor(Screen:getWidth() * 0.9), -- max width, will get smaller width = math.floor(Screen:getWidth() * 0.9), -- max width, will get smaller

@ -2,11 +2,11 @@ local Blitbuffer = require("ffi/blitbuffer")
local Cache = require("cache") local Cache = require("cache")
local Device = require("device") local Device = require("device")
local Geom = require("ui/geometry") local Geom = require("ui/geometry")
local InputContainer = require("ui/widget/container/inputcontainer")
local Persist = require("persist") local Persist = require("persist")
local RenderImage = require("ui/renderimage") local RenderImage = require("ui/renderimage")
local TileCacheItem = require("document/tilecacheitem") local TileCacheItem = require("document/tilecacheitem")
local UIManager = require("ui/uimanager") local UIManager = require("ui/uimanager")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local Screen = Device.screen local Screen = Device.screen
local ffiutil = require("ffi/util") local ffiutil = require("ffi/util")
local logger = require("logger") local logger = require("logger")
@ -18,15 +18,13 @@ local _ = require("gettext")
-- It handles launching via the menu or Dispatcher/Gestures two fullscreen -- It handles launching via the menu or Dispatcher/Gestures two fullscreen
-- widgets related to showing pages and thumbnails that will make use of -- widgets related to showing pages and thumbnails that will make use of
-- its services: BookMap and PageBrowser. -- its services: BookMap and PageBrowser.
local ReaderThumbnail = InputContainer:extend{} local ReaderThumbnail = WidgetContainer:extend{}
function ReaderThumbnail:init() function ReaderThumbnail:init()
self:registerKeyEvents() if not Device:isTouchDevice() then
if not Device:isTouchDevice() and not Device:useDPadAsActionKeys() then
-- The BookMap and PageBrowser widgets depend too much on gestures, -- The BookMap and PageBrowser widgets depend too much on gestures,
-- making them work with not enough keys on Non-Touch would be hard and very limited, so -- making them work with keys would be hard and very limited, so
-- just don't make them available. -- just don't make them available.
-- We will only let BookMap run on useDPadAsActionKeys devices.
return return
end end
@ -64,16 +62,6 @@ function ReaderThumbnail:init()
end end
end end
function ReaderThumbnail:registerKeyEvents()
if Device:hasDPad() and Device:useDPadAsActionKeys() then
if Device:hasKeyboard() then
self.key_events.ShowBookMap = { { "Shift", "Down" } }
else
self.key_events.ShowBookMap = { { "ScreenKB", "Down" } }
end
end
end
function ReaderThumbnail:addToMainMenu(menu_items) function ReaderThumbnail:addToMainMenu(menu_items)
menu_items.book_map = { menu_items.book_map = {
text = _("Book map"), text = _("Book map"),
@ -87,8 +75,6 @@ function ReaderThumbnail:addToMainMenu(menu_items)
self:onShowBookMap(true) self:onShowBookMap(true)
end, end,
} }
-- PageBrowser still needs some work before we can let it run on non-touch devices with useDPadAsActionKeys
if Device:hasDPad() and Device:useDPadAsActionKeys() then return end
menu_items.page_browser = { menu_items.page_browser = {
text = _("Page browser"), text = _("Page browser"),
callback = function() callback = function()
@ -200,11 +186,11 @@ function ReaderThumbnail:removeFromCache(hash_subs, remove_only_non_matching)
return nb_removed, size_removed return nb_removed, size_removed
end end
function ReaderThumbnail:resetCachedPagesForBookmarks(annotations) function ReaderThumbnail:resetCachedPagesForBookmarks(...)
-- Multiple bookmarks may be provided -- Multiple bookmarks may be provided
local start_page, end_page local start_page, end_page
for i = 1, #annotations do for i = 1, select("#", ...) do
local bm = annotations[i] local bm = select(i, ...)
if self.ui.rolling then if self.ui.rolling then
-- Look at all properties that may be xpointers -- Look at all properties that may be xpointers
for _, k in ipairs({"page", "pos0", "pos1"}) do for _, k in ipairs({"page", "pos0", "pos1"}) do
@ -551,6 +537,9 @@ end
ReaderThumbnail.onDocumentRerendered = ReaderThumbnail.resetCache ReaderThumbnail.onDocumentRerendered = ReaderThumbnail.resetCache
ReaderThumbnail.onDocumentPartiallyRerendered = ReaderThumbnail.resetCache ReaderThumbnail.onDocumentPartiallyRerendered = ReaderThumbnail.resetCache
-- Emitted When adding/removing/updating bookmarks and highlights -- Emitted When adding/removing/updating bookmarks and highlights
ReaderThumbnail.onAnnotationsModified = ReaderThumbnail.resetCachedPagesForBookmarks ReaderThumbnail.onBookmarkAdded = ReaderThumbnail.resetCachedPagesForBookmarks
ReaderThumbnail.onBookmarkRemoved = ReaderThumbnail.resetCachedPagesForBookmarks
ReaderThumbnail.onBookmarkUpdated = ReaderThumbnail.resetCachedPagesForBookmarks
ReaderThumbnail.onBookmarkEdited = ReaderThumbnail.resetCachedPagesForBookmarks
return ReaderThumbnail return ReaderThumbnail

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

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

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

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

@ -113,17 +113,6 @@ function ReaderWikipedia:addToMainMenu(menu_items)
}) })
end, 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 = { menu_items.wikipedia_settings = {
text = _("Wikipedia settings"), text = _("Wikipedia settings"),
sub_item_table = { sub_item_table = {
@ -181,7 +170,6 @@ function ReaderWikipedia:addToMainMenu(menu_items)
UIManager:show(wikilang_input) UIManager:show(wikilang_input)
wikilang_input:onShowKeyboard() wikilang_input:onShowKeyboard()
end, end,
separator = true,
}, },
{ -- setting used by dictquicklookup { -- setting used by dictquicklookup
text = _("Set Wikipedia 'Save as EPUB' folder"), text = _("Set Wikipedia 'Save as EPUB' folder"),
@ -211,41 +199,6 @@ You can choose an existing folder, or use a default folder named "Wikipedia" in
callback = function() callback = function()
G_reader_settings:flipNilOrFalse("wikipedia_save_in_book_dir") G_reader_settings:flipNilOrFalse("wikipedia_save_in_book_dir")
end, 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, separator = true,
}, },
{ {

@ -230,10 +230,11 @@ function ReaderZooming:onReadSettings(config)
-- Otherwise, build it from the split genus & type settings -- Otherwise, build it from the split genus & type settings
local zoom_mode_genus = config:readSetting("kopt_zoom_mode_genus") local zoom_mode_genus = config:readSetting("kopt_zoom_mode_genus")
or G_reader_settings: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") local zoom_mode_type = config:readSetting("kopt_zoom_mode_type")
or G_reader_settings:readSetting("kopt_zoom_mode_type") or G_reader_settings:readSetting("kopt_zoom_mode_type")
zoom_mode = self:combo_to_mode(zoom_mode_genus, zoom_mode_type) if zoom_mode_genus or zoom_mode_type then
zoom_mode = self:combo_to_mode(zoom_mode_genus, zoom_mode_type)
end
-- Validate it -- Validate it
zoom_mode = self.zoom_mode_label[zoom_mode] and zoom_mode or self.DEFAULT_ZOOM_MODE zoom_mode = self.zoom_mode_label[zoom_mode] and zoom_mode or self.DEFAULT_ZOOM_MODE

@ -21,11 +21,9 @@ local InfoMessage = require("ui/widget/infomessage")
local InputContainer = require("ui/widget/container/inputcontainer") local InputContainer = require("ui/widget/container/inputcontainer")
local InputDialog = require("ui/widget/inputdialog") local InputDialog = require("ui/widget/inputdialog")
local LanguageSupport = require("languagesupport") local LanguageSupport = require("languagesupport")
local NetworkListener = require("ui/network/networklistener")
local Notification = require("ui/widget/notification") local Notification = require("ui/widget/notification")
local PluginLoader = require("pluginloader") local PluginLoader = require("pluginloader")
local ReaderActivityIndicator = require("apps/reader/modules/readeractivityindicator") local ReaderActivityIndicator = require("apps/reader/modules/readeractivityindicator")
local ReaderAnnotation = require("apps/reader/modules/readerannotation")
local ReaderBack = require("apps/reader/modules/readerback") local ReaderBack = require("apps/reader/modules/readerback")
local ReaderBookmark = require("apps/reader/modules/readerbookmark") local ReaderBookmark = require("apps/reader/modules/readerbookmark")
local ReaderConfig = require("apps/reader/modules/readerconfig") local ReaderConfig = require("apps/reader/modules/readerconfig")
@ -62,7 +60,6 @@ local Screenshoter = require("ui/widget/screenshoter")
local SettingsMigration = require("ui/data/settings_migration") local SettingsMigration = require("ui/data/settings_migration")
local UIManager = require("ui/uimanager") local UIManager = require("ui/uimanager")
local ffiUtil = require("ffi/util") local ffiUtil = require("ffi/util")
local filemanagerutil = require("apps/filemanager/filemanagerutil")
local lfs = require("libs/libkoreader-lfs") local lfs = require("libs/libkoreader-lfs")
local logger = require("logger") local logger = require("logger")
local time = require("ui/time") local time = require("ui/time")
@ -86,7 +83,7 @@ local ReaderUI = InputContainer:extend{
password = nil, password = nil,
postInitCallback = nil, postInitCallback = nil,
postReaderReadyCallback = nil, postReaderCallback = nil,
} }
function ReaderUI:registerModule(name, ui_module, always_active) function ReaderUI:registerModule(name, ui_module, always_active)
@ -105,8 +102,8 @@ function ReaderUI:registerPostInitCallback(callback)
table.insert(self.postInitCallback, callback) table.insert(self.postInitCallback, callback)
end end
function ReaderUI:registerPostReaderReadyCallback(callback) function ReaderUI:registerPostReadyCallback(callback)
table.insert(self.postReaderReadyCallback, callback) table.insert(self.postReaderCallback, callback)
end end
function ReaderUI:init() function ReaderUI:init()
@ -119,7 +116,7 @@ function ReaderUI:init()
Device:setIgnoreInput(true) -- Avoid ANRs on Android with unprocessed events. Device:setIgnoreInput(true) -- Avoid ANRs on Android with unprocessed events.
self.postInitCallback = {} self.postInitCallback = {}
self.postReaderReadyCallback = {} self.postReaderCallback = {}
-- if we are not the top level dialog ourselves, it must be given in the table -- if we are not the top level dialog ourselves, it must be given in the table
if not self.dialog then if not self.dialog then
self.dialog = self self.dialog = self
@ -185,12 +182,6 @@ function ReaderUI:init()
view = self.view, view = self.view,
ui = self ui = self
}) })
self:registerModule("annotation", ReaderAnnotation:new{
dialog = self.dialog,
view = self.view,
ui = self,
document = self.document,
})
-- reader goto controller -- reader goto controller
-- "goto" being a dirty keyword in Lua? -- "goto" being a dirty keyword in Lua?
self:registerModule("gotopage", ReaderGoto:new{ self:registerModule("gotopage", ReaderGoto:new{
@ -436,12 +427,6 @@ function ReaderUI:init()
view = self.view, view = self.view,
ui = self, ui = self,
}) })
self:registerModule("networklistener", NetworkListener:new {
document = self.document,
view = self.view,
ui = self,
})
-- koreader plugins -- koreader plugins
for _, plugin_module in ipairs(PluginLoader:loadPlugins()) do for _, plugin_module in ipairs(PluginLoader:loadPlugins()) do
local ok, plugin_or_err = PluginLoader:createPluginInstance( local ok, plugin_or_err = PluginLoader:createPluginInstance(
@ -459,6 +444,15 @@ function ReaderUI:init()
end end
end end
if Device:hasWifiToggle() then
local NetworkListener = require("ui/network/networklistener")
self:registerModule("networklistener", NetworkListener:new {
document = self.document,
view = self.view,
ui = self,
})
end
-- Allow others to change settings based on external factors -- Allow others to change settings based on external factors
-- Must be called after plugins are loaded & before setting are read. -- Must be called after plugins are loaded & before setting are read.
self:handleEvent(Event:new("DocSettingsLoad", self.doc_settings, self.document)) self:handleEvent(Event:new("DocSettingsLoad", self.doc_settings, self.document))
@ -497,10 +491,10 @@ function ReaderUI:init()
-- Need the same event for PDF document -- Need the same event for PDF document
self:handleEvent(Event:new("ReaderReady", self.doc_settings)) self:handleEvent(Event:new("ReaderReady", self.doc_settings))
for _,v in ipairs(self.postReaderReadyCallback) do for _,v in ipairs(self.postReaderCallback) do
v() v()
end end
self.postReaderReadyCallback = nil self.postReaderCallback = nil
Device:setIgnoreInput(false) -- Allow processing of events (on Android). Device:setIgnoreInput(false) -- Allow processing of events (on Android).
Input:inhibitInputUntil(0.2) Input:inhibitInputUntil(0.2)
@ -523,18 +517,6 @@ function ReaderUI:registerKeyEvents()
if Device:hasKeys() then if Device:hasKeys() then
self.key_events.Home = { { "Home" } } self.key_events.Home = { { "Home" } }
self.key_events.Reload = { { "F5" } } 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
end end
@ -563,12 +545,13 @@ function ReaderUI:getLastDirFile(to_file_browser)
return last_dir, last_file return last_dir, last_file
end end
function ReaderUI:showFileManager(file, selected_files) function ReaderUI:showFileManager(file)
local FileManager = require("apps/filemanager/filemanager") local FileManager = require("apps/filemanager/filemanager")
local last_dir, last_file local last_dir, last_file
if file then if file then
last_dir = util.splitFilePathName(file) last_dir = util.splitFilePathName(file)
last_dir = last_dir:match("(.*)/")
last_file = file last_file = file
else else
last_dir, last_file = self:getLastDirFile(true) last_dir, last_file = self:getLastDirFile(true)
@ -576,7 +559,7 @@ function ReaderUI:showFileManager(file, selected_files)
if FileManager.instance then if FileManager.instance then
FileManager.instance:reinit(last_dir, last_file) FileManager.instance:reinit(last_dir, last_file)
else else
FileManager:showFiles(last_dir, last_file, selected_files) FileManager:showFiles(last_dir, last_file)
end end
end end
@ -601,17 +584,16 @@ end
function ReaderUI:showReader(file, provider, seamless) function ReaderUI:showReader(file, provider, seamless)
logger.dbg("show reader ui") logger.dbg("show reader ui")
file = ffiUtil.realpath(file)
if lfs.attributes(file, "mode") ~= "file" then if lfs.attributes(file, "mode") ~= "file" then
UIManager:show(InfoMessage:new{ 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 return
end end
if not DocumentRegistry:hasProvider(file) and provider == nil then if not DocumentRegistry:hasProvider(file) and provider == nil then
UIManager:show(InfoMessage:new{ 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) self:showFileManager(file)
return return
@ -627,7 +609,7 @@ end
function ReaderUI:showReaderCoroutine(file, provider, seamless) function ReaderUI:showReaderCoroutine(file, provider, seamless)
UIManager:show(InfoMessage:new{ 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, timeout = 0.0,
invisible = seamless, invisible = seamless,
}) })

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

@ -41,11 +41,8 @@ local Device = {
hasAuxBattery = no, hasAuxBattery = no,
hasKeyboard = no, hasKeyboard = no,
hasKeys = no, hasKeys = no,
hasScreenKB = no, -- in practice only some Kindles
hasSymKey = no, -- in practice only some Kindles
canKeyRepeat = no, canKeyRepeat = no,
hasDPad = no, hasDPad = no,
useDPadAsActionKeys = no,
hasExitOptions = yes, hasExitOptions = yes,
hasFewKeys = no, hasFewKeys = no,
hasWifiToggle = yes, hasWifiToggle = yes,
@ -65,7 +62,6 @@ local Device = {
hasExternalSD = no, -- or other storage volume that cannot be accessed using the File Manager hasExternalSD = no, -- or other storage volume that cannot be accessed using the File Manager
canHWDither = no, canHWDither = no,
canHWInvert = no, canHWInvert = no,
hasKaleidoWfm = no,
canDoSwipeAnimation = no, canDoSwipeAnimation = no,
canModifyFBInfo = no, -- some NTX boards do wonky things with the rotate flag after a FBIOPUT_VSCREENINFO ioctl canModifyFBInfo = no, -- some NTX boards do wonky things with the rotate flag after a FBIOPUT_VSCREENINFO ioctl
canUseCBB = yes, -- The C BB maintains a 1:1 feature parity with the Lua BB, except that is has NO support for BB4, and limited support for BBRGB24 canUseCBB = yes, -- The C BB maintains a 1:1 feature parity with the Lua BB, except that is has NO support for BB4, and limited support for BBRGB24
@ -170,30 +166,6 @@ function Device:invertButtons()
end end
end end
function Device:invertButtonsLeft()
if self:hasKeys() and self.input and self.input.event_map then
for key, value in pairs(self.input.event_map) do
if value == "LPgFwd" then
self.input.event_map[key] = "LPgBack"
elseif value == "LPgBack" then
self.input.event_map[key] = "LPgFwd"
end
end
end
end
function Device:invertButtonsRight()
if self:hasKeys() and self.input and self.input.event_map then
for key, value in pairs(self.input.event_map) do
if value == "RPgFwd" then
self.input.event_map[key] = "RPgBack"
elseif value == "RPgBack" then
self.input.event_map[key] = "RPgFwd"
end
end
end
end
function Device:init() function Device:init()
if not self.screen then if not self.screen then
error("screen/framebuffer must be implemented") error("screen/framebuffer must be implemented")
@ -257,12 +229,6 @@ function Device:init()
if G_reader_settings:isTrue("input_invert_page_turn_keys") then if G_reader_settings:isTrue("input_invert_page_turn_keys") then
self:invertButtons() self:invertButtons()
end end
if G_reader_settings:isTrue("input_invert_left_page_turn_keys") then
self:invertButtonsLeft()
end
if G_reader_settings:isTrue("input_invert_right_page_turn_keys") then
self:invertButtonsRight()
end
end end
if self:hasGSensor() then if self:hasGSensor() then
@ -602,7 +568,7 @@ function Device:exit()
G_reader_settings:close() G_reader_settings:close()
-- I/O teardown -- I/O teardown
self.input.teardown() require("ffi/input"):closeAll()
end end
-- Lifted from busybox's libbb/inet_cksum.c -- Lifted from busybox's libbb/inet_cksum.c

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

@ -139,7 +139,7 @@ local Input = {
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
"N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
"Up", "Down", "Left", "Right", "Press", "Backspace", "End", "Up", "Down", "Left", "Right", "Press", "Backspace", "End",
"Back", "Sym", "AA", "Menu", "Home", "Del", "ScreenKB", "Back", "Sym", "AA", "Menu", "Home", "Del",
"LPgBack", "RPgBack", "LPgFwd", "RPgFwd" "LPgBack", "RPgBack", "LPgFwd", "RPgFwd"
}, },
}, },
@ -171,7 +171,6 @@ local Input = {
Shift = false, Shift = false,
Sym = false, Sym = false,
Meta = false, Meta = false,
ScreenKB = false,
}, },
-- repeat state: -- repeat state:
@ -179,7 +178,6 @@ local Input = {
-- touch state: -- touch state:
main_finger_slot = 0, main_finger_slot = 0,
pen_slot = 4,
cur_slot = 0, cur_slot = 0,
MTSlots = nil, -- table, object may be replaced at runtime MTSlots = nil, -- table, object may be replaced at runtime
active_slots = nil, -- ditto active_slots = nil, -- ditto
@ -196,10 +194,6 @@ local Input = {
setClipboardText = function(text) setClipboardText = function(text)
_internal_clipboard_text = text or "" _internal_clipboard_text = text or ""
end, end,
-- open'ed input devices hashmap (key: path, value: fd number)
-- Must be a class member, both because Input is a singleton and that state is process-wide anyway.
opened_devices = {},
} }
function Input:new(o) function Input:new(o)
@ -225,10 +219,6 @@ function Input:init()
}, },
} }
-- Always send pen data to a slot far enough away from our main finger slot that it can never be matched with a finger buddy in GestureDetector (i.e., +/- 1),
-- with an extra bit of leeway, since we don't even actually support three finger gestures ;).
self.pen_slot = self.main_finger_slot + 4
self.gesture_detector = GestureDetector:new{ self.gesture_detector = GestureDetector:new{
screen = self.device.screen, screen = self.device.screen,
input = self, input = self,
@ -300,97 +290,14 @@ function Input:disableRotationMap()
end end
--[[-- --[[--
Wrapper for our Lua/C input module's open. Wrapper for FFI input open.
Note that we adhere to the "." syntax here for compatibility. Note that we adhere to the "." syntax here for compatibility.
The `name` argument is optional, and used for logging purposes only. @todo Clean up separation FFI/this.
--]] --]]
function Input.open(path, name) function Input.open(device, is_emu_events)
-- Make sure we don't open the same device twice. return input.open(device, is_emu_events and 1 or 0)
if not Input.opened_devices[path] then
local fd = input.open(path)
if fd then
Input.opened_devices[path] = fd
if name then
logger.dbg("Opened fd", fd, "for input device", name, "@", path)
else
logger.dbg("Opened fd", fd, "for input device @", path)
end
end
-- No need to log failures, input will have raised an error already,
-- and we want to make those fatal, so we don't protect this call.
return fd
end
end
--[[--
Wrapper for our Lua/C input module's fdopen.
Note that we adhere to the "." syntax here for compatibility.
The `name` argument is optional, and used for logging purposes only.
`path` is mandatory, though!
--]]
function Input.fdopen(fd, path, name)
-- Make sure we don't open the same device twice.
if not Input.opened_devices[path] then
input.fdopen(fd)
-- As with input.open, it will throw on error (closing the fd first)
Input.opened_devices[path] = fd
if name then
logger.dbg("Kept fd", fd, "open for input device", name, "@", path)
else
logger.dbg("Kept fd", fd, "open for input device @", path)
end
return fd
end
end
--[[--
Wrapper for our Lua/C input module's close.
Note that we adhere to the "." syntax here for compatibility.
--]]
function Input.close(path)
-- Make sure we actually know about this device
local fd = Input.opened_devices[path]
if fd then
local ok, err = input.close(fd)
if ok or err == C.ENODEV then
-- Either the call succeeded,
-- or the backend had already caught an ENODEV in waitForInput and closed the fd internally.
-- (Because the EvdevInputRemove Event comes from an UsbDevicePlugOut uevent forwarded as an... *input* EV_KEY event ;)).
-- Regardless, that device is gone, so clear its spot in the hashmap.
Input.opened_devices[path] = nil
end
else
logger.warn("Tried to close an unknown input device @", path)
end
end
--[[--
Wrapper for our Lua/C input module's closeAll.
Note that we adhere to the "." syntax here for compatibility.
--]]
function Input.teardown()
input.closeAll()
Input.opened_devices = {}
end
-- Wrappers for the custom FFI implementations with no concept of paths or fd
if input.is_ffi then
-- Pass args as-is. None of 'em actually *take* arguments, but some may be invoked as methods...
function Input.open(...)
return input.open(...)
end
function Input.close(...)
return input.close(...)
end
function Input.teardown(...)
return input.closeAll(...)
end
end end
--[[-- --[[--
@ -656,30 +563,24 @@ function Input:handleKeyBoardEv(ev)
end end
elseif self.wacom_protocol then elseif self.wacom_protocol then
if ev.code == C.BTN_TOOL_PEN then if ev.code == C.BTN_TOOL_PEN then
-- Switch to the dedicated pen slot, and make sure it's active, as this can come in a dedicated input frame -- Always send pen data to slot 2
self:setupSlotData(self.pen_slot) self:setupSlotData(2)
if ev.value == 1 then if ev.value == 1 then
self:setCurrentMtSlot("tool", TOOL_TYPE_PEN) self:setCurrentMtSlot("tool", TOOL_TYPE_PEN)
else else
self:setCurrentMtSlot("tool", TOOL_TYPE_FINGER) self:setCurrentMtSlot("tool", TOOL_TYPE_FINGER)
-- Switch back to our main finger slot
self.cur_slot = self.main_finger_slot
end end
return return
elseif ev.code == C.BTN_TOUCH then elseif ev.code == C.BTN_TOUCH then
-- BTN_TOUCH is bracketed by BTN_TOOL_PEN, so we can limit this to pens, to avoid stomping on panel slots. -- BTN_TOUCH is bracketed by BTN_TOOL_PEN, so we can limit this to pens, to avoid stomping on panel slots.
if self:getCurrentMtSlotData("tool") == TOOL_TYPE_PEN then if self:getCurrentMtSlotData("tool") == TOOL_TYPE_PEN then
-- Make sure the pen slot is active, as this can come in a dedicated input frame
-- (i.e., we need it to be referenced by self.MTSlots for the lift to be picked up in the EV_SYN:SYN_REPORT handler).
-- (Conversely, getCurrentMtSlotData pokes at the *persistent* slot data in self.ev_slots,
-- so it can keep track of data across input frames).
self:setupSlotData(self.pen_slot)
-- Much like on snow, use this to detect contact down & lift, -- Much like on snow, use this to detect contact down & lift,
-- as ABS_PRESSURE may be entirely omitted from hover events, -- as ABS_PRESSURE may be entirely omitted from hover events,
-- and ABS_DISTANCE is not very clear cut... -- and ABS_DISTANCE is not very clear cut...
self:setupSlotData(2)
if ev.value == 1 then if ev.value == 1 then
self:setCurrentMtSlot("id", self.pen_slot) self:setCurrentMtSlot("id", 2)
else else
self:setCurrentMtSlot("id", -1) self:setCurrentMtSlot("id", -1)
end end
@ -820,16 +721,6 @@ function Input:handlePowerManagementOnlyEv(ev)
end end
end end
-- Make sure we don't leave modifiers in an inconsistent state
if self.modifiers[keycode] ~= nil then
if ev.value == KEY_PRESS then
self.modifiers[keycode] = true
elseif ev.value == KEY_RELEASE then
self.modifiers[keycode] = false
end
return
end
-- Nothing to see, move along! -- Nothing to see, move along!
return return
end end

@ -3,6 +3,7 @@ local UIManager
local time = require("ui/time") local time = require("ui/time")
local lfs = require("libs/libkoreader-lfs") local lfs = require("libs/libkoreader-lfs")
local logger = require("logger") local logger = require("logger")
local util = require("util")
-- We're going to need a few <linux/fb.h> & <linux/input.h> constants... -- We're going to need a few <linux/fb.h> & <linux/input.h> constants...
local ffi = require("ffi") local ffi = require("ffi")
@ -10,7 +11,6 @@ local C = ffi.C
require("ffi/linux_fb_h") require("ffi/linux_fb_h")
require("ffi/linux_input_h") require("ffi/linux_input_h")
require("ffi/posix_h") require("ffi/posix_h")
require("ffi/fbink_input_h")
local function yes() return true end local function yes() return true end
local function no() return false end -- luacheck: ignore local function no() return false end -- luacheck: ignore
@ -206,58 +206,6 @@ function Kindle:supportsScreensaver()
end end
end end
function Kindle:openInputDevices()
-- Auto-detect input devices (via FBInk's fbink_input_scan)
local ok, FBInkInput = pcall(ffi.load, "fbink_input")
if not ok then
-- NOP fallback for the testsuite...
FBInkInput = { fbink_input_scan = function() end }
end
local dev_count = ffi.new("size_t[1]")
-- We care about: the touchscreen, a properly scaled stylus, pagination buttons, a home button and a fiveway.
local match_mask = bit.bor(C.INPUT_TOUCHSCREEN, C.INPUT_SCALED_TABLET, C.INPUT_PAGINATION_BUTTONS, C.INPUT_HOME_BUTTON, C.INPUT_DPAD)
local devices = FBInkInput.fbink_input_scan(match_mask, 0, 0, dev_count)
if devices ~= nil then
for i = 0, tonumber(dev_count[0]) - 1 do
local dev = devices[i]
if dev.matched then
self.input.fdopen(tonumber(dev.fd), ffi.string(dev.path), ffi.string(dev.name))
end
end
C.free(devices)
else
-- Auto-detection failed, warn and fall back to defaults
logger.warn("We failed to auto-detect the proper input devices, input handling may be inconsistent!")
if self.touch_dev then
-- We've got a preferred path specified for the touch panel
self.input.open(self.touch_dev)
else
-- That generally works out well enough on legacy devices...
self.input.open("/dev/input/event0")
self.input.open("/dev/input/event1")
end
end
-- Getting the device where rotation events end up without catching a bunch of false-positives is... trickier,
-- thanks to the inane event code being used...
if self:hasGSensor() then
-- i.e., we want something that reports EV_ABS:ABS_PRESSURE that isn't *also* a pen (because those are pretty much guaranteed to report pressure...).
-- And let's add that isn't also a touchscreen to the mix, because while not true at time of writing, that's an event touchscreens sure can support...
devices = FBInkInput.fbink_input_scan(C.INPUT_ROTATION_EVENT, bit.bor(C.INPUT_TABLET, C.INPUT_TOUCHSCREEN), C.NO_RECAP, dev_count)
if devices ~= nil then
for i = 0, tonumber(dev_count[0]) - 1 do
local dev = devices[i]
if dev.matched then
self.input.fdopen(tonumber(dev.fd), ffi.string(dev.path), ffi.string(dev.name))
end
end
C.free(devices)
end
end
self.input.open("fake_events")
end
function Kindle:init() function Kindle:init()
-- Check if the device supports deep sleep/quick boot -- Check if the device supports deep sleep/quick boot
if lfs.attributes("/sys/devices/platform/falconblk/uevent", "mode") == "file" then if lfs.attributes("/sys/devices/platform/falconblk/uevent", "mode") == "file" then
@ -286,14 +234,6 @@ function Kindle:init()
self.canDeepSleep = false self.canDeepSleep = false
end end
-- If the device-specific init hasn't done so already (devices without keys don't), instantiate Input.
if not self.input then
self.input = require("device/input"):new{ device = self }
end
-- Auto-detect & open input devices
self:openInputDevices()
Generic.init(self) Generic.init(self)
end end
@ -518,9 +458,7 @@ local Kindle2 = Kindle:extend{
isREAGL = no, isREAGL = no,
hasKeyboard = yes, hasKeyboard = yes,
hasKeys = yes, hasKeys = yes,
hasSymKey = yes,
hasDPad = yes, hasDPad = yes,
useDPadAsActionKeys = yes,
canHWInvert = no, canHWInvert = no,
canModifyFBInfo = no, canModifyFBInfo = no,
canUseCBB = no, -- 4bpp canUseCBB = no, -- 4bpp
@ -533,9 +471,7 @@ local KindleDXG = Kindle:extend{
isREAGL = no, isREAGL = no,
hasKeyboard = yes, hasKeyboard = yes,
hasKeys = yes, hasKeys = yes,
hasSymKey = yes,
hasDPad = yes, hasDPad = yes,
useDPadAsActionKeys = yes,
canHWInvert = no, canHWInvert = no,
canModifyFBInfo = no, canModifyFBInfo = no,
canUseCBB = no, -- 4bpp canUseCBB = no, -- 4bpp
@ -548,9 +484,7 @@ local Kindle3 = Kindle:extend{
isREAGL = no, isREAGL = no,
hasKeyboard = yes, hasKeyboard = yes,
hasKeys = yes, hasKeys = yes,
hasSymKey = yes,
hasDPad = yes, hasDPad = yes,
useDPadAsActionKeys = yes,
canHWInvert = no, canHWInvert = no,
canModifyFBInfo = no, canModifyFBInfo = no,
canUseCBB = no, -- 4bpp canUseCBB = no, -- 4bpp
@ -561,9 +495,7 @@ local Kindle4 = Kindle:extend{
model = "Kindle4", model = "Kindle4",
isREAGL = no, isREAGL = no,
hasKeys = yes, hasKeys = yes,
hasScreenKB = yes,
hasDPad = yes, hasDPad = yes,
useDPadAsActionKeys = yes,
canHWInvert = no, canHWInvert = no,
canModifyFBInfo = no, canModifyFBInfo = no,
-- NOTE: It could *technically* use the C BB, as it's running @ 8bpp, but it's expecting an inverted palette... -- NOTE: It could *technically* use the C BB, as it's running @ 8bpp, but it's expecting an inverted palette...
@ -751,6 +683,9 @@ function Kindle2:init()
device = self, device = self,
event_map = require("device/kindle/event_map_keyboard"), event_map = require("device/kindle/event_map_keyboard"),
} }
self.input.open("/dev/input/event0")
self.input.open("/dev/input/event1")
self.input.open("fake_events")
Kindle.init(self) Kindle.init(self)
end end
@ -765,6 +700,9 @@ function KindleDXG:init()
event_map = require("device/kindle/event_map_keyboard"), event_map = require("device/kindle/event_map_keyboard"),
} }
self.keyboard_layout = require("device/kindle/keyboard_layout") self.keyboard_layout = require("device/kindle/keyboard_layout")
self.input.open("/dev/input/event0")
self.input.open("/dev/input/event1")
self.input.open("fake_events")
Kindle.init(self) Kindle.init(self)
end end
@ -780,6 +718,9 @@ function Kindle3:init()
event_map = require("device/kindle/event_map_kindle4"), event_map = require("device/kindle/event_map_kindle4"),
} }
self.keyboard_layout = require("device/kindle/keyboard_layout") self.keyboard_layout = require("device/kindle/keyboard_layout")
self.input.open("/dev/input/event0")
self.input.open("/dev/input/event1")
self.input.open("fake_events")
Kindle.init(self) Kindle.init(self)
end end
@ -794,6 +735,9 @@ function Kindle4:init()
device = self, device = self,
event_map = require("device/kindle/event_map_kindle4"), event_map = require("device/kindle/event_map_kindle4"),
} }
self.input.open("/dev/input/event0")
self.input.open("/dev/input/event1")
self.input.open("fake_events")
Kindle.init(self) Kindle.init(self)
end end
@ -813,6 +757,11 @@ function KindleTouch:init()
-- Kindle Touch needs event modification for proper coordinates -- Kindle Touch needs event modification for proper coordinates
self.input:registerEventAdjustHook(self.input.adjustTouchScale, {x=600/4095, y=800/4095}) self.input:registerEventAdjustHook(self.input.adjustTouchScale, {x=600/4095, y=800/4095})
-- event0 in KindleTouch is "WM8962 Beep Generator" (useless)
-- event1 in KindleTouch is "imx-yoshi Headset" (useless)
self.input.open("/dev/input/event2") -- Home button
self.input.open(self.touch_dev) -- touchscreen
self.input.open("fake_events")
Kindle.init(self) Kindle.init(self)
end end
@ -826,6 +775,9 @@ function KindlePaperWhite:init()
} }
Kindle.init(self) Kindle.init(self)
self.input.open(self.touch_dev)
self.input.open("fake_events")
end end
function KindlePaperWhite2:init() function KindlePaperWhite2:init()
@ -839,6 +791,9 @@ function KindlePaperWhite2:init()
} }
Kindle.init(self) Kindle.init(self)
self.input.open(self.touch_dev)
self.input.open("fake_events")
end end
function KindleBasic:init() function KindleBasic:init()
@ -851,6 +806,9 @@ function KindleBasic:init()
} }
Kindle.init(self) Kindle.init(self)
self.input.open(self.touch_dev)
self.input.open("fake_events")
end end
function KindleVoyage:init() function KindleVoyage:init()
@ -901,7 +859,11 @@ function KindleVoyage:init()
Kindle.init(self) Kindle.init(self)
-- Re-enable WhisperTouch keys when started without framework self.input.open(self.touch_dev)
self.input.open("/dev/input/event2") -- WhisperTouch
self.input.open("fake_events")
-- reenable WhisperTouch keys when started without framework
if self.framework_lipc_handle then if self.framework_lipc_handle then
self.framework_lipc_handle:set_int_property("com.lab126.deviced", "fsrkeypadEnable", 1) self.framework_lipc_handle:set_int_property("com.lab126.deviced", "fsrkeypadEnable", 1)
self.framework_lipc_handle:set_int_property("com.lab126.deviced", "fsrkeypadPrevEnable", 1) self.framework_lipc_handle:set_int_property("com.lab126.deviced", "fsrkeypadPrevEnable", 1)
@ -920,6 +882,9 @@ function KindlePaperWhite3:init()
} }
Kindle.init(self) Kindle.init(self)
self.input.open(self.touch_dev)
self.input.open("fake_events")
end end
-- HAL for gyro orientation switches (EV_ABS:ABS_PRESSURE (?!) w/ custom values to EV_MSC:MSC_GYRO w/ our own custom values) -- HAL for gyro orientation switches (EV_ABS:ABS_PRESSURE (?!) w/ custom values to EV_MSC:MSC_GYRO w/ our own custom values)
@ -1027,6 +992,21 @@ function KindleOasis:init()
return this:handleGyroEv(ev) return this:handleGyroEv(ev)
end end
end end
self.input.open(self.touch_dev)
self.input.open("/dev/input/by-path/platform-gpiokey.0-event")
-- get rotate dev by EV=d
local std_out = io.popen("grep -e 'Handlers\\|EV=' /proc/bus/input/devices | grep -B1 'EV=d' | grep -o 'event[0-9]'", "r")
if std_out then
local rotation_dev = std_out:read("*line")
std_out:close()
if rotation_dev then
self.input.open("/dev/input/"..rotation_dev)
end
end
self.input.open("fake_events")
end end
-- HAL for gyro orientation switches (EV_ABS:ABS_PRESSURE (?!) w/ custom values to EV_MSC:MSC_GYRO w/ our own custom values) -- HAL for gyro orientation switches (EV_ABS:ABS_PRESSURE (?!) w/ custom values to EV_MSC:MSC_GYRO w/ our own custom values)
@ -1138,6 +1118,21 @@ function KindleOasis2:init()
return this:handleGyroEv(ev) return this:handleGyroEv(ev)
end end
end end
self.input.open(self.touch_dev)
self.input.open("/dev/input/by-path/platform-gpio-keys-event")
-- Get accelerometer device by looking for EV=d
local std_out = io.popen("grep -e 'Handlers\\|EV=' /proc/bus/input/devices | grep -B1 'EV=d' | grep -o 'event[0-9]\\{1,2\\}'", "r")
if std_out then
local rotation_dev = std_out:read("*line")
std_out:close()
if rotation_dev then
self.input.open("/dev/input/"..rotation_dev)
end
end
self.input.open("fake_events")
end end
function KindleOasis3:init() function KindleOasis3:init()
@ -1206,6 +1201,21 @@ function KindleOasis3:init()
return this:handleGyroEv(ev) return this:handleGyroEv(ev)
end end
end end
self.input.open(self.touch_dev)
self.input.open("/dev/input/by-path/platform-gpio-keys-event")
-- Get accelerometer device by looking for EV=d
local std_out = io.popen("grep -e 'Handlers\\|EV=' /proc/bus/input/devices | grep -B1 'EV=d' | grep -o 'event[0-9]\\{1,2\\}'", "r")
if std_out then
local rotation_dev = std_out:read("*line")
std_out:close()
if rotation_dev then
self.input.open("/dev/input/"..rotation_dev)
end
end
self.input.open("fake_events")
end end
function KindleBasic2:init() function KindleBasic2:init()
@ -1219,6 +1229,9 @@ function KindleBasic2:init()
} }
Kindle.init(self) Kindle.init(self)
self.input.open(self.touch_dev)
self.input.open("fake_events")
end end
function KindlePaperWhite4:init() function KindlePaperWhite4:init()
@ -1233,6 +1246,19 @@ function KindlePaperWhite4:init()
} }
Kindle.init(self) Kindle.init(self)
-- So, look for a goodix TS input device (c.f., #5110)...
local std_out = io.popen("grep -e 'Handlers\\|Name=' /proc/bus/input/devices | grep -A1 'goodix-ts' | grep -o 'event[0-9]'", "r")
if std_out then
local goodix_dev = std_out:read("*line")
std_out:close()
if goodix_dev then
self.touch_dev = "/dev/input/" .. goodix_dev
end
end
self.input.open(self.touch_dev)
self.input.open("fake_events")
end end
function KindleBasic3:init() function KindleBasic3:init()
@ -1252,6 +1278,29 @@ function KindleBasic3:init()
-- so we have to rely on contact lift detection via BTN_TOUCH:0, -- so we have to rely on contact lift detection via BTN_TOUCH:0,
-- c.f., https://github.com/koreader/koreader/issues/5070 -- c.f., https://github.com/koreader/koreader/issues/5070
self.input.snow_protocol = true self.input.snow_protocol = true
self.input.open(self.touch_dev)
self.input.open("fake_events")
end
local function findInputDevices()
-- Walk /sys/class/input and pick up any evdev input device with *any* EV_ABS capabilities
local devices = {}
for evdev in lfs.dir("/sys/class/input/") do
if evdev:match("event.*") then
local abs_cap = "/sys/class/input/" .. evdev .. "/device/capabilities/abs"
local f = io.open(abs_cap, "r")
if f then
local bitmap_str = f:read("l")
f:close()
if bitmap_str ~= "0" then
logger.info("Potential input device found at", evdev, "because of ABS caps:", bitmap_str)
table.insert(devices, "/dev/input/" .. evdev)
end
end
end
end
return devices
end end
function KindlePaperWhite5:init() function KindlePaperWhite5:init()
@ -1270,10 +1319,25 @@ function KindlePaperWhite5:init()
self.screen:_MTK_ToggleFastMode(true) self.screen:_MTK_ToggleFastMode(true)
Kindle.init(self) Kindle.init(self)
-- Some HW/FW variants stash their input device without a by-path symlink...
if util.pathExists("/dev/input/by-path/platform-1001e000.i2c-event") then
self.touch_dev = "/dev/input/by-path/platform-1001e000.i2c-event"
self.input.open(self.touch_dev)
else
local devices = findInputDevices()
for _, touch in ipairs(devices) do
-- There should only be one match on the PW5 anyway...
self.touch_dev = touch
self.input.open(touch)
end
end
self.input.open("fake_events")
end end
function KindleBasic4:init() function KindleBasic4:init()
self.screen = require("ffi/framebuffer_mxcfb"):new{device = self, debug = logger.dbg} self.screen = require("ffi/framebuffer_mxcfb"):new{device = self, debug = logger.dbg}
-- TBD, assume PW5 for now
self.powerd = require("device/kindle/powerd"):new{ self.powerd = require("device/kindle/powerd"):new{
device = self, device = self,
fl_intensity_file = "/sys/class/backlight/fp9966-bl1/brightness", fl_intensity_file = "/sys/class/backlight/fp9966-bl1/brightness",
@ -1287,6 +1351,20 @@ function KindleBasic4:init()
self.screen:_MTK_ToggleFastMode(true) self.screen:_MTK_ToggleFastMode(true)
Kindle.init(self) Kindle.init(self)
-- Some HW/FW variants stash their input device without a by-path symlink...
if util.pathExists("/dev/input/by-path/platform-1001e000.i2c-event") then
self.touch_dev = "/dev/input/by-path/platform-1001e000.i2c-event"
self.input.open(self.touch_dev)
else
local devices = findInputDevices()
for _, touch in ipairs(devices) do
-- There should only be one match on the PW5 anyway...
self.touch_dev = touch
self.input.open(touch)
end
end
self.input.open("fake_events")
end end
function KindleScribe:init() function KindleScribe:init()
@ -1306,11 +1384,11 @@ function KindleScribe:init()
hall_file = "/sys/devices/platform/eink_hall/hall_enable", hall_file = "/sys/devices/platform/eink_hall/hall_enable",
} }
Kindle.init(self)
-- Enable the so-called "fast" mode, so as to prevent the driver from silently promoting refreshes to REAGL. -- Enable the so-called "fast" mode, so as to prevent the driver from silently promoting refreshes to REAGL.
self.screen:_MTK_ToggleFastMode(true) self.screen:_MTK_ToggleFastMode(true)
Kindle.init(self)
--- @note The same quirks as on the Oasis 2 and 3 apply ;). --- @note The same quirks as on the Oasis 2 and 3 apply ;).
local haslipc, lipc = pcall(require, "liblipclua") local haslipc, lipc = pcall(require, "liblipclua")
if haslipc and lipc then if haslipc and lipc then
@ -1321,10 +1399,14 @@ function KindleScribe:init()
logger.dbg("orientation_code =", orientation_code) logger.dbg("orientation_code =", orientation_code)
local rotation_mode = 0 local rotation_mode = 0
if orientation_code then if orientation_code then
if orientation_code == "U" or "L" then if orientation_code == "U" then
rotation_mode = self.screen.DEVICE_ROTATED_UPRIGHT rotation_mode = self.screen.DEVICE_ROTATED_UPRIGHT
elseif orientation_code == "D" or "R" then elseif orientation_code == "R" then
rotation_mode = self.screen.DEVICE_ROTATED_CLOCKWISE
elseif orientation_code == "D" then
rotation_mode = self.screen.DEVICE_ROTATED_UPSIDE_DOWN rotation_mode = self.screen.DEVICE_ROTATED_UPSIDE_DOWN
elseif orientation_code == "L" then
rotation_mode = self.screen.DEVICE_ROTATED_COUNTER_CLOCKWISE
end end
end end
if rotation_mode > 0 then if rotation_mode > 0 then
@ -1346,9 +1428,15 @@ function KindleScribe:init()
return this:handleGyroEv(ev) return this:handleGyroEv(ev)
end end
end end
-- Get accelerometer device
self.input.open("/dev/input/by-path/platform-11007000.i2c-event-joystick")
self.input.open(self.touch_dev)
self.input.open("fake_events")
-- Setup pen input -- Setup pen input
self.input.wacom_protocol = true self.input.wacom_protocol = true
self.input.open("/dev/input/event4")
end end
function KindleTouch:exit() function KindleTouch:exit()
@ -1358,9 +1446,6 @@ function KindleTouch:exit()
end end
if self.framework_lipc_handle then if self.framework_lipc_handle then
-- Fixes missing *stock Amazon UI* screensavers on exiting out of "no framework" started KOReader
-- module was unloaded in frameworkStopped() function but wasn't (re)loaded on KOReader exit
self.framework_lipc_handle:set_string_property("com.lab126.blanket", "load", "screensaver")
self.framework_lipc_handle:close() self.framework_lipc_handle:close()
end end

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

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

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

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

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

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

@ -38,7 +38,6 @@ local CreDocument = Document:extend{
authors = "doc.authors", authors = "doc.authors",
series = "doc.series.name", series = "doc.series.name",
series_index = "doc.series.number", series_index = "doc.series.number",
identifiers = "doc.identifiers",
}, },
-- Reasons for the fallback font ordering: -- Reasons for the fallback font ordering:
@ -116,15 +115,6 @@ end
function CreDocument:engineInit() function CreDocument:engineInit()
if not engine_initialized then if not engine_initialized then
cre = require("libs/libkoreader-cre") cre = require("libs/libkoreader-cre")
-- When forking to execute any stuff in a sub-process,
-- as that stuff may not care about properly closing
-- the document, skip cre.cpp finalizer to avoid any
-- assertion failure.
require("ffi/util").addRunInSubProcessAfterForkFunc("cre_skip_teardown", function()
cre.setSkipTearDown(true)
end)
-- initialize cache -- initialize cache
self:cacheInit() self:cacheInit()
@ -183,7 +173,6 @@ function CreDocument:init()
self.default_css = "./data/epub.css" self.default_css = "./data/epub.css"
if file_type == "fb2" or file_type == "fb3" then if file_type == "fb2" or file_type == "fb3" then
self.default_css = "./data/fb2.css" self.default_css = "./data/fb2.css"
self.is_fb2 = true -- FB2 won't look good with any html-oriented stylesheet
end end
-- This mode must be the same as the default one set as ReaderView.view_mode -- This mode must be the same as the default one set as ReaderView.view_mode
@ -1378,11 +1367,6 @@ function CreDocument:setBatteryState(state)
self._document:setBatteryState(state) self._document:setBatteryState(state)
end end
function CreDocument:setPageInfoOverride(pageinfo)
logger.dbg("CreDocument: set page info", pageinfo)
self._document:setPageInfoOverride(pageinfo)
end
function CreDocument:isXPointerInCurrentPage(xp) function CreDocument:isXPointerInCurrentPage(xp)
logger.dbg("CreDocument: check xpointer in current page", xp) logger.dbg("CreDocument: check xpointer in current page", xp)
return self._document:isXPointerInCurrentPage(xp) return self._document:isXPointerInCurrentPage(xp)

@ -201,7 +201,6 @@ function Document:getProps(cached_doc_metadata)
local language = makeNilIfEmpty(props.language or props.Language) local language = makeNilIfEmpty(props.language or props.Language)
local keywords = makeNilIfEmpty(props.keywords or props.Keywords) local keywords = makeNilIfEmpty(props.keywords or props.Keywords)
local description = makeNilIfEmpty(props.description or props.Description or props.subject) local description = makeNilIfEmpty(props.description or props.Description or props.subject)
local identifiers = makeNilIfEmpty(props.identifiers)
return { return {
title = title, title = title,
authors = authors, authors = authors,
@ -210,7 +209,6 @@ function Document:getProps(cached_doc_metadata)
language = language, language = language,
keywords = keywords, keywords = keywords,
description = description, description = description,
identifiers = identifiers,
} }
end end

@ -20,10 +20,7 @@ local util = require("util")
local KoptInterface = { local KoptInterface = {
ocrengine = "ocrengine", ocrengine = "ocrengine",
-- If `$TESSDATA_PREFIX` is set, don't override it: let libk2pdfopt honor it tessocr_data = DataStorage:getDataDir() .. "/data",
-- (which includes checking for data in both `$TESSDATA_PREFIX/tessdata` and
-- in `$TESSDATA_PREFIX/` on more recent versions).
tessocr_data = not os.getenv('TESSDATA_PREFIX') and DataStorage:getDataDir().."/data/tessdata" or nil,
ocr_lang = "eng", ocr_lang = "eng",
ocr_type = 3, -- default 0, for more accuracy use 3 ocr_type = 3, -- default 0, for more accuracy use 3
last_context_size = nil, last_context_size = nil,
@ -1067,10 +1064,6 @@ function KoptInterface:getWordFromReflowPosition(doc, boxes, pos)
local pageno = pos.page local pageno = pos.page
local scratch_reflowed_page_boxes = self:getReflowedTextBoxesFromScratch(doc, pageno) local scratch_reflowed_page_boxes = self:getReflowedTextBoxesFromScratch(doc, pageno)
if not DEBUG.dassert(scratch_reflowed_page_boxes and next(scratch_reflowed_page_boxes) ~= nil, "scratch_reflowed_page_boxes shouldn't be nil/{}") then
return
end
local scratch_reflowed_word_box = self:getWordFromBoxes(scratch_reflowed_page_boxes, pos) local scratch_reflowed_word_box = self:getWordFromBoxes(scratch_reflowed_page_boxes, pos)
local reflowed_page_boxes = self:getReflowedTextBoxes(doc, pageno) local reflowed_page_boxes = self:getReflowedTextBoxes(doc, pageno)
@ -1244,15 +1237,8 @@ function KoptInterface:getTextFromReflowPositions(doc, native_boxes, pos0, pos1)
local reflowed_page_boxes = self:getReflowedTextBoxes(doc, pageno) local reflowed_page_boxes = self:getReflowedTextBoxes(doc, pageno)
local scratch_reflowed_word_box0 = self:getWordFromBoxes(scratch_reflowed_page_boxes, pos0) local scratch_reflowed_word_box0 = self:getWordFromBoxes(scratch_reflowed_page_boxes, pos0)
if not DEBUG.dassert(scratch_reflowed_word_box0 and next(scratch_reflowed_word_box0) ~= nil, "scratch_reflowed_word_box0 shouldn't be nil/{}") then
return
end
local reflowed_word_box0 = self:getWordFromBoxes(reflowed_page_boxes, pos0) local reflowed_word_box0 = self:getWordFromBoxes(reflowed_page_boxes, pos0)
local scratch_reflowed_word_box1 = self:getWordFromBoxes(scratch_reflowed_page_boxes, pos1) local scratch_reflowed_word_box1 = self:getWordFromBoxes(scratch_reflowed_page_boxes, pos1)
if not DEBUG.dassert(scratch_reflowed_word_box1 and next(scratch_reflowed_word_box1) ~= nil, "scratch_reflowed_word_box1 shouldn't be nil/{}") then
return
end
local reflowed_word_box1 = self:getWordFromBoxes(reflowed_page_boxes, pos1) local reflowed_word_box1 = self:getWordFromBoxes(reflowed_page_boxes, pos1)
local reflowed_pos_abs0 = scratch_reflowed_word_box0.box:center() local reflowed_pos_abs0 = scratch_reflowed_word_box0.box:center()

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

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

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

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

@ -419,13 +419,6 @@ Further small adjustments can be done with 'Line Spacing' in the bottom menu.]])
}, },
{ {
title = _("Font size and families"), title = _("Font size and families"),
{
id = "font_no_presentational_hints",
title = _("Ignore font related HTML presentational hints"),
description = _("Ignore HTML attributes that contribute to styles on the elements <body> (bgcolor, text…) and <font> (face, size, color…)."),
css = [[body, font { -cr-hint: no-presentational; }]],
separator = true,
},
{ {
id = "font_family_all_inherit", id = "font_family_all_inherit",
title = _("Ignore publisher font families"), title = _("Ignore publisher font families"),
@ -571,13 +564,6 @@ body, h1, h2, h3, h4, h5, h6, div, li, td, th { text-indent: 0 !important; }
title = _("Tables, links, images"), title = _("Tables, links, images"),
{ {
title = _("Tables"), title = _("Tables"),
{
id = "table_no_presentational_hints",
title = _("Ignore tables related HTML presentational hints"),
description = _("Ignore HTML attributes that contribute to styles on the <table> element and its sub-elements (ie. align, valign, frame, rules, border, cellpadding, cellspacing…)."),
css = [[table, caption, colgroup, col, thead, tbody, tfoot, tr, td, th { -cr-hint: no-presentational; }]],
separator = true,
},
{ {
id = "table_full_width", id = "table_full_width",
title = _("Full-width tables"), title = _("Full-width tables"),

@ -2073,7 +2073,7 @@ local dictionaries = {
lang_out = "eng", lang_out = "eng",
entries = 20431, entries = 20431,
license = "Dual-licensed under CC-BY-SA 3.0 and GFDL", license = "Dual-licensed under CC-BY-SA 3.0 and GFDL",
url = "https://github.com/Vuizur/Wiktionary-Dictionaries/raw/master/Norwegian%20Bokm%C3%A5l-English%20Wiktionary%20dictionary%20stardict.tar.gz", url = "https://github.com/Vuizur/Wiktionary-Dictionaries/raw/master/Norwegian%20Bokmål-English%20Wiktionary%20dictionary%20stardict.tar.gz",
}, },
{ {
name = "Norwegian Nynorsk-English Wiktionary", name = "Norwegian Nynorsk-English Wiktionary",

@ -6,7 +6,6 @@ local locale_lang_map = {
-- @translators Most of these language name have already been translated at <https://hosted.weblate.org/projects/iso-codes/iso-639-2/>. Click "Automatic suggestions" to see them below the textfield. -- @translators Most of these language name have already been translated at <https://hosted.weblate.org/projects/iso-codes/iso-639-2/>. Click "Automatic suggestions" to see them below the textfield.
aar = _("Afar"), aar = _("Afar"),
afr = _("Afrikaans"), afr = _("Afrikaans"),
amh = _("Amharic"),
ara = _("Arabic"), -- macrolanguage ara = _("Arabic"), -- macrolanguage
asm = _("Assamese"), asm = _("Assamese"),
aze = _("Azerbaijani"), -- macrolanguage aze = _("Azerbaijani"), -- macrolanguage
@ -14,19 +13,15 @@ local locale_lang_map = {
bel = _("Belarusian"), bel = _("Belarusian"),
ben = _("Bengali"), ben = _("Bengali"),
bod = _("Tibetan"), bod = _("Tibetan"),
bos = _("Bosnian"),
bre = _("Breton"), bre = _("Breton"),
bul = _("Bulgarian"), bul = _("Bulgarian"),
cat = _("Catalan"),
ces = _("Czech"), ces = _("Czech"),
chu = _("Church Slavic"), chu = _("Church Slavic"),
cor = _("Cornish"), cor = _("Cornish"),
cos = _("Corsican"),
cym = _("Welsh"), cym = _("Welsh"),
dan = _("Danish"), dan = _("Danish"),
deu = _("German"), deu = _("German"),
div = _("Dhivehi"), div = _("Dhivehi"),
dzo = _("Dzongkha"),
ell = _("Modern Greek"), -- (1453-) ell = _("Modern Greek"), -- (1453-)
eng = _("English"), eng = _("English"),
epo = _("Esperanto"), -- constructed language epo = _("Esperanto"), -- constructed language
@ -34,7 +29,6 @@ local locale_lang_map = {
eus = _("Basque"), eus = _("Basque"),
fao = _("Faroese"), fao = _("Faroese"),
fas = _("Persian"), -- macrolanguage fas = _("Persian"), -- macrolanguage
fil = _("Filipino"),
fin = _("Finnish"), fin = _("Finnish"),
fra = _("French"), fra = _("French"),
fry = _("Western Frisian"), fry = _("Western Frisian"),
@ -48,7 +42,6 @@ local locale_lang_map = {
hbs = _("Serbo-Croatian"), -- macrolanguage hbs = _("Serbo-Croatian"), -- macrolanguage
heb = _("Hebrew"), heb = _("Hebrew"),
hin = _("Hindi"), hin = _("Hindi"),
hrv = _("Croatian"),
hun = _("Hungarian"), hun = _("Hungarian"),
hye = _("Armenian"), hye = _("Armenian"),
ido = _("Ido"), -- constructed language ido = _("Ido"), -- constructed language
@ -89,7 +82,6 @@ local locale_lang_map = {
nob = _("Norwegian Bokmål"), nob = _("Norwegian Bokmål"),
nor = _("Norwegian"), -- macrolanguage nor = _("Norwegian"), -- macrolanguage
oci = _("Occitan"), -- (post 1500) oci = _("Occitan"), -- (post 1500)
ori = _("Oriya"),
orm = _("Oromo"), -- macrolanguage orm = _("Oromo"), -- macrolanguage
pan = _("Panjabi"), pan = _("Panjabi"),
pli = _("Pali"), pli = _("Pali"),
@ -101,10 +93,8 @@ local locale_lang_map = {
ron = _("Romanian"), ron = _("Romanian"),
rus = _("Russian"), rus = _("Russian"),
san = _("Sanskrit"), san = _("Sanskrit"),
sin = _("Sinhala"),
slk = _("Slovak"), slk = _("Slovak"),
slv = _("Slovenian"), slv = _("Slovenian"),
snd = _("Sindhi"),
spa = _("Spanish"), spa = _("Spanish"),
sqi = _("Albanian"), -- macrolanguage sqi = _("Albanian"), -- macrolanguage
srp = _("Serbian"), srp = _("Serbian"),
@ -112,15 +102,12 @@ local locale_lang_map = {
sun = _("Sundanese"), sun = _("Sundanese"),
swa = _("Swahili"), -- macrolanguage swa = _("Swahili"), -- macrolanguage
swe = _("Swedish"), swe = _("Swedish"),
syr = _("Syriac"),
tam = _("Tamil"), tam = _("Tamil"),
tat = _("Tatar"), tat = _("Tatar"),
tel = _("Telugu"), tel = _("Telugu"),
tgk = _("Tajik"), tgk = _("Tajik"),
tgl = _("Tagalog"), tgl = _("Tagalog"),
tha = _("Thai"), tha = _("Thai"),
tir = _("Tigrinya"),
ton = _("Tonga"),
tur = _("Turkish"), tur = _("Turkish"),
uig = _("Uighur"), uig = _("Uighur"),
ukr = _("Ukrainian"), ukr = _("Ukrainian"),
@ -217,21 +204,6 @@ local locale_lang_map = {
xxpw = _("Proto-West Germanic"), xxpw = _("Proto-West Germanic"),
xxps = _("Proto-Sami"), xxps = _("Proto-Sami"),
xxsl = _("Proto-Slavic"), xxsl = _("Proto-Slavic"),
-- These are Tesseract-specific extensions.
-- See <https://tesseract-ocr.github.io/tessdoc/Data-Files-in-different-versions.html>.
aze_cyrl = _("Azerbaijani - Cyrillic"),
chi_sim = _("Chinese - Simplified"),
chi_tra = _("Chinese - Traditional"),
dan_frak = _("Danish - Fraktur"),
deu_frak = _("German - Fraktur"),
equ = _("Math equation"),
ita_old = _("Old Italian"),
kat_old = _("Old Georgian"),
slk_frak = _("Slovak - Fraktur"),
spa_old = _("Old Spanish"),
srp_latn = _("Serbian - Latin"),
uzb_cyrl = _("Uzbek -Cyrillic"),
} }
local iso_bcp47_map = { local iso_bcp47_map = {

@ -27,7 +27,6 @@ local raa = ar_popup.raa
local sheen = ar_popup.sheen local sheen = ar_popup.sheen
local taa = ar_popup.taa local taa = ar_popup.taa
local thaa = ar_popup.thaa local thaa = ar_popup.thaa
local th_aa = ar_popup.th_aa
local thaal = ar_popup.thaal local thaal = ar_popup.thaal
local dhad = ar_popup.dhad local dhad = ar_popup.dhad
local ghayn = ar_popup.ghayn local ghayn = ar_popup.ghayn
@ -97,7 +96,7 @@ return {
{ "'", taamarbouta, "'", "]", }, { "'", taamarbouta, "'", "]", },
{ arabic_comma, waw, "#", "", }, { arabic_comma, waw, "#", "", },
{ ".", zay, "@", "", }, { ".", zay, "@", "", },
{ "؟", th_aa, "!", _at, }, { "؟", thaa, "!", _at, },
{ label = "", { label = "",
width = 1.5, width = 1.5,
bold = false bold = false

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

File diff suppressed because one or more lines are too long

@ -1,6 +1,5 @@
local BD = require("ui/bidi") local BD = require("ui/bidi")
local Device = require("device") local Device = require("device")
local IsoLanguage = require("ui/data/isolanguage")
local optionsutil = require("ui/data/optionsutil") local optionsutil = require("ui/data/optionsutil")
local util = require("util") local util = require("util")
local _ = require("gettext") local _ = require("gettext")
@ -12,16 +11,6 @@ local FONT_SCALE_FACTORS = {0.2, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.1, 1.3, 1.
-- Font sizes used for the font size widget only -- Font sizes used for the font size widget only
local FONT_SCALE_DISPLAY_SIZE = {12, 14, 15, 16, 17, 18, 19, 20, 22, 25, 30, 35} local FONT_SCALE_DISPLAY_SIZE = {12, 14, 15, 16, 17, 18, 19, 20, 22, 25, 30, 35}
local KOPTREADER_CONFIG_DOC_LANGS_TEXT = {}
for _, lang in ipairs(G_defaults:readSetting("DKOPTREADER_CONFIG_DOC_LANGS_CODE")) do
local langName = IsoLanguage:getLocalizedLanguage(lang)
if langName then
table.insert(KOPTREADER_CONFIG_DOC_LANGS_TEXT, langName)
else
table.insert(KOPTREADER_CONFIG_DOC_LANGS_TEXT, lang)
end
end
-- Get font scale numbers as a table of strings -- Get font scale numbers as a table of strings
local tableOfNumbersToTableOfStrings = function(numbers) local tableOfNumbersToTableOfStrings = function(numbers)
local t = {} local t = {}
@ -566,7 +555,7 @@ This can also be used to remove some gray background or to convert a grayscale o
{ {
name = "doc_language", name = "doc_language",
name_text = _("Document Language"), name_text = _("Document Language"),
toggle = KOPTREADER_CONFIG_DOC_LANGS_TEXT, toggle = G_defaults:readSetting("DKOPTREADER_CONFIG_DOC_LANGS_TEXT"),
values = G_defaults:readSetting("DKOPTREADER_CONFIG_DOC_LANGS_CODE"), values = G_defaults:readSetting("DKOPTREADER_CONFIG_DOC_LANGS_CODE"),
default_value = G_defaults:readSetting("DKOPTREADER_CONFIG_DOC_DEFAULT_LANG_CODE"), default_value = G_defaults:readSetting("DKOPTREADER_CONFIG_DOC_DEFAULT_LANG_CODE"),
event = "DocLangUpdate", event = "DocLangUpdate",

@ -10,7 +10,7 @@ local util = require("util")
local _ = require("gettext") local _ = require("gettext")
-- Date at which the last migration snippet was added -- Date at which the last migration snippet was added
local CURRENT_MIGRATION_DATE = 20240616 local CURRENT_MIGRATION_DATE = 20231217
-- Retrieve the date of the previous migration, if any -- Retrieve the date of the previous migration, if any
local last_migration_date = G_reader_settings:readSetting("last_migration_date", 0) local last_migration_date = G_reader_settings:readSetting("last_migration_date", 0)
@ -234,9 +234,8 @@ end
-- 20210518, ReaderFooter, https://github.com/koreader/koreader/pull/7702 -- 20210518, ReaderFooter, https://github.com/koreader/koreader/pull/7702
-- 20210622, ReaderFooter, https://github.com/koreader/koreader/pull/7876 -- 20210622, ReaderFooter, https://github.com/koreader/koreader/pull/7876
-- 20240616, ReaderFooter, https://github.com/koreader/koreader/pull/11999 if last_migration_date < 20210622 then
if last_migration_date < 20240616 then logger.info("Performing one-time migration for 20210622")
logger.info("Performing one-time migration for 20240616")
local ReaderFooter = require("apps/reader/modules/readerfooter") local ReaderFooter = require("apps/reader/modules/readerfooter")
local settings = G_reader_settings:readSetting("footer", ReaderFooter.default_settings) local settings = G_reader_settings:readSetting("footer", ReaderFooter.default_settings)
@ -648,16 +647,5 @@ if last_migration_date < 20231217 then
end end
end end
-- 20240408, drop sleep screen/screensaver image_file setting in favor of document cover
if last_migration_date < 20240408 then
logger.info("Performing one-time migration for 20240408")
local image_file = G_reader_settings:readSetting("screensaver_type") == "image_file" and G_reader_settings:readSetting("screensaver_image")
if image_file then
G_reader_settings:saveSetting("screensaver_type", "document_cover")
G_reader_settings:saveSetting("screensaver_document_cover", image_file)
end
end
-- We're done, store the current migration date -- We're done, store the current migration date
G_reader_settings:saveSetting("last_migration_date", CURRENT_MIGRATION_DATE) G_reader_settings:saveSetting("last_migration_date", CURRENT_MIGRATION_DATE)

@ -76,15 +76,15 @@ common_settings.time = {
text = _("Time and date"), text = _("Time and date"),
sub_item_table = { sub_item_table = {
{ {
text = _("12-hour clock"), text = _("12-hour clock"),
keep_menu_open = true, keep_menu_open = true,
checked_func = function() checked_func = function()
return G_reader_settings:isTrue("twelve_hour_clock") return G_reader_settings:isTrue("twelve_hour_clock")
end, end,
callback = function() callback = function()
G_reader_settings:flipNilOrFalse("twelve_hour_clock") G_reader_settings:flipNilOrFalse("twelve_hour_clock")
UIManager:broadcastEvent(Event:new("TimeFormatChanged")) UIManager:broadcastEvent(Event:new("TimeFormatChanged"))
end, end,
}, },
{ {
text_func = function () text_func = function ()
@ -285,6 +285,10 @@ if Device:isTouchDevice() then
} }
common_settings.screen_disable_double_tab = require("ui/elements/screen_disable_double_tap_table") common_settings.screen_disable_double_tab = require("ui/elements/screen_disable_double_tap_table")
common_settings.menu_activate = require("ui/elements/menu_activate") common_settings.menu_activate = require("ui/elements/menu_activate")
common_settings.keyboard_layout = {
text = _("Keyboard"),
sub_item_table = require("ui/elements/menu_keyboard_layout"),
}
end end
-- NOTE: Allow disabling color if it's mistakenly enabled on a Grayscale screen (after a settings import?) -- NOTE: Allow disabling color if it's mistakenly enabled on a Grayscale screen (after a settings import?)
@ -477,30 +481,13 @@ if Device:hasKeyboard() then
end end
common_settings.opening_page_location_stack = { common_settings.opening_page_location_stack = {
text = _("Add opening page to location history"), text = _("Add opening page to location history"),
checked_func = function() checked_func = function()
return G_reader_settings:isTrue("opening_page_location_stack") return G_reader_settings:isTrue("opening_page_location_stack")
end, end,
callback = function() callback = function()
G_reader_settings:flipNilOrFalse("opening_page_location_stack") G_reader_settings:flipNilOrFalse("opening_page_location_stack")
end, end,
}
local skim_dialog_position_string = {
top = _("Top"),
center = _("Center"),
bottom = _("Bottom"),
}
common_settings.skim_dialog_position = {
text_func = function()
local position = G_reader_settings:readSetting("skim_dialog_position") or "center"
return T(_"Skim dialog position: %1", skim_dialog_position_string[position]:lower())
end,
sub_item_table = {
genGenericMenuEntry(skim_dialog_position_string["top"], "skim_dialog_position", "top"),
genGenericMenuEntry(skim_dialog_position_string["center"], "skim_dialog_position", nil), -- default
genGenericMenuEntry(skim_dialog_position_string["bottom"], "skim_dialog_position", "bottom"),
},
} }
-- Auto-save settings: default value, info text and warning, and menu items -- Auto-save settings: default value, info text and warning, and menu items
@ -728,11 +715,6 @@ common_settings.device = {
text = _("Device"), text = _("Device"),
} }
common_settings.keyboard_layout = {
text = _("Keyboard"),
sub_item_table = require("ui/elements/menu_keyboard_layout"),
}
common_settings.font_ui_fallbacks = require("ui/elements/font_ui_fallbacks") common_settings.font_ui_fallbacks = require("ui/elements/font_ui_fallbacks")
common_settings.units = { common_settings.units = {

@ -77,7 +77,6 @@ local order = {
"android_back_button", "android_back_button",
"----------------------------", "----------------------------",
"opening_page_location_stack", "opening_page_location_stack",
"skim_dialog_position",
}, },
network = { network = {
"network_wifi", "network_wifi",
@ -169,7 +168,6 @@ local order = {
"history", "history",
"open_last_document", "open_last_document",
"----------------------------", "----------------------------",
"favorites",
"collections", "collections",
"----------------------------", "----------------------------",
"mass_storage_actions", -- if Device:canToggleMassStorage() "mass_storage_actions", -- if Device:canToggleMassStorage()

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

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

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

@ -9,49 +9,16 @@ local PhysicalButtons = {
sub_item_table = { sub_item_table = {
{ {
text = _("Invert page turn buttons"), text = _("Invert page turn buttons"),
enabled_func = function()
return not (G_reader_settings:isTrue("input_invert_left_page_turn_keys") or G_reader_settings:isTrue("input_invert_right_page_turn_keys"))
end,
checked_func = function() checked_func = function()
return G_reader_settings:isTrue("input_invert_page_turn_keys") return G_reader_settings:isTrue("input_invert_page_turn_keys")
end, end,
callback = function() callback = function()
UIManager:broadcastEvent(Event:new("SwapPageTurnButtons")) UIManager:broadcastEvent(Event:new("SwapPageTurnButtons"))
end, end,
separator = true,
} }
}, },
} }
if Device:hasDPad() and Device:useDPadAsActionKeys() then
table.insert(PhysicalButtons.sub_item_table, {
text = _("Invert left-side page turn buttons"),
enabled_func = function()
return not G_reader_settings:isTrue("input_invert_page_turn_keys")
end,
checked_func = function()
return G_reader_settings:isTrue("input_invert_left_page_turn_keys")
end,
callback = function()
G_reader_settings:flipNilOrFalse("input_invert_left_page_turn_keys")
Device:invertButtonsLeft()
end,
})
table.insert(PhysicalButtons.sub_item_table, {
text = _("Invert right-side page turn buttons"),
enabled_func = function()
return not G_reader_settings:isTrue("input_invert_page_turn_keys")
end,
checked_func = function()
return G_reader_settings:isTrue("input_invert_right_page_turn_keys")
end,
callback = function()
G_reader_settings:flipNilOrFalse("input_invert_right_page_turn_keys")
Device:invertButtonsRight()
end,
})
end
if Device:canKeyRepeat() then if Device:canKeyRepeat() then
table.insert(PhysicalButtons.sub_item_table, { table.insert(PhysicalButtons.sub_item_table, {
text = _("Disable key repeat"), text = _("Disable key repeat"),

@ -35,7 +35,6 @@ local order = {
"----------------------------", "----------------------------",
"toc_items_per_page", "toc_items_per_page",
"toc_items_font_size", "toc_items_font_size",
"toc_items_show_chapter_length",
"toc_items_with_dots", "toc_items_with_dots",
"----------------------------", "----------------------------",
"toc_alt_toc", "toc_alt_toc",
@ -126,7 +125,6 @@ local order = {
"android_back_button", "android_back_button",
"----------------------------", "----------------------------",
"opening_page_location_stack", "opening_page_location_stack",
"skim_dialog_position",
}, },
network = { network = {
"network_wifi", "network_wifi",
@ -228,7 +226,6 @@ local order = {
"history", "history",
"open_previous_document", "open_previous_document",
"----------------------------", "----------------------------",
"favorites",
"collections", "collections",
"----------------------------", "----------------------------",
"book_status", "book_status",

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

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

@ -6,7 +6,6 @@ local Language = {
language_names = { language_names = {
C = "English", C = "English",
en = "English", en = "English",
en_GB = "English (United Kingdom)",
ca = "Catalá", ca = "Catalá",
cs = "Čeština", cs = "Čeština",
da = "Dansk", da = "Dansk",
@ -119,7 +118,6 @@ function Language:getLangMenuTable()
-- NOTE: language with no translation are commented out for now -- NOTE: language with no translation are commented out for now
sub_item_table = { sub_item_table = {
self:genLanguageSubItem("C"), self:genLanguageSubItem("C"),
self:genLanguageSubItem("en_GB"),
self:genLanguageSubItem("ca"), self:genLanguageSubItem("ca"),
self:genLanguageSubItem("cs"), self:genLanguageSubItem("cs"),
self:genLanguageSubItem("de"), self:genLanguageSubItem("de"),
@ -140,7 +138,7 @@ function Language:getLangMenuTable()
--self:genLanguageSubItem("pl_PL"), --self:genLanguageSubItem("pl_PL"),
self:genLanguageSubItem("pt_PT"), self:genLanguageSubItem("pt_PT"),
self:genLanguageSubItem("pt_BR"), self:genLanguageSubItem("pt_BR"),
self:genLanguageSubItem("ro"), --self:genLanguageSubItem("ro"),
self:genLanguageSubItem("ro_MD"), self:genLanguageSubItem("ro_MD"),
self:genLanguageSubItem("sk"), self:genLanguageSubItem("sk"),
self:genLanguageSubItem("sv"), self:genLanguageSubItem("sv"),

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

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

@ -49,12 +49,6 @@ local ota_channels = {
nightly = _("Development"), nightly = _("Development"),
} }
-- Try to detect Kindle running hardfp firmware
function OTAManager:_isKindleHardFP()
local util = require("util")
return util.pathExists("/lib/ld-linux-armhf.so.3")
end
-- Try to detect WARIO+ Kindle boards (i.MX6 & i.MX7) -- Try to detect WARIO+ Kindle boards (i.MX6 & i.MX7)
function OTAManager:_isKindleWarioOrMore() function OTAManager:_isKindleWarioOrMore()
local cpu_hw = nil local cpu_hw = nil
@ -99,9 +93,7 @@ function OTAManager:getOTAModel()
return "cervantes" return "cervantes"
elseif Device:isKindle() then elseif Device:isKindle() then
if Device:isTouchDevice() or Device.model == "Kindle4" then if Device:isTouchDevice() or Device.model == "Kindle4" then
if self:_isKindleHardFP() then if self:_isKindleWarioOrMore() then
return "kindlehf"
elseif self:_isKindleWarioOrMore() then
return "kindlepw2" return "kindlepw2"
else else
return "kindle" return "kindle"

@ -3,7 +3,6 @@
local DataStorage = require("datastorage") local DataStorage = require("datastorage")
local FileConverter = require("apps/filemanager/filemanagerconverter") local FileConverter = require("apps/filemanager/filemanagerconverter")
local DocSettings = require("docsettings") local DocSettings = require("docsettings")
local Language = require("ui/language")
local Version = require("version") local Version = require("version")
local FFIUtil = require("ffi/util") local FFIUtil = require("ffi/util")
local T = FFIUtil.template local T = FFIUtil.template
@ -258,9 +257,6 @@ function QuickStart:getQuickStart()
relpath = relpath .. dst relpath = relpath .. dst
relpath = relpath:gsub("//", "/") -- make it prettier relpath = relpath:gsub("//", "/") -- make it prettier
quickstart_html = quickstart_html:gsub([[src="resources/]], [[src="]]..relpath..[[resources/]]) quickstart_html = quickstart_html:gsub([[src="resources/]], [[src="]]..relpath..[[resources/]])
if Language:isLanguageRTL(language) then
quickstart_html = quickstart_html:gsub('<html>', '<html dir="rtl">')
end
-- Write the fixed HTML content -- Write the fixed HTML content
FileConverter:writeStringToFile(quickstart_html, quickstart_filename) FileConverter:writeStringToFile(quickstart_html, quickstart_filename)
end end

@ -148,13 +148,7 @@ function Screensaver:expandSpecial(message, fallback)
time_left_chapter = self:_calcAverageTimeForPages(ui.toc:getChapterPagesLeft(currentpage) or doc:getTotalPagesLeft(currentpage)) time_left_chapter = self:_calcAverageTimeForPages(ui.toc:getChapterPagesLeft(currentpage) or doc:getTotalPagesLeft(currentpage))
time_left_document = self:_calcAverageTimeForPages(doc:getTotalPagesLeft(currentpage)) time_left_document = self:_calcAverageTimeForPages(doc:getTotalPagesLeft(currentpage))
end end
if currentpage == 1 then percent = Math.round((currentpage * 100) / totalpages)
percent = 0
elseif currentpage == totalpages then
percent = 100
else
percent = Math.round(Math.clamp(((currentpage * 100) / totalpages), 1, 99))
end
props = ui.doc_props props = ui.doc_props
elseif DocSettings:hasSidecarFile(lastfile) then elseif DocSettings:hasSidecarFile(lastfile) then
-- If there's no ReaderUI instance, but the file has sidecar data, use that -- If there's no ReaderUI instance, but the file has sidecar data, use that
@ -162,13 +156,7 @@ function Screensaver:expandSpecial(message, fallback)
totalpages = doc_settings:readSetting("doc_pages") or totalpages totalpages = doc_settings:readSetting("doc_pages") or totalpages
percent = doc_settings:readSetting("percent_finished") or percent percent = doc_settings:readSetting("percent_finished") or percent
currentpage = Math.round(percent * totalpages) currentpage = Math.round(percent * totalpages)
if currentpage == 1 then percent = Math.round(percent * 100)
percent = 0
elseif currentpage == totalpages then
percent = 100
else
percent = Math.round(Math.clamp(percent * 100, 1, 99))
end
props = FileManagerBookInfo.extendProps(doc_settings:readSetting("doc_props"), lastfile) props = FileManagerBookInfo.extendProps(doc_settings:readSetting("doc_props"), lastfile)
-- Unable to set time_left_chapter and time_left_document without ReaderUI, so leave N/A -- Unable to set time_left_chapter and time_left_document without ReaderUI, so leave N/A
end end
@ -265,7 +253,7 @@ local function addOverlayMessage(widget, widget_height, text)
end end
function Screensaver:chooseFolder() function Screensaver:chooseFolder()
local title_header = _("Current random image folder:") local title_header = _("Current screensaver folder:")
local current_path = G_reader_settings:readSetting("screensaver_dir") local current_path = G_reader_settings:readSetting("screensaver_dir")
local caller_callback = function(path) local caller_callback = function(path)
G_reader_settings:saveSetting("screensaver_dir", path) G_reader_settings:saveSetting("screensaver_dir", path)
@ -273,15 +261,26 @@ function Screensaver:chooseFolder()
filemanagerutil.showChooseDialog(title_header, caller_callback, current_path) filemanagerutil.showChooseDialog(title_header, caller_callback, current_path)
end end
function Screensaver:chooseFile() function Screensaver:chooseFile(document_cover)
local title_header, current_path, file_filter, caller_callback local title_header, current_path, file_filter, caller_callback
title_header = _("Current image or document cover:") if document_cover then
current_path = G_reader_settings:readSetting("screensaver_document_cover") title_header = _("Current screensaver document cover:")
file_filter = function(filename) current_path = G_reader_settings:readSetting("screensaver_document_cover")
return DocumentRegistry:hasProvider(filename) file_filter = function(filename)
end return DocumentRegistry:hasProvider(filename)
caller_callback = function(path) end
G_reader_settings:saveSetting("screensaver_document_cover", path) caller_callback = function(path)
G_reader_settings:saveSetting("screensaver_document_cover", path)
end
else
title_header = _("Current screensaver image:")
current_path = G_reader_settings:readSetting("screensaver_image")
file_filter = function(filename)
return DocumentRegistry:isImageFile(filename)
end
caller_callback = function(path)
G_reader_settings:saveSetting("screensaver_image", path)
end
end end
filemanagerutil.showChooseDialog(title_header, caller_callback, current_path, nil, file_filter) filemanagerutil.showChooseDialog(title_header, caller_callback, current_path, nil, file_filter)
end end
@ -314,9 +313,9 @@ function Screensaver:setMessage()
or self.default_screensaver_message or self.default_screensaver_message
local input_dialog local input_dialog
input_dialog = InputDialog:new{ input_dialog = InputDialog:new{
title = _("Sleep screen message"), title = _("Screensaver message"),
description = _([[ description = _([[
Enter a custom message to be displayed on the sleep screen. The following escape sequences are available: Enter the message to be displayed by the screensaver. The following escape sequences can be used:
%T title %T title
%A author(s) %A author(s)
%S series %S series
@ -395,6 +394,7 @@ end
function Screensaver:modeIsImage() function Screensaver:modeIsImage()
return self.screensaver_type == "cover" return self.screensaver_type == "cover"
or self.screensaver_type == "random_image" or self.screensaver_type == "random_image"
or self.screensaver_type == "image_file"
end end
function Screensaver:withBackground() function Screensaver:withBackground()
@ -446,13 +446,9 @@ function Screensaver:setup(event, event_message)
end end
if not excluded then if not excluded then
if lastfile and lfs.attributes(lastfile, "mode") == "file" then if lastfile and lfs.attributes(lastfile, "mode") == "file" then
if DocumentRegistry:isImageFile(lastfile) then self.image = FileManagerBookInfo:getCoverImage(ui and ui.document, lastfile)
self.image_file = lastfile if self.image == nil then
else self.screensaver_type = "random_image"
self.image = FileManagerBookInfo:getCoverImage(ui and ui.document, lastfile)
if self.image == nil then
self.screensaver_type = "random_image"
end
end end
else else
self.screensaver_type = "random_image" self.screensaver_type = "random_image"
@ -467,6 +463,13 @@ function Screensaver:setup(event, event_message)
self.screensaver_type = "random_image" self.screensaver_type = "random_image"
end end
end end
if self.screensaver_type == "image_file" then
self.image_file = G_reader_settings:readSetting(self.prefix .. "screensaver_image")
or G_reader_settings:readSetting("screensaver_image")
if self.image_file == nil or lfs.attributes(self.image_file, "mode") ~= "file" then
self.screensaver_type = "random_image"
end
end
if self.screensaver_type == "readingprogress" then if self.screensaver_type == "readingprogress" then
-- This is implemented by the Statistics plugin -- This is implemented by the Statistics plugin
if Screensaver.getReaderProgress == nil then if Screensaver.getReaderProgress == nil then
@ -539,22 +542,15 @@ function Screensaver:show()
-- Build the main widget for the effective mode, all the sanity checks were handled in setup -- Build the main widget for the effective mode, all the sanity checks were handled in setup
local widget = nil local widget = nil
if self.screensaver_type == "cover" or self.screensaver_type == "random_image" then if self.screensaver_type == "cover" then
local widget_settings = { widget = ImageWidget:new{
image = self.image,
image_disposable = true,
width = Screen:getWidth(), width = Screen:getWidth(),
height = Screen:getHeight(), height = Screen:getHeight(),
scale_factor = G_reader_settings:isFalse("screensaver_stretch_images") and 0 or nil, scale_factor = G_reader_settings:isFalse("screensaver_stretch_images") and 0 or nil,
stretch_limit_percentage = G_reader_settings:readSetting("screensaver_stretch_limit_percentage"), stretch_limit_percentage = G_reader_settings:readSetting("screensaver_stretch_limit_percentage"),
} }
if self.image then
widget_settings.image = self.image
widget_settings.image_disposable = true
elseif self.image_file then
widget_settings.file = self.image_file
widget_settings.file_do_cache = false
widget_settings.alpha = true
end
widget = ImageWidget:new(widget_settings)
elseif self.screensaver_type == "bookstatus" then elseif self.screensaver_type == "bookstatus" then
local ReaderUI = require("apps/reader/readerui") local ReaderUI = require("apps/reader/readerui")
local ui = ReaderUI.instance local ui = ReaderUI.instance
@ -568,6 +564,16 @@ function Screensaver:show()
ui = ui, ui = ui,
readonly = true, readonly = true,
} }
elseif self.screensaver_type == "random_image" or self.screensaver_type == "image_file" then
widget = ImageWidget:new{
file = self.image_file,
file_do_cache = false,
alpha = true,
width = Screen:getWidth(),
height = Screen:getHeight(),
scale_factor = G_reader_settings:isFalse("screensaver_stretch_images") and 0 or nil,
stretch_limit_percentage = G_reader_settings:readSetting("screensaver_stretch_limit_percentage"),
}
elseif self.screensaver_type == "readingprogress" then elseif self.screensaver_type == "readingprogress" then
widget = Screensaver.getReaderProgress() widget = Screensaver.getReaderProgress()
end end
@ -587,21 +593,23 @@ function Screensaver:show()
local message_height local message_height
if self.show_message then if self.show_message then
-- Handle user settings & fallbacks, with that prefix mess on top... -- Handle user settings & fallbacks, with that prefix mess on top...
local screensaver_message = self.default_screensaver_message local screensaver_message
if G_reader_settings:has(self.prefix .. "screensaver_message") then if G_reader_settings:has(self.prefix .. "screensaver_message") then
screensaver_message = G_reader_settings:readSetting(self.prefix .. "screensaver_message") screensaver_message = G_reader_settings:readSetting(self.prefix .. "screensaver_message")
elseif G_reader_settings:has("screensaver_message") then else
screensaver_message = G_reader_settings:readSetting("screensaver_message") if G_reader_settings:has("screensaver_message") then
end screensaver_message = G_reader_settings:readSetting("screensaver_message")
-- If the message is set to the defaults (which is also the case when it's unset), prefer the event message if there is one. else
if screensaver_message == self.default_screensaver_message then -- In the absence of a custom message, use the event message if any, barring that, use the default message.
if self.event_message then if self.event_message then
screensaver_message = self.event_message screensaver_message = self.event_message
-- The overlay is only ever populated with the event message, and we only want to show it once ;). -- The overlay is only ever populated with the event message, and we only want to show it once ;).
self.overlay_message = nil self.overlay_message = nil
else
screensaver_message = self.default_screensaver_message
end
end end
end end
-- NOTE: Only attempt to expand if there are special characters in the message. -- NOTE: Only attempt to expand if there are special characters in the message.
if screensaver_message:find("%%") then if screensaver_message:find("%%") then
screensaver_message = self:expandSpecial(screensaver_message, self.event_message or self.default_screensaver_message) screensaver_message = self:expandSpecial(screensaver_message, self.event_message or self.default_screensaver_message)
@ -690,7 +698,7 @@ function Screensaver:show()
self.screensaver_widget.modal = true self.screensaver_widget.modal = true
self.screensaver_widget.dithered = true self.screensaver_widget.dithered = true
UIManager:show(self.screensaver_widget, Device:hasKaleidoWfm() and "color" or "full") UIManager:show(self.screensaver_widget, "full")
end end
-- Setup the gesture lock through an additional invisible widget, so that it works regardless of the configuration. -- Setup the gesture lock through an additional invisible widget, so that it works regardless of the configuration.

@ -77,9 +77,9 @@ All other time variables (a handful) get the appropriate suffix `_ms`, `_us`, `_
local start_time = time.now() local start_time = time.now()
-- Do some stuff. -- Do some stuff.
-- You can add and subtract `fts times` objects -- You can add and subtract `fts times` objects.
local duration = time.now() - start_time local duration = time.now() - start.fts
-- and convert that object to various more human-readable formats, e.g., -- And convert that object to various more human-readable formats, e.g.,
print(string.format("Stuff took %.3fms", time.to_ms(duration))) print(string.format("Stuff took %.3fms", time.to_ms(duration)))
local offset = time.s(100) local offset = time.s(100)
@ -107,25 +107,25 @@ local FTS2US = 1 / US2FTS
-- Fixed point time -- Fixed point time
local time = {} local time = {}
--- Sometimes we need a very large time. --- Sometimes we need a very large time
time.huge = math.huge time.huge = math.huge
--- Creates a time (fts) from a number in seconds. --- Creates a time (fts) from a number in seconds
function time.s(seconds) function time.s(seconds)
return math.floor(seconds * S2FTS) return math.floor(seconds * S2FTS)
end end
--- Creates a time (fts) from a number in milliseconds. --- Creates a time (fts) from a number in milliseconds
function time.ms(msec) function time.ms(msec)
return math.floor(msec * MS2FTS) return math.floor(msec * MS2FTS)
end end
--- Creates a time (fts) from a number in microseconds. --- Creates a time (fts) from a number in microseconds
function time.us(usec) function time.us(usec)
return math.floor(usec * US2FTS) return math.floor(usec * US2FTS)
end end
--- Creates a time (fts) from a structure similar to timeval. --- Creates a time (fts) from a structure similar to timeval
function time.timeval(tv) function time.timeval(tv)
return tv.sec * S2FTS + tv.usec * US2FTS return tv.sec * S2FTS + tv.usec * US2FTS
end end
@ -159,7 +159,7 @@ end
--[[-- Compare a past *MONOTONIC* fts time to *now*, returning the elapsed time between the two. (sec.usecs variant) --[[-- Compare a past *MONOTONIC* fts time to *now*, returning the elapsed time between the two. (sec.usecs variant)
Returns a Lua (decimal) number (sec.usecs, with decimal places) (accurate to the µs). Returns a Lua (decimal) number (sec.usecs, with decimal places) (accurate to the µs)
]] ]]
function time.since(start_time) function time.since(start_time)
-- Time difference -- Time difference

@ -231,7 +231,11 @@ function Translator:genSettingsMenu()
return T("%1 (%2)", lang_name, lang_key) return T("%1 (%2)", lang_name, lang_key)
end, end,
checked_func = function() checked_func = function()
return lang_key == (G_reader_settings:readSetting(setting_name) or default_checked_item) if G_reader_settings:has(setting_name) then
return lang_key == G_reader_settings:readSetting(setting_name)
else
return lang_key == default_checked_item
end
end, end,
callback = function() callback = function()
G_reader_settings:saveSetting(setting_name, lang_key) G_reader_settings:saveSetting(setting_name, lang_key)
@ -243,26 +247,6 @@ function Translator:genSettingsMenu()
return { return {
text = _("Translation settings"), text = _("Translation settings"),
sub_item_table = { sub_item_table = {
{
text_func = function()
local __, name = self:getDocumentLanguage()
return T(_("Translate from book language: %1"), name or _("N/A"))
end,
help_text = _([[
With books that specify their main language in their metadata (most EPUBs and FB2s), enabling this option will make this language the source language. Otherwise, auto-detection or the selected language will be used.
This is useful:
- For books in a foreign language, where consistent translation is needed and words in other languages are rare.
- For books in familiar languages, to get definitions for words from the translation service.]]),
enabled_func = function()
return self:getDocumentLanguage() ~= nil
end,
checked_func = function()
return G_reader_settings:isTrue("translator_from_doc_lang")
end,
callback = function()
G_reader_settings:flipTrue("translator_from_doc_lang")
end,
},
{ {
text = _("Auto-detect source language"), text = _("Auto-detect source language"),
help_text = _("This setting is best suited for foreign text found in books written in your native language."), help_text = _("This setting is best suited for foreign text found in books written in your native language."),
@ -271,7 +255,6 @@ This is useful:
end, end,
checked_func = function() checked_func = function()
return G_reader_settings:nilOrTrue("translator_from_auto_detect") return G_reader_settings:nilOrTrue("translator_from_auto_detect")
and not (G_reader_settings:isTrue("translator_from_doc_lang") and self:getDocumentLanguage() ~= nil)
end, end,
callback = function() callback = function()
G_reader_settings:flipNilOrTrue("translator_from_auto_detect") G_reader_settings:flipNilOrTrue("translator_from_auto_detect")
@ -288,6 +271,27 @@ This is useful:
and not (G_reader_settings:isTrue("translator_from_doc_lang") and self:getDocumentLanguage() ~= nil) and not (G_reader_settings:isTrue("translator_from_doc_lang") and self:getDocumentLanguage() ~= nil)
end, end,
sub_item_table = genLanguagesItems("translator_from_language"), sub_item_table = genLanguagesItems("translator_from_language"),
keep_menu_open = true,
},
{
text_func = function()
local __, name = self:getDocumentLanguage()
return T(_("Translate from book language: %1"), name or _("N/A"))
end,
help_text = _([[
With books that specify their main language in their metadata (most EPUBs and FB2s), enabling this option will make this language the source language. Otherwise, auto-detection or the selected language will be used.
This is useful:
- For books in a foreign language, where consistent translation is needed and words in other languages are rare.
- For books in familiar languages, to get definitions for words from the translation service.]]),
enabled_func = function()
return self:getDocumentLanguage() ~= nil
end,
checked_func = function()
return G_reader_settings:isTrue("translator_from_doc_lang")
end,
callback = function()
G_reader_settings:flipTrue("translator_from_doc_lang")
end,
separator = true, separator = true,
}, },
{ {
@ -296,6 +300,7 @@ This is useful:
return T(_("Translate to: %1"), self:getLanguageName(lang, "")) return T(_("Translate to: %1"), self:getLanguageName(lang, ""))
end, end,
sub_item_table = genLanguagesItems("translator_to_language", self:getTargetLanguage()), sub_item_table = genLanguagesItems("translator_to_language", self:getTargetLanguage()),
keep_menu_open = true,
}, },
}, },
} }
@ -498,14 +503,14 @@ Show translated text in TextViewer, with alternate translations
@string source_lang[opt="auto"] (`"en"`, `"fr"`, ``) or `"auto"` to auto-detect source language @string source_lang[opt="auto"] (`"en"`, `"fr"`, ``) or `"auto"` to auto-detect source language
@string target_lang[opt] (`"en"`, `"fr"`, ``) @string target_lang[opt] (`"en"`, `"fr"`, ``)
--]] --]]
function Translator:showTranslation(text, detailed_view, source_lang, target_lang, from_highlight, index) function Translator:showTranslation(text, detailed_view, source_lang, target_lang, from_highlight, page, index)
if Device:hasClipboard() then if Device:hasClipboard() then
Device.input.setClipboardText(text) Device.input.setClipboardText(text)
end end
local NetworkMgr = require("ui/network/manager") local NetworkMgr = require("ui/network/manager")
if NetworkMgr:willRerunWhenOnline(function() if NetworkMgr:willRerunWhenOnline(function()
self:showTranslation(text, detailed_view, source_lang, target_lang, from_highlight, index) self:showTranslation(text, detailed_view, source_lang, target_lang, from_highlight, page, index)
end) then end) then
return return
end end
@ -514,11 +519,11 @@ function Translator:showTranslation(text, detailed_view, source_lang, target_lan
-- translation service query. -- translation service query.
local Trapper = require("ui/trapper") local Trapper = require("ui/trapper")
Trapper:wrap(function() Trapper:wrap(function()
self:_showTranslation(text, detailed_view, source_lang, target_lang, from_highlight, index) self:_showTranslation(text, detailed_view, source_lang, target_lang, from_highlight, page, index)
end) end)
end end
function Translator:_showTranslation(text, detailed_view, source_lang, target_lang, from_highlight, index) function Translator:_showTranslation(text, detailed_view, source_lang, target_lang, from_highlight, page, index)
if not target_lang then if not target_lang then
target_lang = self:getTargetLanguage() target_lang = self:getTargetLanguage()
end end
@ -627,8 +632,8 @@ function Translator:_showTranslation(text, detailed_view, source_lang, target_la
UIManager:close(textviewer) UIManager:close(textviewer)
UIManager:close(ui.highlight.highlight_dialog) UIManager:close(ui.highlight.highlight_dialog)
ui.highlight.highlight_dialog = nil ui.highlight.highlight_dialog = nil
if index then if page then
ui.highlight:editHighlight(index, false, text_main) ui.highlight:editHighlight(page, index, false, text_main)
else else
ui.highlight:addNote(text_main) ui.highlight:addNote(text_main)
end end
@ -640,8 +645,8 @@ function Translator:_showTranslation(text, detailed_view, source_lang, target_la
UIManager:close(textviewer) UIManager:close(textviewer)
UIManager:close(ui.highlight.highlight_dialog) UIManager:close(ui.highlight.highlight_dialog)
ui.highlight.highlight_dialog = nil ui.highlight.highlight_dialog = nil
if index then if page then
ui.highlight:editHighlight(index, false, text_all) ui.highlight:editHighlight(page, index, false, text_all)
else else
ui.highlight:addNote(text_all) ui.highlight:addNote(text_all)
end end

@ -616,7 +616,7 @@ function Trapper:dismissableRunInSubprocess(task, trap_widget_or_string, task_re
end end
-- The go_on_func resumed us: we have not been dismissed. -- The go_on_func resumed us: we have not been dismissed.
-- Check if sub process has ended -- Check if sub process has ended
-- Depending on the size of what the child has to write, -- Depending on the the size of what the child has to write,
-- it may has ended (if data fits in the kernel pipe buffer) or -- it may has ended (if data fits in the kernel pipe buffer) or
-- it may still be alive blocking on write() (if data exceeds -- it may still be alive blocking on write() (if data exceeds
-- the kernel pipe buffer) -- the kernel pipe buffer)

@ -62,15 +62,11 @@ function UIManager:init()
UsbDevicePlugIn = function(input_event) UsbDevicePlugIn = function(input_event)
-- Retrieve the argument set by Input:handleKeyBoardEv -- Retrieve the argument set by Input:handleKeyBoardEv
local evdev = table.remove(Input.fake_event_args[input_event]) local evdev = table.remove(Input.fake_event_args[input_event])
local path = "/dev/input/event" .. tostring(evdev) self:broadcastEvent(Event:new("EvdevInputInsert", evdev))
self:broadcastEvent(Event:new("EvdevInputInsert", path))
end, end,
UsbDevicePlugOut = function(input_event) UsbDevicePlugOut = function(input_event)
local evdev = table.remove(Input.fake_event_args[input_event]) local evdev = table.remove(Input.fake_event_args[input_event])
local path = "/dev/input/event" .. tostring(evdev) self:broadcastEvent(Event:new("EvdevInputRemove", evdev))
self:broadcastEvent(Event:new("EvdevInputRemove", path))
end, end,
} }
self.poweroff_action = function() self.poweroff_action = function()
@ -138,7 +134,7 @@ For more details about refreshtype, refreshregion & refreshdither see the descri
If refreshtype is omitted, no refresh will be enqueued at this time. If refreshtype is omitted, no refresh will be enqueued at this time.
@param widget a @{ui.widget.widget|widget} object @param widget a @{ui.widget.widget|widget} object
@string refreshtype `"color"`, `"colortext"`, `"full"`, `"flashpartial"`, `"flashui"`, `"[partial]"`, `"[ui]"`, `"partial"`, `"ui"`, `"fast"`, `"a2"` (optional) @string refreshtype `"full"`, `"flashpartial"`, `"flashui"`, `"[partial]"`, `"[ui]"`, `"partial"`, `"ui"`, `"fast"`, `"a2"` (optional)
@param refreshregion a rectangle @{ui.geometry.Geom|Geom} object (optional, requires refreshtype to be set) @param refreshregion a rectangle @{ui.geometry.Geom|Geom} object (optional, requires refreshtype to be set)
@int x horizontal screen offset (optional, `0` if omitted) @int x horizontal screen offset (optional, `0` if omitted)
@int y vertical screen offset (optional, `0` if omitted) @int y vertical screen offset (optional, `0` if omitted)
@ -195,7 +191,7 @@ For more details about refreshtype, refreshregion & refreshdither see the descri
If refreshtype is omitted, no extra refresh will be enqueued at this time, leaving only those from the uncovered widgets. If refreshtype is omitted, no extra refresh will be enqueued at this time, leaving only those from the uncovered widgets.
@param widget a @{ui.widget.widget|widget} object @param widget a @{ui.widget.widget|widget} object
@string refreshtype `"color"`, `"colortext"`, `"full"`, `"flashpartial"`, `"flashui"`, `"[partial]"`, `"[ui]"`, `"partial"`, `"ui"`, `"fast"`, `"a2"` (optional) @string refreshtype `"full"`, `"flashpartial"`, `"flashui"`, `"[partial]"`, `"[ui]"`, `"partial"`, `"ui"`, `"fast"`, `"a2"` (optional)
@param refreshregion a rectangle @{ui.geometry.Geom|Geom} object (optional, requires refreshtype to be set) @param refreshregion a rectangle @{ui.geometry.Geom|Geom} object (optional, requires refreshtype to be set)
@bool refreshdither `true` if the refresh requires dithering (optional, requires refreshtype to be set) @bool refreshdither `true` if the refresh requires dithering (optional, requires refreshtype to be set)
@see setDirty @see setDirty
@ -464,10 +460,6 @@ It just appends stuff to the paint and/or refresh queues.
Here's a quick rundown of what each refreshtype should be used for: Here's a quick rundown of what each refreshtype should be used for:
* `color`: high-fidelity flashing refresh for color image content on Kaleido panels.
Maps to partial on unsupported devices, as such, better used conditionally behind a Device:hasKaleidoWfm check.
* `colortext`: REAGL refresh for color text (e.g., highlights) on Kaleido panels.
Maps to partial on unsupported devices, as such, better used conditionally behind a Device:hasKaleidoWfm check.
* `full`: high-fidelity flashing refresh (e.g., large images). * `full`: high-fidelity flashing refresh (e.g., large images).
Highest quality, but highest latency. Highest quality, but highest latency.
Don't abuse if you only want a flash (in this case, prefer `flashui` or `flashpartial`). Don't abuse if you only want a flash (in this case, prefer `flashui` or `flashpartial`).
@ -566,7 +558,7 @@ UIManager:setDirty(self.widget, "partial", Geom:new{x=10,y=10,w=100,h=50})
UIManager:setDirty(self.widget, function() return "ui", self.someelement.dimen end) UIManager:setDirty(self.widget, function() return "ui", self.someelement.dimen end)
@param widget a window-level widget object, `"all"`, or `nil` @param widget a window-level widget object, `"all"`, or `nil`
@param refreshtype `"color"`, `"colortext"`, `"full"`, `"flashpartial"`, `"flashui"`, `"[partial]"`, `"[ui]"`, `"partial"`, `"ui"`, `"fast"`, `"a2"` (or a lambda, see description above) @param refreshtype `"full"`, `"flashpartial"`, `"flashui"`, `"[partial]"`, `"[ui]"`, `"partial"`, `"ui"`, `"fast"`, `"a2"` (or a lambda, see description above)
@param refreshregion a rectangle @{ui.geometry.Geom|Geom} object (optional, omitting it means the region will cover the full screen) @param refreshregion a rectangle @{ui.geometry.Geom|Geom} object (optional, omitting it means the region will cover the full screen)
@bool refreshdither `true` if widget requires dithering (optional) @bool refreshdither `true` if widget requires dithering (optional)
]] ]]
@ -1058,7 +1050,7 @@ function UIManager:getElapsedTimeSinceBoot()
end end
-- precedence of refresh modes: -- precedence of refresh modes:
local refresh_modes = { a2 = 1, fast = 2, ui = 3, partial = 4, ["[ui]"] = 5, ["[partial]"] = 6, flashui = 7, flashpartial = 8, full = 9, colortext = 10, color = 11 } local refresh_modes = { a2 = 1, fast = 2, ui = 3, partial = 4, ["[ui]"] = 5, ["[partial]"] = 6, flashui = 7, flashpartial = 8, full = 9 }
-- NOTE: We might want to introduce a "force_a2" that points to fast, but has the highest priority, -- NOTE: We might want to introduce a "force_a2" that points to fast, but has the highest priority,
-- for the few cases where we might *really* want to enforce fast (for stuff like panning or skimming?). -- for the few cases where we might *really* want to enforce fast (for stuff like panning or skimming?).
-- refresh methods in framebuffer implementation -- refresh methods in framebuffer implementation
@ -1072,8 +1064,6 @@ local refresh_methods = {
flashui = Screen.refreshFlashUI, flashui = Screen.refreshFlashUI,
flashpartial = Screen.refreshFlashPartial, flashpartial = Screen.refreshFlashPartial,
full = Screen.refreshFull, full = Screen.refreshFull,
colortext = Screen.refreshColorText,
color = Screen.refreshColor,
} }
--[[ --[[
@ -1111,7 +1101,7 @@ Widgets call this in their `paintTo()` method in order to notify
UIManager that a certain part of the screen is to be refreshed. UIManager that a certain part of the screen is to be refreshed.
@string mode @string mode
refresh mode (`"color"`, `"colortext"`, `"full"`, `"flashpartial"`, `"flashui"`, `"[partial]"`, `"[ui]"`, `"partial"`, `"ui"`, `"fast"`, `"a2"`) refresh mode (`"full"`, `"flashpartial"`, `"flashui"`, `"[partial]"`, `"[ui]"`, `"partial"`, `"ui"`, `"fast"`, `"a2"`)
@param region @param region
A rectangle @{ui.geometry.Geom|Geom} object that specifies the region to be updated. A rectangle @{ui.geometry.Geom|Geom} object that specifies the region to be updated.
Optional, update will affect whole screen if not specified. Optional, update will affect whole screen if not specified.

@ -632,20 +632,13 @@ function BookMapWidget:init()
self.covers_fullscreen = true -- hint for UIManager:_repaint() self.covers_fullscreen = true -- hint for UIManager:_repaint()
if Device:hasKeys() then if Device:hasKeys() then
self.key_events.Close = { { Device.input.group.Back } } self.key_events = {
self.key_events.ShowBookMapMenu = { { "Menu" } } Close = { { Input.group.Back } },
self.key_events.ScrollPageUp = { { Input.group.PgBack } } ScrollRowUp = { { "Up" } },
self.key_events.ScrollPageDown = { { Input.group.PgFwd } } ScrollRowDown = { { "Down" } },
if Device:hasSymKey() then ScrollPageUp = { { Input.group.PgBack } },
self.key_events.ScrollRowUp = { { "Shift", "Up" } } ScrollPageDown = { { Input.group.PgFwd } },
self.key_events.ScrollRowDown = { { "Shift", "Down" } } }
elseif Device:hasScreenKB() then
self.key_events.ScrollRowUp = { { "ScreenKB", "Up" } }
self.key_events.ScrollRowDown = { { "ScreenKB", "Down" } }
else
self.key_events.ScrollRowUp = { { "Up" } }
self.key_events.ScrollRowDown = { { "Down" } }
end
end end
if Device:isTouchDevice() then if Device:isTouchDevice() then
self.ges_events = { self.ges_events = {
@ -709,7 +702,7 @@ function BookMapWidget:init()
fullscreen = true, fullscreen = true,
title = title, title = title,
left_icon = "appbar.menu", left_icon = "appbar.menu",
left_icon_tap_callback = function() self:onShowBookMapMenu() end, left_icon_tap_callback = function() self:showMenu() end,
left_icon_hold_callback = not self.overview_mode and function() left_icon_hold_callback = not self.overview_mode and function()
self:toggleDefaultSettings() -- toggle between user settings and default view self:toggleDefaultSettings() -- toggle between user settings and default view
end, end,
@ -1188,7 +1181,7 @@ function BookMapWidget:update()
end end
function BookMapWidget:onShowBookMapMenu() function BookMapWidget:showMenu()
local button_dialog local button_dialog
-- Width of our -/+ buttons, so it looks fine with Button's default font size of 20 -- Width of our -/+ buttons, so it looks fine with Button's default font size of 20
local plus_minus_width = Screen:scaleBySize(60) local plus_minus_width = Screen:scaleBySize(60)
@ -1230,7 +1223,7 @@ function BookMapWidget:onShowBookMapMenu()
end, end,
}}, }},
not self.overview_mode and {{ not self.overview_mode and {{
text = _("Switch current/initial view"), text = _("Switch current/initial views"),
align = "left", align = "left",
enabled_func = function() return self.toc_depth > 0 end, enabled_func = function() return self.toc_depth > 0 end,
callback = function() callback = function()
@ -1276,7 +1269,7 @@ function BookMapWidget:onShowBookMapMenu()
}, },
not self.overview_mode and { not self.overview_mode and {
{ {
text = _("Page-slot width"), text = _("Page slot width"),
callback = function() end, callback = function() end,
align = "left", align = "left",
-- Below, minus increases page per row and plus decreases it. -- Below, minus increases page per row and plus decreases it.
@ -1331,10 +1324,6 @@ function BookMapWidget:onShowBookMapMenu()
} }
}, },
} }
-- remove "Page browser on tap" from non-touch devices
if not Device:isTouchDevice() then
table.remove(buttons, 3)
end
-- Remove false buttons from the list if overview_mode -- Remove false buttons from the list if overview_mode
for i = #buttons, 1, -1 do for i = #buttons, 1, -1 do
if not buttons[i] then if not buttons[i] then
@ -1352,12 +1341,13 @@ function BookMapWidget:onShowBookMapMenu()
} }
UIManager:show(button_dialog) UIManager:show(button_dialog)
end end
function BookMapWidget:showAbout() function BookMapWidget:showAbout()
local text = _([[ local text = _([[
Book map provides a summary of a book's content, showing chapters and pages visually. If statistics are enabled, black bars represent pages already read (gray for pages read in the current session), with varying heights based on reading time. Book map displays an overview of the book content.
Map legend: If statistics are enabled, black bars are shown for already read pages (gray for pages read in the current reading session). Their heights vary depending on the time spent reading the page.
Chapters are shown above the pages they encompass.
Under the pages, these indicators may be shown:
current page current page
previous locations previous locations
highlighted text highlighted text
@ -1367,24 +1357,17 @@ Map legend:
if self.overview_mode then if self.overview_mode then
text = text .. "\n\n" .. _([[ text = text .. "\n\n" .. _([[
When in overview mode, the book map is always displayed in grid mode to fit on one screen. The chapter levels can be easily adjusted for the most convenient overview experience.]]) In overview mode, the book map is always in grid mode and made to fit on a single screen. Chapter levels can be changed for the most comfortable overview.]])
else else
text = text .. "\n\n" .. _([[ text = text .. "\n\n" .. _([[
When you first open a book, the book map will begin in grid mode, displaying all chapter levels on one screen for a comprehensive overview of the book's content.]]) On a newly opened book, the book map will start in grid mode showing all chapter levels, fitting on a single screen, to give the best initial overview of the book's content.]])
end end
UIManager:show(InfoMessage:new{ text = text }) UIManager:show(InfoMessage:new{ text = text })
end end
function BookMapWidget:showGestures() function BookMapWidget:showGestures()
local text local text
if not Device:isTouchDevice() then if self.overview_mode then
text = _([[
Use settings in this menu to change the level of chapters to include in the book map, the view type (grid or flat) and the width of page slots.
Use "ScreenKB/Shift" + "Up/Down" to scroll or use the page turn buttons to move at a faster rate.
Press back to exit the book map.]])
elseif self.overview_mode then
text = _([[ text = _([[
Tap on a location in the book to browse thumbnails of the pages there. Tap on a location in the book to browse thumbnails of the pages there.

@ -57,7 +57,6 @@ local UIManager = require("ui/uimanager")
local VerticalGroup = require("ui/widget/verticalgroup") local VerticalGroup = require("ui/widget/verticalgroup")
local VerticalSpan = require("ui/widget/verticalspan") local VerticalSpan = require("ui/widget/verticalspan")
local Screen = Device.screen local Screen = Device.screen
local util = require("util")
local ButtonDialog = InputContainer:extend{ local ButtonDialog = InputContainer:extend{
buttons = nil, buttons = nil,
@ -92,14 +91,8 @@ function ButtonDialog:init()
end end
if self.dismissable then if self.dismissable then
if Device:hasKeys() then if Device:hasKeys() then
local back_group = util.tableDeepCopy(Device.input.group.Back) local close_keys = Device:hasFewKeys() and { "Back", "Left" } or Device.input.group.Back
if Device:hasFewKeys() then self.key_events.Close = { { close_keys } }
table.insert(back_group, "Left")
self.key_events.Close = { { back_group } }
else
table.insert(back_group, "Menu")
self.key_events.Close = { { back_group } }
end
end end
if Device:isTouchDevice() then if Device:isTouchDevice() then
self.ges_events.TapClose = { self.ges_events.TapClose = {

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

Loading…
Cancel
Save