diff --git a/.ci/after_success.sh b/.ci/after_success.sh index e655228ba..011b8d1be 100755 --- a/.ci/after_success.sh +++ b/.ci/after_success.sh @@ -5,7 +5,7 @@ source "${CI_DIR}/common.sh" set +e -make coverage +travis_retry make coverage pushd koreader-*/koreader luajit $(which luacov-coveralls) -v popd diff --git a/.editorconfig b/.editorconfig index 904eab0e8..3db0a7dde 100644 --- a/.editorconfig +++ b/.editorconfig @@ -18,6 +18,10 @@ trim_trailing_whitespace = false indent_style = tab indent_size = 8 +[Makefile.def] +indent_style = tab +indent_size = 4 + [*.{js,css,scss,sass,html,handlebars,tpl}] indent_style = space indent_size = 2 diff --git a/.gitignore b/.gitignore index 8eeaa94e4..bcf7e344c 100644 --- a/.gitignore +++ b/.gitignore @@ -8,13 +8,15 @@ lua-* .vimrc *.o tags -test/*.sdr +test/* *.tar *.log spec/unit/data doc/html - +git-rev emu +luacov.stats.out +trace-out.txt koreader-*.zip koreader-*.apk @@ -37,5 +39,6 @@ koreader-kobo-arm-linux-gnueabihf* koreader-emulator-i686-w64-mingw32 koreader-emulator-x86_64-linux-gnu koreader-emulator-x86_64-pc-linux-gnu +koreader-emulator-x86_64-apple-darwin* koreader-pocketbook-arm-obreey-linux-gnueabi koreader-ubuntu-touch-arm-linux-gnueabihf diff --git a/Makefile b/Makefile index 0435dc7cc..c94379363 100644 --- a/Makefile +++ b/Makefile @@ -63,7 +63,7 @@ ifneq ($(or $(EMULATE_READER),$(WIN32)),) cd $(INSTALL_DIR)/koreader/spec/front/unit && test -e data || \ ln -sf ../../test ./data else - cp -rfL $(KOR_BASE)/$(OUTPUT_DIR)/* $(INSTALL_DIR)/koreader/ + $(RCP) -fL $(KOR_BASE)/$(OUTPUT_DIR)/* $(INSTALL_DIR)/koreader/ endif for f in $(INSTALL_FILES); do \ ln -sf ../../$$f $(INSTALL_DIR)/koreader/; \ @@ -77,9 +77,9 @@ ifdef WIN32 cd $(INSTALL_DIR)/koreader && cp ../../$(WIN32_DIR)/*.dll . endif @echo "[*] Install plugins" - cp -r plugins/* $(INSTALL_DIR)/koreader/plugins/ + $(RCP) plugins/* $(INSTALL_DIR)/koreader/plugins/ @echo "[*] Installresources" - cp -rpL resources/fonts/* $(INSTALL_DIR)/koreader/fonts/ + $(RCP) -pL resources/fonts/* $(INSTALL_DIR)/koreader/fonts/ install -d $(INSTALL_DIR)/koreader/{screenshots,data/{dict,tessdata},fonts/host,ota} ifeq ($(or $(EMULATE_READER),$(WIN32)),) @echo "[*] Clean up, remove unused files for releases" @@ -107,7 +107,10 @@ test: $(MAKE) testfront coverage: $(INSTALL_DIR)/koreader/.luacov - cd $(INSTALL_DIR)/koreader && ./luajit $(shell which busted) -o verbose_print --coverage --exclude-tags=nocov + cd $(INSTALL_DIR)/koreader && \ + ./luajit $(shell which busted) -o verbose_print \ + --no-auto-insulate \ + --coverage --exclude-tags=nocov # coverage report summary cd $(INSTALL_DIR)/koreader && tail -n \ +$$(($$(grep -nm1 Summary luacov.report.out|cut -d: -f1)-1)) \ diff --git a/README.md b/README.md index 540b4fc94..c7e1f6883 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,9 @@ KOReader KOReader is a document viewer application, originally created for Kindle e-ink readers. It currently runs on Kindle, Kobo, PocketBook, Ubuntu Touch -and Android (2.3+) devices. Developers can also run Koreader emulator -for development purpose on desktop PC with Linux and Windows operating system. +and Android (2.3+) devices. Developers can also run KOReader emulator +for development purpose on desktop PC with Linux and Windows and +Mac OSX (experimental for now). Main features for users ----------------------- @@ -91,6 +92,16 @@ sudo apt-get install gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf sudo apt-get install gcc-mingw-w64-i686 g++-mingw-w64-i686 ``` +Packages pkg-config-arm-linux-gnueabihf and pkg-config-arm-linux-gnueabi may +block you to build for Kobo or Kindle, remove them if you got ld error, +`/usr/lib/gcc-cross/arm-linux-gnueabihf/4.8/../../../../arm-linux-gnueabihf/bin/ +ld: cannot find -lglib-2.0` + +Mac OSX users may need to install these tools: +``` +brew install nasm binutils libtool autoconf automake sdl2 +``` + A recent version of Android SDK/NDK and `ant` are needed in order to build Koreader for Android devices. ``` @@ -155,10 +166,10 @@ Then, run this command to build installable package for Android: ./kodev release android ``` -For emulating KOReader on Linux and Windows +For emulating KOReader on Linux, Windows and Mac OSX ------------- -To build an emulator on current Linux machine just run: +To build an emulator on current Linux or OSX machine: ``` ./kodev build ``` diff --git a/base b/base index b5c82144e..edfd1239c 160000 --- a/base +++ b/base @@ -1 +1 @@ -Subproject commit b5c82144e253844849ab66e55bdff363d4991c66 +Subproject commit edfd1239c685e930fd0918bab6eb80267555aead diff --git a/frontend/apps/filemanager/filemanager.lua b/frontend/apps/filemanager/filemanager.lua index 87436b9b8..c670774a0 100644 --- a/frontend/apps/filemanager/filemanager.lua +++ b/frontend/apps/filemanager/filemanager.lua @@ -229,10 +229,10 @@ function FileManager:init() self:handleEvent(Event:new("SetDimensions", self.dimen)) end -function FileManager:resetDimen(dimen) - self.dimen = dimen +function FileManager:reinit(path) + self.dimen = Screen:getSize() -- backup the root path and path items - self.root_path = self.file_chooser.path + self.root_path = path or self.file_chooser.path local path_items_backup = {} for k, v in pairs(self.file_chooser.path_items) do path_items_backup[k] = v diff --git a/frontend/apps/filemanager/filemanagerhistory.lua b/frontend/apps/filemanager/filemanagerhistory.lua index 7dfe9c076..5ab81ef5b 100644 --- a/frontend/apps/filemanager/filemanagerhistory.lua +++ b/frontend/apps/filemanager/filemanagerhistory.lua @@ -1,17 +1,11 @@ local InputContainer = require("ui/widget/container/inputcontainer") local CenterContainer = require("ui/widget/container/centercontainer") local ButtonDialog = require("ui/widget/buttondialog") -local lfs = require("libs/libkoreader-lfs") -local DataStorage = require("datastorage") local UIManager = require("ui/uimanager") -local DocSettings = require("docsettings") local Menu = require("ui/widget/menu") -local joinPath = require("ffi/util").joinPath local Screen = require("device").screen local _ = require("gettext") -local history_dir = DataStorage:getHistoryDir() - local FileManagerHistory = InputContainer:extend{ hist_menu_title = _("History"), } @@ -31,27 +25,8 @@ function FileManagerHistory:addToMainMenu(tab_item_table) end function FileManagerHistory:updateItemTable() - local ReaderUI = require("apps/reader/readerui") - self.hist = {} - - for f in lfs.dir(history_dir) do - local path = joinPath(history_dir, f) - if lfs.attributes(path, "mode") == "file" then - local name = DocSettings:getNameFromHistory(f) - table.insert(self.hist, { - date = lfs.attributes(path, "modification"), - text = name, - histfile = f, - callback = function() - ReaderUI:showReader( - DocSettings:getPathFromHistory(f).. "/" .. name) - end - }) - end - end - table.sort(self.hist, function(v1, v2) return v1.date > v2.date end) - - self.hist_menu:swithItemTable(self.hist_menu_title, self.hist) + self.hist_menu:swithItemTable(self.hist_menu_title, + require("readhistory").hist) end function FileManagerHistory:onSetDimensions(dimen) @@ -65,7 +40,7 @@ function FileManagerHistory:onMenuHold(item) { text = _("Remove this item from history"), callback = function() - os.remove(joinPath(history_dir, item.histfile)) + require("readhistory"):removeItem(item) self._manager:updateItemTable() UIManager:close(self.histfile_dialog) end, diff --git a/frontend/apps/reader/modules/readercropping.lua b/frontend/apps/reader/modules/readercropping.lua index 32f5648b2..c5a75b490 100644 --- a/frontend/apps/reader/modules/readercropping.lua +++ b/frontend/apps/reader/modules/readercropping.lua @@ -168,14 +168,20 @@ end function ReaderCropping:setCropZoomMode(confirmed) if confirmed then -- if original zoom mode is not "content", set zoom mode to "contentwidth" - self.ui:handleEvent(Event:new("SetZoomMode", - self.orig_zoom_mode:find("content") and self.orig_zoom_mode or "contentwidth")) + self:setZoomMode( + self.orig_zoom_mode:find("content") + and self.orig_zoom_mode + or "contentwidth") self.ui:handleEvent(Event:new("InitScrollPageStates")) else - self.ui:handleEvent(Event:new("SetZoomMode", self.orig_zoom_mode)) + self:setZoomMode(self.orig_zoom_mode) end end +function ReaderCropping:setZoomMode(mode) + self.ui:handleEvent(Event:new("SetZoomMode", mode)) +end + function ReaderCropping:onReadSettings(config) self.document.bbox = config:readSetting("bbox") end diff --git a/frontend/apps/reader/modules/readerfooter.lua b/frontend/apps/reader/modules/readerfooter.lua index 9333b1df3..a10d85dcf 100644 --- a/frontend/apps/reader/modules/readerfooter.lua +++ b/frontend/apps/reader/modules/readerfooter.lua @@ -18,9 +18,23 @@ local _ = require("gettext") local util = require("util") +local MODE = { + off = 0, + page_progress = 1, + time = 2, + pages_left = 3, + battery = 4, + percentage = 5, + book_time_to_read = 6, + chapter_time_to_read = 7, +} +local MODE_INDEX = {} +for k,v in pairs(MODE) do + MODE_INDEX[v] = k +end + local ReaderFooter = InputContainer:new{ - mode = 1, - visible = true, + mode = MODE.page_progress, pageno = nil, pages = nil, toc_level = 0, @@ -67,7 +81,6 @@ function ReaderFooter:init() face = Font:getFace(self.text_font_face, self.text_font_size), } self.text_width = self.progress_text:getSize().w + self.text_left_margin - self:applyFooterMode() self.progress_bar = ProgressWidget:new{ width = nil, -- width will be updated in self:resetLayout() height = self.bar_height, @@ -101,7 +114,8 @@ function ReaderFooter:init() } } - self.mode = G_reader_settings:readSetting("reader_footer_mode") or self.mode + self:applyFooterMode( + G_reader_settings:readSetting("reader_footer_mode") or self.mode) self:resetLayout() if self.settings.auto_refresh_time then @@ -295,7 +309,7 @@ function ReaderFooter:updateFooterText() if #ticks_candidates > 0 then self.progress_bar.ticks = ticks_candidates[1] - self.progress_bar.last = self.pages + self.progress_bar.last = self.pages or self.view.document:getPageCount() else -- we still set ticks here so self.progress_bar.ticks will not be -- initialized again if ticks_candidates is empty @@ -328,21 +342,23 @@ function ReaderFooter:updateFooterText() end self.progress_text:setText(table.concat(info, " | ")) else - local info = "" - if self.mode == 1 then + local info + if self.mode == MODE.page_progress then info = self:getProgressInfo() - elseif self.mode == 2 then + elseif self.mode == MODE.time then info = self:getTimeInfo() - elseif self.mode == 3 then + elseif self.mode == MODE.pages_left then info = self:getNextChapterInfo() - elseif self.mode == 4 then + elseif self.mode == MODE.battery then info = self:getBatteryInfo() - elseif self.mode == 5 then + elseif self.mode == MODE.percentage then info = self:getProgressPercentage() - elseif self.mode == 6 then + elseif self.mode == MODE.book_time_to_read then info = self:getBookTimeToRead() - elseif self.mode == 7 then + elseif self.mode == MODE.chapter_time_to_read then info = self:getChapterTimeToRead() + else + info = "" end self.progress_text:setText(info) end @@ -384,16 +400,12 @@ function ReaderFooter:applyFooterMode(mode) -- 6 for from statistics book time to read -- 7 for from statistics chapter time to read if mode ~= nil then self.mode = mode end - if self.mode == 0 then - self.view.footer_visible = false - else - self.view.footer_visible = true - end + self.view.footer_visible = (self.mode ~= MODE.off) end function ReaderFooter:onEnterFlippingMode() self.orig_mode = self.mode - self:applyFooterMode(1) + self:applyFooterMode(MODE.page_progress) end function ReaderFooter:onExitFlippingMode() @@ -410,30 +422,24 @@ function ReaderFooter:onTapFooter(arg, ges) self.ui:handleEvent(Event:new("GotoPercentage", percentage)) end else - self.mode = (self.mode + 1) % 8 - if self.settings.all_at_once and (self.mode > 1) then - self.mode = 0 - end - if (self.mode == 1) and not self.settings.page_progress then - self.mode = 2 - end - if (self.mode == 2) and not self.settings.time then - self.mode = 3 - end - if (self.mode == 3) and not self.settings.pages_left then - self.mode = 4 - end - if (self.mode == 4) and not self.settings.battery then - self.mode = 5 - end - if (self.mode == 5) and not self.settings.percentage then - self.mode = 6 - end - if (self.mode == 6) and not self.settings.book_time_to_read then - self.mode = 7 - end - if (self.mode == 7) and not self.settings.chapter_time_to_read then - self.mode = 0 + if self.settings.all_at_once then + if self.mode >= 1 then + self.mode = MODE.off + else + self.mode = MODE.page_progress + end + else + self.mode = (self.mode + 1) % 8 + for i, m in ipairs(MODE_INDEX) do + if self.mode == MODE.off then break end + if self.mode == i then + if self.settings[m] then + break + else + self.mode = (self.mode + 1) % 8 + end + end + end end self:applyFooterMode() G_reader_settings:saveSetting("reader_footer_mode", self.mode) @@ -443,13 +449,13 @@ function ReaderFooter:onTapFooter(arg, ges) end function ReaderFooter:onHoldFooter(arg, ges) - if self.mode == 0 then return end + if self.mode == MODE.off then return end self.ui:handleEvent(Event:new("ShowGotoDialog")) return true end function ReaderFooter:onSetStatusLine(status_line) - self.view.footer_visible = status_line == 1 and true or false + self.view.footer_visible = (status_line == 1) self.ui.document:setStatusLineProp(status_line) self.ui:handleEvent(Event:new("UpdatePos")) end diff --git a/frontend/apps/reader/modules/readerhighlight.lua b/frontend/apps/reader/modules/readerhighlight.lua index 9fbc4a5f0..cfbbebde7 100644 --- a/frontend/apps/reader/modules/readerhighlight.lua +++ b/frontend/apps/reader/modules/readerhighlight.lua @@ -66,7 +66,7 @@ end function ReaderHighlight:addToMainMenu(tab_item_table) -- insert table to main reader menu table.insert(tab_item_table.typeset, { - text = _("Highlight"), + text = _("Highlight options"), sub_item_table = self:genHighlightDrawerMenu(), }) end diff --git a/frontend/apps/reader/modules/readermenu.lua b/frontend/apps/reader/modules/readermenu.lua index 0d4724290..ffed21153 100644 --- a/frontend/apps/reader/modules/readermenu.lua +++ b/frontend/apps/reader/modules/readermenu.lua @@ -38,14 +38,14 @@ function ReaderMenu:init() self:onTapCloseMenu() self.ui:onClose() local FileManager = require("apps/filemanager/filemanager") + local lastdir = nil + local last_file = G_reader_settings:readSetting("lastfile") + if last_file then + lastdir = last_file:match("(.*)/") + end if FileManager.instance then - FileManager.instance:resetDimen(Screen:getSize()) + FileManager.instance:reinit(lastdir) else - local lastdir = nil - local last_file = G_reader_settings:readSetting("lastfile") - if last_file then - lastdir = last_file:match("(.*)/") - end FileManager:showFiles(lastdir) end end, diff --git a/frontend/apps/reader/modules/readerrolling.lua b/frontend/apps/reader/modules/readerrolling.lua index b4a08c0c1..271f1624e 100644 --- a/frontend/apps/reader/modules/readerrolling.lua +++ b/frontend/apps/reader/modules/readerrolling.lua @@ -357,11 +357,6 @@ end function ReaderRolling:onGotoViewRel(diff) DEBUG("goto relative screen:", diff, ", in mode: ", self.view.view_mode) - local prev_xp - -- save xpointer to check whether we reach the end of the book - if diff > 0 then - prev_xp = self.xpointer - end if self.view.view_mode == "scroll" then local pan_diff = diff * self.ui.dimen.h if self.show_overlap_enable then @@ -371,15 +366,20 @@ function ReaderRolling:onGotoViewRel(diff) pan_diff = pan_diff + self.overlap end end + local old_pos = self.current_pos self:_gotoPos(self.current_pos + pan_diff) + if diff > 0 and old_pos == self.current_pos then + self.ui:handleEvent(Event:new("EndOfBook")) + end elseif self.view.view_mode == "page" then local page_count = self.ui.document:getVisiblePageCount() + local old_page = self.current_page self:_gotoPage(self.current_page + diff*page_count) + if diff > 0 and old_page == self.current_page then + self.ui:handleEvent(Event:new("EndOfBook")) + end end self.xpointer = self.ui.document:getXPointer() - if self.xpointer == prev_xp then - self.ui:handleEvent(Event:new("EndOfBook")) - end return true end diff --git a/frontend/apps/reader/modules/readerview.lua b/frontend/apps/reader/modules/readerview.lua index 225ea21d7..16fe52a07 100644 --- a/frontend/apps/reader/modules/readerview.lua +++ b/frontend/apps/reader/modules/readerview.lua @@ -94,7 +94,6 @@ function ReaderView:addWidgets() self.footer = ReaderFooter:new{ view = self, ui = self.ui, - visible = self.footer_visible, } self.flipping = ReaderFlipping:new{ view = self, @@ -644,7 +643,9 @@ function ReaderView:onReadSettings(config) self.state.gamma = config:readSetting("gamma") or DGLOBALGAMMA local full_screen = config:readSetting("kopt_full_screen") or self.document.configurable.full_screen local status_line = config:readSetting("copt_status_line") or self.document.configurable.status_line - self.footer_visible = (full_screen == 0 or status_line == 1) and true or false + if full_screen == 0 or status_line == 0 then + self.footer_visible = false + end self:resetLayout() local page_scroll = config:readSetting("kopt_page_scroll") or self.document.configurable.page_scroll self.page_scroll = page_scroll == 1 and true or false diff --git a/frontend/apps/reader/readerui.lua b/frontend/apps/reader/readerui.lua index 1ddeaee52..d98664342 100644 --- a/frontend/apps/reader/readerui.lua +++ b/frontend/apps/reader/readerui.lua @@ -12,7 +12,7 @@ local Device = require("device") local Screen = require("device").screen local Event = require("ui/event") local Cache = require("cache") -local DEBUG = require("dbg") +local dbg = require("dbg") local T = require("ffi/util").template local _ = require("gettext") @@ -304,7 +304,7 @@ function ReaderUI:init() }) -- koreader plugins for _,plugin_module in ipairs(PluginLoader:loadPlugins()) do - DEBUG("Loaded plugin", plugin_module.name, "at", plugin_module.path) + dbg("Loaded plugin", plugin_module.name, "at", plugin_module.path) self:registerModule(plugin_module.name, plugin_module:new{ dialog = self.dialog, view = self.view, @@ -313,7 +313,7 @@ function ReaderUI:init() }) end - --DEBUG(self.doc_settings) + --dbg(self.doc_settings) -- we only read settings after all the widgets are initialized self:handleEvent(Event:new("ReadSettings", self.doc_settings)) @@ -328,19 +328,22 @@ function ReaderUI:init() end function ReaderUI:showReader(file) - DEBUG("show reader ui") + dbg("show reader ui") + require("readhistory"):addItem(file) if lfs.attributes(file, "mode") ~= "file" then UIManager:show(InfoMessage:new{ - text = T( _("File '%1' does not exist."), file) + text = T(_("File '%1' does not exist."), file) }) return end UIManager:show(InfoMessage:new{ - text = T( _("Opening file '%1'."), file), - timeout = 0.1, + text = T(_("Opening file '%1'."), file), + timeout = 0.0, }) + -- doShowReader might block for a long time, so force repaint here + UIManager:forceRePaint() UIManager:nextTick(function() - DEBUG("creating coroutine for showing reader") + dbg("creating coroutine for showing reader") local co = coroutine.create(function() self:doShowReader(file) end) @@ -353,12 +356,12 @@ function ReaderUI:showReader(file) end) end -local running_instance = nil +local _running_instance = nil function ReaderUI:doShowReader(file) - DEBUG("opening file", file) + dbg("opening file", file) -- keep only one instance running - if running_instance then - running_instance:onClose() + if _running_instance then + _running_instance:onClose() end local document = DocumentRegistry:openDocument(file) if not document then @@ -368,7 +371,7 @@ function ReaderUI:doShowReader(file) return end if document.is_locked then - DEBUG("document is locked") + dbg("document is locked") self._coroutine = coroutine.running() or self._coroutine self:unlockDocumentWithPassword(document) if coroutine.running() then @@ -385,15 +388,15 @@ function ReaderUI:doShowReader(file) document = document, } UIManager:show(reader) - running_instance = reader + _running_instance = reader end function ReaderUI:_getRunningInstance() - return running_instance + return _running_instance end function ReaderUI:unlockDocumentWithPassword(document, try_again) - DEBUG("show input password dialog") + dbg("show input password dialog") self.password_dialog = InputDialog:new{ title = try_again and _("Password is incorrect, try again?") or _("Input document password"), @@ -481,17 +484,17 @@ function ReaderUI:notifyCloseDocument() end function ReaderUI:onClose() - DEBUG("closing reader") + dbg("closing reader") self:saveSettings() if self.document ~= nil then - DEBUG("closing document") + dbg("closing document") self:notifyCloseDocument() end UIManager:close(self.dialog, "full") -- serialize last used items for later launch Cache:serialize() - if running_instance == self then - running_instance = nil + if _running_instance == self then + _running_instance = nil end return true end diff --git a/frontend/device/generic/device.lua b/frontend/device/generic/device.lua index 07f452c10..e8f671c6c 100644 --- a/frontend/device/generic/device.lua +++ b/frontend/device/generic/device.lua @@ -153,6 +153,9 @@ function Device:suspend() end -- Hardware specific method to resume the device function Device:resume() end +-- Hardware specific method to power off the device +function Device:powerOff() end + function Device:usbPlugIn() if self.charging_mode == false and self.screen_saver_mode == false then self.screen:saveCurrentBB() diff --git a/frontend/device/input.lua b/frontend/device/input.lua index e4f8ef878..016be3b90 100644 --- a/frontend/device/input.lua +++ b/frontend/device/input.lua @@ -258,20 +258,26 @@ function Input:handleKeyBoardEv(ev) end end - if ev.value == EVENT_VALUE_KEY_RELEASE then - if keycode == "Light" then - return keycode - elseif keycode == "Power" then - -- Kobo generates Power keycode only, we need to decide whether it's - -- power-on or power-off ourselves. - if self.device.screen_saver_mode then + if keycode == "Power" then + -- Kobo generates Power keycode only, we need to decide whether it's + -- power-on or power-off ourselves. + if self.device.screen_saver_mode then + if ev.value == EVENT_VALUE_KEY_RELEASE then return "Resume" - else - return "Suspend" + end + else + if ev.value == EVENT_VALUE_KEY_PRESS then + return "PowerPress" + elseif ev.value == EVENT_VALUE_KEY_RELEASE then + return "PowerRelease" end end end + if ev.value == EVENT_VALUE_KEY_RELEASE and keycode == "Light" then + return keycode + end + -- handle modifier keys if self.modifiers[keycode] ~= nil then if ev.value == EVENT_VALUE_KEY_PRESS then diff --git a/frontend/device/kobo/device.lua b/frontend/device/kobo/device.lua index bf8f5b580..8f96561ef 100644 --- a/frontend/device/kobo/device.lua +++ b/frontend/device/kobo/device.lua @@ -86,13 +86,16 @@ function Kobo:init() self.input = require("device/input"):new{ device = self, event_map = { - [59] = "Power_SleepCover", [90] = "Light", [102] = "Home", [116] = "Power", } } + if not G_reader_settings:readSetting("ignore_power_sleepcover") then + self.input.event_map[59] = "Power_SleepCover" + end + Generic.init(self) self.input.open("/dev/input/event0") -- Light button and sleep slider @@ -186,6 +189,10 @@ function Kobo:resume() end end +function Kobo:powerOff() + os.execute("poweroff") +end + -------------- device probe ------------ local codename = Kobo:getCodeName() diff --git a/frontend/docsettings.lua b/frontend/docsettings.lua index da67aa158..9032078c4 100644 --- a/frontend/docsettings.lua +++ b/frontend/docsettings.lua @@ -36,38 +36,37 @@ function DocSettings:purgeDocSettings(doc_path) end function DocSettings:open(docfile) - local history_path - local sidecar_path + -- TODO(zijiehe): Remove history_path, use only sidecar. + local new = { data = {} } + local ok, stored if docfile == ".reader" then -- we handle reader setting as special case - history_path = DataStorage:getDataDir() .. "/settings.reader.lua" + new.history_file = DataStorage:getDataDir() .. "/settings.reader.lua" + + ok, stored = pcall(dofile, new.history_file) else - history_path = self:getHistoryPath(docfile) + new.history_file = self:getHistoryPath(docfile) local sidecar = self:getSidecarDir(docfile) if lfs.attributes(sidecar, "mode") ~= "directory" then lfs.mkdir(sidecar) end - sidecar_path = sidecar.."/"..docfile:match(".*%/(.*)")..".lua" - end - -- construct settings obj - local new = { - history_file = history_path, - sidecar_file = sidecar_path, - data = {} - } - local ok, stored = pcall(dofile, new.sidecar_file or "") - if not ok then - ok, stored = pcall(dofile, new.history_file or "") + new.sidecar_file = sidecar.."/"..docfile:match(".*%/(.*)")..".lua" + + ok, stored = pcall(dofile, new.sidecar_file or "") if not ok then - -- try legacy conf path, for backward compatibility. this also - -- takes care of reader legacy setting - ok, stored = pcall(dofile, docfile..".kpdfview.lua") + ok, stored = pcall(dofile, new.history_file or "") + if not ok then + -- try legacy conf path, for backward compatibility. this also + -- takes care of reader legacy setting + ok, stored = pcall(dofile, docfile..".kpdfview.lua") + end end end if ok and stored then new.data = stored end + return setmetatable(new, { __index = DocSettings}) end diff --git a/frontend/readhistory.lua b/frontend/readhistory.lua new file mode 100644 index 000000000..e0c2c837e --- /dev/null +++ b/frontend/readhistory.lua @@ -0,0 +1,121 @@ +local lfs = require("libs/libkoreader-lfs") +local DataStorage = require("datastorage") +local DocSettings = require("docsettings") +local joinPath = require("ffi/util").joinPath +local dump = require("dump") + +local history_file = joinPath(DataStorage:getDataDir(), "history.lua") + +local ReadHistory = { + hist = {}, +} + +local function buildEntry(input_time, input_file) + return { + time = input_time, + text = input_file:gsub(".*/", ""), + file = input_file, + callback = function() + local ReaderUI = require("apps/reader/readerui") + ReaderUI:showReader(input_file) + end + } +end + +function ReadHistory:_sort() + for i = #self.hist, 1, -1 do + if lfs.attributes(self.hist[i].file, "mode") ~= "file" then + table.remove(self.hist, i) + end + end + table.sort(self.hist, function(l, r) return l.file < r.file end) + -- TODO(zijiehe): Use binary insert instead of a loop to deduplicate. + for i = #self.hist, 2, -1 do + if self.hist[i].file == self.hist[i - 1].file then + if self.hist[i].time < self.hist[i - 1].time then + table.remove(self.hist, i) + else + table.remove(self.hist,i - 1) + end + end + end + table.sort(self.hist, function(v1, v2) return v1.time > v2.time end) + -- TODO(zijiehe): Use binary search to find an item when deleting it. + for i = 1, #self.hist, 1 do + self.hist[i].index = i + end +end + +-- Reduces total count in hist list to a reasonable number by removing last +-- several items. +function ReadHistory:_reduce() + while #self.hist > 500 do + table.remove(self.hist, #self.hist) + end +end + +-- Flushes current history table into file. +function ReadHistory:_flush() + local content = {} + for k, v in pairs(self.hist) do + content[k] = { + time = v.time, + file = v.file + } + end + local f = io.open(history_file, "w") + f:write("return " .. dump(content) .. "\n") + f:close() +end + +-- Reads history table from file +function ReadHistory:_read() + local ok, data = pcall(dofile, history_file) + if ok then + for k, v in pairs(data) do + table.insert(self.hist, buildEntry(v.time, v.file)) + end + end +end + +-- Reads history from legacy history folder +function ReadHistory:_readLegacyHistory() + local history_dir = DataStorage:getHistoryDir() + for f in lfs.dir(history_dir) do + local path = joinPath(history_dir, f) + if lfs.attributes(path, "mode") == "file" then + local file = joinPath(DocSettings:getPathFromHistory(f), + DocSettings:getNameFromHistory(f)) + table.insert(self.hist, + buildEntry(lfs.attributes(path, "modification"), file)) + end + end +end + +function ReadHistory:_init() + self:_read() + self:_readLegacyHistory() + self:_sort() + self:_reduce() +end + +function ReadHistory:removeItem(item) + table.remove(self.hist, item.index) + os.remove(DocSettings:getHistoryPath(item.file)) + self:_flush() +end + +function ReadHistory:addItem(file) + if file ~= nil and lfs.attributes(file, "mode") == "file" then + table.insert(self.hist, 1, buildEntry(os.time(), file)) + -- TODO(zijiehe): We do not need to sort if we can use binary insert and + -- binary search. + self:_sort() + self:_reduce() + self:_flush() + end +end + +ReadHistory:_init() + +return ReadHistory diff --git a/frontend/ui/data/creoptions.lua b/frontend/ui/data/creoptions.lua index a18717f4d..05f1addbc 100644 --- a/frontend/ui/data/creoptions.lua +++ b/frontend/ui/data/creoptions.lua @@ -34,6 +34,16 @@ local CreOptions = { { icon = "resources/icons/appbar.column.two.large.png", options = { + { + name = "view_mode", + name_text = S.VIEW_MODE, + toggle = {S.VIEW_SCROLL, S.VIEW_PAGE}, + values = {1, 0}, + default_value = 0, + args = {"scroll", "page"}, + default_arg = "page", + event = "SetViewMode", + }, { name = "line_spacing", name_text = S.LINE_SPACING, @@ -91,7 +101,6 @@ local CreOptions = { event = "ChangeSize", args = {"decrease", "increase"}, alternate = false, - height = 60, } } }, @@ -129,16 +138,6 @@ local CreOptions = { { icon = "resources/icons/appbar.settings.large.png", options = { - { - name = "view_mode", - name_text = S.VIEW_MODE, - toggle = {S.VIEW_SCROLL, S.VIEW_PAGE}, - values = {1, 0}, - default_value = 0, - args = {"scroll", "page"}, - default_arg = "page", - event = "SetViewMode", - }, { name = "status_line", name_text = S.PROGRESS_BAR, diff --git a/frontend/ui/data/koptoptions.lua b/frontend/ui/data/koptoptions.lua index beb2c7004..4aa4efe8d 100644 --- a/frontend/ui/data/koptoptions.lua +++ b/frontend/ui/data/koptoptions.lua @@ -144,7 +144,6 @@ local KoptOptions = { event = "FineTuningFontSize", args = {-0.05, 0.05}, alternate = false, - height = 60, enabled_func = function(configurable) return enable_if_equals(configurable, "text_wrap", 1) end, diff --git a/frontend/ui/uimanager.lua b/frontend/ui/uimanager.lua index af850fec9..0509f54ac 100644 --- a/frontend/ui/uimanager.lua +++ b/frontend/ui/uimanager.lua @@ -27,6 +27,7 @@ local UIManager = { _zeromqs = {}, _refresh_stack = {}, _refresh_func_stack = {}, + _power_ev_handled = false, } function UIManager:init() @@ -46,15 +47,45 @@ function UIManager:init() -- suspend. So let's unschedule it when suspending, and restart it after -- resume. self:_initAutoSuspend() - self.event_handlers["Suspend"] = function(input_event) + self.event_handlers["Suspend"] = function() self:_stopAutoSuspend() - Device:onPowerEvent(input_event) + Device:onPowerEvent("Suspend") end - self.event_handlers["Resume"] = function(input_event) - Device:onPowerEvent(input_event) + self.event_handlers["Resume"] = function() + Device:onPowerEvent("Resume") self:sendEvent(Event:new("Resume")) self:_startAutoSuspend() end + self.event_handlers["PowerPress"] = function() + self._power_ev_handled = false + local showPowerOffDialog = function() + if self._power_ev_handled then return end + self._power_ev_handled = true + local ConfirmBox = require("ui/widget/confirmbox") + UIManager:show(ConfirmBox:new{ + text = _("Power off?"), + ok_callback = function() + local InfoMessage = require("ui/widget/infomessage") + + UIManager:show(InfoMessage:new{ + text = _("Powered off."), + }) + -- The message can fail to render if this is executed directly + UIManager:scheduleIn(0.1, function() + self:broadcastEvent(Event:new("Close")) + Device:powerOff() + end) + end, + }) + end + UIManager:scheduleIn(3, showPowerOffDialog) + end + self.event_handlers["PowerRelease"] = function() + if not self._power_ev_handled then + self._power_ev_handled = true + self.event_handlers["Suspend"]() + end + end self.event_handlers["Light"] = function() Device:getPowerDevice():toggleFrontlight() end @@ -198,8 +229,8 @@ function UIManager:schedule(time, action) break end else - -- for fairness, it's better to make p+1 is strictly less than p - -- might want to revisit here in the future + -- for fairness, it's better to make p+1 is strictly less than + -- p might want to revisit here in the future break end until e < s @@ -347,7 +378,7 @@ function UIManager:quit() end end --- transmit an event to registered widgets +-- transmit an event to an active widget function UIManager:sendEvent(event) if #self._window_stack == 0 then return end -- top level widget has first access to the event @@ -368,6 +399,20 @@ function UIManager:sendEvent(event) end end +-- transmit an event to all registered widgets +function UIManager:broadcastEvent(event) + -- the widget's event handler might close widgets in which case + -- a simple iterator like ipairs would skip over some entries + local i = 1 + while (i <= #self._window_stack) do + local prev_widget = self._window_stack[i].widget + self._window_stack[i].widget:handleEvent(event) + if (self._window_stack[i].widget == prev_widget) then + i = i + 1 + end + end +end + function UIManager:_checkTasks() local now = { util.gettime() } local now_us = now[1] * MILLION + now[2] @@ -521,6 +566,10 @@ function UIManager:_repaint() self.refresh_counted = false end +function UIManager:forceRePaint() + self:_repaint() +end + function UIManager:setInputTimeout(timeout) self.INPUT_TIMEOUT = timeout or 200*1000 end diff --git a/frontend/ui/widget/bookstatuswidget.lua b/frontend/ui/widget/bookstatuswidget.lua index a727143f9..9ad75146f 100644 --- a/frontend/ui/widget/bookstatuswidget.lua +++ b/frontend/ui/widget/bookstatuswidget.lua @@ -57,7 +57,7 @@ function BookStatusWidget:init() self.stats = { total_time_in_sec = 0, performance_in_pages = {}, - pages = self.document:getPageCount(), + total_pages = self.document:getPageCount(), } self:getStatisticsSettings() if self.settings then @@ -123,8 +123,8 @@ function BookStatusWidget:getStatHours(stats) end function BookStatusWidget:getReadPages(stats) - if stats and stats.performance_in_pages and stats.pages then - return util.tableSize(stats.performance_in_pages) .. "/" .. stats.pages + if stats and stats.performance_in_pages and stats.total_pages then + return util.tableSize(stats.performance_in_pages) .. "/" .. stats.total_pages end return "none" end @@ -253,12 +253,14 @@ function BookStatusWidget:genBookInfoGroup() } ) -- progress bar - local total_pages = self.document:getPageCount() + local total_pages = self.stats.total_pages local read_percentage = self.view.state.page / total_pages local progress_bar = ProgressWidget:new{ width = width * 0.7, height = Screen:scaleBySize(10), percentage = read_percentage, + ticks = nil, + last = nil, } table.insert(book_meta_info_group, CenterContainer:new{ diff --git a/frontend/ui/widget/configdialog.lua b/frontend/ui/widget/configdialog.lua index 786ec8a22..4ccfd5e2e 100644 --- a/frontend/ui/widget/configdialog.lua +++ b/frontend/ui/widget/configdialog.lua @@ -353,9 +353,14 @@ function ConfigOption:init() if self.options[c].toggle then local max_toggle_width = Screen:getWidth() / 2 - local toggle_width = Screen:scaleBySize(self.options[c].width or 216) + local toggle_width = Screen:scaleBySize(self.options[c].width + or 216) + local row_count = self.options[c].row_count or 1 + local toggle_height = Screen:scaleBySize(self.options[c].height + or 30 * row_count) local switch = ToggleSwitch:new{ width = math.min(max_toggle_width, toggle_width), + height = toggle_height, font_face = item_font_face, font_size = item_font_size, name = self.options[c].name, @@ -368,6 +373,7 @@ function ConfigOption:init() events = self.options[c].events, config = self.config, enabled = enabled, + row_count = row_count, } local position = current_item switch:setPosition(position) diff --git a/frontend/ui/widget/imagewidget.lua b/frontend/ui/widget/imagewidget.lua index d17511d46..9ed5540dd 100644 --- a/frontend/ui/widget/imagewidget.lua +++ b/frontend/ui/widget/imagewidget.lua @@ -87,7 +87,7 @@ function ImageWidget:_render() error("cannot render image") end local native_w, native_h = self._bb:getWidth(), self._bb:getHeight() - local w, h + local w, h = self.width, self.height if self.autoscale then local dpi_scale = Screen:getDPI() / 167 -- rounding off to power of 2 to avoid alias with pow(2, floor(log(x)/log(2)) diff --git a/frontend/ui/widget/progresswidget.lua b/frontend/ui/widget/progresswidget.lua index 7500622f1..ba347f869 100644 --- a/frontend/ui/widget/progresswidget.lua +++ b/frontend/ui/widget/progresswidget.lua @@ -14,7 +14,7 @@ Configurable attributes: * rectcolor -- infill color * ticks (list) -- default to nil, use this if you want to insert markers * tick_width - * last -- maximum tick + * last -- maximum tick, used with ticks Example: @@ -68,7 +68,7 @@ function ProgressWidget:paintTo(bb, x, y) bb:paintRect(x+self.margin_h, math.ceil(y+self.margin_v+self.bordersize), math.ceil((my_size.w-2*self.margin_h)*self.percentage), my_size.h-2*(self.margin_v+self.bordersize), self.rectcolor) - if self.ticks then + if self.ticks and self.last then for i=1, #self.ticks do bb:paintRect( x + (my_size.w-2*self.margin_h)*(self.ticks[i]/self.last), diff --git a/frontend/ui/widget/scrolltextwidget.lua b/frontend/ui/widget/scrolltextwidget.lua index 282ebfa19..6d2494d93 100644 --- a/frontend/ui/widget/scrolltextwidget.lua +++ b/frontend/ui/widget/scrolltextwidget.lua @@ -4,10 +4,11 @@ local VerticalScrollBar = require("ui/widget/verticalscrollbar") local Geom = require("ui/geometry") local GestureRange = require("ui/gesturerange") local UIManager = require("ui/uimanager") -local Screen = require("device").screen +local Device = require("device") +local Screen = Device.screen +local Input = Device.input local HorizontalGroup = require("ui/widget/horizontalgroup") local HorizontalSpan = require("ui/widget/horizontalspan") -local Device = require("device") local Blitbuffer = require("ffi/blitbuffer") --[[ @@ -64,13 +65,20 @@ function ScrollTextWidget:init() }, } end + if Device:hasKeyboard() or Device:hasKeys() then + self.key_events = { + ScrollDown = {{Input.group.PgFwd}, doc = "scroll down"}, + ScrollUp = {{Input.group.PgBack}, doc = "scroll up"}, + } + end end -function ScrollTextWidget:onScrollText(arg, ges) - if ges.direction == "north" then +function ScrollTextWidget:scrollText(direction) + if direction == 0 then return end + if direction > 0 then low, high = self.text_widget:scrollDown() self.v_scroll_bar:set(low, high) - elseif ges.direction == "south" then + else low, high = self.text_widget:scrollUp() self.v_scroll_bar:set(low, high) end @@ -79,4 +87,23 @@ function ScrollTextWidget:onScrollText(arg, ges) end) end +function ScrollTextWidget:onScrollText(arg, ges) + if ges.direction == "north" then + self:scrollText(1) + elseif ges.direction == "south" then + self:scrollText(-1) + end + return true +end + +function ScrollTextWidget:onScrollDown() + self:scrollText(1) + return true +end + +function ScrollTextWidget:onScrollUp() + self:scrollText(-1) + return true +end + return ScrollTextWidget diff --git a/frontend/ui/widget/toggleswitch.lua b/frontend/ui/widget/toggleswitch.lua index 8d6dcad4b..79419f20d 100644 --- a/frontend/ui/widget/toggleswitch.lua +++ b/frontend/ui/widget/toggleswitch.lua @@ -3,6 +3,7 @@ local InputContainer = require("ui/widget/container/inputcontainer") local FrameContainer = require("ui/widget/container/framecontainer") local CenterContainer = require("ui/widget/container/centercontainer") local HorizontalGroup = require("ui/widget/horizontalgroup") +local VerticalGroup = require("ui/widget/verticalgroup") local Font = require("ui/font") local Geom = require("ui/geometry") local RenderText = require("ui/rendertext") @@ -31,10 +32,12 @@ local ToggleSwitch = InputContainer:new{ font_face = "cfont", font_size = 16, enabled = true, + row_count = 1, } function ToggleSwitch:init() - self.n_pos = #self.toggle + -- Item count per row + self.n_pos = math.ceil(#self.toggle / self.row_count) self.position = nil self.toggle_frame = FrameContainer:new{ @@ -45,16 +48,24 @@ function ToggleSwitch:init() padding = 2, dim = not self.enabled, } - self.toggle_content = HorizontalGroup:new{} - for i=1,#self.toggle do + self.toggle_content = VerticalGroup:new{} + for i = 1, self.row_count do + table.insert(self.toggle_content, HorizontalGroup:new{}) + end + + local center_dimen = Geom:new{ + w = self.width / self.n_pos, + h = self.height / self.row_count, + } + for i = 1, #self.toggle do local label = ToggleLabel:new{ align = "center", text = self.toggle[i], face = Font:getFace(self.font_face, self.font_size), } local content = CenterContainer:new{ - dimen = Geom:new{w = self.width/self.n_pos, h = self.height}, + dimen = center_dimen, label, } local button = FrameContainer:new{ @@ -66,7 +77,7 @@ function ToggleSwitch:init() padding = 0, content, } - table.insert(self.toggle_content, button) + table.insert(self.toggle_content[math.ceil(i / self.n_pos)], button) end self.toggle_frame[1] = self.toggle_content @@ -94,15 +105,19 @@ end function ToggleSwitch:update() local pos = self.position - for i=1,#self.toggle_content do - if pos == i then - self.toggle_content[i].color = self.fgcolor - self.toggle_content[i].background = self.fgcolor - self.toggle_content[i][1][1].fgcolor = Blitbuffer.COLOR_WHITE - else - self.toggle_content[i].color = self.bgcolor - self.toggle_content[i].background = self.bgcolor - self.toggle_content[i][1][1].fgcolor = Blitbuffer.COLOR_BLACK + for i = 1, #self.toggle_content do + local row = self.toggle_content[i] + for j = 1, #row do + local cell = row[j] + if pos == (i - 1) * self.n_pos + j then + cell.color = self.fgcolor + cell.background = self.fgcolor + cell[1][1].fgcolor = Blitbuffer.COLOR_WHITE + else + cell.color = self.bgcolor + cell.background = self.bgcolor + cell[1][1].fgcolor = Blitbuffer.COLOR_BLACK + end end end end @@ -124,11 +139,15 @@ function ToggleSwitch:togglePosition(position) self:update() end +function ToggleSwitch:calculatePosition(gev) + local x = (gev.pos.x - self.dimen.x) / self.dimen.w * self.n_pos + local y = (gev.pos.y - self.dimen.y) / self.dimen.h * self.row_count + return math.ceil(x) + math.floor(y) * self.n_pos +end + function ToggleSwitch:onTapSelect(arg, gev) if not self.enabled then return true end - local position = math.ceil( - (gev.pos.x - self.dimen.x) / self.dimen.w * self.n_pos - ) + local position = self:calculatePosition(gev) self:togglePosition(position) --[[ if self.values then @@ -152,9 +171,7 @@ function ToggleSwitch:onTapSelect(arg, gev) end function ToggleSwitch:onHoldSelect(arg, gev) - local position = math.ceil( - (gev.pos.x - self.dimen.x) / self.dimen.w * self.n_pos - ) + local position = self:calculatePosition(gev) self.config:onMakeDefault(self.name, self.name_text, self.values or self.args, self.toggle, position) return true diff --git a/kodev b/kodev index bddd743b2..09e689d4b 100755 --- a/kodev +++ b/kodev @@ -3,16 +3,17 @@ CURDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" function assert_ret_zero { - if [ $1 -ne 0 ]; then - if [ ! -z $2 ]; then - echo $2 + if [ "$1" -ne 0 ]; then + if [ ! -z "$2" ]; then + echo "$2" fi exit 1 fi } function setup_env { - files=`ls -d ./koreader-emulator-*/koreader` + files=$(ls -d ./koreader-emulator-*/koreader) + assert_ret_zero $? "Emulator not found, please build it first." export EMU_DIR=${files[0]} } @@ -43,8 +44,8 @@ TARGET: ${SUPPORTED_TARGETS}" while [[ $1 == '-'* ]]; do - PARAM=`echo $1 | awk -F= '{print $1}'` - VALUE=`echo $1 | awk -F= '{print $2}'` + PARAM=$(echo "$1" | awk -F= '{print $1}') + VALUE=$(echo "$1" | awk -F= '{print $2}') case $PARAM in -v | --verbose) export VERBOSE=1 @@ -76,7 +77,7 @@ ${SUPPORTED_TARGETS}" assert_ret_zero $? ;; android) - if [ ! -d ${CURDIR}/base/toolchain/android-toolchain ]; then + if [ ! -d "${CURDIR}/base/toolchain/android-toolchain" ]; then make android-toolchain assert_ret_zero $? fi @@ -84,7 +85,7 @@ ${SUPPORTED_TARGETS}" assert_ret_zero $? ;; pocketbook) - if [ ! -d ${CURDIR}/base/toolchain/pocketbook-toolchain ]; then + if [ ! -d "${CURDIR}/base/toolchain/pocketbook-toolchain" ]; then make pocketbook-toolchain assert_ret_zero $? fi @@ -130,7 +131,7 @@ ${SUPPORTED_TARGETS}" ;; android) make TARGET=android clean - rm -f *.apk + rm -f ./*.apk ;; pocketbook) make TARGET=pocketbook clean @@ -202,7 +203,7 @@ ${SUPPORTED_RELEASE_TARGETS}" function kodev-wbuilder { kodev-build echo "[*] Running wbuilder.lua..." - pushd ${EMU_DIR} + pushd "${EMU_DIR}" EMULATE_READER_W=540 EMULATE_READER_H=720 ./luajit ./utils/wbuilder.lua popd } @@ -221,8 +222,8 @@ OPTIONS: screen_width=540 screen_height=720 while [[ $1 == '-'* ]]; do - PARAM=`echo $1 | awk -F= '{print $1}'` - VALUE=`echo $1 | awk -F= '{print $2}'` + PARAM=$(echo "$1" | awk -F= '{print $1}') + VALUE=$(echo "$1" | awk -F= '{print $2}') case $PARAM in --disable-touch) export DISABLE_TOUCH=1 @@ -256,18 +257,18 @@ OPTIONS: setup_env fi - if [ ! -d ${EMU_DIR} ]; then + if [ ! -d "${EMU_DIR}" ]; then echo "Failed to find emulator directory! Please try build command first." exit 1 fi - echo "[*] Running KOReader with arguments: $@..." - pushd ${EMU_DIR} + echo "[*] Running KOReader with arguments: $*..." + pushd "${EMU_DIR}" if [ $# -lt 1 ]; then args=${CURDIR}/test else - args="$@" + args="$*" [[ $args != /* ]] && args="${CURDIR}/${args}" fi @@ -287,8 +288,8 @@ OPTIONS: --tags=TAGS only run tests with given tags " while [[ $1 == '-'* ]]; do - PARAM=`echo $1 | awk -F= '{print $1}'` - VALUE=`echo $1 | awk -F= '{print $2}'` + PARAM=$(echo "$1" | awk -F= '{print $1}') + VALUE=$(echo "$1" | awk -F= '{print $2}') case $PARAM in --tags) opts="--tags=${VALUE}" @@ -312,19 +313,19 @@ OPTIONS: fi setup_env - make ${EMU_DIR}/.busted - pushd ${EMU_DIR} + make "${EMU_DIR}/.busted" + pushd "${EMU_DIR}" - test_path=./spec/$1/unit + test_path="./spec/$1/unit" - if [ ! -z $2 ]; then + if [ ! -z "$2" ]; then test_path="${test_path}/$2" fi - busted --lua=./luajit ${opts} \ + busted --lua="./luajit ${opts}" \ --no-auto-insulate \ --lazy \ - -o ./spec/$1/unit/verbose_print \ - --exclude-tags=notest ${test_path} + -o "./spec/$1/unit/verbose_print" \ + --exclude-tags=notest "${test_path}" popd } @@ -387,38 +388,38 @@ case $1 in activate) echo "adding ${CURDIR} to \$PATH..." export PATH="${PATH}:${CURDIR}" - eval $(luarocks path bin) - exec ${SHELL} + eval "$(luarocks path bin)" + exec "${SHELL}" ;; fetch-thirdparty) kodev-fetch-thirdparty ;; clean) shift 1 - kodev-clean $@ + kodev-clean "$@" ;; build) shift 1 - kodev-build $@ + kodev-build "$@" ;; release) shift 1 - kodev-release $@ + kodev-release "$@" ;; wbuilder) kodev-wbuilder ;; run) shift 1 - kodev-run $@ + kodev-run "$@" ;; test) shift 1 - kodev-test $@ + kodev-test "$@" ;; log) shift 1 - kodev-log $@ + kodev-log "$@" ;; --help | -h) echo "${HELP_MSG}" diff --git a/platform/kobo/koreader.sh b/platform/kobo/koreader.sh index 8c7016ecd..9647f3dc8 100755 --- a/platform/kobo/koreader.sh +++ b/platform/kobo/koreader.sh @@ -35,22 +35,47 @@ if pkill -0 nickel ; then FROM_NICKEL="true" fi -if [ "${FROM_NICKEL}" == "true" ] ; then - # Siphon a few things from nickel's env... - eval "$(xargs -n 1 -0 < /proc/$(pidof nickel)/environ | grep -e DBUS_SESSION_BUS_ADDRESS -e WIFI_MODULE -e PLATFORM -e WIFI_MODULE_PATH -e INTERFACE -e PRODUCT 2>/dev/null)" - export DBUS_SESSION_BUS_ADDRESS WIFI_MODULE PLATFORM WIFI_MODULE_PATH INTERFACE PRODUCT +if [ "${FROM_NICKEL}" = "true" ] ; then + # Detect if we were started from KFMon + FROM_KFMON="false" + if pkill -0 kfmon ; then + # That's a start, now check if KFMon truly is our parent... + if [ "$(pidof kfmon)" -eq "${PPID}" ] ; then + FROM_KFMON="true" + fi + fi + + if [ "${FROM_KFMON}" = "true" ] ; then + # Siphon nickel's full environment, since KFMon inherits such a minimal one, and that apparently confuses the hell out of Nickel for some reason if we decide to restart it without a reboot... + for env in $(xargs -n 1 -0 < /proc/$(pidof nickel)/environ) ; do + export ${env} + done + else + # Siphon a few things from nickel's env... + eval "$(xargs -n 1 -0 < /proc/$(pidof nickel)/environ | grep -e DBUS_SESSION_BUS_ADDRESS -e WIFI_MODULE -e PLATFORM -e WIFI_MODULE_PATH -e INTERFACE -e PRODUCT 2>/dev/null)" + export DBUS_SESSION_BUS_ADDRESS WIFI_MODULE PLATFORM WIFI_MODULE_PATH INTERFACE PRODUCT + fi # flush disks, might help avoid trashing nickel's DB... sync + # Double the fun! + sleep 1 + sync # stop kobo software because it's running - killall nickel hindenburg fmon 2>/dev/null + killall nickel hindenburg sickel fickel fmon 2>/dev/null + + # NOTE: Not particularly critical, we should be safe leaving it up, but since we reboot on exit anyway... + # Keep KFMon up for now to make sure it's not doing anything overly stupid we might have overlooked ;). + #if [ "${FROM_KFMON}" == "true" ] ; then + # killall kfmon 2>/dev/null + #fi fi # fallback for old fmon (and advboot) users (-> if no args were passed to the sript, start the FM) if [ "$#" -eq 0 ] ; then args="/mnt/onboard" else - args="$@" + args="$*" fi # check whether PLATFORM & PRODUCT have a value assigned by rcS @@ -68,7 +93,7 @@ if [ ! -n "${PLATFORM}" ] ; then PLATFORM="${CPU}-ntx" fi - if [ "${PLATFORM}" == "freescale" ] ; then + if [ "${PLATFORM}" = "freescale" ] ; then if [ ! -s "/lib/firmware/imx/epdc_E60_V220.fw" ] ; then mkdir -p "/lib/firmware/imx" dd if="/dev/mmcblk0" bs=512K skip=10 count=1 | zcat > "/lib/firmware/imx/epdc_E60_V220.fw" @@ -88,9 +113,15 @@ fi ./reader.lua "${args}" > crash.log 2>&1 -if [ "${FROM_NICKEL}" == "true" ] ; then - # start kobo software because it was running before koreader - ./nickel.sh & +if [ "${FROM_NICKEL}" = "true" ] ; then + if [ "${FROM_KFMON}" != "true" ] ; then + # start kobo software because it was running before koreader + ./nickel.sh & + else + # If we were called from KFMon, just reboot, because there's always a (hopefully slim to nonexistent, now) chance Nickel will get its panties in a serious twist on restore for one reason or another... + # And at best, we'd still restart with broken suspend behavior anyway... + reboot + fi else # if we were called from advboot then we must reboot to go to the menu # NOTE: This is actually achieved by checking if KSM or a KSM-related script is running: diff --git a/spec/unit/device_spec.lua b/spec/unit/device_spec.lua index ac26c3228..bfdc4b0ba 100644 --- a/spec/unit/device_spec.lua +++ b/spec/unit/device_spec.lua @@ -94,5 +94,107 @@ describe("device module", function() os.getenv:revert() mock_input.open:revert() end) + + it("should flush book settings before suspend", function() + local sample_pdf = "spec/front/unit/data/tall.pdf" + local ReaderUI = require("apps/reader/readerui") + local Device = require("device") + local NickelConf = require("device/kobo/nickel_conf") + + stub(NickelConf.frontLightLevel, "get") + stub(NickelConf.frontLightState, "get") + NickelConf.frontLightLevel.get.returns(1) + NickelConf.frontLightState.get.returns(0) + + local UIManager = require("ui/uimanager") + stub(Device, "suspend") + stub(Device.powerd, "beforeSuspend") + stub(Device, "isKobo") + + Device.isKobo.returns(true) + local saved_noop = UIManager._resetAutoSuspendTimer + UIManager:init() + + ReaderUI:doShowReader(sample_pdf) + local readerui = ReaderUI._getRunningInstance() + stub(readerui, "onFlushSettings") + UIManager.event_handlers["PowerPress"]() + UIManager.event_handlers["PowerRelease"]() + assert.stub(readerui.onFlushSettings).was_called() + + Device.suspend:revert() + Device.powerd.beforeSuspend:revert() + Device.isKobo:revert() + NickelConf.frontLightLevel.get:revert() + NickelConf.frontLightState.get:revert() + UIManager._startAutoSuspend = nil + UIManager._stopAutoSuspend = nil + UIManager._resetAutoSuspendTimer = saved_noop + readerui:onClose() + end) + end) + + describe("kindle", function() + it("should initialize voyager without error", function() + package.loaded['ffi/framebuffer_mxcfb'] = mock_fb + stub(io, "open") + io.open.returns({ + read = function() + return "XX13XX" + end, + close = function() end + }) + mock_input = require('device/input') + stub(mock_input, "open") + + local kindle_dev = require("device/kindle/device") + assert.is.same(kindle_dev.model, "KindleVoyage") + kindle_dev:init() + assert.is.same(kindle_dev.input.event_map[104], "LPgBack") + assert.is.same(kindle_dev.input.event_map[109], "LPgFwd") + assert.is.same(kindle_dev.powerd.fl_min, 0) + assert.is.same(kindle_dev.powerd.fl_max, 24) + + io.open:revert() + package.loaded['ffi/framebuffer_mxcfb'] = nil + mock_input.open:revert() + end) + + it("should toggle frontlight", function() + package.loaded['ffi/framebuffer_mxcfb'] = mock_fb + stub(io, "open") + io.open.returns({ + read = function() + return "12" + end, + close = function() end + }) + mock_input = require('device/input') + stub(mock_input, "open") + stub(os, "execute") + + local kindle_dev = require("device/kindle/device") + kindle_dev:init() + + assert.is.same(kindle_dev.powerd.fl_intensity, 12) + kindle_dev.powerd:setIntensity(5) + assert.stub(os.execute).was_called_with( + "echo -n 5 > /sys/class/backlight/max77696-bl/brightness") + assert.is.same(kindle_dev.powerd.fl_intensity, 5) + + kindle_dev.powerd:toggleFrontlight() + assert.stub(os.execute).was_called_with( + "echo -n 0 > /sys/class/backlight/max77696-bl/brightness") + assert.is.same(kindle_dev.powerd.fl_intensity, 5) + + kindle_dev.powerd:toggleFrontlight() + assert.stub(os.execute).was_called_with( + "echo -n 5 > /sys/class/backlight/max77696-bl/brightness") + + io.open:revert() + package.loaded['ffi/framebuffer_mxcfb'] = nil + mock_input.open:revert() + os.execute:revert() + end) end) end) diff --git a/spec/unit/readerfooter_spec.lua b/spec/unit/readerfooter_spec.lua index f95b62bfd..c9736a239 100644 --- a/spec/unit/readerfooter_spec.lua +++ b/spec/unit/readerfooter_spec.lua @@ -29,6 +29,67 @@ describe("Readerfooter module", function() }) end) + it("should setup footer as visible", function() + G_reader_settings:saveSetting("reader_footer_mode", 1) + local sample_pdf = "spec/front/unit/data/2col.pdf" + purgeDir(DocSettings:getSidecarDir(sample_pdf)) + os.remove(DocSettings:getHistoryPath(sample_pdf)) + + local readerui = ReaderUI:new{ + document = DocumentRegistry:openDocument(sample_pdf), + } + assert.is.same(true, readerui.view.footer_visible) + G_reader_settings:delSetting("reader_footer_mode") + end) + + it("should setup footer as invisible in full screen mode", function() + G_reader_settings:saveSetting("reader_footer_mode", 1) + local sample_pdf = "spec/front/unit/data/2col.pdf" + purgeDir(DocSettings:getSidecarDir(sample_pdf)) + os.remove(DocSettings:getHistoryPath(sample_pdf)) + local cfg = DocSettings:open(sample_pdf) + cfg:saveSetting("kopt_full_screen", 0) + cfg:flush() + + local readerui = ReaderUI:new{ + document = DocumentRegistry:openDocument(sample_pdf), + } + assert.is.same(false, readerui.view.footer_visible) + G_reader_settings:delSetting("reader_footer_mode") + end) + + it("should setup footer as visible in mini progress bar mode", function() + G_reader_settings:saveSetting("reader_footer_mode", 1) + local sample_pdf = "spec/front/unit/data/2col.pdf" + purgeDir(DocSettings:getSidecarDir(sample_pdf)) + os.remove(DocSettings:getHistoryPath(sample_pdf)) + local cfg = DocSettings:open(sample_pdf) + cfg:saveSetting("kopt_full_screen", 0) + cfg:flush() + + local readerui = ReaderUI:new{ + document = DocumentRegistry:openDocument(sample_pdf), + } + assert.is.same(false, readerui.view.footer_visible) + G_reader_settings:delSetting("reader_footer_mode") + end) + + it("should setup footer as invisible", function() + G_reader_settings:saveSetting("reader_footer_mode", 1) + local sample_epub = "spec/front/unit/data/juliet.epub" + purgeDir(DocSettings:getSidecarDir(sample_epub)) + os.remove(DocSettings:getHistoryPath(sample_epub)) + local cfg = DocSettings:open(sample_epub) + cfg:saveSetting("copt_status_line", 1) + cfg:flush() + + local readerui = ReaderUI:new{ + document = DocumentRegistry:openDocument(sample_epub), + } + assert.is.same(true, readerui.view.footer_visible) + G_reader_settings:delSetting("reader_footer_mode") + end) + it("should setup footer for epub without error", function() local sample_epub = "spec/front/unit/data/juliet.epub" purgeDir(DocSettings:getSidecarDir(sample_epub)) @@ -100,6 +161,41 @@ describe("Readerfooter module", function() assert.are.same('TC: na', footer.progress_text.text) end) + it("should rotate through different modes", function() + local sample_pdf = "spec/front/unit/data/2col.pdf" + local readerui = ReaderUI:new{ + document = DocumentRegistry:openDocument(sample_pdf), + } + local footer = readerui.view.footer + footer.settings.all_at_once = false + footer.mode = 0 + footer:onTapFooter() + assert.is.same(1, footer.mode) + footer:onTapFooter() + assert.is.same(2, footer.mode) + footer:onTapFooter() + assert.is.same(3, footer.mode) + footer:onTapFooter() + assert.is.same(4, footer.mode) + footer:onTapFooter() + assert.is.same(5, footer.mode) + footer:onTapFooter() + assert.is.same(6, footer.mode) + footer:onTapFooter() + assert.is.same(7, footer.mode) + footer:onTapFooter() + assert.is.same(0, footer.mode) + + footer.settings.all_at_once = true + footer.mode = 5 + footer:onTapFooter() + assert.is.same(0, footer.mode) + footer:onTapFooter() + assert.is.same(1, footer.mode) + footer:onTapFooter() + assert.is.same(0, footer.mode) + end) + it("should pick up screen resize in resetLayout", function() local sample_pdf = "spec/front/unit/data/2col.pdf" purgeDir(DocSettings:getSidecarDir(sample_pdf)) diff --git a/spec/unit/readerlink_spec.lua b/spec/unit/readerlink_spec.lua index e950ab04e..beb7fafdf 100644 --- a/spec/unit/readerlink_spec.lua +++ b/spec/unit/readerlink_spec.lua @@ -88,6 +88,8 @@ describe("ReaderLink module", function() zoom = 0.9501187648456056456, }, } + -- disable footer + G_reader_settings:saveSetting("reader_footer_mode", 0) local readerui = ReaderUI:new{ document = DocumentRegistry:openDocument(sample_pdf), } diff --git a/spec/unit/readerrolling_spec.lua b/spec/unit/readerrolling_spec.lua index d9d2a444a..9572997ce 100644 --- a/spec/unit/readerrolling_spec.lua +++ b/spec/unit/readerrolling_spec.lua @@ -20,12 +20,14 @@ describe("Readerrolling module", function() it("should goto portrait screen mode", function() readerui:handleEvent(Event:new("ChangeScreenMode", "portrait")) end) + it("should goto certain page", function() for i = 1, 10, 5 do rolling:onGotoPage(i) assert.are.same(i, rolling.current_page) end end) + it("should goto relative page", function() for i = 20, 40, 5 do rolling:onGotoPage(i) @@ -35,6 +37,7 @@ describe("Readerrolling module", function() assert.are.same(i, rolling.current_page) end end) + it("should goto next chapter", function() local toc = readerui.toc for i = 30, 50, 5 do @@ -43,6 +46,7 @@ describe("Readerrolling module", function() assert.are.same(toc:getNextChapter(i, 0), rolling.current_page) end end) + it("should goto previous chapter", function() local toc = readerui.toc for i = 60, 80, 5 do @@ -51,18 +55,58 @@ describe("Readerrolling module", function() assert.are.same(toc:getPreviousChapter(i, 0), rolling.current_page) end end) - it("should emit EndOfBook event at the end", function() - rolling:onGotoPage(readerui.document:getPageCount()) + + it("should emit EndOfBook event at the end of sample epub", function() local called = false readerui.onEndOfBook = function() called = true end + -- check beginning of the book + rolling:onGotoPage(1) + assert.is.falsy(called) + rolling:onGotoViewRel(-1) + rolling:onGotoViewRel(-1) + assert.is.falsy(called) + -- check end of the book + rolling:onGotoPage(readerui.document:getPageCount()) + assert.is.falsy(called) rolling:onGotoViewRel(1) + assert.is.truthy(called) rolling:onGotoViewRel(1) assert.is.truthy(called) readerui.onEndOfBook = nil end) + + it("should emit EndOfBook event at the end sample txt", function() + local sample_txt = "spec/front/unit/data/sample.txt" + local txt_readerui = ReaderUI:new{ + document = DocumentRegistry:openDocument(sample_txt), + } + local called = false + txt_readerui.onEndOfBook = function() + called = true + end + local txt_rolling = txt_readerui.rolling + -- check beginning of the book + txt_rolling:onGotoPage(1) + assert.is.falsy(called) + txt_rolling:onGotoViewRel(-1) + txt_rolling:onGotoViewRel(-1) + assert.is.falsy(called) + -- not at the end of the book + txt_rolling:onGotoPage(3) + assert.is.falsy(called) + txt_rolling:onGotoViewRel(1) + assert.is.falsy(called) + -- at the end of the book + txt_rolling:onGotoPage(txt_readerui.document:getPageCount()) + assert.is.falsy(called) + txt_rolling:onGotoViewRel(1) + assert.is.truthy(called) + readerui.onEndOfBook = nil + end) end) + describe("test in landscape screen mode", function() it("should go to landscape screen mode", function() readerui:handleEvent(Event:new("ChangeScreenMode", "landscape")) @@ -110,6 +154,7 @@ describe("Readerrolling module", function() readerui.onEndOfBook = nil end) end) + describe("switching screen mode should not change current page number", function() it("for portrait-landscape-portrait switching", function() for i = 80, 100, 10 do diff --git a/spec/unit/readersearch_spec.lua b/spec/unit/readersearch_spec.lua index 3766e0d47..64ad2cd26 100644 --- a/spec/unit/readersearch_spec.lua +++ b/spec/unit/readersearch_spec.lua @@ -87,12 +87,13 @@ describe("Readersearch module", function() for _, word in ipairs(words) do --dbg("found word", word.start) end - doc:gotoXPointer(words[1].start) + doc:gotoXPointer(words[#words].start) words = search:searchNext("Verona", 0) end assert.are.equal(13, count) end) end) + describe("search API for PDF documents", function() local doc, search, paging setup(function() diff --git a/spec/unit/widget_menu_spec.lua b/spec/unit/widget_menu_spec.lua index 0ac1c7926..1fa9f2e38 100644 --- a/spec/unit/widget_menu_spec.lua +++ b/spec/unit/widget_menu_spec.lua @@ -1,8 +1,11 @@ -require("commonrequire") -local Menu = require("ui/widget/menu") -local DEBUG = require("dbg") - describe("Menu widget", function() + local Menu, dbg + setup(function() + require("commonrequire") + Menu = require("ui/widget/menu") + dbg = require("dbg") + end) + it("should convert item table from touch menu properly", function() local cb1 = function() end local cb2 = function() end diff --git a/spec/unit/widget_progresswidget_spec.lua b/spec/unit/widget_progresswidget_spec.lua new file mode 100644 index 000000000..0f3dd0f0e --- /dev/null +++ b/spec/unit/widget_progresswidget_spec.lua @@ -0,0 +1,18 @@ +describe("ProgressWidget widget", function() + local ProgressWidget, Screen + setup(function() + require("commonrequire") + ProgressWidget = require("ui/widget/progresswidget") + Screen = require("device").screen + end) + + it("should not crash with nil self.last", function() + local progress = ProgressWidget:new{ + width = 100, + height = 50, + percentage = 5/100, + ticks = {1}, + } + progress:paintTo(Screen.bb, 0, 0) + end) +end) diff --git a/test/sample.txt b/test/sample.txt new file mode 100644 index 000000000..15d7805fe --- /dev/null +++ b/test/sample.txt @@ -0,0 +1,94 @@ + Lorem ipsum + Dolor sit amet, foo@bar.com + v0.1, 99 September 7999 + + Lorem ipsum dolor sit amet, pri id laudem vulputate disputando, ad mea + pericula consetetur. Nusquam detraxit ad sed, tritani mandamus aliquando et + has, porro graeco at pri. Sale denique ut sit, mel suas erroribus repudiare + ea. Vim probo dicit consequuntur te. + ______________________________________________________________________ + + Table of Contents + + + 1. Eos ex eius iusto delicata + + 2. Illum argumentum sed a + + 3. In eum magna iusto integre + + + + ______________________________________________________________________ + + 1. Eos ex eius iusto delicata + + + Eos ex eius iusto delicata, ius ne facer invenire electram, cu mel assum + novum efficiendi. Duo enim eleifend te. Elitr nihil vivendo vix ex, ex homero + salutatus sed, ea nec posse commune consetetur. Ea iusto labore docendi his, + at per mollis mentitum. Ex esse recteque eos, ex iudicabit gloriatur mei. + + Quisque rutrum. Aenean imperdiet. Etiam ultricies nisi vel augue: + + 1. Maecenas nec odio et ante tincidunt tempus. + + 2. Donec sodales sagittis magna. + + 3. Phasellus viverra nulla ut metus varius laoreet. + + Li Europan lingues es membres del sam familie. Lor separat existentie es un + myth. + + It va esser tam simplic quam Occidental in fact, it va esser Occidental. A un + Angleso it va semblar un simplificat Angles, quam un skeptic Cambridge amico + dit me que Occidental es.Li Europan lingues es membres del sam familie. Lor + separat existentie es un myth. Por scientie, musica, sport etc, litot Europa + usa li sam vocabular. Li lingues differe solmen in li grammatica, li + pronunciation e li plu commun vocabules. Omnicos directe al desirabilite de + un nov lingua franca: On refusa continuar payar custosi traductores. At + solmen va esser necessi far uniform grammatica, pronunciation e plu sommun + paroles. + + + 2. Illum argumentum sed a + + Illum argumentum sed ad, vel accumsan noluisse eu. Nam ne minimum consulatu, + vim nullam quidam ut. Ea pro temporibus ullamcorper, at case aeque vix. Est + id consetetur intellegam. Eu cum oratio gubergren, aeque tritani feugiat vel + te. + + · Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium + doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore + veritatis et quasi architecto beatae vitae dicta sunt explicabo. + + · Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam + nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas + nulla pariatur? + + cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod + maxime placeat facere. + + To install the tar.gz source, use the commands: + + ______________________________________________________________________ + ./configure + make + make install + ______________________________________________________________________ + + + Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed + quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. + Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, + adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore + et dolore magnam aliquam quaerat voluptatem. + + + 3. In eum magna iusto integre + + In eum magna iusto integre, cu solet commodo constituto pro. Te nec tota + altera, diam periculis ius eu, eum te velit partiendo conclusionemque. Diam + mnesarchum at usu, agam nonumes at nec. Vix aliquip liberavisse ex, nam at + quis choro accusam. Eu his zril graecis, latine legendos inimicus eum at, qui + te adolescens adipiscing.