Upload most recent update of standard plugin prior to any personal tweaks

This commit is contained in:
2026-01-31 14:07:48 -07:00
commit 89d0f2d8ed
12 changed files with 1369 additions and 0 deletions

21
LICENSE.txt Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Lena2309
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

63
README.md Normal file
View File

@@ -0,0 +1,63 @@
# pinpad_screenlock_plugin
[![MIT License](https://img.shields.io/badge/License-MIT-orange.svg)](https://opensource.org/licenses/MIT)
A small plugin to enable locking the screen of KOReader with a PIN code.
This work was originally based on the work of yogi81 and their plugin `screenlock_koreader_plugin`.
![Lock Screen Preview](screenshots/lockscreen.png)
## Setup
1. Put the latest release of `pinpadlockscreen.koplugin` into the `koreader/plugins` directory.
2. Activate and change your password through the menu in Settings -> Screen -> PIN Pad Lock.
The default code is "1234". You can also add a custom message to display with the pin pad (e.g. contact details) and change its position and text alignment. You can also jump lines using "\n" in the message.
![Menu Entry Screenshot](screenshots/menu_entry.png)
**Note:**
- There's no limit to the number of digits your code may contain, but good luck if you forget your code (you'll have to dig into your memory or KOReader to find it back).
- You can change the lock logo by replacing the image from the icons folder. The new icon must be SVG file and be named `lock.svg`.
## Features and Gestures
- `Manage PIN Code`:
- `Change PIN Code` will allow you to change your code after entering your current one.
- `Reset PIN Code` will reset the code at `1234`, thought for people with long codes.
- `PIN pad lock message` allows you to manage the custom message you can add to the PIN Pad. This is optional.
- `Cancel` button: a quick tap will delete a digit of the code you entered, a long hold will delete every digits.
- `Advanced Settings`
- You can choose the timeout time, the max number of tries before timeout, among other things.
- The pad will appear on the current screensaver you're using, except when first hooking into KOReader where it will appear on a black background.
- Check for updates directly from your device, without having to open GitHub. This feature needs the device to be connected to Internet.
## Future work
- Features : Currently have no idea, do not hesitate to bring up issues !
- Fix : Screen becomes full black and untouchable when suspending/powering off the device before unlocking the "hook Pin Pad"
**Note:** I do not know when I'll be implementing these features. Also, my work could probably be optimized so I'm open to any new ideas and remarks.
## Recent Features
- Customizable message on PIN pad lock.
- Menu Entry where everything is customizable without the need to restart KOReader or modify the code.
- Using dynamic persistence instead of hardcoded variables.
- Cancel button deletes one digits when entering PIN Code, hold it to delete everything.
- 3 tries limit before a 30 seconds timeout.
- PIN Pad appears on the screensaver background (without sleep message)
- When launching KOReader (on a jailbroken kindle), PIN Pad now appears right away, not leaving the possibility to deactivate or reset the lock.
- Easier installation process with the icon directly in the plugin.
- PIN Pad appearing on the current screensaver without having to rebuild everything.
- Can show digits when typing.
- Less flashy overall.
- Pin pad disappears when suspending device before unlocking the pad.
:)

View File

@@ -0,0 +1,8 @@
local _ = require("gettext")
return {
name = "pinpadlock",
fullname = _("PIN Pad Lock Screen"),
description = _([[Lock the device with a PIN code]]),
version = "v1.1.0"
}

View File

@@ -0,0 +1,165 @@
--[[
Author: Lena2309 (better lock management, boot hooking, event listening, ...), yogi81 (original inspiration and main skeleton)
Inspiration and original skeleton from `https://github.com/yogi81/screenlock_koreader_plugin/tree/main`
Description: implements a screen lock mechanism for KOReader
using a PIN pad interface. Also adds a menu entry in Settings -> Screen
]]
local Dispatcher = require("dispatcher")
local PinPadDialog = require("ui/pinpaddialog")
local EventListener = require("ui/widget/eventlistener")
local _ = require("gettext")
local UIManager = require("ui/uimanager")
local MenuEntryItems = require("menu/menuentryitems")
local logger = require("logger")
-- Default settings
if G_reader_settings:hasNot("pinpadlock_pin_code") then
G_reader_settings:saveSetting("pinpadlock_pin_code", "1234")
end
if G_reader_settings:hasNot("pinpadlock_activated") then
G_reader_settings:makeFalse("pinpadlock_activated")
end
if G_reader_settings:hasNot("pinpadlock_correct_pin_message_activated") then
G_reader_settings:makeTrue("pinpadlock_correct_pin_message_activated")
end
if G_reader_settings:hasNot("pinpadlock_timeout_time") then
G_reader_settings:saveSetting("pinpadlock_timeout_time", "30")
end
if G_reader_settings:hasNot("pinpadlock_max_tries") then
G_reader_settings:saveSetting("pinpadlock_max_tries", "3")
end
if G_reader_settings:hasNot("pinpadlock_show_message") then
G_reader_settings:makeFalse("pinpadlock_show_message")
end
if G_reader_settings:hasNot("pinpadlock_message") then
G_reader_settings:saveSetting("pinpadlock_message", "Locked")
end
if G_reader_settings:hasNot("pinpadlock_message_position") then
G_reader_settings:saveSetting("pinpadlock_message_position", "top")
end
if G_reader_settings:hasNot("pinpadlock_message_alignment") then
G_reader_settings:saveSetting("pinpadlock_message_alignment", "center")
end
if G_reader_settings:hasNot("suspended_device") then
G_reader_settings:makeTrue("suspended_device")
end
local ScreenLock = EventListener:extend {
pinPadDialog = nil,
}
function ScreenLock:erasePinPadDialog()
if self.pinPadDialog then
self.pinPadDialog:close(function()
self.pinPadDialog = nil
end)
end
end
------------------------------------------------------------------------------
--- DEVICE LISTENER OVERRIDE
------------------------------------------------------------------------------
local ref_self = nil
local _original_PowerOff = UIManager.poweroff_action
UIManager.poweroff_action = function()
logger.warn("in suspend ref_self: " .. tostring(ref_self))
if ref_self then
logger.warn("before erase dialog: " .. tostring(ref_self))
ref_self:erasePinPadDialog()
logger.warn("after erase dialog: " .. tostring(ref_self))
end
UIManager:nextTick(function()
_original_PowerOff()
end)
end
------------------------------------------------------------------------------
--- REGISTER DISPATCHER ACTIONS
------------------------------------------------------------------------------
function ScreenLock:onDispatcherRegisterActions()
Dispatcher:registerAction("screenlock_pin_pad_lock_screen", {
category = "none",
event = "LockScreenPinPad",
title = _("Lock Screen (PinPad)"),
filemanager = true,
device = true,
})
end
------------------------------------------------------------------------------
--- EVENT LISTENING
------------------------------------------------------------------------------
---KOReader exit and restart
function ScreenLock:onExit()
G_reader_settings:makeTrue("suspended_device")
end
function ScreenLock:onRestart()
G_reader_settings:makeTrue("suspended_device")
end
--- Device suspension, reboot or power off
function ScreenLock:onRequestSuspend()
G_reader_settings:makeTrue("suspended_device")
end
function ScreenLock:onRequestReboot()
G_reader_settings:makeTrue("suspended_device")
end
function ScreenLock:onRequestPowerOff()
G_reader_settings:makeTrue("suspended_device")
end
------------------------------------------------------------------------------
--- INIT & WAKE-UP HANDLING
------------------------------------------------------------------------------
function ScreenLock:init()
self:onDispatcherRegisterActions()
self.ui.menu:registerToMainMenu(self)
ref_self = self
if G_reader_settings:isTrue("pinpadlock_activated") and G_reader_settings:isTrue("suspended_device") then
ref_self:lockScreen()
end
return self
end
function ScreenLock:onResume()
if G_reader_settings:isTrue("pinpadlock_activated") then
self:lockScreen()
end
end
------------------------------------------------------------------------------
--- DISPATCHER HANDLER
------------------------------------------------------------------------------
function ScreenLock:onLockScreen()
self:lockScreen()
return true
end
------------------------------------------------------------------------------
--- LOCK SCREEN
------------------------------------------------------------------------------
function ScreenLock:lockScreen()
ref_self = self
UIManager:nextTick(function()
if self.pinPadDialog then self.pinPadDialog:closeDialogs() end
self.pinPadDialog = PinPadDialog:init()
self.pinPadDialog:showPinPad()
end)
ref_self = self
end
------------------------------------------------------------------------------
--- MAIN MENU ENTRY
------------------------------------------------------------------------------
function ScreenLock:addToMainMenu(menu_items)
menu_items.pinpad = MenuEntryItems
end
return ScreenLock

View File

@@ -0,0 +1,135 @@
--[[
Author: Lena2309
Description: Displays items in the Screen Menu Entry to manage the plugin
]]
local PinPadMenuEntry = require("menu/pinpadmenuentry")
local _ = require("gettext")
return {
text = _("PIN Pad Lock"),
sorting_hint = "screen",
sub_item_table = {
{
text = _("Activated"),
checked_func = function()
return PinPadMenuEntry:pinPadEnabled()
end,
callback = function()
G_reader_settings:toggle("pinpadlock_activated")
end,
separator = true,
},
{
text = _("Manage PIN Code"),
keep_menu_open = true,
sub_item_table = {
{
text = _("Change PIN Code"),
callback = function()
PinPadMenuEntry:changePinCode()
end
},
{
text = _("Reset PIN Code"),
callback = function()
PinPadMenuEntry:resetPinCode()
end,
},
}
},
{
text = _("PIN pad lock message"),
separator = true,
sub_item_table = {
{
text = _("Add custom message to lock"),
checked_func = function()
return PinPadMenuEntry:showMessageEnabled()
end,
callback = function()
G_reader_settings:toggle("pinpadlock_show_message")
end,
separator = true,
},
{
text = _("Edit PIN pad lock message"),
enabled_func = function()
return PinPadMenuEntry:showMessageEnabled()
end,
keep_menu_open = true,
callback = function()
PinPadMenuEntry:setMessage()
end,
},
{
text = _("Message position"),
enabled_func = function()
return PinPadMenuEntry:showMessageEnabled()
end,
sub_item_table = {
PinPadMenuEntry:genRadioMenuItem(_("Top"), "pinpadlock_message_position", "top"),
PinPadMenuEntry:genRadioMenuItem(_("Middle"), "pinpadlock_message_position", "middle"),
PinPadMenuEntry:genRadioMenuItem(_("Bottom"), "pinpadlock_message_position", "bottom"),
},
},
{
text = _("Message alignment"),
enabled_func = function()
return PinPadMenuEntry:showMessageEnabled()
end,
sub_item_table = {
PinPadMenuEntry:genRadioMenuItem(_("Left"), "pinpadlock_message_alignment", "left"),
PinPadMenuEntry:genRadioMenuItem(_("Center"), "pinpadlock_message_alignment", "center"),
PinPadMenuEntry:genRadioMenuItem(_("Right"), "pinpadlock_message_alignment", "right"),
},
},
}
},
{
text = _("Advanced Settings"),
sub_item_table = {
{
text = _("Activate correct PIN pop-up"),
checked_func = function()
return PinPadMenuEntry:correctPinMessageEnabled()
end,
callback = function()
G_reader_settings:toggle("pinpadlock_correct_pin_message_activated")
end,
},
{
text = _("Display entered digit before hiding it"),
checked_func = function()
return PinPadMenuEntry:displayDigitEnabled()
end,
callback = function()
G_reader_settings:toggle("pinpadlock_display_digit_activated")
end,
},
{
text = _("Set Timeout time"),
sub_item_table = {
PinPadMenuEntry:genRadioMenuItem(_("10 seconds"), "pinpadlock_timeout_time", "10"),
PinPadMenuEntry:genRadioMenuItem(_("30 seconds"), "pinpadlock_timeout_time", "30"),
PinPadMenuEntry:genRadioMenuItem(_("60 seconds"), "pinpadlock_timeout_time", "60"),
},
},
{
text = _("Set max tries before timeout"),
sub_item_table = {
PinPadMenuEntry:genRadioMenuItem(_("1"), "pinpadlock_max_tries", "1"),
PinPadMenuEntry:genRadioMenuItem(_("3"), "pinpadlock_max_tries", "3"),
PinPadMenuEntry:genRadioMenuItem(_("6"), "pinpadlock_max_tries", "6"),
},
},
},
},
{
text = _("Check for updates"),
callback = function()
PinPadMenuEntry:checkForUpdates()
end,
}
}
}

View File

@@ -0,0 +1,187 @@
--[[
Author: Lena2309
Description: Execute actions when tapping on a menu entry item
]]
local ConfirmBox = require("ui/widget/confirmbox")
local InfoMessage = require("ui/widget/infomessage")
local InputDialog = require("ui/widget/inputdialog")
local PinPadDialog = require("ui/pinpaddialog")
local UIManager = require("ui/uimanager")
local _ = require("gettext")
local http = require("socket.http")
local ltn12 = require("ltn12")
local json = require("dkjson")
local PinPadMenuEntry = {}
function PinPadMenuEntry:setMessage()
local lock_message = G_reader_settings:readSetting("pinpadlock_message")
local input_dialog
input_dialog = InputDialog:new {
title = _("PIN pad lock message"),
description = _("Enter a custom message to be displayed on the PIN pad lock."),
input = lock_message,
buttons = {
{
{
text = _("Cancel"),
id = "close",
callback = function()
UIManager:close(input_dialog)
end,
},
{
text = _("Set message"),
is_enter_default = true,
callback = function()
G_reader_settings:saveSetting("pinpadlock_message", input_dialog:getInputText())
UIManager:close(input_dialog)
end,
},
},
},
}
UIManager:show(input_dialog)
input_dialog:onShowKeyboard()
end
function PinPadMenuEntry:showMessageEnabled()
return G_reader_settings:isTrue("pinpadlock_show_message")
end
function PinPadMenuEntry:pinPadEnabled()
return G_reader_settings:isTrue("pinpadlock_activated")
end
function PinPadMenuEntry:correctPinMessageEnabled()
return G_reader_settings:isTrue("pinpadlock_correct_pin_message_activated")
end
function PinPadMenuEntry:displayDigitEnabled()
return G_reader_settings:isTrue("pinpadlock_display_digit_activated")
end
function PinPadMenuEntry:genRadioMenuItem(text, setting, value)
return {
text = text,
checked_func = function()
return G_reader_settings:readSetting(setting) == value
end,
callback = function()
G_reader_settings:saveSetting(setting, value)
end,
radio = true,
}
end
function PinPadMenuEntry:changePinCode()
local confirmCurrentCode = PinPadDialog:new { stage = "confirm_current_code" }
confirmCurrentCode:showPinPad()
end
function PinPadMenuEntry:resetPinCode()
local confirmBox
confirmBox = ConfirmBox:new {
text = _("Do you really want to reset the PIN code to \"1234\" ?"),
ok_text = _("Yes"),
ok_callback = function()
G_reader_settings:saveSetting("pinpadlock_pin_code", "1234")
UIManager:close(confirmBox)
UIManager:show(InfoMessage:new { text = _("PIN Code resetted successfully."), timeout = 1 })
end,
cancel_callback = function()
UIManager:close(confirmBox)
end
}
UIManager:show(confirmBox)
end
-- Parse a version string like "v1.2.3"
local function parseVersion(version)
local major, minor, patch = version:match("^v(%d+)%.(%d+)%.(%d+)$")
return {
major = tonumber(major) or 0,
minor = tonumber(minor) or 0,
patch = tonumber(patch) or 0
}
end
-- Returns -1 if a < b, 0 if equal, 1 if a > b
local function compareVersions(local_version, remote_version)
if local_version.major ~= remote_version.major then
if local_version.major > remote_version.major then
return 1
else
return -1
end
elseif local_version.minor ~= remote_version.minor then
if local_version.minor > remote_version.minor then
return 1
else
return -1
end
elseif local_version.patch ~= remote_version.patch then
if local_version.patch > remote_version.patch then
return 1
else
return -1
end
end
return 0
end
function PinPadMenuEntry:checkForUpdates()
local meta = dofile("plugins/pinpadlockscreen.koplugin/_meta.lua")
local local_version = meta.version or "unknown"
local response_body = {}
local ok, status = http.request {
url = "https://api.github.com/repos/Lena2309/pinpad_screenlock_plugin/releases/latest",
sink = ltn12.sink.table(response_body),
redirect = true
}
if not ok or status ~= 200 then
UIManager:show(InfoMessage:new {
text = _("Unable to check for updates. Make sure your device is connected to the Internet."),
timeout = 3,
})
return
end
local body = table.concat(response_body)
local data, pos, err = json.decode(body, 1, nil)
if not data or not data.tag_name then
UIManager:show(InfoMessage:new {
text = _("Error parsing GitHub release info."),
timeout = 3,
})
return
end
local remote_version = data.tag_name
local cmp = compareVersions(parseVersion(local_version), parseVersion(remote_version))
if cmp < 0 then
UIManager:show(InfoMessage:new {
text = _("New version available: ") .. remote_version ..
_("\nYou are running: ") .. local_version,
timeout = 5,
})
else
if cmp > 0 then
UIManager:show(InfoMessage:new {
text = _("Youre ahead of the official release.\nLatest official version: " .. remote_version),
timeout = 5,
})
else
UIManager:show(InfoMessage:new {
text = _("You are up to date!"),
timeout = 3,
})
end
end
end
return PinPadMenuEntry

View File

@@ -0,0 +1,18 @@
<svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512">
<g transform="translate(0, 512) scale(0.1, -0.1)"
fill="#000000" stroke="none">
<path d="M2390 5110 c-157 -22 -329 -75 -461 -140 -95 -48 -234 -147 -319
-227 -203 -191 -334 -415 -407 -697 -27 -101 -27 -102 -30 -573 l-4 -473 -123
0 c-201 0 -285 -41 -348 -167 l-33 -68 0 -1260 c0 -1052 2 -1267 14 -1300 32
-93 104 -163 197 -191 73 -22 3296 -21 3371 1 78 23 136 74 175 152 l33 68 0
1265 0 1265 -33 68 c-63 127 -147 167 -349 167 l-123 0 0 428 c0 461 -5 523
-55 697 -86 295 -296 581 -557 756 -162 107 -310 170 -496 209 -111 24 -350
34 -452 20z m405 -519 c317 -91 554 -334 637 -653 20 -77 21 -113 25 -510 l5
-428 -902 0 -902 0 5 428 c4 397 5 433 25 510 87 335 351 591 687 666 99 22
324 15 420 -13z m-114 -2497 c151 -48 252 -192 253 -361 0 -74 -4 -88 -37
-157 -25 -50 -53 -89 -86 -118 l-49 -43 54 -250 c29 -137 53 -258 54 -267 0
-17 -21 -18 -310 -18 -289 0 -310 1 -309 18 0 9 24 130 53 267 l53 250 -48 43
c-94 83 -143 229 -119 349 47 228 269 357 491 287z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,235 @@
--[[
Author: Lena2309
Description: customizes KOReader's existing ButtonDialog class solely
for aesthetic purposes (icon widget, vertical layout...),
with the primary modification in the `init` function.
]]
local Blitbuffer = require("ffi/blitbuffer")
local ButtonDialog = require("ui/widget/buttondialog")
local ButtonTable = require("ui/widget/buttontable")
local CenterContainer = require("ui/widget/container/centercontainer")
local Device = require("device")
local FrameContainer = require("ui/widget/container/framecontainer")
local Geom = require("ui/geometry")
local GestureRange = require("ui/gesturerange")
local ImageWidget = require("ui/widget/imagewidget")
local LineWidget = require("ui/widget/linewidget")
local MovableContainer = require("ui/widget/container/movablecontainer")
local ScrollTextWidget = require("ui/widget/scrolltextwidget")
local Size = require("ui/size")
local TextBoxWidget = require("ui/widget/textboxwidget")
local UIManager = require("ui/uimanager")
local VerticalGroup = require("ui/widget/verticalgroup")
local VerticalSpan = require("ui/widget/verticalspan")
local Screen = Device.screen
local util = require("util")
local DGENERIC_ICON_SIZE = G_defaults:readSetting("DGENERIC_ICON_SIZE")
local PinpadButtonDialog = ButtonDialog:extend {
override_show_message = false, -- will prevent showing custom lock message even if enabled (specific for changing PIN Code)
}
function PinpadButtonDialog:init()
if not self.width_factor then
self.width_factor = 2 / 3
end
self.width = math.floor(math.min(Screen:getWidth(), Screen:getHeight()) * self.width_factor)
if self.dismissable then
if Device:hasKeys() then
local back_group = util.tableDeepCopy(Device.input.group.Back)
if Device:hasFewKeys() then
table.insert(back_group, "Left")
self.key_events.Close = { { back_group } }
else
table.insert(back_group, "Menu")
self.key_events.Close = { { back_group } }
end
end
if Device:isTouchDevice() then
self.ges_events.TapClose = {
GestureRange:new {
ges = "tap",
range = Geom:new {
x = 0, y = 0,
w = Screen:getWidth(),
h = Screen:getHeight(),
}
}
}
end
end
local title_face
if self.use_info_style then
title_face = self.info_face
else
title_face = self.title_face
end
local currentFileSource = debug.getinfo(1, "S").source
local plugin_dir
if currentFileSource:find("^@") then
plugin_dir = currentFileSource:gsub("^@(.*)/[^/]*", "%1")
end
self.title_widget = TextBoxWidget:new {
text = self.title,
face = title_face,
width = self.width,
alignment = self.title_align,
}
local aesthetic_space = VerticalSpan:new { width = Size.margin.default + Size.padding.default }
local text_pin_content = VerticalGroup:new {
align = "center",
ImageWidget:new {
file = plugin_dir .. "/icons/lock.svg",
alpha = true,
width = Screen:scaleBySize(DGENERIC_ICON_SIZE) * 1.25,
height = Screen:scaleBySize(DGENERIC_ICON_SIZE) * 1.25,
scale_factor = 0,
original_in_nightmode = false,
},
aesthetic_space,
VerticalGroup:new {
align = "center",
self.title_widget
},
}
self.buttontable = ButtonTable:new {
buttons = self.buttons,
width = text_pin_content:getSize().w,
shrink_unneeded_width = self.shrink_unneeded_width,
shrink_min_width = self.shrink_min_width,
show_parent = self,
}
local separator = LineWidget:new {
background = Blitbuffer.COLOR_GRAY,
dimen = Geom:new {
w = text_pin_content:getSize().w,
h = Size.line.medium,
},
}
local content
if G_reader_settings:isTrue("pinpadlock_show_message") and not self.override_show_message then
local lock_message = G_reader_settings:readSetting("pinpadlock_message")
lock_message = lock_message:gsub("\\n", "\n") -- enabling jumping lines in the message
local lock_message_alignment = G_reader_settings:readSetting("pinpadlock_message_alignment")
local lock_message_position = G_reader_settings:readSetting("pinpadlock_message_position")
local lock_message_widget = TextBoxWidget:new {
text = lock_message,
face = title_face,
width = self.width,
alignment = lock_message_alignment,
}
-- If the custom message ends up being too tall and makes the Pin Pad taller than the screen,
-- wrap it inside a ScrollableContainer
local max_height = self.buttontable:getSize().h + 3 * text_pin_content:getSize().h + Size.line.medium
local height = self.buttontable:getSize().h + text_pin_content:getSize().h + Size.line.medium +
lock_message_widget:getSize().h
if height > max_height then
local scroll_height = 3 * text_pin_content:getSize().h
lock_message_widget = ScrollTextWidget:new {
text = lock_message,
face = title_face,
width = self.width,
alignment = lock_message_alignment,
dialog = self,
height = scroll_height,
}
end
if lock_message_position == "top" then
content = VerticalGroup:new {
align = "center",
aesthetic_space,
lock_message_widget,
aesthetic_space,
separator,
aesthetic_space,
text_pin_content,
aesthetic_space,
separator,
self.buttontable,
}
elseif lock_message_position == "middle" then
content = VerticalGroup:new {
align = "center",
aesthetic_space,
text_pin_content,
aesthetic_space,
separator,
aesthetic_space,
lock_message_widget,
aesthetic_space,
separator,
self.buttontable,
}
else
content = VerticalGroup:new {
align = "center",
aesthetic_space,
text_pin_content,
aesthetic_space,
separator,
self.buttontable,
separator,
aesthetic_space,
lock_message_widget,
aesthetic_space,
}
end
else
content = VerticalGroup:new {
align = "center",
aesthetic_space,
text_pin_content,
aesthetic_space,
separator,
self.buttontable,
}
end
self.movable = MovableContainer:new {
alpha = self.alpha,
anchor = self.anchor,
FrameContainer:new {
background = Blitbuffer.COLOR_WHITE,
bordersize = Size.border.window,
radius = Size.radius.window,
padding = Size.padding.default,
padding_top = 0,
padding_bottom = 0,
content,
},
unmovable = true,
}
self.layout = self.buttontable.layout
self.buttontable.layout = nil
self[1] = CenterContainer:new {
dimen = Screen:getSize(),
self.movable,
}
end
function PinpadButtonDialog:updateTitle(text)
if self.title_widget then
self.title = text
self.title_widget:setText(text)
UIManager:setDirty(self, "ui")
end
end
return PinpadButtonDialog

View File

@@ -0,0 +1,484 @@
--[[
Author: Lena2309
Description: represents a PIN pad interface and its behavior within KOReader.
It handles user input, PIN validation, and UI interactions.
]]
local Blitbuffer = require("ffi/blitbuffer")
local BookStatusWidget = require("ui/widget/bookstatuswidget")
local Button = require("ui/widget/button")
local Device = require("device")
local FrameContainer = require("ui/widget/container/framecontainer")
local InfoMessage = require("ui/widget/infomessage")
local ImageWidget = require("ui/widget/imagewidget")
local PinPadButtonDialog = require("ui/pinpadbuttondialog")
local RenderImage = require("ui/renderimage")
local Screensaver = require("ui/screensaver")
local ScreenSaverWidget = require("ui/widget/screensaverwidget")
local UIManager = require("ui/uimanager")
local ffiUtil = require("ffi/util")
local util = require("util")
local _ = require("gettext")
local Screen = Device.screen
local ScreensaverUtil = require("util/screensaverutil")
local DGENERIC_ICON_SIZE = G_defaults:readSetting("DGENERIC_ICON_SIZE")
local LOCKED_TEXT = _("Enter your PIN")
local CONFIRM_CURRENT_CODE_TEXT = _("Enter the current PIN Code")
local NEW_CODE_TEXT = _("Enter the new PIN Code")
local MAX_TRIES_LIMIT = tonumber(G_reader_settings:readSetting("pinpadlock_max_tries"))
local TIMEOUT_TIME = tonumber(G_reader_settings:readSetting("pinpadlock_timeout_time"))
if G_reader_settings:hasNot("current_tries_number") then
G_reader_settings:saveSetting("current_tries_number", 0)
end
if G_reader_settings:hasNot("block_start_time") then
G_reader_settings:saveSetting("block_start_time", 0)
end
local PinPadDialog = FrameContainer:extend {
stage = "locked", -- "confirm_current_code" or "enter_new_code"
icon_size = Screen:scaleBySize(DGENERIC_ICON_SIZE) * 1.25,
}
function PinPadDialog:init()
self:setDialogText()
self.pin = ""
self.buttons = self:initializeButtons()
return self
end
function PinPadDialog:setDialogText()
if self.stage == "locked" then
self.dialog_text = LOCKED_TEXT
elseif self.stage == "confirm_current_code" then
self.dialog_text = CONFIRM_CURRENT_CODE_TEXT
else
self.dialog_text = NEW_CODE_TEXT
end
end
function PinPadDialog:createButton(text, callback, hold_callback)
local button = Button:new {
text = text,
callback = callback,
hold_callback = hold_callback,
}
return button
end
function PinPadDialog:initializeButtons()
local ref_self = self
local buttons = {
{
self:createButton("1", function() ref_self:onAppendToPin("1") end),
self:createButton("2", function() ref_self:onAppendToPin("2") end),
self:createButton("3", function() ref_self:onAppendToPin("3") end),
},
{
self:createButton("4", function() ref_self:onAppendToPin("4") end),
self:createButton("5", function() ref_self:onAppendToPin("5") end),
self:createButton("6", function() ref_self:onAppendToPin("6") end),
},
{
self:createButton("7", function() ref_self:onAppendToPin("7") end),
self:createButton("8", function() ref_self:onAppendToPin("8") end),
self:createButton("9", function() ref_self:onAppendToPin("9") end),
},
{
self:createButton("Cancel", function() ref_self:onDelete() end, function() ref_self:onCancel() end),
self:createButton("0", function() ref_self:onAppendToPin("0") end),
self:createButton("OK", function() ref_self:onOk() end),
}
}
return buttons
end
function PinPadDialog:initializeScreensaverBackground()
if Screensaver.screensaver_type == "disable" and not Screensaver.show_message and not Screensaver.overlay_message then
return
end
local rotation_mode = Screen:getRotationMode()
if Screensaver:modeExpectsPortrait() then
Device.orig_rotation_mode = rotation_mode
if bit.band(Device.orig_rotation_mode, 1) == 1 then
Screen:setRotationMode(Screen.DEVICE_ROTATED_UPRIGHT)
else
Device.orig_rotation_mode = nil
end
if Device:hasEinkScreen() and Screensaver:modeIsImage() then
if Screensaver:withBackground() then
Screen:clear()
end
Screen:refreshFull(0, 0, Screen:getWidth(), Screen:getHeight())
if Device:isKobo() and Device:isSunxi() then
ffiUtil.usleep(150 * 1000)
end
end
else
Device.orig_rotation_mode = nil
end
local widget = nil
if Screensaver.screensaver_type == "cover" or Screensaver.screensaver_type == "random_image" then
local widget_settings = {
width = Screen:getWidth(),
height = Screen:getHeight(),
scale_factor = G_reader_settings:isFalse("screensaver_stretch_images") and 0 or nil,
stretch_limit_percentage = G_reader_settings:readSetting("screensaver_stretch_limit_percentage"),
}
if Screensaver.image then
widget_settings.image = Screensaver.image
widget_settings.image_disposable = true
elseif Screensaver.image_file then
if G_reader_settings:isTrue("screensaver_rotate_auto_for_best_fit") then
if util.getFileNameSuffix(Screensaver.image_file) == "svg" then
widget_settings.image = RenderImage:renderSVGImageFile(Screensaver.image_file, nil, nil, 1)
else
widget_settings.image = RenderImage:renderImageFile(Screensaver.image_file, false, nil, nil)
end
if not widget_settings.image then
widget_settings.image = RenderImage:renderCheckerboard(Screen:getWidth(), Screen:getHeight(),
Screen.bb:getType())
end
widget_settings.image_disposable = true
else
widget_settings.file = Screensaver.image_file
widget_settings.file_do_cache = false
end
widget_settings.alpha = true
end
if G_reader_settings:isTrue("screensaver_rotate_auto_for_best_fit") then
local angle = rotation_mode == 3 and 180 or 0
if (widget_settings.image:getWidth() < widget_settings.image:getHeight()) ~= (widget_settings.width < widget_settings.height) then
angle = angle + (G_reader_settings:isTrue("imageviewer_rotation_landscape_invert") and -90 or 90)
end
widget_settings.rotation_angle = angle
end
widget = ImageWidget:new(widget_settings)
elseif Screensaver.screensaver_type == "bookstatus" then
local ReaderUI = require("apps/reader/readerui")
widget = BookStatusWidget:new {
ui = ReaderUI.instance,
readonly = true,
}
elseif Screensaver.screensaver_type == "readingprogress" then
widget = Screensaver.getReaderProgress()
end
local covers_fullscreen = true
local background
if Screensaver.screensaver_background == "white" then
background = Blitbuffer.COLOR_WHITE
elseif Screensaver.screensaver_background == "none" then
background = nil
else
background = Blitbuffer.COLOR_BLACK
end
UIManager:setIgnoreTouchInput(false)
if widget then
self.screensaver_widget = ScreenSaverWidget:new {
widget = widget,
background = background,
covers_fullscreen = covers_fullscreen,
}
self.screensaver_widget.dithered = true
else
local currentFileSource = debug.getinfo(1, "S").source
local plugin_dir
if currentFileSource:find("^@") then
plugin_dir = currentFileSource:gsub("^@(.*)/[^/]*", "%1")
end
-- dummy widget
widget = ImageWidget:new {
file = plugin_dir .. "/icons/lock.svg",
alpha = true,
width = self.icon_size,
height = self.icon_size,
scale_factor = 0,
original_in_nightmode = false,
}
self.screensaver_widget = ScreenSaverWidget:new {
widget = widget,
background = Blitbuffer.COLOR_BLACK,
covers_fullscreen = covers_fullscreen,
}
self.screensaver_widget.dithered = true
end
end
function PinPadDialog:isBlocked()
local block_start = G_reader_settings:readSetting("block_start_time") or 0
if block_start == 0 then
if self.blocking_dialog then
UIManager:close(self.blocking_dialog, "ui")
self.blocking_dialog = nil
end
return false
end
local elapsed = os.time() - block_start
if elapsed < 0 then
G_reader_settings:saveSetting("block_start_time", 0)
G_reader_settings:saveSetting("current_tries_number", 0)
if self.blocking_dialog then
UIManager:close(self.blocking_dialog, "ui")
self.blocking_dialog = nil
end
return false
end
if elapsed >= TIMEOUT_TIME then
if self.blocking_dialog then
UIManager:close(self.blocking_dialog, "ui")
self.blocking_dialog = nil
end
return false
else
self.remaining_block_time = TIMEOUT_TIME - elapsed
return true
end
end
function PinPadDialog:showPinPad()
local screensaver_visible = UIManager:isWidgetShown(Screensaver.screensaver_widget)
if screensaver_visible then
ScreensaverUtil.freeze()
self.reused_screensaver = true
else
Screensaver:setup()
self:initializeScreensaverBackground()
if self.screensaver_widget then
UIManager:show(self.screensaver_widget)
end
self.reused_screensaver = false
end
local currentFileSource = debug.getinfo(1, "S").source
local plugin_dir
if currentFileSource:find("^@") then
plugin_dir = currentFileSource:gsub("^@(.*)/[^/]*", "%1")
end
if self.stage == "locked" then
self.dialog = PinPadButtonDialog:new {
title = self.dialog_text,
title_align = "center",
use_info_style = false,
buttons = self.buttons,
dismissable = false,
ImageWidget:new {
file = plugin_dir .. "/icons/lock.svg",
alpha = true,
width = self.icon_size,
height = self.icon_size,
scale_factor = 0,
original_in_nightmode = false,
}
}
else
self.dialog = PinPadButtonDialog:new {
title = self.dialog_text,
title_align = "center",
use_info_style = false,
buttons = self.buttons,
dismissable = true,
override_show_message = true,
ImageWidget:new {
file = plugin_dir .. "/icons/lock.svg",
alpha = true,
width = self.icon_size,
height = self.icon_size,
scale_factor = 0,
original_in_nightmode = false,
}
}
end
UIManager:show(self.dialog, "ui")
UIManager:nextTick(function()
for i = 1, #UIManager._window_stack do
if UIManager._window_stack[i].widget == self.dialog then
local dialog_entry = table.remove(UIManager._window_stack, i)
table.insert(UIManager._window_stack, dialog_entry)
UIManager:setDirty(self.dialog, "ui")
break
end
end
end)
if self.stage == "locked" and self:isBlocked() then
self:showBlockingDialog(self.remaining_block_time)
end
end
function PinPadDialog:showBlockingDialog(remaining_time)
self.blocking_dialog = InfoMessage:new {
text = _("Too many failed attempts. Wait " .. remaining_time .. " seconds."),
timeout = remaining_time,
dismissable = false,
}
UIManager:show(self.blocking_dialog, "ui")
UIManager:nextTick(function()
if not self.blocking_dialog then return end
for i = 1, #UIManager._window_stack do
if UIManager._window_stack[i].widget == self.blocking_dialog then
local blocking_entry = table.remove(UIManager._window_stack, i)
table.insert(UIManager._window_stack, blocking_entry)
UIManager:setDirty(self.blocking_dialog, "ui")
break
end
end
end)
end
function PinPadDialog:reset()
self.pin = ""
self:setDialogText()
end
function PinPadDialog:closeDialogs()
if self.digit_display_timer then
self.digit_display_timer:stop()
self.digit_display_timer = nil
end
if self.blocking_dialog then
UIManager:close(self.blocking_dialog, "ui")
end
if self.dialog then
UIManager:close(self.dialog, "ui")
end
end
function PinPadDialog:close(callback_function)
self:reset()
if not callback_function then
self:closeDialogs()
end
if self.reused_screensaver then
ScreensaverUtil.forceClose(function()
if callback_function then
self:closeDialogs()
callback_function()
end
end)
else
if self.screensaver_widget then
ScreensaverUtil.forceClose(function()
if callback_function then
self:closeDialogs()
callback_function()
end
end)
UIManager:close(self.screensaver_widget)
self.screensaver_widget = nil
end
end
end
function PinPadDialog:onAppendToPin(digit)
if self.digit_display_timer then
self.digit_display_timer:stop()
self.digit_display_timer = nil
end
if self.dialog_text == LOCKED_TEXT or self.dialog_text == CONFIRM_CURRENT_CODE_TEXT or self.dialog_text == NEW_CODE_TEXT then
self.dialog_text = ""
end
self.pin = self.pin .. digit
if G_reader_settings:isTrue("pinpadlock_display_digit_activated") then
local temp_display_text = self.dialog_text .. digit
self.dialog:updateTitle(temp_display_text)
self.dialog_text = self.dialog_text .. "*"
self.digit_display_timer = UIManager:scheduleIn(1, function()
self.dialog:updateTitle(self.dialog_text)
self.digit_display_timer = nil
end)
else
self.dialog_text = self.dialog_text .. "*"
self.dialog:updateTitle(self.dialog_text)
end
end
function PinPadDialog:onOk()
if self.stage == "locked" then
if self.pin == G_reader_settings:readSetting("pinpadlock_pin_code") then
self:close()
if G_reader_settings:isTrue("pinpadlock_correct_pin_message_activated") then
UIManager:show(InfoMessage:new { text = _("Correct PIN, have fun !"), timeout = 2 })
end
G_reader_settings:saveSetting("current_tries_number", 0)
G_reader_settings:saveSetting("block_start_time", 0)
G_reader_settings:makeFalse("suspended_device")
else
if self.pin == "" then
return
end
local current_tries = G_reader_settings:readSetting("current_tries_number")
if (current_tries + 1) >= MAX_TRIES_LIMIT then
G_reader_settings:saveSetting("block_start_time", os.time())
self:showBlockingDialog(TIMEOUT_TIME)
else
G_reader_settings:saveSetting("current_tries_number", current_tries + 1)
UIManager:show(InfoMessage:new { text = _("Wrong PIN, try again."), timeout = 2 })
end
self:onCancel()
end
elseif self.stage == "confirm_current_code" then
if self.pin == G_reader_settings:readSetting("pinpadlock_pin_code") then
self:close()
UIManager:show(InfoMessage:new { text = _("Correct PIN, you may change your PIN Code !"), timeout = 2 })
local enterNewCode = PinPadDialog:new { stage = "enter_new_code" }
enterNewCode:showPinPad()
else
UIManager:show(InfoMessage:new { text = _("Wrong PIN, try again."), timeout = 2 })
self:onCancel()
end
else
G_reader_settings:saveSetting("pinpadlock_pin_code", self.pin)
self:close()
UIManager:show(InfoMessage:new { text = _("PIN Code changed successfully !"), timeout = 2 })
end
end
function PinPadDialog:onDelete()
if self.pin == "" then
return
elseif #self.pin == 1 then
self.pin = ""
else
self.pin = self.pin:sub(1, #self.pin - 1)
end
if #self.dialog_text == 1 then
self:setDialogText()
elseif not (self.dialog_text == LOCKED_TEXT or self.dialog_text == CONFIRM_CURRENT_CODE_TEXT or self.dialog_text == NEW_CODE_TEXT) then
self.dialog_text = self.dialog_text:sub(1, #self.dialog_text - 1)
end
self.dialog:updateTitle(self.dialog_text)
end
function PinPadDialog:onCancel()
if self.pin == "" then
return
end
self:reset()
self.dialog:updateTitle(self.dialog_text)
end
return PinPadDialog

View File

@@ -0,0 +1,53 @@
--[[
Author: Lena2309, oleasteo (original inspiration)
Inspiration and original skeleton from `https://github.com/oleasteo/koreader-screenlockpin`
Description: Utility to prevent screensaver from closing and manage z-order
]]
local Device = require("device")
local Screensaver = require("ui/screensaver")
local ScreensaverUtil = {}
local _original_setup = nil
local _original_close = nil
local _frozen = false
-- Prevent screensaver from closing
function ScreensaverUtil.freeze()
if _frozen then return end
Device.screen_saver_lock = false
Device.screen_saver_mode = false
if not _original_setup then
_original_setup = Screensaver.setup
end
_original_close = Screensaver.close
Screensaver.setup = function() end
Screensaver.close = function() end
_frozen = true
end
-- Restore screensaver normal behavior
function ScreensaverUtil.unfreeze(callback)
if callback and _original_setup then
Screensaver.setup = function()
callback()
_original_setup(Screensaver)
end
elseif _original_setup then
Screensaver.setup = _original_setup
_original_setup = nil
end
if _original_close then
Screensaver.close = _original_close
_original_close = nil
end
_frozen = false
end
function ScreensaverUtil.forceClose(callback)
ScreensaverUtil.unfreeze(callback)
Screensaver:close_widget()
end
return ScreensaverUtil

BIN
screenshots/lockscreen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

BIN
screenshots/menu_entry.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB