mirror of https://github.com/koreader/koreader
Compare commits
No commits in common. 'master' and 'v2020.07' have entirely different histories.
@ -1,42 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
CI_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=/dev/null
|
||||
source "${CI_DIR}/common.sh"
|
||||
|
||||
set +e
|
||||
|
||||
echo -e "\\n${ANSI_GREEN}Updating translation source file."
|
||||
make pot
|
||||
pushd l10n && {
|
||||
git checkout master
|
||||
# If only one line was added and removed, it was just the timestamp.
|
||||
git diff --numstat | grep "1[[:space:]]1[[:space:]]templates/koreader.pot" && echo -e "\\n${ANSI_GREEN}No updated translations found." || {
|
||||
git -c user.name="KOReader build bot" -c user.email="non-reply@koreader.rocks" \
|
||||
commit templates/koreader.pot -m "Updated translation source file"
|
||||
git push --quiet "https://${TRANSLATIONS_GITHUB_TOKEN}@github.com/koreader/koreader-translations.git" master
|
||||
echo -e "\\n${ANSI_GREEN}Translation update pushed."
|
||||
}
|
||||
} && popd || exit
|
||||
|
||||
echo -e "\\n${ANSI_GREEN}Checking out koreader/doc for update."
|
||||
git clone git@github.com:koreader/doc.git koreader_doc
|
||||
|
||||
# push doc update
|
||||
pushd doc && {
|
||||
luajit "$(command -v ldoc)" .
|
||||
if [ ! -d html ]; then
|
||||
echo "Failed to generate documents..."
|
||||
exit 1
|
||||
fi
|
||||
} && popd || exit
|
||||
|
||||
cp -r doc/html/* koreader_doc/
|
||||
pushd koreader_doc && {
|
||||
git add -A
|
||||
echo -e "\\n${ANSI_GREEN}Pushing document update..."
|
||||
git -c user.name="KOReader build bot" -c user.email="non-reply@koreader.rocks" \
|
||||
commit -a --amend -m 'Automated documentation build from travis-ci.'
|
||||
git push -f --quiet "https://${DOCS_GITHUB_TOKEN}@github.com/koreader/doc.git" gh-pages >/dev/null
|
||||
echo -e "\\n${ANSI_GREEN}Documentation update pushed."
|
||||
} && popd || exit
|
@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# don't do this for clang
|
||||
if [ "${CXX}" = "g++" ]; then
|
||||
export CXX="g++-5" CC="gcc-5"
|
||||
fi
|
||||
# in case anything ignores the environment variables, override through PATH
|
||||
mkdir bin
|
||||
ln -s "$(command -v gcc-5)" bin/cc
|
||||
ln -s "$(command -v gcc-5)" bin/gcc
|
||||
ln -s "$(command -v c++)" bin/c++
|
||||
ln -s "$(command -v g++-5)" bin/g++
|
||||
|
||||
# Travis only makes a shallow clone of --depth=50. KOReader is small enough that
|
||||
# we can just grab it all. This is necessary to generate the version number,
|
||||
# without which some tests will fail.
|
||||
# git fetch --unshallow
|
@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
CI_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=/dev/null
|
||||
source "${CI_DIR}/common.sh"
|
||||
|
||||
rm -rf "${HOME}/.luarocks"
|
||||
mkdir "${HOME}/.luarocks"
|
||||
cp "${CI_BUILD_DIR}/install/etc/luarocks/config.lua" "${HOME}/.luarocks/config.lua"
|
||||
echo "wrap_bin_scripts = false" >>"${HOME}/.luarocks/config.lua"
|
||||
travis_retry luarocks --local install luafilesystem
|
||||
# for verbose_print module
|
||||
travis_retry luarocks --local install ansicolors
|
||||
travis_retry luarocks --local install busted 2.0.rc13-0
|
||||
#- mv -f $HOME/.luarocks/bin/busted_bootstrap $HOME/.luarocks/bin/busted
|
||||
# Apply junit testcase time fix. This can be removed once there is a busted 2.0.rc13 or final
|
||||
# See https://github.com/Olivine-Labs/busted/commit/830f175c57ca3f9e79f95b8c4eaacf58252453d7
|
||||
sed -i 's|testcase_node.time = formatDuration(element.duration)|testcase_node:set_attrib("time", formatDuration(element.duration))|' "${HOME}/.luarocks/share/lua/5.1/busted/outputHandlers/junit.lua"
|
||||
|
||||
travis_retry luarocks --local install luacheck
|
||||
travis_retry luarocks --local install lanes # for parallel luacheck
|
||||
|
||||
# used only on master branch but added to cache for better speed
|
||||
travis_retry luarocks --local install ldoc
|
||||
travis_retry luarocks --local install luacov
|
@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
CI_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=/dev/null
|
||||
source "${CI_DIR}/common.sh"
|
||||
|
||||
# print some useful info
|
||||
echo "BUILD_DIR: ${CI_BUILD_DIR}"
|
||||
echo "pwd: $(pwd)"
|
||||
ls
|
||||
|
||||
# toss submodules if there are any changes
|
||||
# if [ "$(git status --ignore-submodules=dirty --porcelain)" ]; then
|
||||
# "--ignore-submodules=dirty", removed temporarily, as it did not notice as
|
||||
# expected that base was updated and kept using old cached base
|
||||
if [ "$(git status --ignore-submodules=dirty --porcelain)" ]; then
|
||||
# what changed?
|
||||
git status
|
||||
# purge and reinit submodules
|
||||
git submodule deinit -f .
|
||||
git submodule update --init
|
||||
else
|
||||
echo -e "${ANSI_GREEN}Using cached submodules."
|
||||
fi
|
||||
|
||||
# install our own updated luarocks
|
||||
echo "luarocks installation path: ${CI_BUILD_DIR}"
|
||||
if [ ! -f "${CI_BUILD_DIR}/install/bin/luarocks" ]; then
|
||||
git clone https://github.com/torch/luajit-rocks.git
|
||||
pushd luajit-rocks && {
|
||||
git checkout 6529891
|
||||
cmake . -DWITH_LUAJIT21=ON -DCMAKE_INSTALL_PREFIX="${CI_BUILD_DIR}/install"
|
||||
make install
|
||||
} && popd || exit
|
||||
else
|
||||
echo -e "${ANSI_GREEN}Using cached luarocks."
|
||||
fi
|
||||
|
||||
if [ ! -d "${HOME}/.luarocks" ] || [ ! -f "${HOME}/.luarocks/$(md5sum <"${CI_DIR}/helper_luarocks.sh")" ]; then
|
||||
echo -e "${ANSI_GREEN}Grabbing new .luarocks."
|
||||
sudo apt-get update
|
||||
# install openssl devel for luasec
|
||||
sudo apt-get -y install libssl-dev
|
||||
|
||||
"${CI_DIR}/helper_luarocks.sh"
|
||||
touch "${HOME}/.luarocks/$(md5sum <"${CI_DIR}/helper_luarocks.sh")"
|
||||
else
|
||||
echo -e "${ANSI_GREEN}Using cached .luarocks."
|
||||
fi
|
||||
|
||||
#install our own updated shellcheck
|
||||
SHELLCHECK_VERSION="v0.7.0"
|
||||
SHELLCHECK_URL="https://storage.googleapis.com/shellcheck/shellcheck-${SHELLCHECK_VERSION?}.linux.x86_64.tar.xz"
|
||||
if ! command -v shellcheck; then
|
||||
curl -sSL "${SHELLCHECK_URL}" | tar --exclude 'SHA256SUMS' --strip-components=1 -C "${HOME}/bin" -xJf -
|
||||
chmod +x "${HOME}/bin/shellcheck"
|
||||
shellcheck --version
|
||||
else
|
||||
echo -e "${ANSI_GREEN}Using cached shellcheck."
|
||||
fi
|
||||
|
||||
# install shfmt
|
||||
SHFMT_URL="https://github.com/mvdan/sh/releases/download/v3.0.1/shfmt_v3.0.1_linux_amd64"
|
||||
if [ "$(shfmt --version)" != "v3.0.1" ]; then
|
||||
curl -sSL "${SHFMT_URL}" -o "${HOME}/bin/shfmt"
|
||||
chmod +x "${HOME}/bin/shfmt"
|
||||
else
|
||||
echo -e "${ANSI_GREEN}Using cached shfmt."
|
||||
fi
|
@ -1,6 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
@ -1,181 +0,0 @@
|
||||
name: macos
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
|
||||
macos:
|
||||
|
||||
# macos-11, macos-12 & macos-13 are broken at this time being.
|
||||
# https://github.com/koreader/koreader/issues/8686,
|
||||
# https://github.com/koreader/koreader/issues/8686#issuecomment-1172950236
|
||||
|
||||
# Please don't update to newer macOS version unless you can test that the new
|
||||
# action produces working binaries.
|
||||
# 10.15 is no longer supported so we are running 13 just to make sure the build does not break.
|
||||
runs-on: macos-13
|
||||
|
||||
env:
|
||||
# Bump number to reset all caches.
|
||||
CACHE_EPOCH: '0'
|
||||
CLICOLOR_FORCE: '1'
|
||||
MACOSX_DEPLOYMENT_TARGET: '10.15'
|
||||
MAKEFLAGS: 'OUTPUT_DIR=build INSTALL_DIR=install TARGET=macos'
|
||||
|
||||
steps:
|
||||
- name: XCode version
|
||||
run: xcode-select -p
|
||||
|
||||
# Checkout / fetch. {{{
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: false
|
||||
fetch-depth: 0
|
||||
filter: tree:0
|
||||
show-progress: false
|
||||
|
||||
- name: Fetch
|
||||
run: make fetchthirdparty
|
||||
|
||||
# }}}
|
||||
|
||||
# Restore / setup caches. {{{
|
||||
|
||||
- name: Generate cache key
|
||||
run: make -C base TARGET= cache-key
|
||||
|
||||
- name: Restore build directory
|
||||
id: build-restore
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: base/build
|
||||
key: ${{ env.CACHE_EPOCH }}-${{ runner.os }}-build-${{ hashFiles('base/cache-key') }}
|
||||
|
||||
- name: Restore build cache
|
||||
id: ccache-restore
|
||||
if: steps.build-restore.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: /Users/runner/Library/Caches/ccache
|
||||
key: ${{ env.CACHE_EPOCH }}-${{ runner.os }}-ccache-${{ hashFiles('base/cache-key') }}
|
||||
restore-keys: ${{ env.CACHE_EPOCH }}-${{ runner.os }}-ccache-
|
||||
|
||||
- name: Install ccache
|
||||
if: steps.build-restore.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
wget --progress=dot:mega https://github.com/ccache/ccache/releases/download/v4.9.1/ccache-4.9.1-darwin.tar.gz
|
||||
tar xf ccache-4.9.1-darwin.tar.gz
|
||||
printf '%s\n' "$PWD/ccache-4.9.1-darwin" >>"${GITHUB_PATH}"
|
||||
|
||||
- name: Setup build cache
|
||||
if: steps.build-restore.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
set -x
|
||||
which ccache
|
||||
ccache --version
|
||||
ccache --zero-stats
|
||||
ccache --max-size=256M
|
||||
ccache --show-config
|
||||
|
||||
# }}}
|
||||
|
||||
# Install dependencies. {{{
|
||||
|
||||
- name: Setup Python
|
||||
if: steps.build-restore.outputs.cache-hit != 'true'
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
# Note: Python 3.12 removal of `distutils` breaks GLib's build.
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install homebrew dependencies
|
||||
# Compared to the README, adds p7zip.
|
||||
run: |
|
||||
packages=(
|
||||
nasm binutils coreutils libtool autoconf automake cmake make
|
||||
sdl2 lua@5.1 luarocks gettext pkg-config wget
|
||||
gnu-getopt grep p7zip ninja
|
||||
)
|
||||
# Lua 5.1 is disabled, so we need to work around that:
|
||||
# - fetch all packages
|
||||
brew fetch "${packages[@]}"
|
||||
# - disable auto-updates
|
||||
export HOMEBREW_NO_AUTO_UPDATE=1
|
||||
# - install lua@5.1 from cache
|
||||
brew install "$(brew --cache lua@5.1)"
|
||||
# - and install the rest
|
||||
brew install "${packages[@]}"
|
||||
|
||||
- name: Update PATH
|
||||
run: >
|
||||
printf '%s\n'
|
||||
"$(brew --prefix)/opt/gettext/bin"
|
||||
"$(brew --prefix)/opt/gnu-getopt/bin"
|
||||
"$(brew --prefix)/opt/grep/libexec/gnubin"
|
||||
"$(brew --prefix)/opt/make/libexec/gnubin"
|
||||
| tee "${GITHUB_PATH}"
|
||||
|
||||
# }}}
|
||||
|
||||
# Build. {{{
|
||||
|
||||
- name: Build
|
||||
if: steps.build-restore.outputs.cache-hit != 'true'
|
||||
run: make base
|
||||
|
||||
- name: Dump binaries runtime path & dependencies
|
||||
run: make bindeps
|
||||
|
||||
# }}}
|
||||
|
||||
# Clean / save caches. {{{
|
||||
|
||||
- name: Clean caches
|
||||
if: steps.build-restore.outputs.cache-hit != 'true' && always()
|
||||
run: |
|
||||
set -x
|
||||
# Trim the build directory.
|
||||
rm -rf base/build/{cmake,staging,thirdparty}
|
||||
ccache --cleanup >/dev/null
|
||||
ccache --show-stats --verbose
|
||||
|
||||
- name: Save build cache
|
||||
uses: actions/cache/save@v4
|
||||
if: steps.build-restore.outputs.cache-hit != 'true' && steps.ccache-restore.outputs.cache-hit != 'true'
|
||||
with:
|
||||
path: /Users/runner/Library/Caches/ccache
|
||||
key: ${{ steps.ccache-restore.outputs.cache-primary-key }}
|
||||
|
||||
- name: Save build directory
|
||||
uses: actions/cache/save@v4
|
||||
if: steps.build-restore.outputs.cache-hit != 'true'
|
||||
with:
|
||||
path: base/build
|
||||
key: ${{ steps.build-restore.outputs.cache-primary-key }}
|
||||
|
||||
# }}}
|
||||
|
||||
# Generate / upload artifact. {{{
|
||||
|
||||
- name: Generate artifact
|
||||
run: make update --assume-old=base
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: koreader-macos
|
||||
path: '*.7z'
|
||||
|
||||
# }}}
|
||||
|
||||
# vim: foldmethod=marker foldlevel=0
|
@ -1,5 +0,0 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json",
|
||||
"runtime.version": "LuaJIT",
|
||||
}
|
||||
|
@ -0,0 +1,48 @@
|
||||
language: c
|
||||
|
||||
# sudo: false
|
||||
sudo: true
|
||||
dist: trusty
|
||||
|
||||
compiler:
|
||||
- gcc
|
||||
|
||||
env:
|
||||
global:
|
||||
- "PATH=${HOME}/bin:${PATH}"
|
||||
matrix:
|
||||
- EMULATE_READER=1
|
||||
|
||||
cache:
|
||||
apt: true
|
||||
directories:
|
||||
- "${HOME}/bin"
|
||||
# compiled luarocks binaries
|
||||
- "${TRAVIS_BUILD_DIR}/install"
|
||||
# base build
|
||||
- "${TRAVIS_BUILD_DIR}/base"
|
||||
- "${HOME}/.ccache"
|
||||
- "${HOME}/.luarocks"
|
||||
before_cache:
|
||||
# don't quote like you normally would or it won't expand
|
||||
- rm -frv ${TRAVIS_BUILD_DIR}/base/build/*/cache/*
|
||||
# don't cache unit tests
|
||||
- rm -frv ${TRAVIS_BUILD_DIR}/base/build/*/spec
|
||||
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- ubuntu-toolchain-r-test
|
||||
packages:
|
||||
- g++-4.8
|
||||
- libsdl1.2-dev
|
||||
# luasec dependencies
|
||||
- libssl1.0.0
|
||||
- nasm
|
||||
# OpenSSL likes this (package contains makedepend)
|
||||
- xutils-dev
|
||||
|
||||
before_install: .ci/before_install.sh
|
||||
install: .ci/install.sh
|
||||
script: .ci/script.sh
|
||||
after_success: .ci/after_success.sh
|
@ -1 +1 @@
|
||||
Subproject commit 67474697169dd88800bc37af5b7fb87ce0596ee8
|
||||
Subproject commit 5956ba1cc56a61e0eacbf19e1c54bcbbbf6cc980
|
@ -1,191 +0,0 @@
|
||||
local DataStorage = require("datastorage")
|
||||
local Font = require("ui/font")
|
||||
local InfoMessage = require("ui/widget/infomessage")
|
||||
local LuaSettings = require("luasettings")
|
||||
local Menu = require("ui/widget/menu")
|
||||
local NetworkMgr = require("ui/network/manager")
|
||||
local Notification = require("ui/widget/notification")
|
||||
local Screen = require("device").screen
|
||||
local UIManager = require("ui/uimanager")
|
||||
local ffiutil = require("ffi/util")
|
||||
local util = require("util")
|
||||
|
||||
local _ = require("gettext")
|
||||
|
||||
local server_types = {
|
||||
dropbox = _("Dropbox"),
|
||||
webdav = _("WebDAV"),
|
||||
}
|
||||
local indent = ""
|
||||
|
||||
local SyncService = Menu:extend{
|
||||
no_title = false,
|
||||
show_parent = nil,
|
||||
is_popout = false,
|
||||
is_borderless = true,
|
||||
title = _("Cloud sync settings"),
|
||||
title_face = Font:getFace("smallinfofontbold"),
|
||||
}
|
||||
|
||||
function SyncService:init()
|
||||
self.item_table = self:generateItemTable()
|
||||
self.width = Screen:getWidth()
|
||||
self.height = Screen:getHeight()
|
||||
Menu.init(self)
|
||||
end
|
||||
|
||||
function SyncService:generateItemTable()
|
||||
local item_table = {}
|
||||
-- select and/or add server
|
||||
local added_servers = LuaSettings:open(DataStorage:getSettingsDir().."/cloudstorage.lua"):readSetting("cs_servers") or {}
|
||||
for _, server in ipairs(added_servers) do
|
||||
if server.type == "dropbox" or server.type == "webdav" then
|
||||
local item = {
|
||||
text = indent .. server.name,
|
||||
address = server.address,
|
||||
username = server.username,
|
||||
password = server.password,
|
||||
type = server.type,
|
||||
url = server.url,
|
||||
mandatory = server_types[server.type],
|
||||
}
|
||||
item.callback = function()
|
||||
require("ui/downloadmgr"):new{
|
||||
item = item,
|
||||
onConfirm = function(path)
|
||||
server.url = path
|
||||
self.onConfirm(server)
|
||||
self:onClose()
|
||||
end,
|
||||
}:chooseCloudDir()
|
||||
end
|
||||
table.insert(item_table, item)
|
||||
end
|
||||
end
|
||||
if #item_table > 0 then
|
||||
table.insert(item_table, 1, {
|
||||
text = _("Choose cloud service:"),
|
||||
bold = true,
|
||||
})
|
||||
end
|
||||
table.insert(item_table, {
|
||||
text = _("Add service"),
|
||||
bold = true,
|
||||
callback = function()
|
||||
local cloud_storage = require("apps/cloudstorage/cloudstorage"):new{}
|
||||
local onClose = cloud_storage.onClose
|
||||
cloud_storage.onClose = function(this)
|
||||
onClose(this)
|
||||
self:switchItemTable(nil, self:generateItemTable())
|
||||
end
|
||||
UIManager:show(cloud_storage)
|
||||
end
|
||||
})
|
||||
return item_table
|
||||
end
|
||||
|
||||
function SyncService.getReadablePath(server)
|
||||
local url = util.stringStartsWith(server.url, "/") and server.url:sub(2) or server.url
|
||||
url = util.urlDecode(url) or url
|
||||
url = util.stringEndsWith(url, "/") and url or url .. "/"
|
||||
if server.type == "dropbox" then
|
||||
url = "/" .. url
|
||||
elseif server.type == "webdav" then
|
||||
url = (server.address:sub(-1) == "/" and server.address or server.address .. "/") .. url
|
||||
end
|
||||
if url:sub(-2) == "//" then url = url:sub(1, -2) end
|
||||
return url
|
||||
end
|
||||
|
||||
-- Prepares three files for sync_cb to call to do the actual syncing:
|
||||
-- * local_file (one that is being used)
|
||||
-- * income_file (one that has just been downloaded from Cloud to be merged, then to be deleted)
|
||||
-- * cached_file (the one that was uploaded in the previous round of syncing)
|
||||
--
|
||||
-- How it works:
|
||||
--
|
||||
-- If we simply merge the local file with the income file (ignore duplicates), then items that have been deleted locally
|
||||
-- but not remotely (on other devices) will re-emerge in the result file. The same goes for items deleted remotely but
|
||||
-- not locally. To avoid this, we first need to delete them from both the income file and local file.
|
||||
--
|
||||
-- The problem is how to identify them, and that is when the cached file comes into play.
|
||||
-- The cached file represents what local and remote agreed on previously (was identical to local and remote after being uploaded
|
||||
-- the previous round), by comparing it with local file, items no longer in local file are ones being recently deleted.
|
||||
-- The same applies to income file. Then we can delete them from both local and income files to be ready for merging. (The actual
|
||||
-- deletion and merging procedures happen in sync_cb as users of this service will have different file specifications)
|
||||
--
|
||||
-- After merging, the income file is no longer needed and is deleted. The local file is uploaded and then a copy of it is saved
|
||||
-- and renamed to replace the old cached file (thus the naming). The cached file stays (in the same folder) till being replaced
|
||||
-- in the next round.
|
||||
function SyncService.sync(server, file_path, sync_cb, is_silent)
|
||||
if NetworkMgr:willRerunWhenOnline(function() SyncService.sync(server, file_path, sync_cb, is_silent) end) then
|
||||
return
|
||||
end
|
||||
local file_name = ffiutil.basename(file_path)
|
||||
local income_file_path = file_path .. ".temp" -- file downloaded from server
|
||||
local cached_file_path = file_path .. ".sync" -- file uploaded to server last time
|
||||
|
||||
local fail_msg = _("Something went wrong when syncing, please check your network connection and try again later.")
|
||||
local show_msg = function(msg)
|
||||
if is_silent then return end
|
||||
UIManager:show(InfoMessage:new{
|
||||
text = msg or fail_msg,
|
||||
timeout = 3,
|
||||
})
|
||||
end
|
||||
if server.type ~= "dropbox" and server.type ~= "webdav" then
|
||||
show_msg(_("Wrong server type."))
|
||||
return
|
||||
end
|
||||
local code_response = 412 -- If-Match header failed
|
||||
local etag
|
||||
local api = server.type == "dropbox" and require("apps/cloudstorage/dropboxapi") or require("apps/cloudstorage/webdavapi")
|
||||
local token = server.password
|
||||
if server.type == "dropbox" and not (server.address == nil or server.address == "") then
|
||||
token = api:getAccessToken(server.password, server.address)
|
||||
end
|
||||
while code_response == 412 do
|
||||
os.remove(income_file_path)
|
||||
if server.type == "dropbox" then
|
||||
local url_base = server.url:sub(-1) == "/" and server.url or server.url.."/"
|
||||
code_response, etag = api:downloadFile(url_base..file_name, token, income_file_path)
|
||||
elseif server.type == "webdav" then
|
||||
local path = api:getJoinedPath(server.address, server.url)
|
||||
path = api:getJoinedPath(path, file_name)
|
||||
code_response, etag = api:downloadFile(path, server.username, server.password, income_file_path)
|
||||
end
|
||||
if code_response ~= 200 and code_response ~= 404
|
||||
and not (server.type == "dropbox" and code_response == 409) then
|
||||
show_msg()
|
||||
return
|
||||
end
|
||||
local ok, cb_return = pcall(sync_cb, file_path, cached_file_path, income_file_path)
|
||||
if not ok or not cb_return then
|
||||
show_msg()
|
||||
if not ok then require("logger").err("sync service callback failed:", cb_return) end
|
||||
return
|
||||
end
|
||||
if server.type == "dropbox" then
|
||||
local url_base = server.url == "/" and "" or server.url
|
||||
code_response = api:uploadFile(url_base, token, file_path, etag, true)
|
||||
elseif server.type == "webdav" then
|
||||
local path = api:getJoinedPath(server.address, server.url)
|
||||
path = api:getJoinedPath(path, file_name)
|
||||
code_response = api:uploadFile(path, server.username, server.password, file_path, etag)
|
||||
end
|
||||
end
|
||||
os.remove(income_file_path)
|
||||
if type(code_response) == "number" and code_response >= 200 and code_response < 300 then
|
||||
os.remove(cached_file_path)
|
||||
ffiutil.copyFile(file_path, cached_file_path)
|
||||
UIManager:show(Notification:new{
|
||||
text = _("Successfully synchronized."),
|
||||
timeout = 2,
|
||||
})
|
||||
else
|
||||
show_msg()
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
return SyncService
|
File diff suppressed because it is too large
Load Diff
@ -1,340 +1,185 @@
|
||||
local ButtonDialog = require("ui/widget/buttondialog")
|
||||
local CheckButton = require("ui/widget/checkbutton")
|
||||
local ConfirmBox = require("ui/widget/confirmbox")
|
||||
local DocSettings = require("docsettings")
|
||||
local CenterContainer = require("ui/widget/container/centercontainer")
|
||||
local DocumentRegistry = require("document/documentregistry")
|
||||
local FileChooser = require("ui/widget/filechooser")
|
||||
local Font = require("ui/font")
|
||||
local InfoMessage = require("ui/widget/infomessage")
|
||||
local InputContainer = require("ui/widget/container/inputcontainer")
|
||||
local InputDialog = require("ui/widget/inputdialog")
|
||||
local Menu = require("ui/widget/menu")
|
||||
local UIManager = require("ui/uimanager")
|
||||
local WidgetContainer = require("ui/widget/container/widgetcontainer")
|
||||
local Utf8Proc = require("ffi/utf8proc")
|
||||
local filemanagerutil = require("apps/filemanager/filemanagerutil")
|
||||
local lfs = require("libs/libkoreader-lfs")
|
||||
local BaseUtil = require("ffi/util")
|
||||
local util = require("util")
|
||||
local _ = require("gettext")
|
||||
local N_ = _.ngettext
|
||||
local T = require("ffi/util").template
|
||||
local Screen = require("device").screen
|
||||
|
||||
local FileSearcher = WidgetContainer:extend{
|
||||
case_sensitive = false,
|
||||
include_subfolders = true,
|
||||
include_metadata = false,
|
||||
}
|
||||
|
||||
function FileSearcher:onShowFileSearch(search_string)
|
||||
local search_dialog
|
||||
local check_button_case, check_button_subfolders, check_button_metadata
|
||||
search_dialog = InputDialog:new{
|
||||
title = _("Enter text to search for in filename"),
|
||||
input = search_string or self.search_string,
|
||||
buttons = {
|
||||
{
|
||||
{
|
||||
text = _("Cancel"),
|
||||
id = "close",
|
||||
callback = function()
|
||||
UIManager:close(search_dialog)
|
||||
end,
|
||||
},
|
||||
{
|
||||
text = _("Home folder"),
|
||||
enabled = G_reader_settings:has("home_dir"),
|
||||
callback = function()
|
||||
self.search_string = search_dialog:getInputText()
|
||||
if self.search_string == "" then return end
|
||||
UIManager:close(search_dialog)
|
||||
self.path = G_reader_settings:readSetting("home_dir")
|
||||
self:doSearch()
|
||||
end,
|
||||
},
|
||||
{
|
||||
text = self.ui.file_chooser and _("Current folder") or _("Book folder"),
|
||||
is_enter_default = true,
|
||||
callback = function()
|
||||
self.search_string = search_dialog:getInputText()
|
||||
if self.search_string == "" then return end
|
||||
UIManager:close(search_dialog)
|
||||
self.path = self.ui.file_chooser and self.ui.file_chooser.path or self.ui:getLastDirFile()
|
||||
self:doSearch()
|
||||
end,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
check_button_case = CheckButton:new{
|
||||
text = _("Case sensitive"),
|
||||
checked = self.case_sensitive,
|
||||
parent = search_dialog,
|
||||
callback = function()
|
||||
self.case_sensitive = check_button_case.checked
|
||||
end,
|
||||
}
|
||||
search_dialog:addWidget(check_button_case)
|
||||
check_button_subfolders = CheckButton:new{
|
||||
text = _("Include subfolders"),
|
||||
checked = self.include_subfolders,
|
||||
parent = search_dialog,
|
||||
callback = function()
|
||||
self.include_subfolders = check_button_subfolders.checked
|
||||
end,
|
||||
}
|
||||
search_dialog:addWidget(check_button_subfolders)
|
||||
if self.ui.coverbrowser then
|
||||
check_button_metadata = CheckButton:new{
|
||||
text = _("Also search in book metadata"),
|
||||
checked = self.include_metadata,
|
||||
parent = search_dialog,
|
||||
callback = function()
|
||||
self.include_metadata = check_button_metadata.checked
|
||||
end,
|
||||
}
|
||||
search_dialog:addWidget(check_button_metadata)
|
||||
end
|
||||
UIManager:show(search_dialog)
|
||||
search_dialog:onShowKeyboard()
|
||||
end
|
||||
local FileSearcher = InputContainer:new{
|
||||
search_dialog = nil,
|
||||
|
||||
function FileSearcher:doSearch()
|
||||
local results
|
||||
local dirs, files = self:getList()
|
||||
-- If we have a FileChooser instance, use it, to be able to make use of its natsort cache
|
||||
if self.ui.file_chooser then
|
||||
results = self.ui.file_chooser:genItemTable(dirs, files)
|
||||
else
|
||||
results = FileChooser:genItemTable(dirs, files)
|
||||
end
|
||||
if #results > 0 then
|
||||
self:showSearchResults(results)
|
||||
else
|
||||
self:showSearchResultsMessage(true)
|
||||
end
|
||||
end
|
||||
--filesearcher
|
||||
-- state buffer
|
||||
dirs = {},
|
||||
files = {},
|
||||
results = {},
|
||||
items = 0,
|
||||
commands = nil,
|
||||
|
||||
function FileSearcher:getList()
|
||||
self.no_metadata_count = 0
|
||||
local sys_folders = { -- do not search in sys_folders
|
||||
["/dev"] = true,
|
||||
["/proc"] = true,
|
||||
["/sys"] = true,
|
||||
}
|
||||
local collate = FileChooser:getCollate()
|
||||
local search_string = self.search_string
|
||||
if search_string ~= "*" then -- one * to show all files
|
||||
if not self.case_sensitive then
|
||||
search_string = Utf8Proc.lowercase(util.fixUtf8(search_string, "?"))
|
||||
end
|
||||
-- replace '.' with '%.'
|
||||
search_string = search_string:gsub("%.","%%%.")
|
||||
-- replace '*' with '.*'
|
||||
search_string = search_string:gsub("%*","%.%*")
|
||||
-- replace '?' with '.'
|
||||
search_string = search_string:gsub("%?","%.")
|
||||
end
|
||||
--filemanagersearch
|
||||
use_previous_search_results = false,
|
||||
lastsearch = nil,
|
||||
}
|
||||
|
||||
local dirs, files = {}, {}
|
||||
local scan_dirs = {self.path}
|
||||
while #scan_dirs ~= 0 do
|
||||
function FileSearcher:readDir()
|
||||
self.dirs = {self.path}
|
||||
self.files = {}
|
||||
while #self.dirs ~= 0 do
|
||||
local new_dirs = {}
|
||||
-- handle each dir
|
||||
for _, d in ipairs(scan_dirs) do
|
||||
for __, d in pairs(self.dirs) do
|
||||
-- handle files in d
|
||||
local ok, iter, dir_obj = pcall(lfs.dir, d)
|
||||
if ok then
|
||||
for f in iter, dir_obj do
|
||||
local fullpath = "/" .. f
|
||||
if d ~= "/" then
|
||||
fullpath = d .. fullpath
|
||||
end
|
||||
local attributes = lfs.attributes(fullpath) or {}
|
||||
-- Don't traverse hidden folders if we're not showing them
|
||||
if attributes.mode == "directory" and f ~= "." and f ~= ".."
|
||||
and (FileChooser.show_hidden or not util.stringStartsWith(f, "."))
|
||||
and FileChooser:show_dir(f) then
|
||||
if self.include_subfolders and not sys_folders[fullpath] then
|
||||
table.insert(new_dirs, fullpath)
|
||||
end
|
||||
if self:isFileMatch(f, fullpath, search_string) then
|
||||
table.insert(dirs, FileChooser:getListItem(nil, f, fullpath, attributes, collate))
|
||||
end
|
||||
-- Always ignore macOS resource forks, too.
|
||||
elseif attributes.mode == "file" and not util.stringStartsWith(f, "._")
|
||||
and (FileChooser.show_unsupported or DocumentRegistry:hasProvider(fullpath))
|
||||
and FileChooser:show_file(f) then
|
||||
if self:isFileMatch(f, fullpath, search_string, true) then
|
||||
table.insert(files, FileChooser:getListItem(nil, f, fullpath, attributes, collate))
|
||||
end
|
||||
end
|
||||
for f in lfs.dir(d) do
|
||||
local fullpath = d.."/"..f
|
||||
local attributes = lfs.attributes(fullpath) or {}
|
||||
-- Don't traverse hidden folders if we're not showing them
|
||||
if attributes.mode == "directory" and f ~= "." and f ~= ".." and (G_reader_settings:isTrue("show_hidden") or not util.stringStartsWith(f, ".")) then
|
||||
table.insert(new_dirs, fullpath)
|
||||
table.insert(self.files, {name = f, path = fullpath, attr = attributes})
|
||||
-- Always ignore macOS resource forks, too.
|
||||
elseif attributes.mode == "file" and not util.stringStartsWith(f, "._") and DocumentRegistry:hasProvider(fullpath) then
|
||||
table.insert(self.files, {name = f, path = fullpath, attr = attributes})
|
||||
end
|
||||
end
|
||||
end
|
||||
scan_dirs = new_dirs
|
||||
end
|
||||
return dirs, files
|
||||
end
|
||||
|
||||
function FileSearcher:isFileMatch(filename, fullpath, search_string, is_file)
|
||||
if search_string == "*" then
|
||||
return true
|
||||
end
|
||||
if not self.case_sensitive then
|
||||
filename = Utf8Proc.lowercase(util.fixUtf8(filename, "?"))
|
||||
end
|
||||
if string.find(filename, search_string) then
|
||||
return true
|
||||
end
|
||||
if self.include_metadata and is_file and DocumentRegistry:hasProvider(fullpath) then
|
||||
local book_props = self.ui.coverbrowser:getBookInfo(fullpath) or
|
||||
self.ui.bookinfo.getDocProps(fullpath, nil, true) -- do not open the document
|
||||
if next(book_props) ~= nil then
|
||||
if self.ui.bookinfo:findInProps(book_props, search_string, self.case_sensitive) then
|
||||
return true
|
||||
end
|
||||
else
|
||||
self.no_metadata_count = self.no_metadata_count + 1
|
||||
end
|
||||
self.dirs = new_dirs
|
||||
end
|
||||
end
|
||||
|
||||
function FileSearcher:showSearchResultsMessage(no_results)
|
||||
local text = no_results and T(_("No results for '%1'."), self.search_string)
|
||||
if self.no_metadata_count == 0 then
|
||||
UIManager:show(InfoMessage:new{ text = text })
|
||||
function FileSearcher:setSearchResults()
|
||||
local FileManager = require("apps/filemanager/filemanager")
|
||||
local ReaderUI = require("apps/reader/readerui")
|
||||
local keywords = self.search_value
|
||||
self.results = {}
|
||||
if keywords == " " then -- one space to show all files
|
||||
self.results = self.files
|
||||
else
|
||||
local txt = T(N_("1 book has been skipped.", "%1 books have been skipped.",
|
||||
self.no_metadata_count), self.no_metadata_count) .. "\n" ..
|
||||
_("Not all books metadata extracted yet.\nExtract metadata now?")
|
||||
text = no_results and text .. "\n\n" .. txt or txt
|
||||
UIManager:show(ConfirmBox:new{
|
||||
text = text,
|
||||
ok_text = _("Extract"),
|
||||
ok_callback = function()
|
||||
if not no_results then
|
||||
self.search_menu.close_callback()
|
||||
for __,f in pairs(self.files) do
|
||||
if string.find(string.lower(f.name), string.lower(keywords)) and string.sub(f.name,-4) ~= ".sdr" then
|
||||
if f.attr.mode == "directory" then
|
||||
f.text = f.name.."/"
|
||||
f.name = nil
|
||||
f.callback = function()
|
||||
FileManager:showFiles(f.path)
|
||||
end
|
||||
table.insert(self.results, f)
|
||||
else
|
||||
f.text = f.name
|
||||
f.name = nil
|
||||
f.callback = function()
|
||||
ReaderUI:showReader(f.path)
|
||||
end
|
||||
table.insert(self.results, f)
|
||||
end
|
||||
self.ui.coverbrowser:extractBooksInDirectory(self.path)
|
||||
end
|
||||
})
|
||||
end
|
||||
end
|
||||
self.keywords = keywords
|
||||
self.items = #self.results
|
||||
end
|
||||
|
||||
function FileSearcher:showSearchResults(results)
|
||||
self.search_menu = Menu:new{
|
||||
title = T(_("Search results (%1)"), #results),
|
||||
subtitle = T(_("Query: %1"), self.search_string),
|
||||
item_table = results,
|
||||
ui = self.ui,
|
||||
covers_fullscreen = true, -- hint for UIManager:_repaint()
|
||||
is_borderless = true,
|
||||
is_popout = false,
|
||||
title_bar_fm_style = true,
|
||||
onMenuSelect = self.onMenuSelect,
|
||||
onMenuHold = self.onMenuHold,
|
||||
handle_hold_on_hold_release = true,
|
||||
}
|
||||
self.search_menu.close_callback = function()
|
||||
UIManager:close(self.search_menu)
|
||||
if self.ui.file_chooser then
|
||||
self.ui.file_chooser:refreshPath()
|
||||
function FileSearcher:close()
|
||||
if self.search_value then
|
||||
UIManager:close(self.search_dialog)
|
||||
if string.len(self.search_value) > 0 then
|
||||
self:readDir() --- @todo this probably doesn't need to be repeated once it's been done
|
||||
self:setSearchResults() --- @todo doesn't have to be repeated if the search term is the same
|
||||
if #self.results > 0 then
|
||||
self:showSearchResults() --- @todo something about no results
|
||||
else
|
||||
UIManager:show(
|
||||
InfoMessage:new{
|
||||
text = BaseUtil.template(_("Found no files matching '%1'."),
|
||||
self.search_value)
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
UIManager:show(self.search_menu)
|
||||
if self.no_metadata_count ~= 0 then
|
||||
self:showSearchResultsMessage()
|
||||
end
|
||||
end
|
||||
|
||||
function FileSearcher:onMenuSelect(item)
|
||||
local file = item.path
|
||||
local bookinfo, dialog
|
||||
local function close_dialog_callback()
|
||||
UIManager:close(dialog)
|
||||
end
|
||||
local function close_dialog_menu_callback()
|
||||
UIManager:close(dialog)
|
||||
self.close_callback()
|
||||
end
|
||||
local buttons = {}
|
||||
if item.is_file then
|
||||
local is_currently_opened = self.ui.document and self.ui.document.file == file
|
||||
if DocumentRegistry:hasProvider(file) or DocSettings:hasSidecarFile(file) then
|
||||
bookinfo = self.ui.coverbrowser and self.ui.coverbrowser:getBookInfo(file)
|
||||
local doc_settings_or_file = is_currently_opened and self.ui.doc_settings or file
|
||||
table.insert(buttons, filemanagerutil.genStatusButtonsRow(doc_settings_or_file, close_dialog_callback))
|
||||
table.insert(buttons, {}) -- separator
|
||||
table.insert(buttons, {
|
||||
filemanagerutil.genResetSettingsButton(file, close_dialog_callback, is_currently_opened),
|
||||
self.ui.collections:genAddToCollectionButton(file, close_dialog_callback),
|
||||
})
|
||||
end
|
||||
table.insert(buttons, {
|
||||
function FileSearcher:onShowFileSearch()
|
||||
local dummy = self.search_value
|
||||
local enabled_search_home_dir = true
|
||||
if not G_reader_settings:readSetting("home_dir") then
|
||||
enabled_search_home_dir = false
|
||||
end
|
||||
self.search_dialog = InputDialog:new{
|
||||
title = _("Search for books by filename"),
|
||||
input = self.search_value,
|
||||
width = math.floor(Screen:getWidth() * 0.9),
|
||||
buttons = {
|
||||
{
|
||||
text = _("Delete"),
|
||||
enabled = not is_currently_opened,
|
||||
callback = function()
|
||||
local function post_delete_callback()
|
||||
UIManager:close(dialog)
|
||||
for i, menu_item in ipairs(self.item_table) do
|
||||
if menu_item.path == file then
|
||||
table.remove(self.item_table, i)
|
||||
break
|
||||
end
|
||||
self:switchItemTable(T(_("Search results (%1)"), #self.item_table), self.item_table)
|
||||
{
|
||||
text = _("Cancel"),
|
||||
enabled = true,
|
||||
callback = function()
|
||||
self.search_dialog:onClose()
|
||||
UIManager:close(self.search_dialog)
|
||||
end,
|
||||
},
|
||||
{
|
||||
text = _("Current folder"),
|
||||
enabled = true,
|
||||
callback = function()
|
||||
self.path = self.ui.file_chooser and self.ui.file_chooser.path or self.ui:getLastDirFile()
|
||||
self.search_value = self.search_dialog:getInputText()
|
||||
if self.search_value == dummy then -- probably DELETE this if/else block
|
||||
self.use_previous_search_results = true
|
||||
else
|
||||
self.use_previous_search_results = false
|
||||
end
|
||||
end
|
||||
local FileManager = require("apps/filemanager/filemanager")
|
||||
FileManager:showDeleteFileDialog(file, post_delete_callback)
|
||||
end,
|
||||
self:close()
|
||||
end,
|
||||
},
|
||||
{
|
||||
text = _("Home folder"),
|
||||
enabled = enabled_search_home_dir,
|
||||
callback = function()
|
||||
self.path = G_reader_settings:readSetting("home_dir")
|
||||
self.search_value = self.search_dialog:getInputText()
|
||||
if self.search_value == dummy then -- probably DELETE this if/else block
|
||||
self.use_previous_search_results = true
|
||||
else
|
||||
self.use_previous_search_results = false
|
||||
end
|
||||
self:close()
|
||||
end,
|
||||
},
|
||||
},
|
||||
filemanagerutil.genBookInformationButton(file, bookinfo, close_dialog_callback),
|
||||
})
|
||||
end
|
||||
table.insert(buttons, {
|
||||
filemanagerutil.genShowFolderButton(file, close_dialog_menu_callback),
|
||||
{
|
||||
text = _("Open"),
|
||||
enabled = DocumentRegistry:hasProvider(file, nil, true), -- allow auxiliary providers
|
||||
callback = function()
|
||||
close_dialog_callback()
|
||||
local FileManager = require("apps/filemanager/filemanager")
|
||||
FileManager.openFile(self.ui, file, nil, self.close_callback)
|
||||
end,
|
||||
},
|
||||
})
|
||||
local title = file
|
||||
if bookinfo then
|
||||
if bookinfo.title then
|
||||
title = title .. "\n\n" .. T(_("Title: %1"), bookinfo.title)
|
||||
end
|
||||
if bookinfo.authors then
|
||||
title = title .. "\n" .. T(_("Authors: %1"), bookinfo.authors:gsub("[\n\t]", "|"))
|
||||
end
|
||||
end
|
||||
dialog = ButtonDialog:new{
|
||||
title = title .. "\n",
|
||||
buttons = buttons,
|
||||
}
|
||||
UIManager:show(dialog)
|
||||
UIManager:show(self.search_dialog)
|
||||
self.search_dialog:onShowKeyboard()
|
||||
end
|
||||
|
||||
function FileSearcher:onMenuHold(item)
|
||||
if item.is_file then
|
||||
if DocumentRegistry:hasProvider(item.path, nil, true) then
|
||||
local FileManager = require("apps/filemanager/filemanager")
|
||||
FileManager.openFile(self.ui, item.path, nil, self.close_callback)
|
||||
end
|
||||
else
|
||||
self.close_callback()
|
||||
if self.ui.file_chooser then
|
||||
local pathname = util.splitFilePathName(item.path)
|
||||
self.ui.file_chooser:changeToPath(pathname, item.path)
|
||||
else -- called from Reader
|
||||
self.ui:onClose()
|
||||
self.ui:showFileManager(item.path)
|
||||
end
|
||||
function FileSearcher:showSearchResults()
|
||||
local menu_container = CenterContainer:new{
|
||||
dimen = Screen:getSize(),
|
||||
}
|
||||
self.search_menu = Menu:new{
|
||||
width = Screen:getWidth()-15,
|
||||
height = Screen:getHeight()-15,
|
||||
show_parent = menu_container,
|
||||
onMenuHold = self.onMenuHold,
|
||||
cface = Font:getFace("smallinfofont"),
|
||||
perpage = G_reader_settings:readSetting("items_per_page") or 14,
|
||||
_manager = self,
|
||||
}
|
||||
table.insert(menu_container, self.search_menu)
|
||||
self.search_menu.close_callback = function()
|
||||
UIManager:close(menu_container)
|
||||
end
|
||||
return true
|
||||
table.sort(self.results, function(v1,v2) return v1.text < v2.text end)
|
||||
self.search_menu:switchItemTable(_("Search Results"), self.results)
|
||||
UIManager:show(menu_container)
|
||||
end
|
||||
|
||||
return FileSearcher
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,238 +1,236 @@
|
||||
local BD = require("ui/bidi")
|
||||
local ButtonDialog = require("ui/widget/buttondialog")
|
||||
local InfoMessage = require("ui/widget/infomessage")
|
||||
local InputContainer = require("ui/widget/container/inputcontainer")
|
||||
local InputDialog = require("ui/widget/inputdialog")
|
||||
local Menu = require("ui/widget/menu")
|
||||
local Screen = require("device").screen
|
||||
local UIManager = require("ui/uimanager")
|
||||
local WidgetContainer = require("ui/widget/container/widgetcontainer")
|
||||
local lfs = require("libs/libkoreader-lfs")
|
||||
local util = require("ffi/util")
|
||||
local _ = require("gettext")
|
||||
local T = require("ffi/util").template
|
||||
|
||||
local FileManagerShortcuts = WidgetContainer:extend{
|
||||
title = _("Folder shortcuts"),
|
||||
folder_shortcuts = G_reader_settings:readSetting("folder_shortcuts", {}),
|
||||
}
|
||||
local FileManagerShortcuts = InputContainer:extend{}
|
||||
|
||||
function FileManagerShortcuts:updateItemTable()
|
||||
local item_table = {}
|
||||
for folder, item in pairs(self.folder_shortcuts) do
|
||||
local folder_shortcuts = G_reader_settings:readSetting("folder_shortcuts") or {}
|
||||
table.insert(item_table, {
|
||||
text = _("Add new folder shortcut"),
|
||||
callback = function()
|
||||
self:addNewFolder()
|
||||
end,
|
||||
})
|
||||
for _, item in ipairs(folder_shortcuts) do
|
||||
table.insert(item_table, {
|
||||
text = string.format("%s (%s)", item.text, folder),
|
||||
folder = folder,
|
||||
name = item.text,
|
||||
})
|
||||
end
|
||||
table.sort(item_table, function(l, r)
|
||||
return l.text < r.text
|
||||
end)
|
||||
self.shortcuts_menu:switchItemTable(nil, item_table, -1)
|
||||
end
|
||||
text = string.format("%s (%s)", item.text, item.folder),
|
||||
folder = item.folder,
|
||||
friendly_name = item.text,
|
||||
deletable = true,
|
||||
editable = true,
|
||||
callback = function()
|
||||
UIManager:close(self.fm_bookmark)
|
||||
|
||||
function FileManagerShortcuts:hasFolderShortcut(folder)
|
||||
return self.folder_shortcuts[folder] and true or false
|
||||
end
|
||||
local folder = item.folder
|
||||
if folder ~= nil and lfs.attributes(folder, "mode") == "directory" then
|
||||
if self.ui.file_chooser then
|
||||
self.ui.file_chooser:changeToPath(folder)
|
||||
else -- called from Reader
|
||||
local FileManager = require("apps/filemanager/filemanager")
|
||||
|
||||
function FileManagerShortcuts:onMenuChoice(item)
|
||||
local folder = item.folder
|
||||
if lfs.attributes(folder, "mode") ~= "directory" then return end
|
||||
if self.select_callback then
|
||||
self.select_callback(folder)
|
||||
else
|
||||
if self._manager.ui.file_chooser then
|
||||
self._manager.ui.file_chooser:changeToPath(folder)
|
||||
else -- called from Reader
|
||||
self._manager.ui:onClose()
|
||||
self._manager.ui:showFileManager(folder .. "/")
|
||||
end
|
||||
self.ui:onClose()
|
||||
if FileManager.instance then
|
||||
FileManager.instance:reinit(folder)
|
||||
else
|
||||
FileManager:showFiles(folder)
|
||||
end
|
||||
end
|
||||
end
|
||||
end,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
function FileManagerShortcuts:onMenuHold(item)
|
||||
local dialog
|
||||
local buttons = {
|
||||
{
|
||||
{
|
||||
text = _("Remove shortcut"),
|
||||
callback = function()
|
||||
UIManager:close(dialog)
|
||||
self._manager:removeShortcut(item.folder)
|
||||
end
|
||||
},
|
||||
{
|
||||
text = _("Rename shortcut"),
|
||||
callback = function()
|
||||
UIManager:close(dialog)
|
||||
self._manager:editShortcut(item.folder)
|
||||
end
|
||||
},
|
||||
},
|
||||
self._manager.ui.file_chooser and self._manager.ui.clipboard and {
|
||||
{
|
||||
text = _("Paste to folder"),
|
||||
callback = function()
|
||||
UIManager:close(dialog)
|
||||
self._manager.ui:pasteFileFromClipboard(item.folder)
|
||||
end
|
||||
},
|
||||
},
|
||||
}
|
||||
dialog = ButtonDialog:new{
|
||||
title = item.name .. "\n" .. BD.dirpath(item.folder),
|
||||
title_align = "center",
|
||||
buttons = buttons,
|
||||
}
|
||||
UIManager:show(dialog)
|
||||
return true
|
||||
end
|
||||
-- try to stay on current page
|
||||
local select_number = nil
|
||||
|
||||
function FileManagerShortcuts:removeShortcut(folder)
|
||||
self.folder_shortcuts[folder] = nil
|
||||
if self.shortcuts_menu then
|
||||
self.fm_updated = true
|
||||
self:updateItemTable()
|
||||
if self.fm_bookmark.page and self.fm_bookmark.perpage then
|
||||
select_number = (self.fm_bookmark.page - 1) * self.fm_bookmark.perpage + 1
|
||||
end
|
||||
end
|
||||
|
||||
function FileManagerShortcuts:editShortcut(folder, post_callback)
|
||||
local item = self.folder_shortcuts[folder]
|
||||
local name = item and item.text -- rename
|
||||
local input_dialog
|
||||
input_dialog = InputDialog:new {
|
||||
title = _("Enter folder shortcut name"),
|
||||
input = name,
|
||||
description = BD.dirpath(folder),
|
||||
buttons = {{
|
||||
{
|
||||
text = _("Cancel"),
|
||||
id = "close",
|
||||
callback = function()
|
||||
UIManager:close(input_dialog)
|
||||
end,
|
||||
},
|
||||
{
|
||||
text = _("Save"),
|
||||
is_enter_default = true,
|
||||
callback = function()
|
||||
local new_name = input_dialog:getInputText()
|
||||
if new_name == "" or new_name == name then return end
|
||||
UIManager:close(input_dialog)
|
||||
if item then
|
||||
item.text = new_name
|
||||
else
|
||||
self.folder_shortcuts[folder] = { text = new_name, time = os.time() }
|
||||
if post_callback then
|
||||
post_callback()
|
||||
end
|
||||
end
|
||||
if self.shortcuts_menu then
|
||||
self.fm_updated = true
|
||||
self:updateItemTable()
|
||||
end
|
||||
end,
|
||||
},
|
||||
}},
|
||||
}
|
||||
UIManager:show(input_dialog)
|
||||
input_dialog:onShowKeyboard()
|
||||
self.fm_bookmark:switchItemTable(nil,
|
||||
item_table, select_number)
|
||||
end
|
||||
|
||||
function FileManagerShortcuts:addShortcut()
|
||||
function FileManagerShortcuts:addNewFolder()
|
||||
local PathChooser = require("ui/widget/pathchooser")
|
||||
local path_chooser = PathChooser:new{
|
||||
select_directory = true,
|
||||
select_file = false,
|
||||
path = self.ui.file_chooser and self.ui.file_chooser.path or self.ui:getLastDirFile(),
|
||||
path = self.fm_bookmark.curr_path,
|
||||
onConfirm = function(path)
|
||||
if self:hasFolderShortcut(path) then
|
||||
UIManager:show(InfoMessage:new{
|
||||
text = _("Shortcut already exists."),
|
||||
})
|
||||
else
|
||||
self:editShortcut(path)
|
||||
end
|
||||
end,
|
||||
local add_folder_input
|
||||
local friendly_name = util.basename(path) or _("my folder")
|
||||
add_folder_input = InputDialog:new{
|
||||
title = self.title,
|
||||
input = friendly_name,
|
||||
input_type = "text",
|
||||
description = T(_("Title for selected folder:\n%1"), BD.dirpath(path)),
|
||||
buttons = {
|
||||
{
|
||||
{
|
||||
text = _("Cancel"),
|
||||
callback = function()
|
||||
UIManager:close(add_folder_input)
|
||||
end,
|
||||
},
|
||||
{
|
||||
text = _("Add"),
|
||||
is_enter_default = true,
|
||||
callback = function()
|
||||
self:addFolderFromInput(add_folder_input:getInputValue(), path)
|
||||
UIManager:close(add_folder_input)
|
||||
end,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
UIManager:show(add_folder_input)
|
||||
add_folder_input:onShowKeyboard()
|
||||
end
|
||||
}
|
||||
UIManager:show(path_chooser)
|
||||
end
|
||||
|
||||
function FileManagerShortcuts:genShowFolderShortcutsButton(pre_callback)
|
||||
return {
|
||||
text = self.title,
|
||||
callback = function()
|
||||
pre_callback()
|
||||
self:onShowFolderShortcutsDialog()
|
||||
end,
|
||||
}
|
||||
function FileManagerShortcuts:addFolderFromInput(friendly_name, folder)
|
||||
for __, item in ipairs(G_reader_settings:readSetting("folder_shortcuts") or {}) do
|
||||
if item.text == friendly_name and item.folder == folder then
|
||||
UIManager:show(InfoMessage:new{
|
||||
text = _("A shortcut to this folder already exists."),
|
||||
})
|
||||
return
|
||||
end
|
||||
end
|
||||
local folder_shortcuts = G_reader_settings:readSetting("folder_shortcuts") or {}
|
||||
table.insert(folder_shortcuts, {
|
||||
text = friendly_name,
|
||||
folder = folder,
|
||||
})
|
||||
G_reader_settings:saveSetting("folder_shortcuts", folder_shortcuts)
|
||||
self:updateItemTable()
|
||||
end
|
||||
|
||||
function FileManagerShortcuts:genAddRemoveShortcutButton(folder, pre_callback, post_callback)
|
||||
if self:hasFolderShortcut(folder) then
|
||||
return {
|
||||
text = _("Remove from folder shortcuts"),
|
||||
callback = function()
|
||||
pre_callback()
|
||||
self:removeShortcut(folder)
|
||||
post_callback()
|
||||
end,
|
||||
}
|
||||
else
|
||||
return {
|
||||
text = _("Add to folder shortcuts"),
|
||||
callback = function()
|
||||
pre_callback()
|
||||
self:editShortcut(folder, post_callback)
|
||||
end,
|
||||
function FileManagerShortcuts:onMenuHold(item)
|
||||
if item.deletable or item.editable then
|
||||
local folder_shortcuts_dialog
|
||||
folder_shortcuts_dialog = ButtonDialog:new{
|
||||
buttons = {
|
||||
{
|
||||
{
|
||||
text = _("Paste file"),
|
||||
enabled = (self._manager.ui.file_chooser and self._manager.ui.clipboard) and true or false,
|
||||
callback = function()
|
||||
UIManager:close(folder_shortcuts_dialog)
|
||||
self._manager.ui:pasteHere(item.folder)
|
||||
end
|
||||
},
|
||||
{
|
||||
text = _("Edit"),
|
||||
enabled = item.editable,
|
||||
callback = function()
|
||||
UIManager:close(folder_shortcuts_dialog)
|
||||
self._manager:editFolderShortcut(item)
|
||||
end
|
||||
},
|
||||
{
|
||||
text = _("Delete"),
|
||||
enabled = item.deletable,
|
||||
callback = function()
|
||||
UIManager:close(folder_shortcuts_dialog)
|
||||
self._manager:deleteFolderShortcut(item)
|
||||
end
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
UIManager:show(folder_shortcuts_dialog)
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
function FileManagerShortcuts:onSetDimensions(dimen)
|
||||
self.dimen = dimen
|
||||
function FileManagerShortcuts:editFolderShortcut(item)
|
||||
local edit_folder_input
|
||||
edit_folder_input = InputDialog:new {
|
||||
title = _("Edit friendly name"),
|
||||
input = item.friendly_name,
|
||||
input_type = "text",
|
||||
description = T(_("Rename title for selected folder:\n%1"), BD.dirpath(item.folder)),
|
||||
buttons = {
|
||||
{
|
||||
{
|
||||
text = _("Cancel"),
|
||||
callback = function()
|
||||
UIManager:close(edit_folder_input)
|
||||
end,
|
||||
},
|
||||
{
|
||||
text = _("Apply"),
|
||||
is_enter_default = true,
|
||||
callback = function()
|
||||
self:renameFolderShortcut(item, edit_folder_input:getInputText())
|
||||
UIManager:close(edit_folder_input)
|
||||
end,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
UIManager:show(edit_folder_input)
|
||||
edit_folder_input:onShowKeyboard()
|
||||
end
|
||||
|
||||
function FileManagerShortcuts:MenuSetRotationModeHandler(rotation)
|
||||
if rotation ~= nil and rotation ~= Screen:getRotationMode() then
|
||||
UIManager:close(self._manager.shortcuts_menu)
|
||||
if self._manager.ui.view and self._manager.ui.view.onSetRotationMode then
|
||||
self._manager.ui.view:onSetRotationMode(rotation)
|
||||
elseif self._manager.ui.onSetRotationMode then
|
||||
self._manager.ui:onSetRotationMode(rotation)
|
||||
else
|
||||
Screen:setRotationMode(rotation)
|
||||
function FileManagerShortcuts:renameFolderShortcut(item, new_name)
|
||||
local folder_shortcuts = {}
|
||||
for _, element in ipairs(G_reader_settings:readSetting("folder_shortcuts") or {}) do
|
||||
if element.text == item.friendly_name and element.folder == item.folder then
|
||||
element.text = new_name
|
||||
end
|
||||
self._manager:onShowFolderShortcutsDialog()
|
||||
table.insert(folder_shortcuts, element)
|
||||
end
|
||||
return true
|
||||
G_reader_settings:saveSetting("folder_shortcuts", folder_shortcuts)
|
||||
self:updateItemTable()
|
||||
end
|
||||
|
||||
function FileManagerShortcuts:onShowFolderShortcutsDialog(select_callback)
|
||||
self.shortcuts_menu = Menu:new{
|
||||
title = self.title,
|
||||
covers_fullscreen = true,
|
||||
is_borderless = true,
|
||||
function FileManagerShortcuts:deleteFolderShortcut(item)
|
||||
local folder_shortcuts = {}
|
||||
for _, element in ipairs(G_reader_settings:readSetting("folder_shortcuts") or {}) do
|
||||
if element.text ~= item.friendly_name or element.folder ~= item.folder then
|
||||
table.insert(folder_shortcuts, element)
|
||||
end
|
||||
end
|
||||
G_reader_settings:saveSetting("folder_shortcuts", folder_shortcuts)
|
||||
self:updateItemTable()
|
||||
end
|
||||
|
||||
function FileManagerShortcuts:onSetDimensions(dimen)
|
||||
self.dimen = dimen
|
||||
end
|
||||
|
||||
function FileManagerShortcuts:onShowFolderShortcutsDialog()
|
||||
self.fm_bookmark = Menu:new{
|
||||
title = _("Folder shortcuts"),
|
||||
show_parent = self.ui,
|
||||
width = Screen:getWidth(),
|
||||
height = Screen:getHeight(),
|
||||
no_title = false,
|
||||
parent = nil,
|
||||
has_close_button = true,
|
||||
is_popout = false,
|
||||
select_callback = select_callback, -- called from PathChooser titlebar left button
|
||||
title_bar_left_icon = not select_callback and "plus" or nil,
|
||||
onLeftButtonTap = function() self:addShortcut() end,
|
||||
onMenuChoice = self.onMenuChoice,
|
||||
onMenuHold = not select_callback and self.onMenuHold or nil,
|
||||
onSetRotationMode = self.MenuSetRotationModeHandler,
|
||||
is_borderless = true,
|
||||
curr_path = self.ui.file_chooser and self.ui.file_chooser.path or self.ui:getLastDirFile(),
|
||||
onMenuHold = self.onMenuHold,
|
||||
_manager = self,
|
||||
}
|
||||
self.shortcuts_menu.close_callback = function()
|
||||
UIManager:close(self.shortcuts_menu)
|
||||
if self.fm_updated then
|
||||
if self.ui.file_chooser then
|
||||
self.ui.file_chooser:refreshPath()
|
||||
self.ui:updateTitleBarPath()
|
||||
end
|
||||
self.fm_updated = nil
|
||||
end
|
||||
self.shortcuts_menu = nil
|
||||
end
|
||||
self:updateItemTable()
|
||||
UIManager:show(self.shortcuts_menu)
|
||||
UIManager:show(self.fm_bookmark)
|
||||
end
|
||||
|
||||
return FileManagerShortcuts
|
||||
|
@ -1,423 +0,0 @@
|
||||
local WidgetContainer = require("ui/widget/container/widgetcontainer")
|
||||
local logger = require("logger")
|
||||
local _ = require("gettext")
|
||||
local T = require("ffi/util").template
|
||||
|
||||
local ReaderAnnotation = WidgetContainer:extend{
|
||||
annotations = nil, -- array sorted by annotation position order, ascending
|
||||
}
|
||||
|
||||
-- build, read, save
|
||||
|
||||
function ReaderAnnotation:buildAnnotation(bm, highlights, init)
|
||||
-- bm: associated single bookmark ; highlights: tables with all highlights
|
||||
local note = bm.text
|
||||
if note == "" then
|
||||
note = nil
|
||||
end
|
||||
local chapter = bm.chapter
|
||||
local hl, pageno = self:getHighlightByDatetime(highlights, bm.datetime)
|
||||
if init then
|
||||
if note and self.ui.bookmark:isBookmarkAutoText(bm) then
|
||||
note = nil
|
||||
end
|
||||
if chapter == nil then
|
||||
chapter = self.ui.toc:getTocTitleByPage(bm.page)
|
||||
end
|
||||
pageno = self.ui.paging and bm.page or self.document:getPageFromXPointer(bm.page)
|
||||
end
|
||||
if self.ui.paging and bm.pos0 and not bm.pos0.page then
|
||||
-- old single-page reflow highlights do not have page in position
|
||||
bm.pos0.page = bm.page
|
||||
bm.pos1.page = bm.page
|
||||
end
|
||||
if not hl then -- page bookmark or orphaned bookmark
|
||||
hl = {}
|
||||
if bm.highlighted then -- orphaned bookmark
|
||||
hl.drawer = self.view.highlight.saved_drawer
|
||||
hl.color = self.view.highlight.saved_color
|
||||
if self.ui.paging then
|
||||
if bm.pos0.page == bm.pos1.page then
|
||||
hl.pboxes = self.document:getPageBoxesFromPositions(bm.page, bm.pos0, bm.pos1)
|
||||
else -- multi-page highlight, restore the first box only
|
||||
hl.pboxes = self.document:getPageBoxesFromPositions(bm.page, bm.pos0, bm.pos0)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return { -- annotation
|
||||
datetime = bm.datetime, -- creation time, not changeable
|
||||
drawer = hl.drawer, -- highlight drawer
|
||||
color = hl.color, -- highlight color
|
||||
text = bm.notes, -- highlighted text, editable
|
||||
text_edited = hl.edited, -- true if highlighted text has been edited
|
||||
note = note, -- user's note, editable
|
||||
chapter = chapter, -- book chapter title
|
||||
pageno = pageno, -- book page number
|
||||
page = bm.page, -- highlight location, xPointer or number (pdf)
|
||||
pos0 = bm.pos0, -- highlight start position, xPointer (== page) or table (pdf)
|
||||
pos1 = bm.pos1, -- highlight end position, xPointer or table (pdf)
|
||||
pboxes = hl.pboxes, -- pdf pboxes, used only and changeable by addMarkupAnnotation
|
||||
ext = hl.ext, -- pdf multi-page highlight
|
||||
}
|
||||
end
|
||||
|
||||
function ReaderAnnotation:getHighlightByDatetime(highlights, datetime)
|
||||
for pageno, page_highlights in pairs(highlights) do
|
||||
for _, highlight in ipairs(page_highlights) do
|
||||
if highlight.datetime == datetime then
|
||||
return highlight, pageno
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ReaderAnnotation:getAnnotationsFromBookmarksHighlights(bookmarks, highlights, init)
|
||||
local annotations = {}
|
||||
for i = #bookmarks, 1, -1 do
|
||||
table.insert(annotations, self:buildAnnotation(bookmarks[i], highlights, init))
|
||||
end
|
||||
if init then
|
||||
self:sortItems(annotations)
|
||||
end
|
||||
return annotations
|
||||
end
|
||||
|
||||
function ReaderAnnotation:onReadSettings(config)
|
||||
local annotations = config:readSetting("annotations")
|
||||
if annotations then
|
||||
-- KOHighlights may set this key when it has merged annotations from different sources:
|
||||
-- we want to make sure they are updated and sorted
|
||||
local needs_update = config:isTrue("annotations_externally_modified")
|
||||
local needs_sort -- if incompatible annotations were built of old highlights/bookmarks
|
||||
-- Annotation formats in crengine and mupdf are incompatible.
|
||||
local has_annotations = #annotations > 0
|
||||
local annotations_type = has_annotations and type(annotations[1].page)
|
||||
if self.ui.rolling and annotations_type ~= "string" then -- incompatible format loaded, or empty
|
||||
if has_annotations then -- backup incompatible format if not empty
|
||||
config:saveSetting("annotations_paging", annotations)
|
||||
end
|
||||
-- load compatible format
|
||||
annotations = config:readSetting("annotations_rolling") or {}
|
||||
config:delSetting("annotations_rolling")
|
||||
needs_sort = true
|
||||
elseif self.ui.paging and annotations_type ~= "number" then
|
||||
if has_annotations then
|
||||
config:saveSetting("annotations_rolling", annotations)
|
||||
end
|
||||
annotations = config:readSetting("annotations_paging") or {}
|
||||
config:delSetting("annotations_paging")
|
||||
needs_sort = true
|
||||
end
|
||||
self.annotations = annotations
|
||||
if needs_update or needs_sort then
|
||||
if self.ui.rolling then
|
||||
self.ui:registerPostInitCallback(function()
|
||||
self:updatedAnnotations(needs_update, needs_sort)
|
||||
end)
|
||||
else
|
||||
self:updatedAnnotations(needs_update, needs_sort)
|
||||
end
|
||||
config:delSetting("annotations_externally_modified")
|
||||
end
|
||||
else -- first run
|
||||
if self.ui.rolling then
|
||||
self.ui:registerPostInitCallback(function()
|
||||
self:migrateToAnnotations(config)
|
||||
end)
|
||||
else
|
||||
self:migrateToAnnotations(config)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ReaderAnnotation:migrateToAnnotations(config)
|
||||
local bookmarks = config:readSetting("bookmarks") or {}
|
||||
local highlights = config:readSetting("highlight") or {}
|
||||
|
||||
if config:hasNot("highlights_imported") then
|
||||
-- before 2014, saved highlights were not added to bookmarks when they were created.
|
||||
for page, hls in pairs(highlights) do
|
||||
for _, hl in ipairs(hls) do
|
||||
local hl_page = self.ui.paging and page or hl.pos0
|
||||
-- highlights saved by some old versions don't have pos0 field
|
||||
-- we just ignore those highlights
|
||||
if hl_page then
|
||||
local item = {
|
||||
datetime = hl.datetime,
|
||||
highlighted = true,
|
||||
notes = hl.text,
|
||||
page = hl_page,
|
||||
pos0 = hl.pos0,
|
||||
pos1 = hl.pos1,
|
||||
}
|
||||
if self.ui.paging then
|
||||
item.pos0.page = page
|
||||
item.pos1.page = page
|
||||
end
|
||||
table.insert(bookmarks, item)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Bookmarks/highlights formats in crengine and mupdf are incompatible.
|
||||
local has_bookmarks = #bookmarks > 0
|
||||
local bookmarks_type = has_bookmarks and type(bookmarks[1].page)
|
||||
if self.ui.rolling then
|
||||
if bookmarks_type == "string" then -- compatible format loaded, check for incompatible old backup
|
||||
if config:has("bookmarks_paging") then -- save incompatible old backup
|
||||
local bookmarks_paging = config:readSetting("bookmarks_paging")
|
||||
local highlights_paging = config:readSetting("highlight_paging")
|
||||
local annotations = self:getAnnotationsFromBookmarksHighlights(bookmarks_paging, highlights_paging)
|
||||
config:saveSetting("annotations_paging", annotations)
|
||||
config:delSetting("bookmarks_paging")
|
||||
config:delSetting("highlight_paging")
|
||||
end
|
||||
else -- incompatible format loaded, or empty
|
||||
if has_bookmarks then -- save incompatible format if not empty
|
||||
local annotations = self:getAnnotationsFromBookmarksHighlights(bookmarks, highlights)
|
||||
config:saveSetting("annotations_paging", annotations)
|
||||
end
|
||||
-- load compatible format
|
||||
bookmarks = config:readSetting("bookmarks_rolling") or {}
|
||||
highlights = config:readSetting("highlight_rolling") or {}
|
||||
config:delSetting("bookmarks_rolling")
|
||||
config:delSetting("highlight_rolling")
|
||||
end
|
||||
else -- self.ui.paging
|
||||
if bookmarks_type == "number" then
|
||||
if config:has("bookmarks_rolling") then
|
||||
local bookmarks_rolling = config:readSetting("bookmarks_rolling")
|
||||
local highlights_rolling = config:readSetting("highlight_rolling")
|
||||
local annotations = self:getAnnotationsFromBookmarksHighlights(bookmarks_rolling, highlights_rolling)
|
||||
config:saveSetting("annotations_rolling", annotations)
|
||||
config:delSetting("bookmarks_rolling")
|
||||
config:delSetting("highlight_rolling")
|
||||
end
|
||||
else
|
||||
if has_bookmarks then
|
||||
local annotations = self:getAnnotationsFromBookmarksHighlights(bookmarks, highlights)
|
||||
config:saveSetting("annotations_rolling", annotations)
|
||||
end
|
||||
bookmarks = config:readSetting("bookmarks_paging") or {}
|
||||
highlights = config:readSetting("highlight_paging") or {}
|
||||
config:delSetting("bookmarks_paging")
|
||||
config:delSetting("highlight_paging")
|
||||
end
|
||||
end
|
||||
|
||||
self.annotations = self:getAnnotationsFromBookmarksHighlights(bookmarks, highlights, true)
|
||||
end
|
||||
|
||||
function ReaderAnnotation:onDocumentRerendered()
|
||||
self.needs_update = true
|
||||
end
|
||||
|
||||
function ReaderAnnotation:onCloseDocument()
|
||||
self:updatePageNumbers()
|
||||
end
|
||||
|
||||
function ReaderAnnotation:onSaveSettings()
|
||||
self:updatePageNumbers()
|
||||
self.ui.doc_settings:saveSetting("annotations", self.annotations)
|
||||
end
|
||||
|
||||
-- items handling
|
||||
|
||||
function ReaderAnnotation:updatePageNumbers()
|
||||
if self.needs_update and self.ui.rolling then -- triggered by ReaderRolling on document layout change
|
||||
for _, item in ipairs(self.annotations) do
|
||||
item.pageno = self.document:getPageFromXPointer(item.page)
|
||||
end
|
||||
end
|
||||
self.needs_update = nil
|
||||
end
|
||||
|
||||
function ReaderAnnotation:sortItems(items)
|
||||
if #items > 1 then
|
||||
local sort_func = self.ui.rolling and function(a, b) return self:isItemInPositionOrderRolling(a, b) end
|
||||
or function(a, b) return self:isItemInPositionOrderPaging(a, b) end
|
||||
table.sort(items, sort_func)
|
||||
end
|
||||
end
|
||||
|
||||
function ReaderAnnotation:updatedAnnotations(needs_update, needs_sort)
|
||||
if needs_update then
|
||||
self.needs_update = true
|
||||
self:updatePageNumbers()
|
||||
needs_sort = true
|
||||
end
|
||||
if needs_sort then
|
||||
self:sortItems(self.annotations)
|
||||
end
|
||||
end
|
||||
|
||||
function ReaderAnnotation:updateItemByXPointer(item)
|
||||
-- called by ReaderRolling:checkXPointersAndProposeDOMVersionUpgrade()
|
||||
local chapter = self.ui.toc:getTocTitleByPage(item.page)
|
||||
if chapter == "" then
|
||||
chapter = nil
|
||||
end
|
||||
if not item.drawer then -- page bookmark
|
||||
item.text = chapter and T(_("in %1"), chapter) or nil
|
||||
end
|
||||
item.chapter = chapter
|
||||
item.pageno = self.document:getPageFromXPointer(item.page)
|
||||
end
|
||||
|
||||
function ReaderAnnotation:isItemInPositionOrderRolling(a, b)
|
||||
local a_page = self.document:getPageFromXPointer(a.page)
|
||||
local b_page = self.document:getPageFromXPointer(b.page)
|
||||
if a_page == b_page then -- both items in the same page
|
||||
if a.drawer and b.drawer then -- both items are highlights, compare positions
|
||||
local compare_xp = self.document:compareXPointers(a.page, b.page)
|
||||
if compare_xp then
|
||||
if compare_xp == 0 then -- both highlights with the same start, compare ends
|
||||
compare_xp = self.document:compareXPointers(a.pos1, b.pos1)
|
||||
if compare_xp then
|
||||
return compare_xp > 0
|
||||
end
|
||||
logger.warn("Invalid xpointer in highlight:", a.pos1, b.pos1)
|
||||
return true
|
||||
end
|
||||
return compare_xp > 0
|
||||
end
|
||||
-- if compare_xp is nil, some xpointer is invalid and "a" will be sorted first to page 1
|
||||
logger.warn("Invalid xpointer in highlight:", a.page, b.page)
|
||||
return true
|
||||
end
|
||||
return not a.drawer -- have page bookmarks before highlights
|
||||
end
|
||||
return a_page < b_page
|
||||
end
|
||||
|
||||
function ReaderAnnotation:isItemInPositionOrderPaging(a, b)
|
||||
if a.page == b.page then -- both items in the same page
|
||||
if a.drawer and b.drawer then -- both items are highlights, compare positions
|
||||
local is_reflow = self.document.configurable.text_wrap -- save reflow mode
|
||||
self.document.configurable.text_wrap = 0 -- native positions
|
||||
-- sort start and end positions of each highlight
|
||||
local a_start, a_end, b_start, b_end, result
|
||||
if self.document:comparePositions(a.pos0, a.pos1) > 0 then
|
||||
a_start, a_end = a.pos0, a.pos1
|
||||
else
|
||||
a_start, a_end = a.pos1, a.pos0
|
||||
end
|
||||
if self.document:comparePositions(b.pos0, b.pos1) > 0 then
|
||||
b_start, b_end = b.pos0, b.pos1
|
||||
else
|
||||
b_start, b_end = b.pos1, b.pos0
|
||||
end
|
||||
-- compare start positions
|
||||
local compare_pos = self.document:comparePositions(a_start, b_start)
|
||||
if compare_pos == 0 then -- both highlights with the same start, compare ends
|
||||
result = self.document:comparePositions(a_end, b_end) > 0
|
||||
else
|
||||
result = compare_pos > 0
|
||||
end
|
||||
self.document.configurable.text_wrap = is_reflow -- restore reflow mode
|
||||
return result
|
||||
end
|
||||
return not a.drawer -- have page bookmarks before highlights
|
||||
end
|
||||
return a.page < b.page
|
||||
end
|
||||
|
||||
function ReaderAnnotation:getItemIndex(item, no_binary)
|
||||
local doesMatch
|
||||
if item.datetime then
|
||||
doesMatch = function(a, b)
|
||||
return a.datetime == b.datetime
|
||||
end
|
||||
else
|
||||
if self.ui.rolling then
|
||||
doesMatch = function(a, b)
|
||||
if a.text ~= b.text or a.pos0 ~= b.pos0 or a.pos1 ~= b.pos1 then
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
else
|
||||
doesMatch = function(a, b)
|
||||
if a.text ~= b.text or a.pos0.page ~= b.pos0.page
|
||||
or a.pos0.x ~= b.pos0.x or a.pos1.x ~= b.pos1.x
|
||||
or a.pos0.y ~= b.pos0.y or a.pos1.y ~= b.pos1.y then
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not no_binary then
|
||||
local isInOrder = self.ui.rolling and self.isItemInPositionOrderRolling or self.isItemInPositionOrderPaging
|
||||
local _start, _end, _middle = 1, #self.annotations
|
||||
while _start <= _end do
|
||||
_middle = bit.rshift(_start + _end, 1)
|
||||
local v = self.annotations[_middle]
|
||||
if doesMatch(item, v) then
|
||||
return _middle
|
||||
elseif isInOrder(self, item, v) then
|
||||
_end = _middle - 1
|
||||
else
|
||||
_start = _middle + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for i, v in ipairs(self.annotations) do
|
||||
if doesMatch(item, v) then
|
||||
return i
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ReaderAnnotation:getInsertionIndex(item)
|
||||
local isInOrder = self.ui.rolling and self.isItemInPositionOrderRolling or self.isItemInPositionOrderPaging
|
||||
local _start, _end, _middle, direction = 1, #self.annotations, 1, 0
|
||||
while _start <= _end do
|
||||
_middle = bit.rshift(_start + _end, 1)
|
||||
if isInOrder(self, item, self.annotations[_middle]) then
|
||||
_end, direction = _middle - 1, 0
|
||||
else
|
||||
_start, direction = _middle + 1, 1
|
||||
end
|
||||
end
|
||||
return _middle + direction
|
||||
end
|
||||
|
||||
function ReaderAnnotation:addItem(item)
|
||||
item.datetime = os.date("%Y-%m-%d %H:%M:%S")
|
||||
item.pageno = self.ui.paging and item.page or self.document:getPageFromXPointer(item.page)
|
||||
local index = self:getInsertionIndex(item)
|
||||
table.insert(self.annotations, index, item)
|
||||
return index
|
||||
end
|
||||
|
||||
-- info
|
||||
|
||||
function ReaderAnnotation:hasAnnotations()
|
||||
return #self.annotations > 0
|
||||
end
|
||||
|
||||
function ReaderAnnotation:getNumberOfAnnotations()
|
||||
return #self.annotations
|
||||
end
|
||||
|
||||
function ReaderAnnotation:getNumberOfHighlightsAndNotes() -- for Statistics plugin
|
||||
local highlights = 0
|
||||
local notes = 0
|
||||
for _, item in ipairs(self.annotations) do
|
||||
if item.drawer then
|
||||
if item.note then
|
||||
notes = notes + 1
|
||||
else
|
||||
highlights = highlights + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
return highlights, notes
|
||||
end
|
||||
|
||||
return ReaderAnnotation
|
@ -1,189 +1,89 @@
|
||||
local ConfirmBox = require("ui/widget/confirmbox")
|
||||
local Device = require("device")
|
||||
local Event = require("ui/event")
|
||||
local EventListener = require("ui/widget/eventlistener")
|
||||
local Notification = require("ui/widget/notification")
|
||||
local UIManager = require("ui/uimanager")
|
||||
local logger = require("logger")
|
||||
local util = require("util")
|
||||
local _ = require("gettext")
|
||||
|
||||
-- This module handles the "Back" key (and the "Back" gesture action).
|
||||
-- When global setting "back_in_reader" == "previous_read_page", it
|
||||
-- additionally handles a location stack for each visited page or
|
||||
-- page view change (when scrolling in a same page)
|
||||
|
||||
local ReaderBack = EventListener:extend{
|
||||
location_stack = nil, -- array
|
||||
local ReaderBack = EventListener:new{
|
||||
location_stack = {},
|
||||
-- a limit not intended to be a practical limit but just a failsafe
|
||||
max_stack = 5000,
|
||||
-- allow for disabling back history, and having Back key
|
||||
-- quit immediately (useful for some developers)
|
||||
disabled = G_reader_settings:isFalse("enable_back_history"),
|
||||
}
|
||||
|
||||
function ReaderBack:init()
|
||||
self:registerKeyEvents()
|
||||
-- Regular function wrapping our method, to avoid re-creating
|
||||
-- an anonymous function at each page turn
|
||||
self._addPreviousLocationToStackCallback = function()
|
||||
self:_addPreviousLocationToStack()
|
||||
end
|
||||
end
|
||||
|
||||
function ReaderBack:registerKeyEvents()
|
||||
if Device:hasKeys() then
|
||||
self.ui.key_events.Back = { { Device.input.group.Back } }
|
||||
self.ui.key_events.Back = { {"Back"}, doc = "Reader back" }
|
||||
end
|
||||
end
|
||||
|
||||
ReaderBack.onPhysicalKeyboardConnected = ReaderBack.registerKeyEvents
|
||||
|
||||
function ReaderBack:_getCurrentLocation()
|
||||
local current_location
|
||||
|
||||
if self.ui.document.info.has_pages then
|
||||
return self.ui.paging:getBookLocation()
|
||||
current_location = self.ui.paging:getBookLocation()
|
||||
else
|
||||
return {
|
||||
current_location = {
|
||||
xpointer = self.ui.rolling:getBookLocation(),
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
function ReaderBack:_areLocationsSimilar(location1, location2)
|
||||
if self.ui.document.info.has_pages then
|
||||
-- locations are arrays of k/v tables
|
||||
if #location1 ~= #location2 then
|
||||
return false
|
||||
end
|
||||
for i=1, #location1 do
|
||||
if not util.tableEquals(location1[i], location2[i]) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
else
|
||||
return location1.xpointer == location2.xpointer
|
||||
end
|
||||
return current_location
|
||||
end
|
||||
|
||||
function ReaderBack:_addPreviousLocationToStack()
|
||||
local ignore_location
|
||||
|
||||
function ReaderBack:addCurrentLocationToStack()
|
||||
local location_stack = self.location_stack
|
||||
local new_location = self:_getCurrentLocation()
|
||||
|
||||
if self.cur_location and new_location then
|
||||
if self:_areLocationsSimilar(self.cur_location, new_location) then
|
||||
-- Unchanged, don't add it yet
|
||||
return
|
||||
end
|
||||
table.insert(self.location_stack, self.cur_location)
|
||||
if #self.location_stack > self.max_stack then
|
||||
table.remove(self.location_stack, 1)
|
||||
end
|
||||
end
|
||||
if util.tableEquals(ignore_location, new_location) then return end
|
||||
|
||||
table.insert(location_stack, new_location)
|
||||
|
||||
if new_location then
|
||||
self.cur_location = new_location
|
||||
if #location_stack > self.max_stack then
|
||||
table.remove(location_stack, 1)
|
||||
end
|
||||
end
|
||||
|
||||
-- Scroll mode crengine
|
||||
function ReaderBack:onPosUpdate()
|
||||
if self.disabled then return end
|
||||
self:addCurrentLocationToStack()
|
||||
end
|
||||
|
||||
-- Paged media
|
||||
function ReaderBack:onPageUpdate()
|
||||
if self.disabled then return end
|
||||
self:addCurrentLocationToStack()
|
||||
end
|
||||
|
||||
-- Called when loading new document
|
||||
function ReaderBack:onReadSettings(config)
|
||||
self.location_stack = {}
|
||||
self.cur_location = nil
|
||||
end
|
||||
|
||||
function ReaderBack:_onViewPossiblyUpdated()
|
||||
if G_reader_settings:readSetting("back_in_reader") == "previous_read_page" then
|
||||
-- As multiple modules will have their :onPageUpdate()/... called,
|
||||
-- and some of them will set up the new page with it, we need to
|
||||
-- delay our handling after all of them are called (otherwise,
|
||||
-- depending on the order of the calls, we may be have the location
|
||||
-- of either the previous page or the current one).
|
||||
UIManager:nextTick(self._addPreviousLocationToStackCallback)
|
||||
end
|
||||
self.back_resist = nil
|
||||
end
|
||||
function ReaderBack:onBack()
|
||||
local location_stack = self.location_stack
|
||||
|
||||
-- Hook to events that do/may change page/view (more than one of these events
|
||||
-- may be sent on a single page turn/scroll, _addPreviousLocationToStack()
|
||||
-- will ignore those for the same book location):
|
||||
-- Called after initial page is set up
|
||||
ReaderBack.onReaderReady = ReaderBack._onViewPossiblyUpdated
|
||||
-- New page on paged media or crengine in page mode
|
||||
ReaderBack.onPageUpdate = ReaderBack._onViewPossiblyUpdated
|
||||
-- New page on crengine in scroll mode
|
||||
ReaderBack.onPosUpdate = ReaderBack._onViewPossiblyUpdated
|
||||
-- View updated (possibly on the same page) on paged media
|
||||
ReaderBack.onViewRecalculate = ReaderBack._onViewPossiblyUpdated
|
||||
-- View updated (possibly on the same page) on paged media (needed in Reflow mode)
|
||||
ReaderBack.onPagePositionUpdated = ReaderBack._onViewPossiblyUpdated
|
||||
if self.disabled then
|
||||
self.ui:handleEvent(Event:new("Close"))
|
||||
elseif #location_stack > 1 then
|
||||
local saved_location = table.remove(location_stack)
|
||||
|
||||
function ReaderBack:onBack()
|
||||
local back_in_reader = G_reader_settings:readSetting("back_in_reader", "previous_location")
|
||||
local back_to_exit = G_reader_settings:readSetting("back_to_exit", "prompt")
|
||||
|
||||
if back_in_reader == "previous_read_page" then
|
||||
if #self.location_stack > 0 then
|
||||
local saved_location = table.remove(self.location_stack)
|
||||
if saved_location then
|
||||
-- Reset self.cur_location, which will be updated with the restored
|
||||
-- saved_location, which will then not be added to the stack
|
||||
self.cur_location = nil
|
||||
logger.dbg("[ReaderBack] restoring:", saved_location)
|
||||
self.ui:handleEvent(Event:new('RestoreBookLocation', saved_location))
|
||||
-- Ensure we always have self.cur_location updated, as in some
|
||||
-- cases (same page), no event that we handle might be sent.
|
||||
UIManager:nextTick(self._addPreviousLocationToStackCallback)
|
||||
return true
|
||||
end
|
||||
elseif not self.back_resist or back_to_exit == "disable" then
|
||||
-- Show a one time notification when location stack is empty.
|
||||
-- On next "Back" only, proceed with the default behaviour (unless
|
||||
-- it's disabled, in which case we always show this notification)
|
||||
self.back_resist = true
|
||||
UIManager:show(Notification:new{
|
||||
text = _("Location history is empty."),
|
||||
})
|
||||
if saved_location then
|
||||
ignore_location = self:_getCurrentLocation()
|
||||
logger.dbg("[ReaderBack] restoring:", saved_location)
|
||||
self.ui:handleEvent(Event:new('RestoreBookLocation', saved_location))
|
||||
return true
|
||||
else
|
||||
self.back_resist = nil
|
||||
end
|
||||
elseif back_in_reader == "previous_location" then
|
||||
-- ReaderLink maintains its own location_stack of less frequent jumps
|
||||
-- (links or TOC entries followed, skim document...)
|
||||
if back_to_exit == "disable" then
|
||||
-- Let ReaderLink always show its notification if empty
|
||||
self.ui.link:onGoBackLink(true) -- show_notification_if_empty=true
|
||||
return true
|
||||
end
|
||||
if self.back_resist then
|
||||
-- Notification "Location history is empty" previously shown by ReaderLink
|
||||
self.back_resist = nil
|
||||
elseif self.ui.link:onGoBackLink(true) then -- show_notification_if_empty=true
|
||||
return true -- some location restored
|
||||
else
|
||||
-- ReaderLink has shown its notification that location stack is empty.
|
||||
-- On next "Back" only, proceed with the default behaviour
|
||||
self.back_resist = true
|
||||
return true
|
||||
end
|
||||
elseif back_in_reader == "filebrowser" then
|
||||
else
|
||||
logger.dbg("[ReaderBack] no location history, closing")
|
||||
self.ui:handleEvent(Event:new("Home"))
|
||||
-- Filebrowser will handle next "Back" and ensure back_to_exit
|
||||
return true
|
||||
end
|
||||
|
||||
-- location stack empty, or back_in_reader == "default"
|
||||
if back_to_exit == "always" then
|
||||
self.ui:handleEvent(Event:new("Close"))
|
||||
elseif back_to_exit == "disable" then
|
||||
return true
|
||||
elseif back_to_exit == "prompt" then
|
||||
UIManager:show(ConfirmBox:new{
|
||||
text = _("Exit KOReader?"),
|
||||
ok_text = _("Exit"),
|
||||
ok_callback = function()
|
||||
self.ui:handleEvent(Event:new("Close"))
|
||||
end
|
||||
})
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
return ReaderBack
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,462 +1,27 @@
|
||||
local Event = require("ui/event")
|
||||
local Device = require("device")
|
||||
local EventListener = require("ui/widget/eventlistener")
|
||||
local Geom = require("ui/geometry")
|
||||
local InfoMessage = require("ui/widget/infomessage")
|
||||
local UIManager = require("ui/uimanager")
|
||||
local logger = require("logger")
|
||||
local T = require("ffi/util").template
|
||||
local _ = require("gettext")
|
||||
|
||||
local ReaderCoptListener = EventListener:extend{}
|
||||
|
||||
local CRE_HEADER_DEFAULT_SIZE = 20
|
||||
local ReaderCoptListener = EventListener:new{}
|
||||
|
||||
function ReaderCoptListener:onReadSettings(config)
|
||||
local view_mode_name = self.document.configurable.view_mode == 0 and "page" or "scroll"
|
||||
-- Let crengine know of the view mode before rendering, as it can
|
||||
-- cause a rendering change (2-pages would become 1-page in
|
||||
-- scroll mode).
|
||||
self.document:setViewMode(view_mode_name)
|
||||
-- ReaderView is the holder of the view_mode state
|
||||
self.view.view_mode = view_mode_name
|
||||
|
||||
-- crengine top status bar can only show author and title together
|
||||
self.title = G_reader_settings:readSetting("cre_header_title", 1)
|
||||
self.clock = G_reader_settings:readSetting("cre_header_clock", 1)
|
||||
self.header_auto_refresh = G_reader_settings:readSetting("cre_header_auto_refresh", 1)
|
||||
self.page_number = G_reader_settings:readSetting("cre_header_page_number", 1)
|
||||
self.page_count = G_reader_settings:readSetting("cre_header_page_count", 1)
|
||||
self.reading_percent = G_reader_settings:readSetting("cre_header_reading_percent", 0)
|
||||
self.battery = G_reader_settings:readSetting("cre_header_battery", 1)
|
||||
self.battery_percent = G_reader_settings:readSetting("cre_header_battery_percent", 0)
|
||||
self.chapter_marks = G_reader_settings:readSetting("cre_header_chapter_marks", 1)
|
||||
|
||||
self.document._document:setIntProperty("window.status.title", self.title)
|
||||
self.document._document:setIntProperty("window.status.clock", self.clock)
|
||||
self.document._document:setIntProperty("window.status.pos.page.number", self.page_number)
|
||||
self.document._document:setIntProperty("window.status.pos.page.count", self.page_count)
|
||||
self.document._document:setIntProperty("crengine.page.header.chapter.marks", self.chapter_marks)
|
||||
self.document._document:setIntProperty("window.status.battery", self.battery)
|
||||
self.document._document:setIntProperty("window.status.battery.percent", self.battery_percent)
|
||||
self.document._document:setIntProperty("window.status.pos.percent", self.reading_percent)
|
||||
|
||||
-- We will build the top status bar page info string ourselves,
|
||||
-- if we have to display any chunk of it
|
||||
self.page_info_override = self.page_number == 1 or self.page_count == 1 or self.reading_percent == 1
|
||||
self.document:setPageInfoOverride("") -- an empty string lets crengine display its own page info
|
||||
|
||||
self:onTimeFormatChanged()
|
||||
|
||||
-- Enable or disable crengine header status line (note that for crengine, 0=header enabled, 1=header disabled)
|
||||
self.ui:handleEvent(Event:new("SetStatusLine", self.document.configurable.status_line))
|
||||
|
||||
self.old_battery_level = self.ui.rolling:updateBatteryState()
|
||||
|
||||
-- Have this ready in case auto-refresh is enabled, now or later
|
||||
self.headerRefresh = function()
|
||||
-- Only draw it if the header is shown...
|
||||
if self.document.configurable.status_line == 0 and self.view.view_mode == "page" then
|
||||
-- ...and something has changed
|
||||
local new_battery_level = self.ui.rolling:updateBatteryState()
|
||||
if self.clock == 1 or (self.battery == 1 and new_battery_level ~= self.old_battery_level) then
|
||||
self.old_battery_level = new_battery_level
|
||||
self:updateHeader()
|
||||
end
|
||||
end
|
||||
self:rescheduleHeaderRefreshIfNeeded() -- schedule (or not) next refresh
|
||||
local view_mode = config:readSetting("copt_view_mode") or
|
||||
G_reader_settings:readSetting("copt_view_mode")
|
||||
if view_mode == 0 then
|
||||
self.ui:registerPostReadyCallback(function()
|
||||
self.view:onSetViewMode("page")
|
||||
end)
|
||||
elseif view_mode == 1 then
|
||||
self.ui:registerPostReadyCallback(function()
|
||||
self.view:onSetViewMode("scroll")
|
||||
end)
|
||||
end
|
||||
self:rescheduleHeaderRefreshIfNeeded() -- schedule (or not) first refresh
|
||||
end
|
||||
|
||||
function ReaderCoptListener:onReaderReady()
|
||||
-- custom metadata support for alt status bar and cre synthetic cover
|
||||
for prop_key in pairs(self.document.prop_to_cre_prop) do
|
||||
local orig_prop_value = self.ui.doc_settings:readSetting(prop_key)
|
||||
local custom_prop_key = prop_key == "title" and "display_title" or prop_key
|
||||
local custom_prop_value = self.ui.doc_props[custom_prop_key]
|
||||
if custom_prop_value ~= orig_prop_value then
|
||||
self.document:setAltDocumentProp(prop_key, custom_prop_value)
|
||||
end
|
||||
end
|
||||
local status_line = config:readSetting("copt_status_line") or G_reader_settings:readSetting("copt_status_line") or 1
|
||||
self.ui:handleEvent(Event:new("SetStatusLine", status_line, true))
|
||||
end
|
||||
|
||||
function ReaderCoptListener:updatePageInfoOverride(pageno)
|
||||
if not (self.document.configurable.status_line == 0 and self.view.view_mode == "page" and self.page_info_override) then
|
||||
return
|
||||
end
|
||||
-- There are a few cases where we may not be updated on change, at least:
|
||||
-- - when toggling ReaderPageMap's "Use reference page numbers"
|
||||
-- - when changing footer's nb of digits after decimal point
|
||||
-- but we will update on next page turn. Let's not bother.
|
||||
|
||||
local page_pre = ""
|
||||
local page_number = pageno
|
||||
local page_sep = " / "
|
||||
local page_count = self.ui.document:getPageCount()
|
||||
local page_post = ""
|
||||
local percentage = page_number / page_count
|
||||
local percentage_pre = ""
|
||||
local percentage_post = ""
|
||||
-- Let's use the same setting for nb of digits after decimal point as configured for the footer
|
||||
local percentage_digits = self.ui.view.footer.settings.progress_pct_format
|
||||
local percentage_fmt = "%." .. percentage_digits .. "f%%"
|
||||
|
||||
-- We want the same output as with ReaderFooter's page_progress() and percentage()
|
||||
-- but here each item (page number, page counte, percentage) is individually toggable,
|
||||
-- so try to get something that make sense when not all are enabled
|
||||
if self.ui.pagemap and self.ui.pagemap:wantsPageLabels() then
|
||||
-- These become strings here
|
||||
page_number = self.ui.pagemap:getCurrentPageLabel(true)
|
||||
page_count = self.ui.pagemap:getLastPageLabel(true)
|
||||
elseif self.ui.document:hasHiddenFlows() then
|
||||
local flow = self.ui.document:getPageFlow(pageno)
|
||||
page_number = tostring(self.ui.document:getPageNumberInFlow(pageno))
|
||||
page_count = tostring(self.ui.document:getTotalPagesInFlow(flow))
|
||||
percentage = page_number / page_count
|
||||
if flow == 0 then
|
||||
page_sep = " // "
|
||||
else
|
||||
page_pre = "["
|
||||
page_post = "]"..tostring(flow)
|
||||
percentage_pre = "["
|
||||
percentage_post = "]"
|
||||
end
|
||||
end
|
||||
|
||||
local page_info = ""
|
||||
if self.page_number or self.page_count then
|
||||
page_info = page_info .. page_pre
|
||||
if self.page_number then
|
||||
page_info = page_info .. page_number
|
||||
if self.page_count then
|
||||
page_info = page_info .. page_sep
|
||||
end
|
||||
end
|
||||
if self.page_count then
|
||||
page_info = page_info .. page_count
|
||||
end
|
||||
page_info = page_info .. page_post
|
||||
if self.reading_percent then
|
||||
page_info = page_info .. " " -- (double space as done by crengine's own drawing)
|
||||
end
|
||||
end
|
||||
if self.reading_percent then
|
||||
page_info = page_info .. percentage_pre .. percentage_fmt:format(percentage*100) .. percentage_post
|
||||
end
|
||||
self.document:setPageInfoOverride(page_info)
|
||||
end
|
||||
|
||||
function ReaderCoptListener:onPageUpdate(pageno)
|
||||
self:updatePageInfoOverride(pageno)
|
||||
end
|
||||
|
||||
function ReaderCoptListener:onPosUpdate(pos, pageno)
|
||||
self:updatePageInfoOverride(pageno)
|
||||
end
|
||||
|
||||
function ReaderCoptListener:onBookMetadataChanged(prop_updated)
|
||||
-- custom metadata support for alt status bar and cre synthetic cover
|
||||
local prop_key = prop_updated and prop_updated.metadata_key_updated
|
||||
if prop_key and self.document.prop_to_cre_prop[prop_key] then
|
||||
self.document:setAltDocumentProp(prop_key, prop_updated.doc_props[prop_key])
|
||||
self:updateHeader()
|
||||
end
|
||||
end
|
||||
|
||||
function ReaderCoptListener:onConfigChange(option_name, option_value)
|
||||
-- font_size and line_spacing are historically and sadly shared by both mupdf and cre reader modules,
|
||||
-- but fortunately they can be distinguished by their different ranges
|
||||
if (option_name == "font_size" or option_name == "line_spacing") and option_value < 5 then return end
|
||||
self.document.configurable[option_name] = option_value
|
||||
self.ui:handleEvent(Event:new("StartActivityIndicator"))
|
||||
return true
|
||||
end
|
||||
|
||||
function ReaderCoptListener:onCharging()
|
||||
self:headerRefresh()
|
||||
end
|
||||
ReaderCoptListener.onNotCharging = ReaderCoptListener.onCharging
|
||||
|
||||
function ReaderCoptListener:onTimeFormatChanged()
|
||||
self.document._document:setIntProperty("window.status.clock.12hours", G_reader_settings:isTrue("twelve_hour_clock") and 1 or 0)
|
||||
end
|
||||
|
||||
function ReaderCoptListener:shouldHeaderBeRepainted()
|
||||
local top_wg = UIManager:getTopmostVisibleWidget() or {}
|
||||
if top_wg.name == "ReaderUI" then
|
||||
-- We're on display, go ahead
|
||||
return true
|
||||
elseif top_wg.covers_fullscreen or top_wg.covers_header then
|
||||
-- We're hidden behind something that definitely covers us, don't do anything
|
||||
return false
|
||||
else
|
||||
-- There's something on top of us, but we might still be visible, request a ReaderUI repaint,
|
||||
-- UIManager will sort it out.
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
function ReaderCoptListener:updateHeader()
|
||||
-- Have crengine display accurate time and battery on its next drawing
|
||||
self.document:resetBufferCache() -- be sure next repaint is a redrawing
|
||||
-- Force a refresh if we're not hidden behind another widget
|
||||
if self:shouldHeaderBeRepainted() then
|
||||
UIManager:setDirty(self.view.dialog, "ui",
|
||||
Geom:new{
|
||||
x = 0, y = 0,
|
||||
w = Device.screen:getWidth(),
|
||||
h = self.document:getHeaderHeight(),
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
function ReaderCoptListener:unscheduleHeaderRefresh()
|
||||
if not self.headerRefresh then return end -- not yet set up
|
||||
UIManager:unschedule(self.headerRefresh)
|
||||
logger.dbg("ReaderCoptListener.headerRefresh unscheduled")
|
||||
end
|
||||
|
||||
function ReaderCoptListener:rescheduleHeaderRefreshIfNeeded()
|
||||
if not self.headerRefresh then return end -- not yet set up
|
||||
local unscheduled = UIManager:unschedule(self.headerRefresh) -- unschedule if already scheduled
|
||||
-- Only schedule an update if the header is actually visible
|
||||
if self.header_auto_refresh == 1
|
||||
and self.document.configurable.status_line == 0 -- top bar enabled
|
||||
and self.view.view_mode == "page" -- not in scroll mode (which would disable the header)
|
||||
and (self.clock == 1 or self.battery == 1) then -- something shown can change in next minute
|
||||
UIManager:scheduleIn(61 - tonumber(os.date("%S")), self.headerRefresh)
|
||||
if not unscheduled then
|
||||
logger.dbg("ReaderCoptListener.headerRefresh scheduled")
|
||||
else
|
||||
logger.dbg("ReaderCoptListener.headerRefresh rescheduled")
|
||||
end
|
||||
elseif unscheduled then
|
||||
logger.dbg("ReaderCoptListener.headerRefresh unscheduled")
|
||||
end
|
||||
end
|
||||
|
||||
-- Schedule or stop scheduling on these events, as they may change what is shown:
|
||||
ReaderCoptListener.onSetStatusLine = ReaderCoptListener.rescheduleHeaderRefreshIfNeeded
|
||||
-- configurable.status_line is set before this event is triggered
|
||||
ReaderCoptListener.onSetViewMode = ReaderCoptListener.rescheduleHeaderRefreshIfNeeded
|
||||
-- ReaderView:onSetViewMode(), which sets view.view_mode, is called before
|
||||
-- ReaderCoptListener.onSetViewMode, so we'll get the updated value
|
||||
function ReaderCoptListener:onResume()
|
||||
-- Don't repaint the header until OutOfScreenSaver if screensaver_delay is enabled...
|
||||
local screensaver_delay = G_reader_settings:readSetting("screensaver_delay")
|
||||
if screensaver_delay and screensaver_delay ~= "disable" then
|
||||
self._delayed_screensaver = true
|
||||
return
|
||||
end
|
||||
|
||||
self:headerRefresh()
|
||||
end
|
||||
|
||||
function ReaderCoptListener:onOutOfScreenSaver()
|
||||
if not self._delayed_screensaver then
|
||||
return
|
||||
end
|
||||
|
||||
self._delayed_screensaver = nil
|
||||
self:headerRefresh()
|
||||
end
|
||||
|
||||
-- Unschedule on these events
|
||||
ReaderCoptListener.onCloseDocument = ReaderCoptListener.unscheduleHeaderRefresh
|
||||
ReaderCoptListener.onSuspend = ReaderCoptListener.unscheduleHeaderRefresh
|
||||
|
||||
function ReaderCoptListener:setAndSave(setting, property, value)
|
||||
self.document._document:setIntProperty(property, value)
|
||||
G_reader_settings:saveSetting(setting, value)
|
||||
self.page_info_override = self.page_number == 1 or self.page_count == 1 or self.reading_percent == 1
|
||||
self.document:setPageInfoOverride("")
|
||||
-- Have crengine redraw it (even if hidden by the menu at this time)
|
||||
self.ui.rolling:updateBatteryState()
|
||||
self:updateHeader()
|
||||
-- And see if we should auto-refresh
|
||||
self:rescheduleHeaderRefreshIfNeeded()
|
||||
end
|
||||
|
||||
local about_text = _([[
|
||||
In CRE documents, an alternative status bar can be displayed at the top of the screen, with or without the regular bottom status bar.
|
||||
|
||||
Enabling this alt status bar, per document or by default, can be done in the bottom menu.
|
||||
|
||||
The alternative status bar can be configured here.]])
|
||||
|
||||
function ReaderCoptListener:getAltStatusBarMenu()
|
||||
return {
|
||||
text = _("Alt status bar"),
|
||||
separator = true,
|
||||
sub_item_table = {
|
||||
{
|
||||
text = _("About alt status bar"),
|
||||
keep_menu_open = true,
|
||||
callback = function()
|
||||
UIManager:show(InfoMessage:new{
|
||||
text = about_text,
|
||||
})
|
||||
end,
|
||||
separator = true,
|
||||
},
|
||||
{
|
||||
text = _("Auto refresh"),
|
||||
checked_func = function()
|
||||
return self.header_auto_refresh == 1
|
||||
end,
|
||||
callback = function()
|
||||
self.header_auto_refresh = self.header_auto_refresh == 0 and 1 or 0
|
||||
G_reader_settings:saveSetting("cre_header_auto_refresh", self.header_auto_refresh)
|
||||
self:rescheduleHeaderRefreshIfNeeded()
|
||||
end,
|
||||
separator = true
|
||||
},
|
||||
{
|
||||
text = _("Book author and title"),
|
||||
checked_func = function()
|
||||
return self.title == 1
|
||||
end,
|
||||
callback = function()
|
||||
self.title = self.title == 0 and 1 or 0
|
||||
self:setAndSave("cre_header_title", "window.status.title", self.title)
|
||||
end,
|
||||
},
|
||||
{
|
||||
text = _("Current time"),
|
||||
checked_func = function()
|
||||
return self.clock == 1
|
||||
end,
|
||||
callback = function()
|
||||
self.clock = self.clock == 0 and 1 or 0
|
||||
self:setAndSave("cre_header_clock", "window.status.clock", self.clock)
|
||||
end,
|
||||
},
|
||||
{
|
||||
text = _("Current page"),
|
||||
checked_func = function()
|
||||
return self.page_number == 1
|
||||
end,
|
||||
callback = function()
|
||||
self.page_number = self.page_number == 0 and 1 or 0
|
||||
self:setAndSave("cre_header_page_number", "window.status.pos.page.number", self.page_number)
|
||||
end,
|
||||
},
|
||||
{
|
||||
text = _("Total pages"),
|
||||
checked_func = function()
|
||||
return self.page_count == 1
|
||||
end,
|
||||
callback = function()
|
||||
self.page_count = self.page_count == 0 and 1 or 0
|
||||
self:setAndSave("cre_header_page_count", "window.status.pos.page.count", self.page_count)
|
||||
end,
|
||||
},
|
||||
{
|
||||
text = _("Progress percentage"),
|
||||
checked_func = function()
|
||||
return self.reading_percent == 1
|
||||
end,
|
||||
callback = function()
|
||||
self.reading_percent = self.reading_percent == 0 and 1 or 0
|
||||
self:setAndSave("cre_header_reading_percent", "window.status.pos.percent", self.reading_percent)
|
||||
end,
|
||||
},
|
||||
{
|
||||
text = _("Chapter marks"),
|
||||
checked_func = function()
|
||||
return self.chapter_marks == 1
|
||||
end,
|
||||
callback = function()
|
||||
self.chapter_marks = self.chapter_marks == 0 and 1 or 0
|
||||
self:setAndSave("cre_header_chapter_marks", "crengine.page.header.chapter.marks", self.chapter_marks)
|
||||
end,
|
||||
},
|
||||
{
|
||||
text_func = function()
|
||||
local status = _("Battery status")
|
||||
if self.battery == 1 then
|
||||
if self.battery_percent == 1 then
|
||||
status = _("Battery status: percentage")
|
||||
else
|
||||
status = _("Battery status: icon")
|
||||
end
|
||||
end
|
||||
return status
|
||||
end,
|
||||
checked_func = function()
|
||||
return self.battery == 1
|
||||
end,
|
||||
sub_item_table = {
|
||||
{
|
||||
text = _("Battery icon"),
|
||||
checked_func = function()
|
||||
return self.battery == 1 and self.battery_percent == 0
|
||||
end,
|
||||
callback = function()
|
||||
if self.battery == 0 then -- self.battery_percent don't care
|
||||
self.battery = 1
|
||||
self.battery_percent = 0
|
||||
elseif self.battery == 1 and self.battery_percent == 1 then
|
||||
self.battery = 1
|
||||
self.battery_percent = 0
|
||||
else
|
||||
self.battery = 0
|
||||
self.battery_percent = 0
|
||||
end
|
||||
self:setAndSave("cre_header_battery", "window.status.battery", self.battery)
|
||||
self:setAndSave("cre_header_battery_percent", "window.status.battery.percent", self.battery_percent)
|
||||
end,
|
||||
},
|
||||
{
|
||||
text = _("Battery percentage"),
|
||||
checked_func = function()
|
||||
return self.battery == 1 and self.battery_percent == 1
|
||||
end,
|
||||
callback = function()
|
||||
if self.battery == 0 then -- self.battery_percent don't care
|
||||
self.battery = 1
|
||||
self.battery_percent = 1
|
||||
elseif self.battery == 1 and self.battery_percent == 0 then
|
||||
self.battery_percent = 1
|
||||
else
|
||||
self.battery = 0
|
||||
self.battery_percent = 0
|
||||
end
|
||||
self:setAndSave("cre_header_battery", "window.status.battery", self.battery)
|
||||
self:setAndSave("cre_header_battery_percent", "window.status.battery.percent", self.battery_percent)
|
||||
end,
|
||||
},
|
||||
},
|
||||
separator = true,
|
||||
},
|
||||
{
|
||||
text_func = function()
|
||||
return T(_("Font size: %1"), G_reader_settings:readSetting("cre_header_status_font_size", CRE_HEADER_DEFAULT_SIZE))
|
||||
end,
|
||||
callback = function()
|
||||
local SpinWidget = require("ui/widget/spinwidget")
|
||||
local start_size = G_reader_settings:readSetting("cre_header_status_font_size", CRE_HEADER_DEFAULT_SIZE)
|
||||
local size_spinner = SpinWidget:new{
|
||||
value = start_size,
|
||||
value_min = 8,
|
||||
value_max = 36,
|
||||
default_value = 14,
|
||||
keep_shown_on_apply = true,
|
||||
title_text = _("Size of top status bar"),
|
||||
ok_text = _("Set size"),
|
||||
callback = function(spin)
|
||||
self:setAndSave("cre_header_status_font_size", "crengine.page.header.font.size", spin.value)
|
||||
-- This will probably needs a re-rendering, so make sure it happens now.
|
||||
self.ui:handleEvent(Event:new("UpdatePos"))
|
||||
end
|
||||
}
|
||||
UIManager:show(size_spinner)
|
||||
end,
|
||||
},
|
||||
},
|
||||
}
|
||||
function ReaderCoptListener:onSetFontSize(font_size)
|
||||
self.document.configurable.font_size = font_size
|
||||
end
|
||||
|
||||
return ReaderCoptListener
|
||||
|
@ -1,351 +1,125 @@
|
||||
local ConfirmBox = require("ui/widget/confirmbox")
|
||||
local ButtonDialogTitle = require("ui/widget/buttondialogtitle")
|
||||
local Device = require("device")
|
||||
local Event = require("ui/event")
|
||||
local InfoMessage = require("ui/widget/infomessage")
|
||||
local SpinWidget = require("ui/widget/spinwidget")
|
||||
local Font = require("ui/font")
|
||||
local InputContainer = require("ui/widget/container/inputcontainer")
|
||||
local Screen = Device.screen
|
||||
local UIManager = require("ui/uimanager")
|
||||
local WidgetContainer = require("ui/widget/container/widgetcontainer")
|
||||
local powerd = Device:getPowerDevice()
|
||||
local _ = require("gettext")
|
||||
local C_ = _.pgettext
|
||||
local T = require("ffi/util").template
|
||||
|
||||
local ReaderDeviceStatus = WidgetContainer:extend{
|
||||
battery_confirm_box = nil,
|
||||
memory_confirm_box = nil,
|
||||
local ReaderDeviceStatus = InputContainer:new{
|
||||
}
|
||||
|
||||
function ReaderDeviceStatus:init()
|
||||
if Device:hasBattery() then
|
||||
self.battery_interval_m = G_reader_settings:readSetting("device_status_battery_interval_minutes", 10)
|
||||
self.battery_threshold = G_reader_settings:readSetting("device_status_battery_threshold", 20)
|
||||
self.battery_threshold_high = G_reader_settings:readSetting("device_status_battery_threshold_high", 100)
|
||||
-- `checkLowBatteryLevel` and `checkHighMemoryUsage` are each supposed to start one second past the top of the minute,
|
||||
-- as some other periodic activities do (e.g. footer). This means that the processor is woken up less often from standby.
|
||||
self.checkLowBatteryLevel = function(sync)
|
||||
local is_charging = powerd:isCharging()
|
||||
if powerd:getCapacity() > 0 or powerd:isCharging() then
|
||||
self.checkLowBattery = function()
|
||||
local threshold = G_reader_settings:readSetting("low_battery_threshold") or 20
|
||||
local battery_capacity = powerd:getCapacity()
|
||||
if powerd:getDismissBatteryStatus() == true then -- alerts dismissed
|
||||
if (is_charging and battery_capacity <= self.battery_threshold_high) or
|
||||
(not is_charging and battery_capacity > self.battery_threshold) then
|
||||
powerd:setDismissBatteryStatus(false)
|
||||
end
|
||||
else
|
||||
if (is_charging and battery_capacity > self.battery_threshold_high) or
|
||||
(not is_charging and battery_capacity <= self.battery_threshold) then
|
||||
if self.battery_confirm_box then
|
||||
UIManager:close(self.battery_confirm_box)
|
||||
end
|
||||
self.battery_confirm_box = ConfirmBox:new {
|
||||
text = is_charging and T(_("High battery level: %1 %\n\nDismiss battery level alert?"), battery_capacity)
|
||||
or T(_("Low battery level: %1 %\n\nDismiss battery level alert?"), battery_capacity),
|
||||
ok_text = _("Dismiss"),
|
||||
dismissable = false,
|
||||
ok_callback = function()
|
||||
powerd:setDismissBatteryStatus(true)
|
||||
end,
|
||||
if powerd:isCharging() then
|
||||
powerd:setDissmisBatteryStatus(false)
|
||||
elseif powerd:getDissmisBatteryStatus() ~= true and battery_capacity <= threshold then
|
||||
local low_battery_info
|
||||
low_battery_info = ButtonDialogTitle:new {
|
||||
modal = true,
|
||||
title = T(_("The battery is getting low.\n%1% remaining."), battery_capacity),
|
||||
title_align = "center",
|
||||
title_face = Font:getFace("infofont"),
|
||||
dismissable = false,
|
||||
buttons = {
|
||||
{
|
||||
{
|
||||
text = _("Dismiss"),
|
||||
callback = function()
|
||||
UIManager:close(low_battery_info)
|
||||
powerd:setDissmisBatteryStatus(true)
|
||||
UIManager:scheduleIn(300, self.checkLowBattery)
|
||||
end,
|
||||
},
|
||||
},
|
||||
}
|
||||
UIManager:show(self.battery_confirm_box)
|
||||
end
|
||||
}
|
||||
UIManager:show(low_battery_info)
|
||||
return
|
||||
elseif powerd:getDissmisBatteryStatus() and battery_capacity > threshold then
|
||||
powerd:setDissmisBatteryStatus(false)
|
||||
end
|
||||
local offset = sync and (os.date("%S") - 1) or 0
|
||||
UIManager:scheduleIn(self.battery_interval_m * 60 - offset, self.checkLowBatteryLevel)
|
||||
UIManager:scheduleIn(300, self.checkLowBattery)
|
||||
end
|
||||
self.ui.menu:registerToMainMenu(self)
|
||||
self:startBatteryChecker()
|
||||
else
|
||||
self.checkLowBattery = nil
|
||||
end
|
||||
|
||||
if not Device:isAndroid() then
|
||||
self.memory_interval_m = G_reader_settings:readSetting("device_status_memory_interval_minutes", 5)
|
||||
self.memory_threshold = G_reader_settings:readSetting("device_status_memory_threshold", 100)
|
||||
-- `checkLowBatteryLevel` and `checkHighMemoryUsage` are each supposed to start one second past the top of the minute,
|
||||
-- as some other periodic activities do (e.g. footer). This means that the processor is woken up less often from standby.
|
||||
self.checkHighMemoryUsage = function(sync)
|
||||
local statm = io.open("/proc/self/statm", "r")
|
||||
if statm then
|
||||
local dummy, rss = statm:read("*number", "*number")
|
||||
statm:close()
|
||||
rss = math.floor(rss * (4096 / 1024 / 1024))
|
||||
if rss >= self.memory_threshold then
|
||||
if self.memory_confirm_box then
|
||||
UIManager:close(self.memory_confirm_box)
|
||||
end
|
||||
if Device:canRestart() then
|
||||
local top_wg = UIManager:getTopmostVisibleWidget() or {}
|
||||
if top_wg.name == "ReaderUI"
|
||||
and G_reader_settings:isTrue("device_status_memory_auto_restart") then
|
||||
UIManager:show(InfoMessage:new{
|
||||
text = _("High memory usage!\n\nKOReader is restarting…"),
|
||||
icon = "notice-warning",
|
||||
})
|
||||
UIManager:nextTick(function()
|
||||
self.ui:handleEvent(Event:new("Restart"))
|
||||
end)
|
||||
else
|
||||
self.memory_confirm_box = ConfirmBox:new {
|
||||
text = T(_("High memory usage: %1 MB\n\nRestart KOReader?"), rss),
|
||||
ok_text = _("Restart"),
|
||||
dismissable = false,
|
||||
ok_callback = function()
|
||||
UIManager:show(InfoMessage:new{
|
||||
text = _("High memory usage!\n\nKOReader is restarting…"),
|
||||
icon = "notice-warning",
|
||||
})
|
||||
UIManager:nextTick(function()
|
||||
self.ui:handleEvent(Event:new("Restart"))
|
||||
end)
|
||||
end,
|
||||
}
|
||||
UIManager:show(self.memory_confirm_box)
|
||||
end
|
||||
else
|
||||
self.memory_confirm_box = ConfirmBox:new {
|
||||
text = T(_("High memory usage: %1 MB\n\nExit KOReader?"), rss),
|
||||
ok_text = _("Exit"),
|
||||
dismissable = false,
|
||||
ok_callback = function()
|
||||
self.ui:handleEvent(Event:new("Exit"))
|
||||
end,
|
||||
}
|
||||
UIManager:show(self.memory_confirm_box)
|
||||
end
|
||||
end
|
||||
end
|
||||
local offset = sync and (os.date("%S") - 1) or 0
|
||||
UIManager:scheduleIn(self.memory_interval_m * 60 - offset, self.checkHighMemoryUsage)
|
||||
end
|
||||
self:startMemoryChecker()
|
||||
end
|
||||
|
||||
self.ui.menu:registerToMainMenu(self)
|
||||
end
|
||||
end
|
||||
|
||||
function ReaderDeviceStatus:addToMainMenu(menu_items)
|
||||
menu_items.device_status_alarm = {
|
||||
text = _("Device status alerts"),
|
||||
sub_item_table = {},
|
||||
}
|
||||
if Device:hasBattery() then
|
||||
table.insert(menu_items.device_status_alarm.sub_item_table,
|
||||
menu_items.battery = {
|
||||
text = _("Low battery alarm"),
|
||||
sub_item_table = {
|
||||
{
|
||||
text = _("Battery level"),
|
||||
text = _("Enable"),
|
||||
checked_func = function()
|
||||
return G_reader_settings:isTrue("device_status_battery_alarm")
|
||||
return G_reader_settings:nilOrTrue("battery_alarm")
|
||||
end,
|
||||
callback = function()
|
||||
G_reader_settings:flipNilOrFalse("device_status_battery_alarm")
|
||||
if G_reader_settings:isTrue("device_status_battery_alarm") then
|
||||
self:startBatteryChecker(true)
|
||||
G_reader_settings:flipNilOrTrue("battery_alarm")
|
||||
if G_reader_settings:nilOrTrue("battery_alarm") then
|
||||
self:startBatteryChecker()
|
||||
else
|
||||
self:stopBatteryChecker()
|
||||
powerd:setDismissBatteryStatus(false)
|
||||
powerd:setDissmisBatteryStatus(false)
|
||||
end
|
||||
end,
|
||||
})
|
||||
table.insert(menu_items.device_status_alarm.sub_item_table,
|
||||
},
|
||||
{
|
||||
text_func = function()
|
||||
return T(_("Check interval: %1 min"), self.battery_interval_m)
|
||||
end,
|
||||
enabled_func = function()
|
||||
return G_reader_settings:isTrue("device_status_battery_alarm")
|
||||
end,
|
||||
keep_menu_open = true,
|
||||
callback = function(touchmenu_instance)
|
||||
UIManager:show(SpinWidget:new{
|
||||
value = self.battery_interval_m,
|
||||
value_min = 1,
|
||||
value_max = 60,
|
||||
default_value = 10,
|
||||
unit = C_("Time", "min"),
|
||||
value_hold_step = 5,
|
||||
title_text = _("Battery check interval"),
|
||||
callback = function(spin)
|
||||
self.battery_interval_m = spin.value
|
||||
G_reader_settings:saveSetting("device_status_battery_interval_minutes", self.battery_interval_m)
|
||||
touchmenu_instance:updateItems()
|
||||
powerd:setDismissBatteryStatus(false)
|
||||
self:stopBatteryChecker()
|
||||
-- schedule first check on a full minute to reduce wakeups from standby)
|
||||
UIManager:scheduleIn(self.battery_interval_m * 60 - os.date("%S") + 1,
|
||||
self.checkLowBatteryLevel)
|
||||
end,
|
||||
})
|
||||
end,
|
||||
})
|
||||
table.insert(menu_items.device_status_alarm.sub_item_table,
|
||||
{
|
||||
text_func = function()
|
||||
return T(_("Thresholds: %1 % / %2 %"), self.battery_threshold, self.battery_threshold_high)
|
||||
end,
|
||||
enabled_func = function()
|
||||
return G_reader_settings:isTrue("device_status_battery_alarm")
|
||||
end,
|
||||
keep_menu_open = true,
|
||||
callback = function(touchmenu_instance)
|
||||
local DoubleSpinWidget = require("/ui/widget/doublespinwidget")
|
||||
local thresholds_widget
|
||||
thresholds_widget = DoubleSpinWidget:new{
|
||||
title_text = _("Battery level alert thresholds"),
|
||||
info_text = _([[
|
||||
Low level threshold is checked when the device is not charging.
|
||||
High level threshold is checked when the device is charging.]]),
|
||||
left_text = _("Low"),
|
||||
left_value = self.battery_threshold,
|
||||
left_min = 1,
|
||||
left_max = self.battery_threshold_high,
|
||||
left_default = 20,
|
||||
left_hold_step = 5,
|
||||
right_text = _("High"),
|
||||
right_value = self.battery_threshold_high,
|
||||
right_min = self.battery_threshold,
|
||||
right_max = 100,
|
||||
right_default = 100,
|
||||
right_hold_step = 5,
|
||||
unit = "%",
|
||||
callback = function(left_value, right_value)
|
||||
self.battery_threshold = left_value
|
||||
self.battery_threshold_high = right_value
|
||||
G_reader_settings:saveSetting("device_status_battery_threshold", self.battery_threshold)
|
||||
G_reader_settings:saveSetting("device_status_battery_threshold_high", self.battery_threshold_high)
|
||||
touchmenu_instance:updateItems()
|
||||
powerd:setDismissBatteryStatus(false)
|
||||
end,
|
||||
}
|
||||
UIManager:show(thresholds_widget)
|
||||
end,
|
||||
separator = true,
|
||||
})
|
||||
end
|
||||
if not Device:isAndroid() then
|
||||
table.insert(menu_items.device_status_alarm.sub_item_table,
|
||||
{
|
||||
text = _("High memory usage"),
|
||||
checked_func = function()
|
||||
return G_reader_settings:isTrue("device_status_memory_alarm")
|
||||
end,
|
||||
text = _("Low battery threshold"),
|
||||
enabled_func = function() return G_reader_settings:nilOrTrue("battery_alarm") end,
|
||||
callback = function()
|
||||
G_reader_settings:flipNilOrFalse("device_status_memory_alarm")
|
||||
if G_reader_settings:isTrue("device_status_memory_alarm") then
|
||||
self:startMemoryChecker(true)
|
||||
else
|
||||
self:stopMemoryChecker()
|
||||
end
|
||||
end,
|
||||
})
|
||||
table.insert(menu_items.device_status_alarm.sub_item_table,
|
||||
{
|
||||
text_func = function()
|
||||
return T(_("Check interval: %1 min"), self.memory_interval_m)
|
||||
end,
|
||||
enabled_func = function()
|
||||
return G_reader_settings:isTrue("device_status_memory_alarm")
|
||||
end,
|
||||
keep_menu_open = true,
|
||||
callback = function(touchmenu_instance)
|
||||
UIManager:show(SpinWidget:new{
|
||||
value = self.memory_interval_m,
|
||||
value_min = 1,
|
||||
value_max = 60,
|
||||
default_value = 5,
|
||||
unit = C_("Time", "min"),
|
||||
value_hold_step = 5,
|
||||
title_text = _("Memory check interval"),
|
||||
callback = function(spin)
|
||||
self.memory_interval_m = spin.value
|
||||
G_reader_settings:saveSetting("device_status_memory_interval_minutes", self.memory_interval_m)
|
||||
touchmenu_instance:updateItems()
|
||||
self:stopMemoryChecker()
|
||||
-- schedule first check on a full minute to reduce wakeups from standby)
|
||||
UIManager:scheduleIn(self.memory_interval_m * 60 - os.date("%S") + 1,
|
||||
self.checkHighMemoryUsage)
|
||||
end,
|
||||
})
|
||||
end,
|
||||
})
|
||||
table.insert(menu_items.device_status_alarm.sub_item_table,
|
||||
{
|
||||
text_func = function()
|
||||
return T(_("Threshold: %1 MB"), self.memory_threshold)
|
||||
end,
|
||||
enabled_func = function()
|
||||
return G_reader_settings:isTrue("device_status_memory_alarm")
|
||||
end,
|
||||
keep_menu_open = true,
|
||||
callback = function(touchmenu_instance)
|
||||
UIManager:show(SpinWidget:new{
|
||||
value = self.memory_threshold,
|
||||
value_min = 20,
|
||||
value_max = 500,
|
||||
default_value = 100,
|
||||
unit = C_("Data storage size", "MB"),
|
||||
value_step = 5,
|
||||
local SpinWidget = require("ui/widget/spinwidget")
|
||||
local curr_items = G_reader_settings:readSetting("low_battery_threshold") or 20
|
||||
local battery_spin = SpinWidget:new {
|
||||
width = math.floor(Screen:getWidth() * 0.6),
|
||||
value = curr_items,
|
||||
value_min = 5,
|
||||
value_max = 90,
|
||||
value_hold_step = 10,
|
||||
title_text = _("Memory alert threshold"),
|
||||
callback = function(spin)
|
||||
self.memory_threshold = spin.value
|
||||
G_reader_settings:saveSetting("device_status_memory_threshold", self.memory_threshold)
|
||||
touchmenu_instance:updateItems()
|
||||
end,
|
||||
})
|
||||
end,
|
||||
})
|
||||
table.insert(menu_items.device_status_alarm.sub_item_table,
|
||||
{
|
||||
text = _("Automatic restart"),
|
||||
enabled_func = function()
|
||||
return G_reader_settings:isTrue("device_status_memory_alarm") and Device:canRestart()
|
||||
end,
|
||||
checked_func = function()
|
||||
return G_reader_settings:isTrue("device_status_memory_auto_restart")
|
||||
end,
|
||||
callback = function()
|
||||
G_reader_settings:flipNilOrFalse("device_status_memory_auto_restart")
|
||||
ok_text = _("Set threshold"),
|
||||
title_text = _("Low battery threshold"),
|
||||
callback = function(battery_spin)
|
||||
G_reader_settings:saveSetting("low_battery_threshold", battery_spin.value)
|
||||
powerd:setDissmisBatteryStatus(false)
|
||||
end
|
||||
}
|
||||
UIManager:show(battery_spin)
|
||||
end,
|
||||
})
|
||||
end
|
||||
},
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
-- `checkLowBatteryLevel` and `checkHighMemoryUsage` are each supposed to start one second past the top of the minute,
|
||||
-- as some other periodic activities do (e.g. footer). This means that the processor is woken up less often from standby.
|
||||
function ReaderDeviceStatus:startBatteryChecker(sync)
|
||||
if G_reader_settings:isTrue("device_status_battery_alarm") then
|
||||
self.checkLowBatteryLevel(sync)
|
||||
function ReaderDeviceStatus:startBatteryChecker()
|
||||
if G_reader_settings:nilOrTrue("battery_alarm") and self.checkLowBattery then
|
||||
self.checkLowBattery()
|
||||
end
|
||||
end
|
||||
|
||||
function ReaderDeviceStatus:stopBatteryChecker()
|
||||
if self.checkLowBatteryLevel then
|
||||
UIManager:unschedule(self.checkLowBatteryLevel)
|
||||
end
|
||||
end
|
||||
|
||||
-- `checkLowBatteryLevel` and `checkHighMemoryUsage` are each supposed to start one second past the top of the minute,
|
||||
-- as some other periodic activities do (e.g. footer). This means that the processor is woken up less often from standby.
|
||||
function ReaderDeviceStatus:startMemoryChecker(sync)
|
||||
if G_reader_settings:isTrue("device_status_memory_alarm") then
|
||||
self.checkHighMemoryUsage(sync)
|
||||
end
|
||||
end
|
||||
|
||||
function ReaderDeviceStatus:stopMemoryChecker()
|
||||
if self.checkHighMemoryUsage then
|
||||
UIManager:unschedule(self.checkHighMemoryUsage)
|
||||
if self.checkLowBattery then
|
||||
UIManager:unschedule(self.checkLowBattery)
|
||||
end
|
||||
end
|
||||
|
||||
function ReaderDeviceStatus:onResume()
|
||||
self:startBatteryChecker(true)
|
||||
self:startMemoryChecker(true)
|
||||
self:startBatteryChecker()
|
||||
end
|
||||
|
||||
function ReaderDeviceStatus:onSuspend()
|
||||
self:stopBatteryChecker()
|
||||
self:stopMemoryChecker()
|
||||
end
|
||||
|
||||
function ReaderDeviceStatus:onCloseWidget()
|
||||
self:stopBatteryChecker()
|
||||
self:stopMemoryChecker()
|
||||
end
|
||||
|
||||
return ReaderDeviceStatus
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,719 +0,0 @@
|
||||
local ConfirmBox = require("ui/widget/confirmbox")
|
||||
local Device = require("device")
|
||||
local Event = require("ui/event")
|
||||
local InfoMessage = require("ui/widget/infomessage")
|
||||
local InputDialog = require("ui/widget/inputdialog")
|
||||
local UIManager = require("ui/uimanager")
|
||||
local WidgetContainer = require("ui/widget/container/widgetcontainer")
|
||||
local util = require("util")
|
||||
local T = require("ffi/util").template
|
||||
local _ = require("gettext")
|
||||
|
||||
local ReaderHandMade = WidgetContainer:extend{
|
||||
custom_toc_symbol = "\u{EAEC}", -- used in a few places
|
||||
}
|
||||
|
||||
function ReaderHandMade:init()
|
||||
self.ui.menu:registerToMainMenu(self)
|
||||
end
|
||||
|
||||
function ReaderHandMade:onReadSettings(config)
|
||||
self.toc_enabled = config:isTrue("handmade_toc_enabled")
|
||||
self.toc_edit_enabled = config:nilOrTrue("handmade_toc_edit_enabled")
|
||||
self.toc = config:readSetting("handmade_toc") or {}
|
||||
self.flows_enabled = config:isTrue("handmade_flows_enabled")
|
||||
self.flows_edit_enabled = config:nilOrTrue("handmade_flows_edit_enabled")
|
||||
self.flow_points = config:readSetting("handmade_flow_points") or {}
|
||||
self.inactive_flow_points = {}
|
||||
|
||||
-- Don't mess toc and flow_points made on that document if saved when
|
||||
-- we were using a different engine - backup them if that's the case.
|
||||
if #self.toc > 0 then
|
||||
local has_xpointers = self.toc[1].xpointer ~= nil
|
||||
if self.ui.rolling and not has_xpointers then
|
||||
config:saveSetting("handmade_toc_paging", self.toc)
|
||||
self.toc = config:readSetting("handmade_toc_rolling") or {}
|
||||
config:delSetting("handmade_toc_rolling")
|
||||
elseif self.ui.paging and has_xpointers then
|
||||
config:saveSetting("handmade_toc_rolling", self.toc)
|
||||
self.toc = config:readSetting("handmade_toc_paging") or {}
|
||||
config:delSetting("handmade_toc_paging")
|
||||
end
|
||||
else
|
||||
if self.ui.rolling and config:has("handmade_toc_rolling") then
|
||||
self.toc = config:readSetting("handmade_toc_rolling")
|
||||
config:delSetting("handmade_toc_rolling")
|
||||
elseif self.ui.paging and config:has("handmade_toc_paging") then
|
||||
self.toc = config:readSetting("handmade_toc_paging")
|
||||
config:delSetting("handmade_toc_paging")
|
||||
end
|
||||
end
|
||||
if #self.flow_points > 0 then
|
||||
local has_xpointers = self.flow_points[1].xpointer ~= nil
|
||||
if self.ui.rolling and not has_xpointers then
|
||||
config:saveSetting("handmade_flow_points_paging", self.flow_points)
|
||||
self.flow_points = config:readSetting("handmade_flow_points_rolling") or {}
|
||||
config:delSetting("handmade_flow_points_rolling")
|
||||
elseif self.ui.paging and has_xpointers then
|
||||
config:saveSetting("handmade_flow_points_rolling", self.flow_points)
|
||||
self.flow_points = config:readSetting("handmade_flow_points_paging") or {}
|
||||
config:delSetting("handmade_flow_points_paging")
|
||||
end
|
||||
else
|
||||
if self.ui.rolling and config:has("handmade_flow_points_rolling") then
|
||||
self.flow_points = config:readSetting("handmade_flow_points_rolling")
|
||||
config:delSetting("handmade_flow_points_rolling")
|
||||
elseif self.ui.paging and config:has("handmade_flow_points_paging") then
|
||||
self.flow_points = config:readSetting("handmade_flow_points_paging")
|
||||
config:delSetting("handmade_flow_points_paging")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ReaderHandMade:onSaveSettings()
|
||||
self.ui.doc_settings:saveSetting("handmade_toc_enabled", self.toc_enabled)
|
||||
self.ui.doc_settings:saveSetting("handmade_toc_edit_enabled", self.toc_edit_enabled)
|
||||
if #self.toc > 0 then
|
||||
self.ui.doc_settings:saveSetting("handmade_toc", self.toc)
|
||||
else
|
||||
self.ui.doc_settings:delSetting("handmade_toc")
|
||||
end
|
||||
self.ui.doc_settings:saveSetting("handmade_flows_enabled", self.flows_enabled)
|
||||
self.ui.doc_settings:saveSetting("handmade_flows_edit_enabled", self.flows_edit_enabled)
|
||||
if #self.flow_points > 0 then
|
||||
self.ui.doc_settings:saveSetting("handmade_flow_points", self.flow_points)
|
||||
else
|
||||
self.ui.doc_settings:delSetting("handmade_flow_points")
|
||||
end
|
||||
end
|
||||
|
||||
function ReaderHandMade:isHandmadeTocEnabled()
|
||||
return self.toc_enabled
|
||||
end
|
||||
|
||||
function ReaderHandMade:isHandmadeTocEditEnabled()
|
||||
return self.toc_edit_enabled
|
||||
end
|
||||
|
||||
function ReaderHandMade:isHandmadeHiddenFlowsEnabled()
|
||||
-- Even if currently empty, we return true, which allows showing '//' in
|
||||
-- the footer and let know hidden flows are enabled.
|
||||
return self.flows_enabled
|
||||
end
|
||||
|
||||
function ReaderHandMade:isHandmadeHiddenFlowsEditEnabled()
|
||||
return self.flows_edit_enabled
|
||||
end
|
||||
|
||||
function ReaderHandMade:onToggleHandmadeToc()
|
||||
self.toc_enabled = not self.toc_enabled
|
||||
self:setupToc()
|
||||
-- Have footer updated, so we may see this took effect
|
||||
self.view.footer:onUpdateFooter(self.view.footer_visible)
|
||||
end
|
||||
|
||||
function ReaderHandMade:onToggleHandmadeFlows()
|
||||
self.flows_enabled = not self.flows_enabled
|
||||
self:setupFlows()
|
||||
-- Have footer updated, so we may see this took effect
|
||||
self.view.footer:onUpdateFooter(self.view.footer_visible)
|
||||
end
|
||||
|
||||
function ReaderHandMade:addToMainMenu(menu_items)
|
||||
-- As it's currently impossible to create custom hidden flows on non-touch, and really impractical to create a custom toc, it's better hide these features completely for now.
|
||||
if not Device:isTouchDevice() then
|
||||
return
|
||||
end
|
||||
menu_items.handmade_toc = {
|
||||
text = _("Custom table of contents") .. " " .. self.custom_toc_symbol,
|
||||
checked_func = function() return self.toc_enabled end,
|
||||
callback = function()
|
||||
self:onToggleHandmadeToc()
|
||||
end,
|
||||
}
|
||||
menu_items.handmade_hidden_flows = {
|
||||
text = _("Custom hidden flows"),
|
||||
checked_func = function() return self.flows_enabled end,
|
||||
callback = function()
|
||||
self:onToggleHandmadeFlows()
|
||||
end,
|
||||
}
|
||||
--[[ Not yet implemented
|
||||
menu_items.handmade_page_numbers = {
|
||||
text = _("Custom page numbers"),
|
||||
checked_func = function() return false end,
|
||||
callback = function()
|
||||
end,
|
||||
}
|
||||
]]--
|
||||
menu_items.handmade_settings = {
|
||||
text = _("Custom layout features"),
|
||||
sub_item_table_func = function()
|
||||
return {
|
||||
{
|
||||
text = _("About custom table of contents") .. " " .. self.custom_toc_symbol,
|
||||
callback = function()
|
||||
UIManager:show(InfoMessage:new{
|
||||
text = _([[
|
||||
If the book has no table of contents or you would like to substitute it with your own, you can create a custom TOC. The original TOC (if available) will not be altered.
|
||||
|
||||
You can create, edit and remove chapters:
|
||||
- in Page browser, by long-pressing on a thumbnail;
|
||||
- on a book page, by selecting some text to be used as the chapter title.
|
||||
(Once you're done building it and don't want to see the buttons anymore, you can disable Edit mode.)
|
||||
|
||||
This custom table of contents is currently limited to a single level and can't have sub-chapters.]])
|
||||
})
|
||||
end,
|
||||
keep_menu_open = true,
|
||||
},
|
||||
{
|
||||
text = _("Edit mode"),
|
||||
enabled_func = function()
|
||||
return self:isHandmadeTocEnabled()
|
||||
end,
|
||||
checked_func = function()
|
||||
return self:isHandmadeTocEditEnabled()
|
||||
end,
|
||||
callback = function()
|
||||
self.toc_edit_enabled = not self.toc_edit_enabled
|
||||
self:updateHighlightDialog()
|
||||
end,
|
||||
},
|
||||
--[[ Not yet implemented
|
||||
{
|
||||
text = _("Add multiple chapter start page numbers"),
|
||||
},
|
||||
]]--
|
||||
{
|
||||
text = _("Clear custom table of contents"),
|
||||
enabled_func = function()
|
||||
return #self.toc > 0
|
||||
end,
|
||||
callback = function(touchmenu_instance)
|
||||
UIManager:show(ConfirmBox:new{
|
||||
text = _("Are you sure you want to clear your custom table of contents?"),
|
||||
ok_callback = function()
|
||||
self.toc = {}
|
||||
self.ui:handleEvent(Event:new("UpdateToc"))
|
||||
-- The footer may be visible, so have it update its chapter related items
|
||||
self.view.footer:onUpdateFooter(self.view.footer_visible)
|
||||
if touchmenu_instance then
|
||||
touchmenu_instance:updateItems()
|
||||
end
|
||||
end,
|
||||
})
|
||||
end,
|
||||
keep_menu_open = true,
|
||||
separator = true,
|
||||
},
|
||||
{
|
||||
text = _("About custom hidden flows"),
|
||||
callback = function()
|
||||
UIManager:show(InfoMessage:new{
|
||||
text = _([[
|
||||
Custom hidden flows can be created to exclude sections of the book from your normal reading flow:
|
||||
- hidden flows will automatically be skipped when turning pages within the regular flow;
|
||||
- pages part of hidden flows are assigned distinct page numbers and won't be considered in the various book & chapter progress and time to read features;
|
||||
- following direct links to pages in hidden flows will still work, including from the TOC or Book map.
|
||||
|
||||
This can be useful to exclude long footnotes or bibliography sections.
|
||||
It can also be handy when interested in reading only a subset of a book.
|
||||
|
||||
In Page browser, you can long-press on a thumbnail to start a hidden flow or restart the regular flow on this page.
|
||||
(Once you're done building it and don't want to see the button anymore, you can disable Edit mode.)
|
||||
|
||||
Hidden flows are shown with gray or hatched background in Book map and Page browser.]])
|
||||
})
|
||||
end,
|
||||
keep_menu_open = true,
|
||||
},
|
||||
{
|
||||
text = _("Edit mode"),
|
||||
enabled_func = function()
|
||||
return self:isHandmadeHiddenFlowsEnabled()
|
||||
end,
|
||||
checked_func = function()
|
||||
return self:isHandmadeHiddenFlowsEditEnabled()
|
||||
end,
|
||||
callback = function()
|
||||
self.flows_edit_enabled = not self.flows_edit_enabled
|
||||
end,
|
||||
},
|
||||
{
|
||||
text_func = function()
|
||||
return T(_("Clear inactive marked pages (%1)"), #self.inactive_flow_points)
|
||||
end,
|
||||
enabled_func = function()
|
||||
return #self.inactive_flow_points > 0
|
||||
end,
|
||||
callback = function(touchmenu_instance)
|
||||
UIManager:show(ConfirmBox:new{
|
||||
text = _("Inactive marked pages are pages that you tagged as start hidden flow or restart regular flow, but that other marked pages made them have no effect.\nAre you sure you want to clear them?"),
|
||||
ok_callback = function()
|
||||
for i=#self.inactive_flow_points, 1, -1 do
|
||||
table.remove(self.flow_points, self.inactive_flow_points[i])
|
||||
end
|
||||
self:updateDocFlows()
|
||||
self.ui:handleEvent(Event:new("UpdateToc"))
|
||||
self.ui:handleEvent(Event:new("InitScrollPageStates"))
|
||||
-- The footer may be visible, so have it update its dependant items
|
||||
self.view.footer:onUpdateFooter(self.view.footer_visible)
|
||||
if touchmenu_instance then
|
||||
touchmenu_instance:updateItems()
|
||||
end
|
||||
end,
|
||||
})
|
||||
end,
|
||||
keep_menu_open = true,
|
||||
},
|
||||
{
|
||||
text = _("Clear all marked pages"),
|
||||
enabled_func = function()
|
||||
return #self.flow_points > 0
|
||||
end,
|
||||
callback = function(touchmenu_instance)
|
||||
UIManager:show(ConfirmBox:new{
|
||||
text = _("Are you sure you want to clear all your custom hidden flows?"),
|
||||
ok_callback = function()
|
||||
self.flow_points = {}
|
||||
self:updateDocFlows()
|
||||
self.ui:handleEvent(Event:new("UpdateToc"))
|
||||
self.ui:handleEvent(Event:new("InitScrollPageStates"))
|
||||
-- The footer may be visible, so have it update its dependant items
|
||||
self.view.footer:onUpdateFooter(self.view.footer_visible)
|
||||
if touchmenu_instance then
|
||||
touchmenu_instance:updateItems()
|
||||
end
|
||||
end,
|
||||
})
|
||||
end,
|
||||
keep_menu_open = true,
|
||||
separator = true,
|
||||
},
|
||||
--[[ Not yet implemented
|
||||
{
|
||||
text = _("About custom page numbers"),
|
||||
},
|
||||
{
|
||||
text = _("Clear custom page numbers"),
|
||||
},
|
||||
]]--
|
||||
}
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
function ReaderHandMade:updateHandmagePages()
|
||||
if not self.ui.rolling then
|
||||
return
|
||||
end
|
||||
for _, item in ipairs(self.toc) do
|
||||
item.page = self.document:getPageFromXPointer(item.xpointer)
|
||||
end
|
||||
for _, item in ipairs(self.flow_points) do
|
||||
item.page = self.document:getPageFromXPointer(item.xpointer)
|
||||
end
|
||||
end
|
||||
|
||||
function ReaderHandMade:onReaderReady()
|
||||
-- Called on load, and with a CRE document when reloading after partial rerendering.
|
||||
-- Notes:
|
||||
-- - ReaderFooter (from ReaderView) will have its onReaderReady() called before ours,
|
||||
-- and it may fillToc(). So, it may happen that the expensive validateAndFixToc()
|
||||
-- is called twice (first with the original ToC, then with ours).
|
||||
-- - ReaderRolling will have its onReaderReady() called after ours, and if we
|
||||
-- have set up hidden flows, we'll have overriden some documents methods so
|
||||
-- its cacheFlows() is a no-op.
|
||||
self:updateHandmagePages()
|
||||
-- Don't have each of these send their own events: we'll send them once afterwards
|
||||
self:setupFlows(true)
|
||||
self:setupToc(true)
|
||||
-- Now send the events
|
||||
if self.toc_enabled or self.flows_enabled then
|
||||
self.ui:handleEvent(Event:new("UpdateToc"))
|
||||
end
|
||||
if self.flows_enabled then
|
||||
-- Needed to skip hidden flows if PDF in scroll mode
|
||||
self.ui:handleEvent(Event:new("InitScrollPageStates"))
|
||||
end
|
||||
end
|
||||
|
||||
function ReaderHandMade:onDocumentRerendered()
|
||||
-- Called with CRE document when partial rerendering not enabled
|
||||
self:updateHandmagePages()
|
||||
-- Don't have these send events their own events
|
||||
self:setupFlows(true)
|
||||
self:setupToc(true)
|
||||
-- ReaderToc will process this event just after us, and will
|
||||
-- call its onUpdateToc: we don't need to send it.
|
||||
-- (Also, no need for InitScrollPageStates with CRE.)
|
||||
end
|
||||
|
||||
function ReaderHandMade:setupToc(no_event)
|
||||
if self.toc_enabled then
|
||||
-- If enabled, plug one method into the document object,
|
||||
-- so it is used instead of the method from its class.
|
||||
self.document.getToc = function(this)
|
||||
-- ReaderToc may add fieds to ToC items: return a copy,
|
||||
-- so the one we will save doesn't get polluted.
|
||||
return util.tableDeepCopy(self.toc)
|
||||
end
|
||||
else
|
||||
-- If disabled, remove our plug so the method from the
|
||||
-- class gets used again.
|
||||
self.document.getToc = nil
|
||||
end
|
||||
self:updateHighlightDialog()
|
||||
if not no_event then
|
||||
self.ui:handleEvent(Event:new("UpdateToc"))
|
||||
end
|
||||
end
|
||||
|
||||
function ReaderHandMade:updateHighlightDialog()
|
||||
if self.toc_enabled and self.toc_edit_enabled then
|
||||
-- We don't want this button to be the last wide one, and rather
|
||||
-- keep having the Search button being that one: so plug this one
|
||||
-- just before 12_search.
|
||||
self.ui.highlight:addToHighlightDialog("12_0_make_handmade_toc_item", function(this)
|
||||
return {
|
||||
text_func = function()
|
||||
local selected_text = this.selected_text
|
||||
local pageno, xpointer
|
||||
if self.ui.rolling then
|
||||
xpointer = selected_text.pos0
|
||||
else
|
||||
pageno = selected_text.pos0.page
|
||||
end
|
||||
local text
|
||||
if self:hasPageTocItem(pageno, xpointer) then
|
||||
text = _("Edit TOC chapter")
|
||||
else
|
||||
text = _("Start TOC chapter")
|
||||
end
|
||||
text = text .. " " .. self.custom_toc_symbol
|
||||
return text
|
||||
end,
|
||||
callback = function()
|
||||
local selected_text = this.selected_text
|
||||
this:onClose()
|
||||
self:addOrEditPageTocItem(nil, nil, selected_text)
|
||||
end,
|
||||
}
|
||||
end)
|
||||
else
|
||||
self.ui.highlight:removeFromHighlightDialog("12_0_make_handmade_toc_item")
|
||||
end
|
||||
end
|
||||
|
||||
function ReaderHandMade:_getItemIndex(tab, pageno, xpointer)
|
||||
if not pageno and xpointer then
|
||||
pageno = self.document:getPageFromXPointer(xpointer)
|
||||
end
|
||||
-- (No need to use a binary search, our user made tables should
|
||||
-- not be too large)
|
||||
local matching_idx
|
||||
local insertion_idx = #tab + 1
|
||||
for i, item in ipairs(tab) do
|
||||
if item.page >= pageno then
|
||||
if item.page > pageno then
|
||||
insertion_idx = i
|
||||
break
|
||||
end
|
||||
-- Same page numbers.
|
||||
-- (We can trust page numbers, and only compare xpointers when both
|
||||
-- resolve to the same page.)
|
||||
if xpointer and item.xpointer then
|
||||
local order = self.document:compareXPointers(xpointer, item.xpointer)
|
||||
if order > 0 then -- item.xpointer after xpointer
|
||||
insertion_idx = i
|
||||
break
|
||||
elseif order == 0 then
|
||||
matching_idx = i
|
||||
break
|
||||
end
|
||||
else
|
||||
matching_idx = i
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
-- We always return an index, and a boolean stating if this index is a match or not
|
||||
-- (if not, the index is the insertion index if we ever want to insert an item with
|
||||
-- the asked pageno/xpointer)
|
||||
return matching_idx or insertion_idx, matching_idx and true or false
|
||||
end
|
||||
|
||||
function ReaderHandMade:hasPageTocItem(pageno, xpointer)
|
||||
local _, is_match = self:_getItemIndex(self.toc, pageno, xpointer)
|
||||
return is_match
|
||||
end
|
||||
|
||||
function ReaderHandMade:addOrEditPageTocItem(pageno, when_updated_callback, selected_text)
|
||||
local xpointer, title
|
||||
if selected_text then
|
||||
-- If we get selected_text, it's from the highlight dialog after text selection
|
||||
title = selected_text.text
|
||||
if self.ui.rolling then
|
||||
xpointer = selected_text.pos0
|
||||
pageno = self.document:getPageFromXPointer(xpointer)
|
||||
else
|
||||
pageno = selected_text.pos0.page
|
||||
end
|
||||
end
|
||||
local idx, item_found = self:_getItemIndex(self.toc, pageno, xpointer)
|
||||
local item
|
||||
if item_found then
|
||||
-- Chapter found: it's an update (edit text or remove item)
|
||||
item = self.toc[idx]
|
||||
else
|
||||
-- No chapter starting on this page or at this xpointer:
|
||||
-- we'll add a new item
|
||||
if not xpointer and self.ui.rolling and type(pageno) == "number" then
|
||||
xpointer = self.document:getPageXPointer(pageno)
|
||||
end
|
||||
item = {
|
||||
title = title or "",
|
||||
page = pageno,
|
||||
xpointer = xpointer,
|
||||
depth = 1, -- we only support 1-level chapters to keep the UX simple
|
||||
}
|
||||
end
|
||||
local dialog
|
||||
dialog = InputDialog:new{
|
||||
title = item_found and _("Edit custom TOC chapter") or _("Create new custom ToC chapter"),
|
||||
input = item.title,
|
||||
input_hint = _("TOC chapter title"),
|
||||
description = T(_([[On page %1.]]), pageno),
|
||||
buttons = {
|
||||
{
|
||||
{
|
||||
text = _("Cancel"),
|
||||
id = "close",
|
||||
callback = function()
|
||||
UIManager:close(dialog)
|
||||
end,
|
||||
},
|
||||
{
|
||||
text = item_found and _("Save") or _("Create"),
|
||||
is_enter_default = true,
|
||||
callback = function()
|
||||
item.title = dialog:getInputText()
|
||||
UIManager:close(dialog)
|
||||
if not item_found then
|
||||
table.insert(self.toc, idx, item)
|
||||
end
|
||||
self.ui:handleEvent(Event:new("UpdateToc"))
|
||||
if when_updated_callback then
|
||||
when_updated_callback()
|
||||
end
|
||||
end,
|
||||
},
|
||||
},
|
||||
item_found and {
|
||||
{
|
||||
text = _("Remove"),
|
||||
callback = function()
|
||||
UIManager:close(dialog)
|
||||
table.remove(self.toc, idx)
|
||||
self.ui:handleEvent(Event:new("UpdateToc"))
|
||||
if when_updated_callback then
|
||||
when_updated_callback()
|
||||
end
|
||||
end,
|
||||
},
|
||||
selected_text and
|
||||
{
|
||||
text = _("Use selected text"),
|
||||
callback = function()
|
||||
-- Just replace the text without saving, to allow editing/fixing it
|
||||
dialog:setInputText(selected_text.text, nil, false)
|
||||
end,
|
||||
} or nil,
|
||||
} or nil,
|
||||
},
|
||||
}
|
||||
UIManager:show(dialog)
|
||||
dialog:onShowKeyboard()
|
||||
return true
|
||||
end
|
||||
|
||||
function ReaderHandMade:isInHiddenFlow(pageno)
|
||||
local idx, is_match = self:_getItemIndex(self.flow_points, pageno)
|
||||
if is_match then
|
||||
return self.flow_points[idx].hidden
|
||||
else
|
||||
if idx > 1 then
|
||||
return self.flow_points[idx-1].hidden
|
||||
end
|
||||
end
|
||||
-- Before any first flow_point: not hidden
|
||||
return false
|
||||
end
|
||||
|
||||
function ReaderHandMade:toggleHiddenFlow(pageno)
|
||||
local idx, is_match = self:_getItemIndex(self.flow_points, pageno)
|
||||
if is_match then
|
||||
-- Just remove the item (it feels we can, and that we don't
|
||||
-- have to just toggle its hidden value)
|
||||
table.remove(self.flow_points, idx)
|
||||
self:updateDocFlows()
|
||||
return
|
||||
end
|
||||
local hidden
|
||||
if idx > 1 then
|
||||
local previous_item = self.flow_points[idx-1]
|
||||
hidden = not previous_item.hidden
|
||||
else
|
||||
-- First item, can only start an hidden flow
|
||||
hidden = true
|
||||
end
|
||||
local xpointer
|
||||
if self.ui.rolling and type(pageno) == "number" then
|
||||
xpointer = self.document:getPageXPointer(pageno)
|
||||
end
|
||||
local item = {
|
||||
hidden = hidden,
|
||||
page = pageno,
|
||||
xpointer = xpointer,
|
||||
}
|
||||
table.insert(self.flow_points, idx, item)
|
||||
-- We could remove any followup item(s) with the same hidden state, but by keeping them,
|
||||
-- we allow users to adjust the start of a flow without killing its end. One can clean
|
||||
-- all the unnefective ones via the "Clear inactive marked pages" menu item.
|
||||
self:updateDocFlows()
|
||||
end
|
||||
|
||||
function ReaderHandMade:updateDocFlows()
|
||||
local flows = {}
|
||||
local inactive_flow_points = {}
|
||||
-- (getPageCount(), needing the document to be fully loaded, is not available
|
||||
-- until ReaderReady, so be sure this is called only after ReaderReady.)
|
||||
local nb_pages = self.document:getPageCount()
|
||||
local nb_hidden_pages = 0
|
||||
local cur_hidden_flow
|
||||
for i, point in ipairs(self.flow_points) do
|
||||
if point.hidden and not cur_hidden_flow then
|
||||
cur_hidden_flow = {point.page, 0}
|
||||
elseif not point.hidden and cur_hidden_flow then
|
||||
local cur_hidden_pages = point.page - cur_hidden_flow[1]
|
||||
if cur_hidden_pages > 0 then
|
||||
cur_hidden_flow[2] = cur_hidden_pages
|
||||
nb_hidden_pages = nb_hidden_pages + cur_hidden_pages
|
||||
table.insert(flows, cur_hidden_flow)
|
||||
end
|
||||
cur_hidden_flow = nil
|
||||
else
|
||||
table.insert(inactive_flow_points, i)
|
||||
end
|
||||
end
|
||||
if cur_hidden_flow then
|
||||
local cur_hidden_pages = nb_pages + 1 - cur_hidden_flow[1]
|
||||
if cur_hidden_pages > 0 then
|
||||
cur_hidden_flow[2] = cur_hidden_pages
|
||||
nb_hidden_pages = nb_hidden_pages + cur_hidden_pages
|
||||
table.insert(flows, cur_hidden_flow)
|
||||
end
|
||||
end
|
||||
local first_linear_page
|
||||
local last_linear_page
|
||||
local prev_flow
|
||||
for i, flow in ipairs(flows) do
|
||||
if not prev_flow or prev_flow[1] + prev_flow[2] < flow[1] then
|
||||
if not first_linear_page and flow[1] > 1 then
|
||||
first_linear_page = prev_flow and prev_flow[1] + prev_flow[2] or 1
|
||||
end
|
||||
last_linear_page = flow[1] - 1
|
||||
end
|
||||
prev_flow = flow
|
||||
end
|
||||
if not prev_flow or prev_flow[1] + prev_flow[2] < nb_pages then
|
||||
last_linear_page = nb_pages
|
||||
end
|
||||
if not first_linear_page then -- no flow met
|
||||
first_linear_page = 1
|
||||
end
|
||||
-- CreDocument adds and item with key [0] with info about the main flow
|
||||
flows[0] = {first_linear_page, nb_pages - nb_hidden_pages}
|
||||
self.last_linear_page = last_linear_page
|
||||
self.flows = flows
|
||||
self.inactive_flow_points = inactive_flow_points
|
||||
-- We plug our flows table into the document, as some code peeks into it
|
||||
self.document.flows = self.flows
|
||||
end
|
||||
|
||||
function ReaderHandMade:setupFlows(no_event)
|
||||
if self.flows_enabled then
|
||||
self:updateDocFlows()
|
||||
-- If enabled, plug some methods into the document object,
|
||||
-- so they are used instead of the methods from its class.
|
||||
self.document.hasHiddenFlows = function(this)
|
||||
return true
|
||||
end
|
||||
self.document.cacheFlows = function(this)
|
||||
return
|
||||
end
|
||||
self.document.getPageFlow = function(this, page)
|
||||
for i, flow in ipairs(self.flows) do
|
||||
if page < flow[1] then
|
||||
return 0 -- page is not in a hidden flow
|
||||
end
|
||||
if page < flow[1] + flow[2] then
|
||||
return i
|
||||
end
|
||||
end
|
||||
return 0
|
||||
end
|
||||
self.document.getFirstPageInFlow = function(this, flow)
|
||||
return self.flows[flow][1]
|
||||
end
|
||||
self.document.getTotalPagesInFlow = function(this, flow)
|
||||
return self.flows[flow][2]
|
||||
end
|
||||
self.document.getPageNumberInFlow = function(this, page)
|
||||
local nb_hidden_pages = 0
|
||||
for i, flow in ipairs(self.flows) do
|
||||
if page < flow[1] then
|
||||
break -- page is not in a hidden flow
|
||||
end
|
||||
if page < flow[1] + flow[2] then
|
||||
return page - flow[1] + 1
|
||||
end
|
||||
nb_hidden_pages = nb_hidden_pages + flow[2]
|
||||
end
|
||||
return page - nb_hidden_pages
|
||||
end
|
||||
self.document.getLastLinearPage = function(this)
|
||||
return self.last_linear_page
|
||||
end
|
||||
-- We can reuse as-is these ones from CreDocument, which uses the ones defined above.
|
||||
-- Note: these could probably be rewritten and simplified.
|
||||
local CreDocument = require("document/credocument")
|
||||
self.document.getTotalPagesLeft = CreDocument.getTotalPagesLeft
|
||||
self.document.getNextPage = CreDocument.getNextPage
|
||||
self.document.getPrevPage = CreDocument.getPrevPage
|
||||
else
|
||||
-- Remove all our overrides, so the class methods can be used again
|
||||
self.document.hasHiddenFlows = nil
|
||||
self.document.cacheFlows = nil
|
||||
self.document.getPageFlow = nil
|
||||
self.document.getFirstPageInFlow = nil
|
||||
self.document.getTotalPagesInFlow = nil
|
||||
self.document.getPageNumberInFlow = nil
|
||||
self.document.getLastLinearPage = nil
|
||||
self.document.getTotalPagesLeft = nil
|
||||
self.document.getNextPage = nil
|
||||
self.document.getPrevPage = nil
|
||||
self.document.flows = nil
|
||||
if self.document.cacheFlows then
|
||||
self.document:cacheFlows()
|
||||
end
|
||||
end
|
||||
if not no_event then
|
||||
self.ui:handleEvent(Event:new("UpdateToc"))
|
||||
-- Needed to skip hidden flows if PDF in scroll mode
|
||||
self.ui:handleEvent(Event:new("InitScrollPageStates"))
|
||||
end
|
||||
end
|
||||
|
||||
return ReaderHandMade
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,418 +0,0 @@
|
||||
local Device = require("device")
|
||||
local Event = require("ui/event")
|
||||
local UIManager = require("ui/uimanager")
|
||||
local WidgetContainer = require("ui/widget/container/widgetcontainer")
|
||||
local logger = require("logger")
|
||||
local time = require("ui/time")
|
||||
local _ = require("gettext")
|
||||
local C_ = _.pgettext
|
||||
local T = require("ffi/util").template
|
||||
local Screen = Device.screen
|
||||
|
||||
-- This module exposes Scrolling settings, and additionnally
|
||||
-- handles inertial scrolling on non-eInk devices.
|
||||
|
||||
local SCROLL_METHOD_CLASSIC = "classic"
|
||||
local SCROLL_METHOD_TURBO = "turbo"
|
||||
local SCROLL_METHOD_ON_RELEASE = "on_release"
|
||||
|
||||
local ReaderScrolling = WidgetContainer:extend{
|
||||
-- Available scrolling methods (make them available to other reader modules)
|
||||
SCROLL_METHOD_CLASSIC = SCROLL_METHOD_CLASSIC,
|
||||
SCROLL_METHOD_TURBO = SCROLL_METHOD_TURBO,
|
||||
SCROLL_METHOD_ON_RELEASE = SCROLL_METHOD_ON_RELEASE,
|
||||
|
||||
scroll_method = SCROLL_METHOD_CLASSIC,
|
||||
scroll_activation_delay_ms = 0, -- 0 ms
|
||||
inertial_scroll = false,
|
||||
|
||||
pan_rate = 30, -- default 30 ops, will be adjusted in readerui
|
||||
scroll_friction = 0.2, -- the lower, the sooner inertial scrolling stops
|
||||
-- go at ending scrolling soon when we reach steps smaller than this
|
||||
end_scroll_dist = Screen:scaleBySize(10),
|
||||
-- no inertial scrolling if 300ms pause without any movement before release
|
||||
pause_before_release_cancel_duration = time.ms(300),
|
||||
|
||||
-- Callbacks to be updated by readerrolling or readerpaging
|
||||
_do_scroll_callback = function(distance) return false end,
|
||||
_scroll_done_callback = function() end,
|
||||
|
||||
_inertial_scroll_supported = false,
|
||||
_inertial_scroll_enabled = false,
|
||||
_inertial_scroll_interval = 1 / 30,
|
||||
_inertial_scroll_action_scheduled = false,
|
||||
_just_reschedule = false,
|
||||
_last_manual_scroll_dy = 0,
|
||||
_velocity = 0,
|
||||
}
|
||||
|
||||
function ReaderScrolling:init()
|
||||
if not Device:isTouchDevice() then
|
||||
-- No scroll support, no menu
|
||||
return
|
||||
end
|
||||
|
||||
-- The different scrolling methods are handled directly by readerpaging/readerrolling
|
||||
self.scroll_method = G_reader_settings:readSetting("scroll_method")
|
||||
|
||||
-- Keep inertial scrolling available on the emulator (which advertizes itself as eInk)
|
||||
if not Device:hasEinkScreen() or Device:isEmulator() then
|
||||
self._inertial_scroll_supported = true
|
||||
end
|
||||
|
||||
if self._inertial_scroll_supported then
|
||||
self.inertial_scroll = G_reader_settings:nilOrTrue("inertial_scroll")
|
||||
self._inertial_scroll_interval = 1 / self.pan_rate
|
||||
-- Set this so we don't have to check for nil, and in case
|
||||
-- we miss a first touch event.
|
||||
-- We can keep it obsolete, which will result in a long
|
||||
-- duration and a small/zero velocity that won't hurt.
|
||||
self._last_manual_scroll_timev = 0
|
||||
self:_setupAction()
|
||||
end
|
||||
|
||||
self.ui.menu:registerToMainMenu(self)
|
||||
end
|
||||
|
||||
function ReaderScrolling:getDefaultScrollActivationDelay_ms()
|
||||
if (self.ui.gestures and self.ui.gestures.multiswipes_enabled)
|
||||
or G_reader_settings:readSetting("activate_menu") ~= "tap" then
|
||||
-- If swipes to show menu or multiswipes are enabled, higher default
|
||||
-- scroll activation delay to avoid scrolling and restoring when
|
||||
-- doing swipes
|
||||
return 500 -- 500ms
|
||||
end
|
||||
-- Otherwise, no need for any delay
|
||||
return 0
|
||||
end
|
||||
|
||||
function ReaderScrolling:addToMainMenu(menu_items)
|
||||
menu_items.scrolling = {
|
||||
text = _("Scrolling"),
|
||||
enabled_func = function()
|
||||
-- Make it only enabled when in continuous/scroll mode
|
||||
-- (different setting in self.view whether rolling or paging document)
|
||||
if self.view and (self.view.page_scroll or self.view.view_mode == "scroll") then
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end,
|
||||
sub_item_table = {
|
||||
{
|
||||
text = _("Classic scrolling"),
|
||||
help_text = _([[Classic scrolling will move the document with your finger.]]),
|
||||
checked_func = function()
|
||||
return self.scroll_method == self.SCROLL_METHOD_CLASSIC
|
||||
end,
|
||||
callback = function()
|
||||
if self.scroll_method ~= self.SCROLL_METHOD_CLASSIC then
|
||||
self.scroll_method = self.SCROLL_METHOD_CLASSIC
|
||||
self:applyScrollSettings()
|
||||
end
|
||||
end,
|
||||
},
|
||||
{
|
||||
text = _("Turbo scrolling"),
|
||||
help_text = _([[
|
||||
Turbo scrolling will scroll the document, at each step, by the distance from your initial finger position (rather than by the distance from your previous finger position).
|
||||
It allows for faster scrolling without the need to lift and reposition your finger.]]),
|
||||
checked_func = function()
|
||||
return self.scroll_method == self.SCROLL_METHOD_TURBO
|
||||
end,
|
||||
callback = function()
|
||||
if self.scroll_method ~= self.SCROLL_METHOD_TURBO then
|
||||
self.scroll_method = self.SCROLL_METHOD_TURBO
|
||||
self:applyScrollSettings()
|
||||
end
|
||||
end,
|
||||
},
|
||||
{
|
||||
text = _("On-release scrolling"),
|
||||
help_text = _([[
|
||||
On-release scrolling will scroll the document by the panned distance only on finger up.
|
||||
This is interesting on eInk if you only pan to better adjust page vertical position.]]),
|
||||
checked_func = function()
|
||||
return self.scroll_method == self.SCROLL_METHOD_ON_RELEASE
|
||||
end,
|
||||
callback = function()
|
||||
if self.scroll_method ~= self.SCROLL_METHOD_ON_RELEASE then
|
||||
self.scroll_method = self.SCROLL_METHOD_ON_RELEASE
|
||||
self:applyScrollSettings()
|
||||
end
|
||||
end,
|
||||
separator = true,
|
||||
},
|
||||
{
|
||||
text_func = function()
|
||||
return T(_("Activation delay: %1 ms"), self.scroll_activation_delay_ms)
|
||||
end,
|
||||
keep_menu_open = true,
|
||||
callback = function(touchmenu_instance)
|
||||
local scroll_activation_delay_default_ms = self:getDefaultScrollActivationDelay_ms()
|
||||
local SpinWidget = require("ui/widget/spinwidget")
|
||||
local widget = SpinWidget:new{
|
||||
title_text = _("Scroll activation delay"),
|
||||
info_text = T(_([[
|
||||
A delay can be used to avoid scrolling when swipes or multiswipes are intended.
|
||||
|
||||
The delay value is in milliseconds and can range from 0 to 2000 (2 seconds).
|
||||
Default value: %1 ms]]), scroll_activation_delay_default_ms),
|
||||
width = math.floor(Screen:getWidth() * 0.75),
|
||||
value = self.scroll_activation_delay_ms,
|
||||
value_min = 0,
|
||||
value_max = 2000,
|
||||
value_step = 100,
|
||||
value_hold_step = 500,
|
||||
unit = C_("Time", "ms"),
|
||||
ok_text = _("Set delay"),
|
||||
default_value = scroll_activation_delay_default_ms,
|
||||
callback = function(spin)
|
||||
self.scroll_activation_delay_ms = spin.value
|
||||
self:applyScrollSettings()
|
||||
if touchmenu_instance then touchmenu_instance:updateItems() end
|
||||
end
|
||||
}
|
||||
UIManager:show(widget)
|
||||
end,
|
||||
},
|
||||
}
|
||||
}
|
||||
if self._inertial_scroll_supported then
|
||||
-- Add it before "Activation delay" to keep checkboxes together
|
||||
table.insert(menu_items.scrolling.sub_item_table, 4, {
|
||||
text = _("Allow inertial scrolling"),
|
||||
enabled_func = function()
|
||||
return self.scroll_method == self.SCROLL_METHOD_CLASSIC
|
||||
end,
|
||||
checked_func = function()
|
||||
return self.scroll_method == self.SCROLL_METHOD_CLASSIC and self.inertial_scroll
|
||||
end,
|
||||
callback = function()
|
||||
self.inertial_scroll = not self.inertial_scroll
|
||||
self:applyScrollSettings()
|
||||
end,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
function ReaderScrolling:onReaderReady()
|
||||
-- We don't know if the gestures plugin is loaded in :init(), but we know it here
|
||||
self.scroll_activation_delay_ms = G_reader_settings:readSetting("scroll_activation_delay")
|
||||
or self:getDefaultScrollActivationDelay_ms()
|
||||
self:applyScrollSettings()
|
||||
end
|
||||
|
||||
function ReaderScrolling:applyScrollSettings()
|
||||
G_reader_settings:saveSetting("scroll_method", self.scroll_method)
|
||||
G_reader_settings:saveSetting("inertial_scroll", self.inertial_scroll)
|
||||
if self.scroll_activation_delay_ms == self:getDefaultScrollActivationDelay_ms() then
|
||||
G_reader_settings:delSetting("scroll_activation_delay")
|
||||
else
|
||||
G_reader_settings:saveSetting("scroll_activation_delay", self.scroll_activation_delay_ms)
|
||||
end
|
||||
if self.scroll_method == self.SCROLL_METHOD_CLASSIC then
|
||||
self._inertial_scroll_enabled = self.inertial_scroll
|
||||
else
|
||||
self._inertial_scroll_enabled = false
|
||||
end
|
||||
self:setupTouchZones()
|
||||
self.ui:handleEvent(Event:new("ScrollSettingsUpdated", self.scroll_method,
|
||||
self._inertial_scroll_enabled, self.scroll_activation_delay_ms))
|
||||
end
|
||||
|
||||
function ReaderScrolling:setupTouchZones()
|
||||
local zones = {
|
||||
{
|
||||
id = "inertial_scrolling_touch",
|
||||
ges = "touch",
|
||||
screen_zone = {
|
||||
ratio_x = 0, ratio_y = 0, ratio_w = 1, ratio_h = 1,
|
||||
},
|
||||
handler = function(ges)
|
||||
-- A touch might set the start of the first pan event,
|
||||
-- that we need to compute its duration
|
||||
self._last_manual_scroll_timev = ges.time
|
||||
-- If we are scrolling, a touch cancels it.
|
||||
-- We want its release (which will trigger a tap) to not change pages.
|
||||
-- This also allows a pan following this touch to skip any scroll
|
||||
-- activation delay
|
||||
self._cancelled_by_touch = self._inertial_scroll_action
|
||||
and self._inertial_scroll_action(false)
|
||||
or false
|
||||
end,
|
||||
},
|
||||
{
|
||||
id = "inertial_scrolling_tap",
|
||||
ges = "tap",
|
||||
screen_zone = {
|
||||
ratio_x = 0, ratio_y = 0, ratio_w = 1, ratio_h = 1,
|
||||
},
|
||||
overrides = {
|
||||
"tap_forward",
|
||||
"tap_backward",
|
||||
"readermenu_tap",
|
||||
"readermenu_ext_tap",
|
||||
"readerconfigmenu_tap",
|
||||
"readerconfigmenu_ext_tap",
|
||||
"readerfooter_tap",
|
||||
"readerhighlight_tap",
|
||||
"tap_link",
|
||||
},
|
||||
handler = function()
|
||||
-- Ignore tap if cancelled by its initial touch
|
||||
if self._cancelled_by_touch then
|
||||
self._cancelled_by_touch = false
|
||||
return true
|
||||
end
|
||||
-- Otherwise, let it be handled by other tap handlers
|
||||
end,
|
||||
},
|
||||
}
|
||||
if self._inertial_scroll_enabled then
|
||||
self.ui:registerTouchZones(zones)
|
||||
else
|
||||
self.ui:unRegisterTouchZones(zones)
|
||||
end
|
||||
end
|
||||
|
||||
function ReaderScrolling:isInertialScrollingEnabled()
|
||||
return self._inertial_scroll_enabled
|
||||
end
|
||||
|
||||
function ReaderScrolling:setInertialScrollCallbacks(do_scroll_callback, scroll_done_callback)
|
||||
self._do_scroll_callback = do_scroll_callback
|
||||
self._scroll_done_callback = scroll_done_callback
|
||||
end
|
||||
|
||||
function ReaderScrolling:startInertialScroll()
|
||||
if not self._inertial_scroll_enabled then
|
||||
return false
|
||||
end
|
||||
return self._inertial_scroll_action(true)
|
||||
end
|
||||
|
||||
function ReaderScrolling:cancelInertialScroll()
|
||||
if not self._inertial_scroll_enabled then
|
||||
return
|
||||
end
|
||||
return self._inertial_scroll_action(false)
|
||||
end
|
||||
|
||||
function ReaderScrolling:cancelledByTouch()
|
||||
return self._cancelled_by_touch
|
||||
end
|
||||
|
||||
function ReaderScrolling:accountManualScroll(dy, timev)
|
||||
if not self._inertial_scroll_enabled then
|
||||
return
|
||||
end
|
||||
self._last_manual_scroll_dy = dy
|
||||
self._last_manual_scroll_duration = timev - self._last_manual_scroll_timev
|
||||
self._last_manual_scroll_timev = timev
|
||||
end
|
||||
|
||||
function ReaderScrolling:_setupAction()
|
||||
self._inertial_scroll_action = function(action)
|
||||
-- action can be:
|
||||
-- - true: stop any previous ongoing inertial scroll, then start a new one
|
||||
-- (returns true if we started one)
|
||||
-- - false: just stop any previous ongoing inertial scroll
|
||||
-- (returns true if we did cancel one)
|
||||
if action ~= nil then
|
||||
local cancelled = false
|
||||
if self._inertial_scroll_action_scheduled then
|
||||
UIManager:unschedule(self._inertial_scroll_action)
|
||||
self._inertial_scroll_action_scheduled = false
|
||||
cancelled = true
|
||||
self._scroll_done_callback()
|
||||
logger.dbg("inertial scrolling cancelled")
|
||||
end
|
||||
if action == false then
|
||||
self._last_manual_scroll_dy = 0
|
||||
return cancelled
|
||||
end
|
||||
|
||||
-- Initiate inertial scrolling (action=true), unless we should not
|
||||
if UIManager:getTime() - self._last_manual_scroll_timev >= self.pause_before_release_cancel_duration then
|
||||
-- but not if no finger move for 0.3s before finger up
|
||||
self._last_manual_scroll_dy = 0
|
||||
return false
|
||||
end
|
||||
if self._last_manual_scroll_duration == 0 or self._last_manual_scroll_dy == 0 then
|
||||
return false
|
||||
end
|
||||
|
||||
-- Initial velocity is the one of the last pan scroll given to accountManualScroll()
|
||||
local delay_us = time.to_us(self._last_manual_scroll_duration)
|
||||
if delay_us < 1 then delay_us = 1 end -- safety check
|
||||
self._velocity = self._last_manual_scroll_dy * time.s(1 / delay_us)
|
||||
self._last_manual_scroll_dy = 0
|
||||
|
||||
self._inertial_scroll_action_scheduled = true
|
||||
-- We'll keep re-scheduling this same action, which will do
|
||||
-- alternatively thanks to the _just_reschedule flag:
|
||||
-- * either, in _inertial_scroll_interval, do a scroll
|
||||
-- * or, then, at next tick, reschedule 1)
|
||||
-- This is needed as the first one will cause a repaint that
|
||||
-- may take more than _inertial_scroll_interval, which if we
|
||||
-- didn't do that could be run before we process any input,
|
||||
-- not allowing us to interrupt this inertial scrolling.
|
||||
self._just_reschedule = false
|
||||
UIManager:scheduleIn(self._inertial_scroll_interval, self._inertial_scroll_action)
|
||||
-- self._stats_scroll_iterations = 0
|
||||
-- self._stats_scroll_distance = 0
|
||||
logger.dbg("inertial scrolling started")
|
||||
return true
|
||||
end
|
||||
if not self._inertial_scroll_action_scheduled then
|
||||
-- Safety check, shouldn't happen
|
||||
return
|
||||
end
|
||||
if not self.ui.document then
|
||||
-- might happen if scheduled and run after document is closed
|
||||
return
|
||||
end
|
||||
|
||||
if self._just_reschedule then
|
||||
-- just re-schedule this, so a real scrolling is done after the delay
|
||||
self._just_reschedule = false
|
||||
UIManager:scheduleIn(self._inertial_scroll_interval, self._inertial_scroll_action)
|
||||
return
|
||||
end
|
||||
|
||||
-- Decrease velocity at each step
|
||||
self._velocity = self._velocity * self.scroll_friction^self._inertial_scroll_interval
|
||||
local dist = math.floor(self._velocity * self._inertial_scroll_interval)
|
||||
if math.abs(dist) < self.end_scroll_dist then
|
||||
-- Decrease it even more so scrolling stops sooner
|
||||
self._velocity = self._velocity * (2/3)
|
||||
end
|
||||
-- self._stats_scroll_iterations = self._stats_scroll_iterations + 1
|
||||
-- self._stats_scroll_distance = self._stats_scroll_distance + dist
|
||||
|
||||
logger.dbg("inertial scrolling by", dist)
|
||||
local did_scroll = self._do_scroll_callback(dist)
|
||||
|
||||
if did_scroll and math.abs(dist) > 2 then
|
||||
-- Schedule at next tick the real re-scheduling
|
||||
self._just_reschedule = true
|
||||
UIManager:nextTick(self._inertial_scroll_action)
|
||||
return
|
||||
end
|
||||
|
||||
-- We're done
|
||||
self._inertial_scroll_action_scheduled = false
|
||||
self._scroll_done_callback()
|
||||
logger.dbg("inertial scrolling ended")
|
||||
|
||||
--[[
|
||||
local Notification = require("ui/widget/notification")
|
||||
UIManager:show(Notification:new{
|
||||
text = string.format("%d iterations, %d px scrolled",
|
||||
self._stats_scroll_iterations, self._stats_scroll_distance),
|
||||
})
|
||||
]]--
|
||||
end
|
||||
end
|
||||
|
||||
return ReaderScrolling
|
@ -1,542 +0,0 @@
|
||||
local Blitbuffer = require("ffi/blitbuffer")
|
||||
local Cache = require("cache")
|
||||
local Device = require("device")
|
||||
local Geom = require("ui/geometry")
|
||||
local Persist = require("persist")
|
||||
local RenderImage = require("ui/renderimage")
|
||||
local TileCacheItem = require("document/tilecacheitem")
|
||||
local UIManager = require("ui/uimanager")
|
||||
local WidgetContainer = require("ui/widget/container/widgetcontainer")
|
||||
local Screen = Device.screen
|
||||
local ffiutil = require("ffi/util")
|
||||
local logger = require("logger")
|
||||
local util = require("util")
|
||||
local _ = require("gettext")
|
||||
|
||||
-- This ReaderThumbnail module provides a service for generating thumbnails
|
||||
-- of book pages.
|
||||
-- It handles launching via the menu or Dispatcher/Gestures two fullscreen
|
||||
-- widgets related to showing pages and thumbnails that will make use of
|
||||
-- its services: BookMap and PageBrowser.
|
||||
local ReaderThumbnail = WidgetContainer:extend{}
|
||||
|
||||
function ReaderThumbnail:init()
|
||||
if not Device:isTouchDevice() then
|
||||
-- The BookMap and PageBrowser widgets depend too much on gestures,
|
||||
-- making them work with keys would be hard and very limited, so
|
||||
-- just don't make them available.
|
||||
return
|
||||
end
|
||||
|
||||
self.ui.menu:registerToMainMenu(self)
|
||||
|
||||
-- Use LuaJIT fast buffer.encode()/decode() when serializing BlitBuffer
|
||||
-- for exchange between subprocess and parent.
|
||||
self.codec = Persist.getCodec("luajit")
|
||||
|
||||
self:setupColor()
|
||||
self.thumbnails_requests = {}
|
||||
self.current_target_size_tag = nil
|
||||
|
||||
-- Ensure no multiple executions, and nextTick() the scheduleIn()
|
||||
-- so we get a chance to process events in-between refreshes and
|
||||
-- this can be interrupted (otherwise, something scheduleIn(0.1),
|
||||
-- if a screen refresh is then done and taking longer than 0.1s,
|
||||
-- would be executed immediately, without emptying any input event).
|
||||
local schedule_step = 0
|
||||
self._ensureTileGeneration_action = function(restart)
|
||||
if restart then
|
||||
UIManager:unschedule(self._ensureTileGeneration_action)
|
||||
schedule_step = 0
|
||||
end
|
||||
if schedule_step == 0 then
|
||||
schedule_step = 1
|
||||
UIManager:nextTick(self._ensureTileGeneration_action)
|
||||
elseif schedule_step == 1 then
|
||||
schedule_step = 2
|
||||
UIManager:scheduleIn(0.1, self._ensureTileGeneration_action)
|
||||
else
|
||||
schedule_step = 0
|
||||
self:ensureTileGeneration()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ReaderThumbnail:addToMainMenu(menu_items)
|
||||
menu_items.book_map = {
|
||||
text = _("Book map"),
|
||||
callback = function()
|
||||
self:onShowBookMap()
|
||||
end,
|
||||
-- Show the alternative overview mode (which is just a restricted
|
||||
-- variation of the main book map) with long-press (let's avoid
|
||||
-- adding another item in the crowded first menu).
|
||||
hold_callback = function()
|
||||
self:onShowBookMap(true)
|
||||
end,
|
||||
}
|
||||
menu_items.page_browser = {
|
||||
text = _("Page browser"),
|
||||
callback = function()
|
||||
self:onShowPageBrowser()
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
function ReaderThumbnail:onShowBookMap(overview_mode)
|
||||
local BookMapWidget = require("ui/widget/bookmapwidget")
|
||||
UIManager:show(BookMapWidget:new{
|
||||
ui = self.ui,
|
||||
overview_mode = overview_mode,
|
||||
})
|
||||
return true
|
||||
end
|
||||
|
||||
function ReaderThumbnail:onShowPageBrowser()
|
||||
local PageBrowserWidget = require("ui/widget/pagebrowserwidget")
|
||||
UIManager:show(PageBrowserWidget:new{
|
||||
ui = self.ui,
|
||||
})
|
||||
return true
|
||||
end
|
||||
|
||||
-- This is made a module local so we can keep track of pids
|
||||
-- to collect across multiple Reader instantiations
|
||||
local pids_to_collect = {}
|
||||
|
||||
function ReaderThumbnail:collectPids()
|
||||
if #pids_to_collect == 0 then
|
||||
return false
|
||||
end
|
||||
for i=#pids_to_collect, 1, -1 do
|
||||
if ffiutil.isSubProcessDone(pids_to_collect[i]) then
|
||||
table.remove(pids_to_collect, i)
|
||||
end
|
||||
end
|
||||
return #pids_to_collect > 0
|
||||
end
|
||||
|
||||
function ReaderThumbnail:setupColor()
|
||||
self.bb_type = self.ui.document.render_color and self.ui.document.color_bb_type or Blitbuffer.TYPE_BB8
|
||||
end
|
||||
|
||||
function ReaderThumbnail:setupCache()
|
||||
if not self.tile_cache then
|
||||
-- We want to allow browsing at least N pages worth of thumbnails
|
||||
-- without cache trashing. A little more than N pages (because inter
|
||||
-- thumbnail margins) will fit in N * screen size.
|
||||
-- With N=5, this should use from 5 to 15 Mb on a classic eInk device.
|
||||
local N = 5
|
||||
local max_bytes = math.ceil(N * Screen:getWidth() * Screen:getHeight() * Blitbuffer.TYPE_TO_BPP[self.bb_type] / 8)
|
||||
-- We don't really care about limiting any number of slots, so allow
|
||||
-- for at least 5 pages of 10x10 tiles
|
||||
local avg_itemsize = math.ceil(max_bytes * (1/500))
|
||||
self.tile_cache = Cache:new{
|
||||
size = max_bytes,
|
||||
avg_itemsize = avg_itemsize, -- will make slots=500
|
||||
enable_eviction_cb = true,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
function ReaderThumbnail:logCacheSize()
|
||||
logger.info(string.format("Thumbnails cache: %d/%d (%s/%s)",
|
||||
self.tile_cache.cache.used_slots(),
|
||||
self.tile_cache.slots,
|
||||
util.getFriendlySize(self.tile_cache.cache.used_size()),
|
||||
util.getFriendlySize(self.tile_cache.size)))
|
||||
end
|
||||
|
||||
function ReaderThumbnail:resetCache()
|
||||
if self.tile_cache then
|
||||
self.tile_cache:clear()
|
||||
self.tile_cache = nil
|
||||
end
|
||||
end
|
||||
|
||||
function ReaderThumbnail:removeFromCache(hash_subs, remove_only_non_matching)
|
||||
-- Remove from cache all tiles matching any hash from hash_subs.
|
||||
-- IF only_non_matching=true, keep those matching and remove all others.
|
||||
if not self.tile_cache then
|
||||
return
|
||||
end
|
||||
if type(hash_subs) ~= "table" then
|
||||
hash_subs = { hash_subs }
|
||||
end
|
||||
local nb_removed, size_removed = 0, 0
|
||||
local to_remove = {}
|
||||
for thash, tile in self.tile_cache.cache:pairs() do
|
||||
local remove = remove_only_non_matching
|
||||
for _, h in ipairs(hash_subs) do
|
||||
if thash:find(h, 1, true) then -- plain text match (no pattern needed)
|
||||
remove = not remove
|
||||
break
|
||||
end
|
||||
end
|
||||
if remove then
|
||||
to_remove[thash] = true
|
||||
nb_removed = nb_removed + 1
|
||||
size_removed = size_removed + tile.size
|
||||
end
|
||||
end
|
||||
for thash, _ in pairs(to_remove) do
|
||||
self.tile_cache.cache:delete(thash)
|
||||
logger.dbg("removed cached thumbnail", thash)
|
||||
end
|
||||
return nb_removed, size_removed
|
||||
end
|
||||
|
||||
function ReaderThumbnail:resetCachedPagesForBookmarks(annotations)
|
||||
-- Multiple bookmarks may be provided
|
||||
local start_page, end_page
|
||||
for i = 1, #annotations do
|
||||
local bm = annotations[i]
|
||||
if self.ui.rolling then
|
||||
-- Look at all properties that may be xpointers
|
||||
for _, k in ipairs({"page", "pos0", "pos1"}) do
|
||||
if bm[k] and type(bm[k]) == "string" then
|
||||
local p = self.ui.document:getPageFromXPointer(bm[k])
|
||||
if not start_page or p < start_page then
|
||||
start_page = p
|
||||
end
|
||||
if not end_page or p > end_page then
|
||||
end_page = p
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
if bm.page and type(bm.page) == "number" then
|
||||
local bm_page0 = (bm.pos0 and bm.pos0.page) or bm.page
|
||||
local bm_page1 = (bm.pos1 and bm.pos1.page) or bm.page
|
||||
for p = bm_page0, bm_page1 do
|
||||
if not start_page or p < start_page then
|
||||
start_page = p
|
||||
end
|
||||
if not end_page or p > end_page then
|
||||
end_page = p
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if start_page and end_page then
|
||||
local hash_subs_to_remove = {}
|
||||
for p=start_page, end_page do
|
||||
table.insert(hash_subs_to_remove, string.format("p%d-", p))
|
||||
end
|
||||
self:removeFromCache(hash_subs_to_remove)
|
||||
end
|
||||
end
|
||||
|
||||
function ReaderThumbnail:tidyCache()
|
||||
if self.current_target_size_tag then
|
||||
-- Remove all thumbnails generated for an older target size
|
||||
self:removeFromCache("-"..self.current_target_size_tag, true)
|
||||
end
|
||||
end
|
||||
|
||||
function ReaderThumbnail:cancelPageThumbnailRequests(batch_id)
|
||||
if batch_id then
|
||||
self.thumbnails_requests[batch_id] = nil
|
||||
else
|
||||
self.thumbnails_requests = {}
|
||||
end
|
||||
if self.req_in_progress and (not batch_id or self.req_in_progress.batch_id == batch_id) then
|
||||
-- Kill any reference to the module cancelling it
|
||||
self.req_in_progress.when_generated_callback = nil
|
||||
end
|
||||
end
|
||||
|
||||
function ReaderThumbnail:getPageThumbnail(page, width, height, batch_id, when_generated_callback)
|
||||
self:setupCache()
|
||||
self.current_target_size_tag = string.format("w%d_h%d", width, height)
|
||||
if self.ui.rolling and Screen.night_mode and self.ui.document.configurable.nightmode_images == 1 then
|
||||
-- We'll get a different bb in this case: it needs its own cache hash
|
||||
self.current_target_size_tag = self.current_target_size_tag .. "_nm"
|
||||
end
|
||||
local hash = string.format("p%d-%s", page, self.current_target_size_tag)
|
||||
local tile = self.tile_cache and self.tile_cache:check(hash)
|
||||
if tile then
|
||||
-- Cached: call callback and we're done.
|
||||
when_generated_callback(tile, batch_id, false)
|
||||
return false -- not delayed
|
||||
end
|
||||
if not self.thumbnails_requests[batch_id] then
|
||||
self.thumbnails_requests[batch_id] = {}
|
||||
end
|
||||
table.insert(self.thumbnails_requests[batch_id], {
|
||||
batch_id = batch_id,
|
||||
hash = hash,
|
||||
page = page,
|
||||
width = width,
|
||||
height = height,
|
||||
when_generated_callback = when_generated_callback,
|
||||
})
|
||||
-- Start tile generation, avoid multiple ones
|
||||
self._ensureTileGeneration_action(true)
|
||||
return true -- delayed
|
||||
end
|
||||
|
||||
function ReaderThumbnail:ensureTileGeneration()
|
||||
if not self._standby_prevented then
|
||||
self._standby_prevented = true
|
||||
UIManager:preventStandby()
|
||||
end
|
||||
local has_pids_still_to_collect = self:collectPids()
|
||||
|
||||
local still_in_progress = false
|
||||
if self.req_in_progress then
|
||||
local pid_still_to_collect
|
||||
still_in_progress, pid_still_to_collect = self:checkTileGeneration(self.req_in_progress)
|
||||
if pid_still_to_collect then
|
||||
has_pids_still_to_collect = true
|
||||
end
|
||||
end
|
||||
if not still_in_progress then
|
||||
self.req_in_progress = nil
|
||||
while true do
|
||||
local req_id, requests = next(self.thumbnails_requests)
|
||||
if not req_id then -- no more requests
|
||||
break
|
||||
end
|
||||
local req = table.remove(requests, 1)
|
||||
if #requests == 0 then
|
||||
self.thumbnails_requests[req_id] = nil
|
||||
end
|
||||
if req.when_generated_callback then -- not cancelled since queued
|
||||
-- It might have been generated and cached by a previous batch
|
||||
local tile = self.tile_cache and self.tile_cache:check(req.hash)
|
||||
if tile then
|
||||
req.when_generated_callback(tile, req.batch_id, true)
|
||||
else
|
||||
if self:startTileGeneration(req) then
|
||||
self.req_in_progress = req
|
||||
break
|
||||
else
|
||||
-- Failure starting it: let requester know in case it cares, and forget it
|
||||
req.when_generated_callback(nil, req.batch_id, true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if self.req_in_progress or has_pids_still_to_collect or next(self.thumbnails_requests) then
|
||||
self._ensureTileGeneration_action()
|
||||
else
|
||||
if self._standby_prevented then
|
||||
self._standby_prevented = false
|
||||
UIManager:allowStandby()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ReaderThumbnail:startTileGeneration(request)
|
||||
local pid, parent_read_fd = ffiutil.runInSubProcess(function(pid, child_write_fd)
|
||||
-- Get page image as if drawn on the screen
|
||||
local bb = self:_getPageImage(request.page)
|
||||
-- Scale it to fit in the requested size
|
||||
local scale_factor = math.min(request.width / bb:getWidth(), request.height / bb:getHeight())
|
||||
local target_w = math.floor(bb:getWidth() * scale_factor)
|
||||
local target_h = math.floor(bb:getHeight() * scale_factor)
|
||||
-- local time = require("ui/time")
|
||||
-- local start_time = time.now()
|
||||
local tile = TileCacheItem:new{
|
||||
bb = RenderImage:scaleBlitBuffer(bb, target_w, target_h, true),
|
||||
pageno = request.page,
|
||||
}
|
||||
tile.size = tonumber(tile.bb.stride) * tile.bb.h
|
||||
-- logger.info("tile size", tile.bb.w, tile.bb.h, "=>", tile.size)
|
||||
-- logger.info(string.format(" scaling took %.3f seconds, %d bpp", time.to_s(time.since(start_time)), tile.bb:getBpp()))
|
||||
-- bb:free() -- no need to spend time freeing, we're dying soon anyway!
|
||||
|
||||
ffiutil.writeToFD(child_write_fd, self.codec.serialize(tile:totable()), true)
|
||||
end, true) -- with_pipe = true
|
||||
if pid then
|
||||
-- Store these in the request object itself
|
||||
request.pid = pid
|
||||
request.parent_read_fd = parent_read_fd
|
||||
return true
|
||||
end
|
||||
logger.warn("PageBrowserWidget thumbnail start failure:", parent_read_fd)
|
||||
return false
|
||||
end
|
||||
|
||||
function ReaderThumbnail:checkTileGeneration(request)
|
||||
local pid, parent_read_fd = request.pid, request.parent_read_fd
|
||||
local stuff_to_read = ffiutil.getNonBlockingReadSize(parent_read_fd) ~= 0
|
||||
local subprocess_done = ffiutil.isSubProcessDone(pid)
|
||||
logger.dbg("subprocess_done:", subprocess_done, " stuff_to_read:", stuff_to_read)
|
||||
if stuff_to_read then
|
||||
-- local time = require("ui/time")
|
||||
-- local start_time = time.now()
|
||||
local result, err = self.codec.deserialize(ffiutil.readAllFromFD(parent_read_fd))
|
||||
if result then
|
||||
local tile = TileCacheItem:new{}
|
||||
tile:fromtable(result)
|
||||
if self.tile_cache then
|
||||
self.tile_cache:insert(request.hash, tile)
|
||||
end
|
||||
if request.when_generated_callback then -- not cancelled
|
||||
request.when_generated_callback(tile, request.batch_id, true)
|
||||
end
|
||||
else
|
||||
logger.warn("PageBrowserWidget thumbnail deserialize() failed:", err)
|
||||
if request.when_generated_callback then -- not cancelled
|
||||
request.when_generated_callback(nil, request.batch_id, true)
|
||||
end
|
||||
end
|
||||
-- logger.info(string.format(" parsing result from subprocess took %.3f seconds", time.to_s(time.since(start_time))))
|
||||
if not subprocess_done then
|
||||
table.insert(pids_to_collect, pid)
|
||||
return false, true
|
||||
end
|
||||
return false
|
||||
elseif subprocess_done then
|
||||
-- subprocess_done: process exited with no output
|
||||
ffiutil.readAllFromFD(parent_read_fd) -- close our fd
|
||||
return false
|
||||
end
|
||||
logger.dbg("process not yet done, will check again soon")
|
||||
return true
|
||||
end
|
||||
|
||||
function ReaderThumbnail:_getPageImage(page)
|
||||
-- This is run in a subprocess: we can tweak all document settings
|
||||
-- to get an adequate image of the page.
|
||||
-- No need to worry about the final state of things: this subprocess
|
||||
-- will die just after drawing the page, and all will be forgotten,
|
||||
-- without impact on the parent process.
|
||||
|
||||
-- Be sure to limit our impact on the disk-saved book state
|
||||
self.ui.saveSettings = function() end -- Be sure nothing is flushed
|
||||
self.ui.statistics = nil -- Don't update statistics for pages we visit
|
||||
|
||||
-- By default, our target page size is the current screen size
|
||||
local target_w, target_h = Screen:getWidth(), Screen:getHeight()
|
||||
|
||||
-- This was all mostly chosen by experimenting.
|
||||
-- Be sure to call the innermost methods enough to get what we want, and
|
||||
-- not upper event handlers that may trigger other unneeded events and stuff.
|
||||
-- Especially, be sure to not trigger any paint on the screen buffer, or
|
||||
-- any processing of input events.
|
||||
-- No need to worry about UIManager:scheduleIn() or :nextTick(), as
|
||||
-- we will die before the callback gets a chance to be run.
|
||||
|
||||
-- Common to ReaderRolling and ReaderPaging
|
||||
self.ui.view.footer_visible = false -- We want no footer on page image
|
||||
if self.ui.view.highlight.lighten_factor < 0.3 then
|
||||
self.ui.view.highlight.lighten_factor = 0.3 -- make lighten highlight a bit darker
|
||||
end
|
||||
self.ui.highlight.select_mode = false -- Remove any select mode icon
|
||||
|
||||
if self.ui.rolling then
|
||||
-- CRE documents: pages all have the aspect ratio of our screen (alt top status bar
|
||||
-- will be croped out after drawing), we will show them just as rendered.
|
||||
self.ui.rolling.rendering_state = nil -- Remove any partial rerendering icon
|
||||
if self.ui.view.view_mode == "scroll" then
|
||||
-- Get out of scroll mode, and be sure we'll be in one-page mode as that
|
||||
-- is what is shown in scroll mode (needs to do the following in that
|
||||
-- order to avoid rendering hash change)
|
||||
self.ui.rolling:onSetVisiblePages(1)
|
||||
self.ui.view:onSetViewMode("page")
|
||||
end
|
||||
if self.ui.document.configurable.font_gamma < 30 then -- Increase font gamma (if not already increased),
|
||||
self.ui.document:setGammaIndex(30) -- as downscaling will make text grayer
|
||||
end
|
||||
self.ui.document:setImageScaling(false) -- No need for smooth scaling as all will be downscaled
|
||||
-- (We keep "nighmode_images" as it was set: we may get and cache a different bb whether nightmode is on or off)
|
||||
self.ui.view.state.page = page -- Be on requested page
|
||||
self.ui.document:gotoPage(page) -- Current xpointer needs to be updated for some of what follows
|
||||
self.ui.bookmark:onPageUpdate(page) -- Update dogear state for this page
|
||||
self.ui.pagemap:onPageUpdate(page) -- Update pagemap labels for this page
|
||||
end
|
||||
|
||||
if self.ui.paging then
|
||||
-- With PDF/DJVU/Pics, we will show the native page (no reflow, no crop, no zoom
|
||||
-- to columns...). This makes thumbnail generation faster, and will allow the user
|
||||
-- to get an overview of the book native pages to better decide which option will
|
||||
-- be best to use for the book.
|
||||
-- We also want to get a thumbnail with the aspect ratio of the native page
|
||||
-- (so we don't get a native landscape page smallish and centered with blank above
|
||||
-- and below in a portrait thumbnail, if the screen is in portrait mode).
|
||||
|
||||
self.ui.view.hinting = false -- Disable hinting
|
||||
self.ui.view.page_scroll = false -- Get out of scroll mode
|
||||
self.ui.view.flipping_visible = false -- No page flipping icon
|
||||
self.ui.document.configurable.text_wrap = false -- Get out of reflow mode
|
||||
self.ui.document.configurable.trim_page = 3 -- Page crop: none
|
||||
-- self.ui.document.configurable.trim_page = 1 -- Page crop: auto (very slower)
|
||||
self.ui.document.configurable.auto_straighten = 0 -- No auto straighten
|
||||
-- We can let dewatermark if the user has enabled it, it helps
|
||||
-- limiting annoying eInk refreshes of light gray areas
|
||||
-- self.ui.document.configurable.page_opt = 0 -- No dewatermark
|
||||
-- We won't touch the contrast (to try making text less gray), as it applies on
|
||||
-- images that could get too dark.
|
||||
|
||||
-- Get native page dimensions, and update our target bb dimensions so it gets the
|
||||
-- same aspect ratio (we don't use native dimensions as is, as they may get huge)
|
||||
local dimen = self.ui.document:getPageDimensions(page, 1, 0)
|
||||
local scale_factor = math.min(target_w / dimen.w, target_h / dimen.h)
|
||||
target_w = math.floor(dimen.w * scale_factor)
|
||||
target_h = math.floor(dimen.h * scale_factor)
|
||||
dimen = Geom:new{ w=target_w, h=target_h }
|
||||
-- logger.info("getPageImage", page, dimen, "=>", target_w, target_h, scale_factor)
|
||||
|
||||
-- This seems to do it all well:
|
||||
-- local Event = require("ui/event")
|
||||
-- self.ui:handleEvent(Event:new("SetDimensions", dimen))
|
||||
-- self.ui.view.dogear[1].dimen.w = dimen.w -- (hack... its code uses the Screen width)
|
||||
-- self.ui:handleEvent(Event:new("PageUpdate", page))
|
||||
-- self.ui:handleEvent(Event:new("SetZoomMode", "page"))
|
||||
|
||||
-- Trying to do as little as needed, knowing the internals:
|
||||
self.ui.view:onSetDimensions(dimen)
|
||||
self.ui.view:onBBoxUpdate(nil) -- drop any bbox, draw native page
|
||||
self.ui.view.state.page = page
|
||||
self.ui.view.state.zoom = scale_factor
|
||||
self.ui.view.state.rotation = 0
|
||||
self.ui.view:recalculate()
|
||||
self.ui.view.dogear[1].dimen.w = dimen.w -- (hack... its code uses the Screen width)
|
||||
self.ui.bookmark:onPageUpdate(page) -- Update dogear state for this page
|
||||
end
|
||||
|
||||
-- Draw the page on a new BB with the targetted size
|
||||
local bb = Blitbuffer.new(target_w, target_h, self.bb_type)
|
||||
self.ui.view:paintTo(bb, 0, 0)
|
||||
|
||||
if self.ui.rolling then
|
||||
-- Crop out the top alt status bar if enabled
|
||||
local header_height = self.ui.document:getHeaderHeight()
|
||||
if header_height > 0 then
|
||||
bb = bb:viewport(0, header_height, bb.w, bb.h - header_height)
|
||||
end
|
||||
end
|
||||
|
||||
return bb
|
||||
end
|
||||
|
||||
function ReaderThumbnail:onCloseDocument()
|
||||
self:cancelPageThumbnailRequests()
|
||||
if self.tile_cache then
|
||||
self:logCacheSize()
|
||||
self.tile_cache:clear()
|
||||
self.tile_cache = nil
|
||||
end
|
||||
if self._standby_prevented then
|
||||
self._standby_prevented = false
|
||||
UIManager:allowStandby()
|
||||
end
|
||||
end
|
||||
|
||||
function ReaderThumbnail:onColorRenderingUpdate()
|
||||
self:setupColor()
|
||||
self:resetCache()
|
||||
end
|
||||
|
||||
-- CRE: emitted after a re-rendering
|
||||
ReaderThumbnail.onDocumentRerendered = ReaderThumbnail.resetCache
|
||||
ReaderThumbnail.onDocumentPartiallyRerendered = ReaderThumbnail.resetCache
|
||||
-- Emitted When adding/removing/updating bookmarks and highlights
|
||||
ReaderThumbnail.onAnnotationsModified = ReaderThumbnail.resetCachedPagesForBookmarks
|
||||
|
||||
return ReaderThumbnail
|
File diff suppressed because it is too large
Load Diff
@ -1,328 +0,0 @@
|
||||
local DataStorage = require("datastorage")
|
||||
local Event = require("ui/event")
|
||||
local FFIUtil = require("ffi/util")
|
||||
local InfoMessage = require("ui/widget/infomessage")
|
||||
local InputDialog = require("ui/widget/inputdialog")
|
||||
local UIManager = require("ui/uimanager")
|
||||
local Utf8Proc = require("ffi/utf8proc")
|
||||
local WidgetContainer = require("ui/widget/container/widgetcontainer")
|
||||
local lfs = require("libs/libkoreader-lfs")
|
||||
local logger = require("logger")
|
||||
local _ = require("gettext")
|
||||
local T = require("ffi/util").template
|
||||
|
||||
-- if sometime in the future crengine is updated to use normalized utf8 for hypenation
|
||||
-- this variable can be set to `true`. (see discussion in : https://github.com/koreader/crengine/pull/466),
|
||||
-- and some `if NORM then` branches can be simplified.
|
||||
local NORM = false
|
||||
|
||||
local ReaderUserHyph = WidgetContainer:extend{
|
||||
-- return values from setUserHyphenationDict (crengine's UserHyphDict::init())
|
||||
USER_DICT_RELOAD = 0,
|
||||
USER_DICT_NOCHANGE = 1,
|
||||
USER_DICT_MALFORMED = 2,
|
||||
USER_DICT_ERROR_NOT_SORTED = 3,
|
||||
}
|
||||
|
||||
-- returns path to the user dictionary
|
||||
function ReaderUserHyph:getDictionaryPath()
|
||||
return FFIUtil.joinPath(DataStorage:getSettingsDir(),
|
||||
"user-" .. tostring(self.ui.document:getTextMainLangDefaultHyphDictionary():gsub(".pattern$", "")) .. ".hyph")
|
||||
end
|
||||
|
||||
-- Load the user dictionary suitable for the actual language
|
||||
-- if reload==true, force a reload
|
||||
-- Unload is done automatically when a new dictionary is loaded.
|
||||
function ReaderUserHyph:loadDictionary(name, reload, no_scrubbing)
|
||||
local cre = require("document/credocument"):engineInit()
|
||||
if G_reader_settings:isTrue("hyph_user_dict") and lfs.attributes(name, "mode") == "file" then
|
||||
logger.dbg("set user hyphenation dict", name, reload, no_scrubbing)
|
||||
local ret = cre.setUserHyphenationDict(name, reload)
|
||||
-- this should only happen, if a user edits a dictionary by hand or the user messed
|
||||
-- with the dictionary file by hand. -> Warning and disable.
|
||||
if ret == self.USER_DICT_ERROR_NOT_SORTED then
|
||||
if no_scrubbing then
|
||||
UIManager:show(InfoMessage:new{
|
||||
text = T(_("The user dictionary\n%1\nis not alphabetically sorted.\n\nIt will be disabled now."), name),
|
||||
})
|
||||
logger.warn("UserHyph: Dictionary " .. name .. " is not sorted alphabetically.")
|
||||
G_reader_settings:makeFalse("hyph_user_dict")
|
||||
else
|
||||
self:scrubDictionary()
|
||||
self:loadDictionary(name, reload, true)
|
||||
end
|
||||
elseif ret == self.USER_DICT_MALFORMED then
|
||||
UIManager:show(InfoMessage:new{
|
||||
text = T(_("The user dictionary\n%1\nhas corrupted entries.\n\nOnly valid entries will be used."), name),
|
||||
})
|
||||
logger.warn("UserHyph: Dictionary " .. name .. " has corrupted entries.")
|
||||
end
|
||||
else
|
||||
logger.dbg("UserHyph: reset user hyphenation dict")
|
||||
cre.setUserHyphenationDict("", true) -- clear crengine user hyph dict
|
||||
end
|
||||
end
|
||||
|
||||
-- Reload on change of the hyphenation language
|
||||
function ReaderUserHyph:onTypographyLanguageChanged()
|
||||
self:loadUserDictionary()
|
||||
end
|
||||
|
||||
-- Reload on "ChangedUserDictionary" event,
|
||||
-- doesn't load dictionary if filesize and filename haven't changed
|
||||
-- if reload==true reload
|
||||
function ReaderUserHyph:loadUserDictionary(reload)
|
||||
self:loadDictionary(self:isAvailable() and self:getDictionaryPath() or "", reload and true or false)
|
||||
self.ui:handleEvent(Event:new("UpdatePos"))
|
||||
end
|
||||
|
||||
-- Functions to use with the UI
|
||||
|
||||
function ReaderUserHyph:isAvailable()
|
||||
return G_reader_settings:isTrue("hyph_user_dict") and self:_enabled()
|
||||
end
|
||||
|
||||
function ReaderUserHyph:_enabled()
|
||||
return self.ui.typography.hyphenation
|
||||
end
|
||||
|
||||
-- add Menu entry
|
||||
function ReaderUserHyph:getMenuEntry()
|
||||
return {
|
||||
text = _("Custom hyphenation rules"),
|
||||
help_text = _("The hyphenation of a word can be changed from its default by long pressing for 3 seconds and selecting 'Hyphenate'."),
|
||||
callback = function()
|
||||
local hyph_user_dict = not G_reader_settings:isTrue("hyph_user_dict")
|
||||
G_reader_settings:saveSetting("hyph_user_dict", hyph_user_dict)
|
||||
self:loadUserDictionary() -- not needed to force a reload here
|
||||
end,
|
||||
checked_func = function()
|
||||
return self:isAvailable()
|
||||
end,
|
||||
enabled_func = function()
|
||||
return self:_enabled()
|
||||
end,
|
||||
separator = true,
|
||||
}
|
||||
end
|
||||
|
||||
-- Helper functions for dictionary entries-------------------------------------------
|
||||
|
||||
-- checks if suggestion is well formated
|
||||
function ReaderUserHyph:checkHyphenation(suggestion, word)
|
||||
if suggestion:find("%-%-") then
|
||||
return false -- two or more consecutive '-'
|
||||
end
|
||||
|
||||
suggestion = suggestion:gsub("-","")
|
||||
if Utf8Proc.lowercase(suggestion, NORM) == Utf8Proc.lowercase(word, NORM) then
|
||||
return true -- characters match (case insensitive)
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function ReaderUserHyph:updateDictionary(word, hyphenation)
|
||||
if not word then
|
||||
logger.err("UserHyph: called without arguments")
|
||||
end
|
||||
local dict_file = self:getDictionaryPath()
|
||||
local new_dict_file = dict_file .. ".new"
|
||||
|
||||
local new_dict = io.open(new_dict_file, "w")
|
||||
if not new_dict then
|
||||
logger.err("UserHyph: could not open " .. new_dict_file)
|
||||
return
|
||||
end
|
||||
|
||||
if NORM then
|
||||
word = Utf8Proc.normalize_NFC(word)
|
||||
end
|
||||
|
||||
local word_lower = Utf8Proc.lowercase(word, NORM)
|
||||
local line
|
||||
|
||||
local dict = io.open(dict_file, "r")
|
||||
if dict then
|
||||
line = dict:read()
|
||||
if NORM then
|
||||
line = line and Utf8Proc.normalize_NFC(line)
|
||||
end
|
||||
--search entry
|
||||
while line and Utf8Proc.lowercase(line:sub(1, line:find(";") - 1), NORM) < word_lower do
|
||||
new_dict:write(line .. "\n")
|
||||
line = dict:read()
|
||||
if NORM then
|
||||
line = line and Utf8Proc.normalize_NFC(line)
|
||||
end
|
||||
end
|
||||
|
||||
-- last word = nil if EOF, else last_word=word if found in file, else last_word is word after the new entry
|
||||
if line then
|
||||
local last_word = Utf8Proc.lowercase(line:sub(1, line:find(";") - 1), NORM)
|
||||
if last_word == word_lower then
|
||||
line = nil -- word found
|
||||
end
|
||||
else
|
||||
line = nil -- EOF
|
||||
end
|
||||
end
|
||||
|
||||
-- write new entry
|
||||
if hyphenation and hyphenation ~= "" then
|
||||
new_dict:write(string.format("%s;%s\n", word, hyphenation))
|
||||
end
|
||||
|
||||
-- write old entry if there was one
|
||||
if line then
|
||||
new_dict:write(line .. "\n")
|
||||
end
|
||||
|
||||
if dict then
|
||||
repeat
|
||||
line = dict:read()
|
||||
if NORM then
|
||||
line = line and Utf8Proc.normalize_NFC(line)
|
||||
end
|
||||
if line then
|
||||
new_dict:write(line .. "\n")
|
||||
end
|
||||
until (not line)
|
||||
dict:close()
|
||||
os.remove(dict_file)
|
||||
end
|
||||
|
||||
new_dict:close()
|
||||
os.rename(new_dict_file, dict_file)
|
||||
|
||||
self:loadUserDictionary(true) -- dictionary has changed, force a reload here
|
||||
end
|
||||
|
||||
-- This is called when the file is badly sorted or has double entries (which should only happen
|
||||
-- if a user has edited the hyphenation file by hand).
|
||||
function ReaderUserHyph:scrubDictionary()
|
||||
logger.dbg("UserHyph: scrubbing and sorting user hyphenation dict")
|
||||
|
||||
local dict_file = self:getDictionaryPath()
|
||||
local dict = io.open(dict_file, "r")
|
||||
if not dict then
|
||||
return
|
||||
end
|
||||
|
||||
local dict_entries = {}
|
||||
|
||||
local line = dict:read()
|
||||
if NORM then
|
||||
line = line and Utf8Proc.normalize_NFC(line)
|
||||
end
|
||||
while line do
|
||||
table.insert(dict_entries, line)
|
||||
line = dict:read()
|
||||
if NORM then
|
||||
line = line and Utf8Proc.normalize_NFC(line)
|
||||
end
|
||||
end
|
||||
dict:close()
|
||||
|
||||
if #dict_entries == 1 then
|
||||
return
|
||||
end
|
||||
|
||||
table.sort(dict_entries, function(a,b) return Utf8Proc.lowercase(a, NORM) < Utf8Proc.lowercase(b, NORM) end)
|
||||
|
||||
-- remove double entries
|
||||
local later_key = Utf8Proc.lowercase(dict_entries[#dict_entries]:gsub(";.*$",""), NORM)
|
||||
for i = #dict_entries-1, 1, -1 do
|
||||
local former_key = Utf8Proc.lowercase(dict_entries[i]:gsub(";.*$",""), NORM)
|
||||
if later_key == former_key then
|
||||
logger.dbg("UserHyph: remove double entry", dict_entries[i])
|
||||
table.remove(dict_entries, i)
|
||||
end
|
||||
later_key = former_key
|
||||
end
|
||||
|
||||
local new_dict_file = dict_file .. ".new"
|
||||
|
||||
local new_dict = io.open(new_dict_file, "w")
|
||||
if not new_dict then
|
||||
logger.err("UserHyph: could not open " .. new_dict_file)
|
||||
return
|
||||
end
|
||||
|
||||
for i = 1, #dict_entries do
|
||||
new_dict:write(dict_entries[i], "\n")
|
||||
end
|
||||
new_dict:close()
|
||||
|
||||
os.remove(dict_file)
|
||||
os.rename(new_dict_file, dict_file)
|
||||
end
|
||||
|
||||
function ReaderUserHyph:modifyUserEntry(word)
|
||||
if word:find("[ ,;-%.]") then return end -- no button if more than one word
|
||||
|
||||
if not self.ui.document then return end
|
||||
|
||||
if NORM then
|
||||
word = Utf8Proc.normalize_NFC(word)
|
||||
end
|
||||
|
||||
local cre = require("document/credocument"):engineInit()
|
||||
local suggested_hyphenation = cre.getHyphenationForWord(word)
|
||||
|
||||
-- word may have some strange punctuation marks (as the upper dot),
|
||||
-- so we use crengine to trimm that.
|
||||
word = suggested_hyphenation:gsub("-","")
|
||||
|
||||
local input_dialog
|
||||
input_dialog = InputDialog:new{
|
||||
title = T(_("Hyphenate: %1"), word),
|
||||
description = _("Add hyphenation positions with hyphens ('-') or spaces (' ')."),
|
||||
input = suggested_hyphenation,
|
||||
old_hyph_lowercase = Utf8Proc.lowercase(suggested_hyphenation, NORM),
|
||||
input_type = "string",
|
||||
buttons = {
|
||||
{
|
||||
{
|
||||
text = _("Cancel"),
|
||||
id = "close",
|
||||
callback = function()
|
||||
UIManager:close(input_dialog)
|
||||
end,
|
||||
},
|
||||
{
|
||||
text = _("Remove"),
|
||||
callback = function()
|
||||
UIManager:close(input_dialog)
|
||||
self:updateDictionary(word)
|
||||
end,
|
||||
},
|
||||
{
|
||||
text = _("Save"),
|
||||
is_enter_default = true,
|
||||
callback = function()
|
||||
local new_suggestion = input_dialog:getInputText()
|
||||
new_suggestion = new_suggestion:gsub(" ","-") -- replace spaces with hyphens
|
||||
new_suggestion = new_suggestion:gsub("^-","") -- remove leading hypenations
|
||||
new_suggestion = new_suggestion:gsub("-$","") -- remove trailing hypenations
|
||||
|
||||
if self:checkHyphenation(new_suggestion, word) then
|
||||
-- don't save if no changes
|
||||
if Utf8Proc.lowercase(new_suggestion, NORM) ~= input_dialog.old_hyph_lowercase then
|
||||
self:updateDictionary(word, new_suggestion)
|
||||
end
|
||||
UIManager:close(input_dialog)
|
||||
else
|
||||
UIManager:show(InfoMessage:new{
|
||||
text = _("Invalid hyphenation!"),
|
||||
})
|
||||
end
|
||||
end,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
UIManager:show(input_dialog)
|
||||
input_dialog:onShowKeyboard()
|
||||
end
|
||||
|
||||
return ReaderUserHyph
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,451 @@
|
||||
local BD = require("ui/bidi")
|
||||
local Blitbuffer = require("ffi/blitbuffer")
|
||||
local Button = require("ui/widget/button")
|
||||
local CenterContainer = require("ui/widget/container/centercontainer")
|
||||
local CloseButton = require("ui/widget/closebutton")
|
||||
local Device = require("device")
|
||||
local Event = require("ui/event")
|
||||
local Font = require("ui/font")
|
||||
local FrameContainer = require("ui/widget/container/framecontainer")
|
||||
local Geom = require("ui/geometry")
|
||||
local GestureRange = require("ui/gesturerange")
|
||||
local HorizontalGroup = require("ui/widget/horizontalgroup")
|
||||
local HorizontalSpan = require("ui/widget/horizontalspan")
|
||||
local InputContainer = require("ui/widget/container/inputcontainer")
|
||||
local LineWidget = require("ui/widget/linewidget")
|
||||
local Math = require("optmath")
|
||||
local MovableContainer = require("ui/widget/container/movablecontainer")
|
||||
local OverlapGroup = require("ui/widget/overlapgroup")
|
||||
local ProgressWidget = require("ui/widget/progresswidget")
|
||||
local Size = require("ui/size")
|
||||
local TextWidget = require("ui/widget/textwidget")
|
||||
local UIManager = require("ui/uimanager")
|
||||
local VerticalGroup = require("ui/widget/verticalgroup")
|
||||
local VerticalSpan = require("ui/widget/verticalspan")
|
||||
local WidgetContainer = require("ui/widget/container/widgetcontainer")
|
||||
local _ = require("gettext")
|
||||
local Screen = Device.screen
|
||||
|
||||
local SkimToWidget = InputContainer:new{
|
||||
title_face = Font:getFace("x_smalltfont"),
|
||||
width = nil,
|
||||
height = nil,
|
||||
}
|
||||
|
||||
function SkimToWidget:init()
|
||||
self.medium_font_face = Font:getFace("ffont")
|
||||
self.screen_width = Screen:getWidth()
|
||||
self.screen_height = Screen:getHeight()
|
||||
self.span = math.ceil(self.screen_height * 0.01)
|
||||
self.width = math.floor(self.screen_width * 0.95)
|
||||
self.button_bordersize = Size.border.button
|
||||
-- the buttons need some kind of separation but maybe I should just implement
|
||||
-- margin_left and margin_right…
|
||||
self.button_margin = self.button_bordersize
|
||||
self.button_width = math.floor(self.screen_width * 0.16) - (2*self.button_margin)
|
||||
if Device:hasKeys() then
|
||||
self.key_events = {
|
||||
Close = { {"Back"}, doc = "close skimto page" }
|
||||
}
|
||||
end
|
||||
if Device:isTouchDevice() then
|
||||
self.ges_events = {
|
||||
TapProgress = {
|
||||
GestureRange:new{
|
||||
ges = "tap",
|
||||
range = Geom:new{
|
||||
x = 0, y = 0,
|
||||
w = self.screen_width,
|
||||
h = self.screen_height,
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
end
|
||||
local dialog_title = _("Skim")
|
||||
self.curr_page = self.ui:getCurrentPage()
|
||||
self.page_count = self.document:getPageCount()
|
||||
|
||||
local curr_page_display = tostring(self.curr_page)
|
||||
if self.ui.pagemap and self.ui.pagemap:wantsPageLabels() then
|
||||
curr_page_display = self.ui.pagemap:getCurrentPageLabel(true)
|
||||
end
|
||||
|
||||
local ticks_candidates = {}
|
||||
if self.ui.toc then
|
||||
local max_level = self.ui.toc:getMaxDepth()
|
||||
for i = 0, -max_level, -1 do
|
||||
local ticks = self.ui.toc:getTocTicks(i)
|
||||
table.insert(ticks_candidates, ticks)
|
||||
end
|
||||
-- find the finest toc ticks by sorting out the largest one
|
||||
table.sort(ticks_candidates, function(a, b) return #a > #b end)
|
||||
end
|
||||
if #ticks_candidates > 0 then
|
||||
self.ticks_candidates = ticks_candidates[1]
|
||||
end
|
||||
|
||||
local skimto_title = FrameContainer:new{
|
||||
padding = Size.padding.default,
|
||||
margin = Size.margin.title,
|
||||
bordersize = 0,
|
||||
TextWidget:new{
|
||||
text = dialog_title,
|
||||
face = self.title_face,
|
||||
bold = true,
|
||||
max_width = math.floor(self.screen_width * 0.95),
|
||||
},
|
||||
}
|
||||
|
||||
self.progress_bar = ProgressWidget:new{
|
||||
width = math.floor(self.screen_width * 0.9),
|
||||
height = Size.item.height_big,
|
||||
percentage = self.curr_page / self.page_count,
|
||||
ticks = self.ticks_candidates,
|
||||
tick_width = Size.line.medium,
|
||||
last = self.page_count,
|
||||
}
|
||||
self.skimto_progress = FrameContainer:new{
|
||||
padding = Size.padding.button,
|
||||
margin = Size.margin.small,
|
||||
bordersize = 0,
|
||||
self.progress_bar,
|
||||
}
|
||||
|
||||
local skimto_line = LineWidget:new{
|
||||
dimen = Geom:new{
|
||||
w = self.width,
|
||||
h = Size.line.thick,
|
||||
}
|
||||
}
|
||||
local skimto_bar = OverlapGroup:new{
|
||||
dimen = {
|
||||
w = self.width,
|
||||
h = skimto_title:getSize().h
|
||||
},
|
||||
skimto_title,
|
||||
CloseButton:new{ window = self, padding_top = Size.margin.title, },
|
||||
}
|
||||
local button_minus = Button:new{
|
||||
text = "-1",
|
||||
bordersize = self.button_bordersize,
|
||||
margin = self.button_margin,
|
||||
radius = 0,
|
||||
enabled = true,
|
||||
width = self.button_width,
|
||||
show_parent = self,
|
||||
callback = function()
|
||||
self:goToPage(self.curr_page - 1)
|
||||
end,
|
||||
}
|
||||
local button_minus_ten = Button:new{
|
||||
text = "-10",
|
||||
bordersize = self.button_bordersize,
|
||||
margin = self.button_margin,
|
||||
radius = 0,
|
||||
enabled = true,
|
||||
width = self.button_width,
|
||||
show_parent = self,
|
||||
callback = function()
|
||||
self:goToPage(self.curr_page - 10)
|
||||
end,
|
||||
}
|
||||
local button_plus = Button:new{
|
||||
text = "+1",
|
||||
bordersize = self.button_bordersize,
|
||||
margin = self.button_margin,
|
||||
radius = 0,
|
||||
enabled = true,
|
||||
width = self.button_width,
|
||||
show_parent = self,
|
||||
callback = function()
|
||||
self:goToPage(self.curr_page + 1)
|
||||
end,
|
||||
}
|
||||
local button_plus_ten = Button:new{
|
||||
text = "+10",
|
||||
bordersize = self.button_bordersize,
|
||||
margin = self.button_margin,
|
||||
radius = 0,
|
||||
enabled = true,
|
||||
width = self.button_width,
|
||||
show_parent = self,
|
||||
callback = function()
|
||||
self:goToPage(self.curr_page + 10)
|
||||
end,
|
||||
}
|
||||
self.current_page_text = Button:new{
|
||||
text = curr_page_display,
|
||||
bordersize = 0,
|
||||
margin = self.button_margin,
|
||||
radius = 0,
|
||||
padding = 0,
|
||||
enabled = true,
|
||||
width = math.floor(self.screen_width * 0.2) - (2*self.button_margin),
|
||||
show_parent = self,
|
||||
callback = function()
|
||||
self.callback_switch_to_goto()
|
||||
end,
|
||||
}
|
||||
|
||||
local chapter_next_text = "▷│"
|
||||
local chapter_prev_text = "│◁"
|
||||
local bookmark_next_text = "☆▷"
|
||||
local bookmark_prev_text = "◁☆"
|
||||
if BD.mirroredUILayout() then
|
||||
chapter_next_text, chapter_prev_text = chapter_prev_text, chapter_next_text
|
||||
bookmark_next_text, bookmark_prev_text = bookmark_prev_text, bookmark_next_text
|
||||
end
|
||||
local button_chapter_next = Button:new{
|
||||
text = chapter_next_text,
|
||||
bordersize = self.button_bordersize,
|
||||
margin = self.button_margin,
|
||||
radius = 0,
|
||||
enabled = true,
|
||||
width = self.button_width,
|
||||
show_parent = self,
|
||||
callback = function()
|
||||
local page = self:getNextChapter(self.curr_page)
|
||||
if page and page >=1 and page <= self.page_count then
|
||||
self:goToPage(page)
|
||||
end
|
||||
end,
|
||||
hold_callback = function()
|
||||
self:goToPage(self.page_count)
|
||||
end,
|
||||
}
|
||||
|
||||
local button_chapter_prev = Button:new{
|
||||
text = chapter_prev_text,
|
||||
bordersize = self.button_bordersize,
|
||||
margin = self.button_margin,
|
||||
radius = 0,
|
||||
enabled = true,
|
||||
width = self.button_width,
|
||||
show_parent = self,
|
||||
callback = function()
|
||||
local page = self:getPrevChapter(self.curr_page)
|
||||
if page and page >=1 and page <= self.page_count then
|
||||
self:goToPage(page)
|
||||
end
|
||||
end,
|
||||
hold_callback = function()
|
||||
self:goToPage(1)
|
||||
end,
|
||||
}
|
||||
|
||||
local button_bookmark_next = Button:new{
|
||||
text = bookmark_next_text,
|
||||
bordersize = self.button_bordersize,
|
||||
margin = self.button_margin,
|
||||
radius = 0,
|
||||
enabled = true,
|
||||
width = self.button_width,
|
||||
show_parent = self,
|
||||
callback = function()
|
||||
self:goToByEvent("GotoNextBookmarkFromPage")
|
||||
end,
|
||||
hold_callback = function()
|
||||
local page = self.ui.bookmark:getLastBookmarkedPageFromPage(self.ui:getCurrentPage())
|
||||
self:goToBookmark(page)
|
||||
end,
|
||||
}
|
||||
|
||||
local button_bookmark_prev = Button:new{
|
||||
text = bookmark_prev_text,
|
||||
bordersize = self.button_bordersize,
|
||||
margin = self.button_margin,
|
||||
radius = 0,
|
||||
enabled = true,
|
||||
width = self.button_width,
|
||||
show_parent = self,
|
||||
callback = function()
|
||||
self:goToByEvent("GotoPreviousBookmarkFromPage")
|
||||
end,
|
||||
hold_callback = function()
|
||||
local page = self.ui.bookmark:getFirstBookmarkedPageFromPage(self.ui:getCurrentPage())
|
||||
self:goToBookmark(page)
|
||||
end,
|
||||
}
|
||||
|
||||
local horizontal_span_up = HorizontalSpan:new{ width = math.floor(self.screen_width * 0.2) }
|
||||
local button_table_up = HorizontalGroup:new{
|
||||
align = "center",
|
||||
button_chapter_prev,
|
||||
button_bookmark_prev,
|
||||
horizontal_span_up,
|
||||
button_bookmark_next,
|
||||
button_chapter_next,
|
||||
}
|
||||
|
||||
local vertical_group_up = VerticalGroup:new{ align = "center" }
|
||||
local padding_span_up = VerticalSpan:new{ width = math.ceil(self.screen_height * 0.015) }
|
||||
table.insert(vertical_group_up, padding_span_up)
|
||||
table.insert(vertical_group_up, button_table_up)
|
||||
table.insert(vertical_group_up, padding_span_up)
|
||||
|
||||
local button_table_down = HorizontalGroup:new{
|
||||
align = "center",
|
||||
button_minus,
|
||||
button_minus_ten,
|
||||
self.current_page_text,
|
||||
button_plus_ten,
|
||||
button_plus,
|
||||
}
|
||||
local vertical_group_down = VerticalGroup:new{ align = "center" }
|
||||
local padding_span = VerticalSpan:new{ width = math.ceil(self.screen_height * 0.015) }
|
||||
table.insert(vertical_group_down, padding_span)
|
||||
table.insert(vertical_group_down, button_table_down)
|
||||
table.insert(vertical_group_down, padding_span)
|
||||
|
||||
self.skimto_frame = FrameContainer:new{
|
||||
radius = Size.radius.window,
|
||||
bordersize = Size.border.window,
|
||||
padding = 0,
|
||||
margin = 0,
|
||||
background = Blitbuffer.COLOR_WHITE,
|
||||
VerticalGroup:new{
|
||||
align = "center",
|
||||
skimto_bar,
|
||||
skimto_line,
|
||||
vertical_group_up,
|
||||
CenterContainer:new{
|
||||
dimen = Geom:new{
|
||||
w = skimto_line:getSize().w,
|
||||
h = self.skimto_progress:getSize().h,
|
||||
},
|
||||
self.skimto_progress,
|
||||
},
|
||||
vertical_group_down,
|
||||
}
|
||||
}
|
||||
self[1] = WidgetContainer:new{
|
||||
align = "center",
|
||||
dimen =Geom:new{
|
||||
x = 0, y = 0,
|
||||
w = self.screen_width,
|
||||
h = self.screen_height,
|
||||
},
|
||||
MovableContainer:new{
|
||||
-- alpha = 0.8,
|
||||
self.skimto_frame,
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
function SkimToWidget:update()
|
||||
if self.curr_page <= 0 then
|
||||
self.curr_page = 1
|
||||
end
|
||||
if self.curr_page > self.page_count then
|
||||
self.curr_page = self.page_count
|
||||
end
|
||||
self.progress_bar.percentage = self.curr_page / self.page_count
|
||||
local curr_page_display = tostring(self.curr_page)
|
||||
if self.ui.pagemap and self.ui.pagemap:wantsPageLabels() then
|
||||
curr_page_display = self.ui.pagemap:getCurrentPageLabel(true)
|
||||
end
|
||||
self.current_page_text:setText(curr_page_display, self.current_page_text.width)
|
||||
end
|
||||
|
||||
function SkimToWidget:addOriginToLocationStack(add_current)
|
||||
-- Only add the page from which we launched the SkimToWidget
|
||||
-- to the location stack, unless add_current = true
|
||||
if not self.orig_page_added_to_stack or add_current then
|
||||
self.ui.link:addCurrentLocationToStack()
|
||||
self.orig_page_added_to_stack = true
|
||||
end
|
||||
end
|
||||
|
||||
function SkimToWidget:getNextChapter(cur_pageno)
|
||||
local next_chapter = nil
|
||||
for i = 1, #self.ticks_candidates do
|
||||
if self.ticks_candidates[i] > cur_pageno then
|
||||
next_chapter = self.ticks_candidates[i]
|
||||
break
|
||||
end
|
||||
end
|
||||
return next_chapter
|
||||
end
|
||||
|
||||
function SkimToWidget:getPrevChapter(cur_pageno)
|
||||
local previous_chapter = nil
|
||||
for i = 1, #self.ticks_candidates do
|
||||
if self.ticks_candidates[i] >= cur_pageno then
|
||||
break
|
||||
end
|
||||
previous_chapter = self.ticks_candidates[i]
|
||||
end
|
||||
return previous_chapter
|
||||
end
|
||||
|
||||
function SkimToWidget:onCloseWidget()
|
||||
UIManager:setDirty(nil, function()
|
||||
return "ui", self.skimto_frame.dimen
|
||||
end)
|
||||
return true
|
||||
end
|
||||
|
||||
function SkimToWidget:onShow()
|
||||
UIManager:setDirty(self, function()
|
||||
return "ui", self.skimto_frame.dimen
|
||||
end)
|
||||
return true
|
||||
end
|
||||
|
||||
function SkimToWidget:goToPage(page)
|
||||
self.curr_page = page
|
||||
self:addOriginToLocationStack()
|
||||
self.ui:handleEvent(Event:new("GotoPage", self.curr_page))
|
||||
self:update()
|
||||
end
|
||||
|
||||
function SkimToWidget:goToBookmark(page)
|
||||
if page then
|
||||
self:addOriginToLocationStack()
|
||||
self.ui.bookmark:gotoBookmark(page)
|
||||
self.curr_page = self.ui:getCurrentPage()
|
||||
self:update()
|
||||
end
|
||||
end
|
||||
|
||||
function SkimToWidget:goToByEvent(event_name)
|
||||
if event_name then
|
||||
self:addOriginToLocationStack()
|
||||
self.ui:handleEvent(Event:new(event_name, false))
|
||||
-- add_current_location_to_stack=false, as we handled it here
|
||||
self.curr_page = self.ui:getCurrentPage()
|
||||
self:update()
|
||||
end
|
||||
end
|
||||
|
||||
function SkimToWidget:onAnyKeyPressed()
|
||||
UIManager:close(self)
|
||||
return true
|
||||
end
|
||||
|
||||
function SkimToWidget:onTapProgress(arg, ges_ev)
|
||||
if ges_ev.pos:intersectWith(self.progress_bar.dimen) then
|
||||
local perc = self.progress_bar:getPercentageFromPosition(ges_ev.pos)
|
||||
if not perc then
|
||||
return true
|
||||
end
|
||||
local page = Math.round(perc * self.page_count)
|
||||
self:addOriginToLocationStack()
|
||||
self.ui:handleEvent(Event:new("GotoPage", page ))
|
||||
self.curr_page = page
|
||||
self:update()
|
||||
elseif not ges_ev.pos:intersectWith(self.skimto_frame.dimen) then
|
||||
-- close if tap outside
|
||||
self:onClose()
|
||||
end
|
||||
-- otherwise, do nothing (it's easy missing taping a button)
|
||||
return true
|
||||
end
|
||||
|
||||
function SkimToWidget:onClose()
|
||||
UIManager:close(self)
|
||||
return true
|
||||
end
|
||||
|
||||
return SkimToWidget
|
@ -0,0 +1,20 @@
|
||||
local user_path = require("datastorage"):getDataDir() .. "/dictionaries.lua"
|
||||
local ok, dicts = pcall(dofile, user_path)
|
||||
|
||||
if ok then
|
||||
return dicts
|
||||
else
|
||||
return {
|
||||
-- tested dictionary applications
|
||||
{ "Aard2", "Aard2", false, "itkach.aard2", "aard2" },
|
||||
{ "Alpus", "Alpus", false, "com.ngcomputing.fora.android", "search" },
|
||||
{ "ColorDict", "ColorDict", false, "com.socialnmobile.colordict", "colordict" },
|
||||
{ "Eudic", "Eudic", false, "com.eusoft.eudic", "send" },
|
||||
{ "Fora", "Fora Dict", false, "com.ngc.fora", "search" },
|
||||
{ "GoldenFree", "GoldenDict Free", false, "mobi.goldendict.android.free", "send" },
|
||||
{ "GoldenPro", "GoldenDict Pro", false, "mobi.goldendict.android", "send" },
|
||||
{ "Kiwix", "Kiwix", false, "org.kiwix.kiwixmobile", "text" },
|
||||
{ "Mdict", "Mdict", false, "cn.mdict", "send" },
|
||||
{ "QuickDic", "QuickDic", false, "de.reimardoeffinger.quickdic", "quickdic" },
|
||||
}
|
||||
end
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue