From 20eb36a03d4283a42d59c8a830e0cdcb5e8afb25 Mon Sep 17 00:00:00 2001 From: Qingping Hou Date: Sat, 25 Jun 2016 17:53:08 -0700 Subject: [PATCH] feat: add network management UI for kobo --- base | 2 +- datastorage.lua | 14 +- frontend/device/kobo/device.lua | 34 +- frontend/docsettings.lua | 92 ++-- .../elements/common_settings_menu_table.lua | 2 +- .../{networkmgr.lua => network/manager.lua} | 75 ++- frontend/ui/network/wpa_supplicant.lua | 113 +++++ frontend/ui/otamanager.lua | 2 +- frontend/ui/widget/confirmbox.lua | 19 +- frontend/ui/widget/imagewidget.lua | 16 +- frontend/ui/widget/inputdialog.lua | 1 + frontend/ui/widget/inputtext.lua | 36 +- frontend/ui/widget/keyvaluepage.lua | 2 +- frontend/ui/widget/listview.lua | 123 +++++ frontend/ui/widget/networksetting.lua | 466 ++++++++++++++++++ frontend/ui/widget/opdsbrowser.lua | 2 +- frontend/ui/widget/textboxwidget.lua | 19 +- frontend/ui/widget/touchmenu.lua | 2 +- plugins/evernote.koplugin/main.lua | 2 +- plugins/kosync.koplugin/main.lua | 2 +- resources/icons/koicon.wifi.open.0.medium.png | Bin 0 -> 729 bytes .../icons/koicon.wifi.open.100.medium.png | Bin 0 -> 774 bytes .../icons/koicon.wifi.open.25.medium.png | Bin 0 -> 758 bytes .../icons/koicon.wifi.open.50.medium.png | Bin 0 -> 792 bytes .../icons/koicon.wifi.open.75.medium.png | Bin 0 -> 800 bytes .../icons/koicon.wifi.secure.0.medium.png | Bin 0 -> 877 bytes .../icons/koicon.wifi.secure.100.medium.png | Bin 0 -> 846 bytes .../icons/koicon.wifi.secure.25.medium.png | Bin 0 -> 865 bytes .../icons/koicon.wifi.secure.50.medium.png | Bin 0 -> 870 bytes .../icons/koicon.wifi.secure.75.medium.png | Bin 0 -> 874 bytes spec/unit/commonrequire.lua | 2 +- utils/wbuilder.lua | 51 +- 32 files changed, 970 insertions(+), 107 deletions(-) rename frontend/ui/{networkmgr.lua => network/manager.lua} (55%) create mode 100644 frontend/ui/network/wpa_supplicant.lua create mode 100644 frontend/ui/widget/listview.lua create mode 100644 frontend/ui/widget/networksetting.lua create mode 100644 resources/icons/koicon.wifi.open.0.medium.png create mode 100644 resources/icons/koicon.wifi.open.100.medium.png create mode 100644 resources/icons/koicon.wifi.open.25.medium.png create mode 100644 resources/icons/koicon.wifi.open.50.medium.png create mode 100644 resources/icons/koicon.wifi.open.75.medium.png create mode 100644 resources/icons/koicon.wifi.secure.0.medium.png create mode 100644 resources/icons/koicon.wifi.secure.100.medium.png create mode 100644 resources/icons/koicon.wifi.secure.25.medium.png create mode 100644 resources/icons/koicon.wifi.secure.50.medium.png create mode 100644 resources/icons/koicon.wifi.secure.75.medium.png diff --git a/base b/base index b1eb0b5bc..f52fbec91 160000 --- a/base +++ b/base @@ -1 +1 @@ -Subproject commit b1eb0b5bc1e6f49536bce550e93342155494d78d +Subproject commit f52fbec914e54ac24be68161951788e9fb12253c diff --git a/datastorage.lua b/datastorage.lua index c6959fd24..80d1b4d9d 100644 --- a/datastorage.lua +++ b/datastorage.lua @@ -4,8 +4,10 @@ local lfs = require("libs/libkoreader-lfs") local DataStorage = {} +local data_dir function DataStorage:getDataDir() - local data_dir + if data_dir then return data_dir end + if isAndroid then data_dir = "/sdcard/koreader" elseif os.getenv("UBUNTU_APPLICATION_ISOLATION") then @@ -19,6 +21,7 @@ function DataStorage:getDataDir() if lfs.attributes(data_dir, "mode") ~= "directory" then lfs.mkdir(data_dir) end + return data_dir end @@ -26,9 +29,16 @@ function DataStorage:getHistoryDir() return self:getDataDir() .. "/history" end +function DataStorage:getSettingsDir() + return self:getDataDir() .. "/settings" +end + local function initDataDir() local data_dir = DataStorage:getDataDir() - local sub_data_dirs = {"cache", "clipboard", "data", "history", "ota", "screenshots"} + local sub_data_dirs = { + "cache", "clipboard", "data", "history", + "ota", "screenshots", "settings", + } for _, dir in ipairs(sub_data_dirs) do local sub_data_dir = data_dir .. "/" .. dir if lfs.attributes(sub_data_dir, "mode") ~= "directory" then diff --git a/frontend/device/kobo/device.lua b/frontend/device/kobo/device.lua index 8157a4502..a17bd00f7 100644 --- a/frontend/device/kobo/device.lua +++ b/frontend/device/kobo/device.lua @@ -2,19 +2,19 @@ local Generic = require("device/generic/device") local TimeVal = require("ui/timeval") local Geom = require("ui/geometry") local dbg = require("dbg") +local sleep = require("ffi/util").sleep +local _ = require("gettext") local function yes() return true end local function koboEnableWifi(toggle) if toggle == 1 then - os.execute("lsmod | grep -q sdio_wifi_pwr || insmod /drivers/$PLATFORM/wifi/sdio_wifi_pwr.ko") - os.execute("lsmod | grep -q dhd || insmod /drivers/$PLATFORM/wifi/dhd.ko") - os.execute("sleep 2") - os.execute("ifconfig eth0 up") + os.execute("/sbin/lsmod | grep -q sdio_wifi_pwr || /sbin/insmod /drivers/$PLATFORM/wifi/sdio_wifi_pwr.ko") + os.execute("/sbin/lsmod | grep -q dhd || /sbin/insmod /drivers/$PLATFORM/wifi/dhd.ko") + sleep(1) + os.execute("/sbin/ifconfig eth0 up") os.execute("wlarm_le -i eth0 up") - os.execute("pidof wpa_supplicant >/dev/null || cd / && env -u LD_LIBRARY_PATH wpa_supplicant -s -i eth0 -c /etc/wpa_supplicant/wpa_supplicant.conf -C /var/run/wpa_supplicant -B") - os.execute("sleep 1") - os.execute("cd / && env -u LD_LIBRARY_PATH /sbin/udhcpc -S -i eth0 -s /etc/udhcpc.d/default.script -t15 -T10 -A3 -b -q >/dev/null 2>&1 &") + os.execute("pidof wpa_supplicant >/dev/null || env -u LD_LIBRARY_PATH wpa_supplicant -s -ieth0 -O /var/run/wpa_supplicant -c/etc/wpa_supplicant/wpa_supplicant.conf -B") else os.execute("killall udhcpc default.script wpa_supplicant 2>/dev/null") os.execute("wlarm_le -i eth0 down") @@ -159,12 +159,28 @@ function Kobo:init() end function Kobo:initNetworkManager(NetworkMgr) - NetworkMgr.turnOffWifi = function() + function NetworkMgr:turnOffWifi(complete_callback) koboEnableWifi(0) + if complete_callback then + complete_callback() + end end - NetworkMgr.turnOnWifi = function() + function NetworkMgr:turnOnWifi(complete_callback) koboEnableWifi(1) + self:showNetworkMenu(complete_callback) + end + + NetworkMgr:setWirelessBackend( + "wpa_supplicant", {ctrl_interface = "/var/run/wpa_supplicant/eth0"}) + + function NetworkMgr:obtainIP() + os.execute("env -u LD_LIBRARY_PATH /sbin/udhcpc -S -i eth0 -s /etc/udhcpc.d/default.script -t15 -T10 -A3 -b -q") + end + + function NetworkMgr:releaseIP() + os.execute("pkill -9 -f '/bin/sh /etc/udhcpc.d/default.script';") + os.execute("/sbin/ifconfig eth0 0.0.0.0") end end diff --git a/frontend/docsettings.lua b/frontend/docsettings.lua index 39c52bd47..098f633b0 100644 --- a/frontend/docsettings.lua +++ b/frontend/docsettings.lua @@ -6,7 +6,6 @@ local purgeDir = require("ffi/util").purgeDir local DocSettings = {} local HISTORY_DIR = DataStorage:getHistoryDir() -local READER_SETTING_FILE = DataStorage:getDataDir() .. "/settings.reader.lua" local function buildCandidate(file_path) if lfs.attributes(file_path, "mode") == "file" then @@ -42,62 +41,55 @@ end function DocSettings:open(docfile) -- TODO(zijiehe): Remove history_path, use only sidecar. - local new = { data = {} } + local new = {} local ok, stored - if docfile == ".reader" then - -- we handle reader setting as special case - new.history_file = READER_SETTING_FILE - ok, stored = pcall(dofile, new.history_file) - else - new.history_file = self:getHistoryPath(docfile) + new.history_file = self:getHistoryPath(docfile) - local sidecar = self:getSidecarDir(docfile) - new.sidecar = sidecar - if lfs.attributes(sidecar, "mode") ~= "directory" then - lfs.mkdir(sidecar) - end - -- If there is a file which has a same name as the sidecar directory, or - -- the file system is read-only, we should not waste time to read it. - if lfs.attributes(sidecar, "mode") == "directory" then - -- New sidecar file name is metadata.{file last suffix}.lua. So we - -- can handle two files with only different suffixes. - new.sidecar_file = sidecar.."/metadata.".. - docfile:match(".*%.(.*)")..".lua" - if docfile:find("/") then - new.legacy_sidecar_file = sidecar.."/".. - docfile:match(".*%/(.*)")..".lua" - else - new.legacy_sidecar_file = sidecar.."/"..docfile..".lua" - end - end + local sidecar = self:getSidecarDir(docfile) + new.sidecar = sidecar + if lfs.attributes(sidecar, "mode") ~= "directory" then + lfs.mkdir(sidecar) + end + -- If there is a file which has a same name as the sidecar directory, or + -- the file system is read-only, we should not waste time to read it. + if lfs.attributes(sidecar, "mode") == "directory" then + -- New sidecar file name is metadata.{file last suffix}.lua. So we + -- can handle two files with only different suffixes. + new.sidecar_file = sidecar.."/metadata.".. + docfile:match(".*%.(.+)")..".lua" + new.legacy_sidecar_file = sidecar.."/".. + docfile:match("([^%/]+%..+)")..".lua" + end - new.candidates = {} - -- New sidecar file - table.insert(new.candidates, buildCandidate(new.sidecar_file)) - -- Legacy sidecar file - table.insert(new.candidates, buildCandidate(new.legacy_sidecar_file)) - -- Legacy history folder - table.insert(new.candidates, buildCandidate(new.history_file)) - -- Legacy kpdfview setting - table.insert(new.candidates, buildCandidate(docfile..".kpdfview.lua")) - table.sort(new.candidates, function(l, r) - if l == nil then - return false - elseif r == nil then - return true - else - return l[2] > r[2] - end - end) - for _, k in pairs(new.candidates) do - ok, stored = pcall(dofile, k[1]) - if ok then - break - end + local candidates = {} + -- New sidecar file + table.insert(candidates, buildCandidate(new.sidecar_file)) + -- Legacy sidecar file + table.insert(candidates, buildCandidate(new.legacy_sidecar_file)) + -- Legacy history folder + table.insert(candidates, buildCandidate(new.history_file)) + -- Legacy kpdfview setting + table.insert(candidates, buildCandidate(docfile..".kpdfview.lua")) + table.sort(candidates, function(l, r) + if l == nil then + return false + elseif r == nil then + return true + else + return l[2] > r[2] + end + end) + for _, k in pairs(candidates) do + ok, stored = pcall(dofile, k[1]) + if ok then + break end end if ok and stored then new.data = stored + new.candidates = candidates + else + new.data = {} end return setmetatable(new, {__index = DocSettings}) diff --git a/frontend/ui/elements/common_settings_menu_table.lua b/frontend/ui/elements/common_settings_menu_table.lua index ad9ffcc5a..bfadb9f35 100644 --- a/frontend/ui/elements/common_settings_menu_table.lua +++ b/frontend/ui/elements/common_settings_menu_table.lua @@ -1,6 +1,6 @@ local Device = require("device") local Language = require("ui/language") -local NetworkMgr = require("ui/networkmgr") +local NetworkMgr = require("ui/network/manager") local UIManager = require("ui/uimanager") local Screen = require("device").screen local _ = require("gettext") diff --git a/frontend/ui/networkmgr.lua b/frontend/ui/network/manager.lua similarity index 55% rename from frontend/ui/networkmgr.lua rename to frontend/ui/network/manager.lua index 9bbe0883c..2d0a3ce96 100644 --- a/frontend/ui/networkmgr.lua +++ b/frontend/ui/network/manager.lua @@ -1,6 +1,8 @@ local InfoMessage = require("ui/widget/infomessage") local ConfirmBox = require("ui/widget/confirmbox") local UIManager = require("ui/uimanager") +local LuaSettings = require("luasettings") +local DataStorage = require("datastorage") local Device = require("device") local T = require("ffi/util").template local _ = require("gettext") @@ -8,26 +10,37 @@ local _ = require("gettext") local NetworkMgr = {} --- Device specific method, needs to be initialized in Device:initNetworkManager -function NetworkMgr:turnOnWifi() end +function NetworkMgr:init() + self.nw_settings = LuaSettings:open(DataStorage:getSettingsDir().."/network.lua") +end --- Device specific method, needs to be initialized in Device:initNetworkManager +-- Following methods are Device specific which need to be initialized in +-- Device:initNetworkManager. Some of them can be set by calling +-- NetworkMgr:setWirelessBackend +function NetworkMgr:turnOnWifi() end function NetworkMgr:turnOffWifi() end +function NetworkMgr:getNetworkList() end +function NetworkMgr:getCurrentNetwork() end +function NetworkMgr:authenticateNetwork() end +function NetworkMgr:disconnectNetwork() end +function NetworkMgr:obtainIP() end +function NetworkMgr:releaseIP() end +-- End of device specific methods -function NetworkMgr:promptWifiOn() +function NetworkMgr:promptWifiOn(complete_callback) UIManager:show(ConfirmBox:new{ text = _("Do you want to turn on Wi-Fi?"), ok_callback = function() - self:turnOnWifi() + self:turnOnWifi(complete_callback) end, }) end -function NetworkMgr:promptWifiOff() +function NetworkMgr:promptWifiOff(complete_callback) UIManager:show(ConfirmBox:new{ text = _("Do you want to turn off Wi-Fi?"), ok_callback = function() - self:turnOffWifi() + self:turnOffWifi(complete_callback) end, }) end @@ -53,11 +66,15 @@ function NetworkMgr:getWifiMenuTable() text = _("Wi-Fi connection"), enabled_func = function() return Device:isKindle() or Device:isKobo() end, checked_func = function() return NetworkMgr:getWifiStatus() end, - callback = function() + callback = function(menu) + local complete_callback = function() + -- notify touch menu to update item check state + menu:updateItems() + end if NetworkMgr:getWifiStatus() then - NetworkMgr:promptWifiOff() + NetworkMgr:promptWifiOff(complete_callback) else - NetworkMgr:promptWifiOn() + NetworkMgr:promptWifiOn(complete_callback) end end } @@ -100,6 +117,44 @@ function NetworkMgr:getProxyMenuTable() } end +function NetworkMgr:showNetworkMenu(complete_callback) + local info = InfoMessage:new{text = _("Scanning…")} + UIManager:show(info) + UIManager:nextTick(function() + local network_list = self:getNetworkList() + UIManager:close(info) + UIManager:show(require("ui/widget/networksetting"):new{ + network_list = network_list, + connect_callback = complete_callback, + }) + end) +end + +function NetworkMgr:saveNetwork(setting) + if not self.nw_settings then self:init() end + self.nw_settings:saveSetting(setting.ssid, { + ssid = setting.ssid, + password = setting.password, + flags = setting.flags, + }) + self.nw_settings:flush() +end + +function NetworkMgr:deleteNetwork(setting) + if not self.nw_settings then self:init() end + self.nw_settings:delSetting(setting.ssid) + self.nw_settings:flush() +end + +function NetworkMgr:getAllSavedNetworks() + if not self.nw_settings then self:init() end + return self.nw_settings +end + +function NetworkMgr:setWirelessBackend(name, options) + require("ui/network/"..name).init(self, options) +end + -- set network proxy if global variable NETWORK_PROXY is defined if NETWORK_PROXY then NetworkMgr:setHTTPProxy(NETWORK_PROXY) diff --git a/frontend/ui/network/wpa_supplicant.lua b/frontend/ui/network/wpa_supplicant.lua new file mode 100644 index 000000000..e171b8e1d --- /dev/null +++ b/frontend/ui/network/wpa_supplicant.lua @@ -0,0 +1,113 @@ +local UIManager = require("ui/uimanager") +local WpaClient = require('lj-wpaclient/wpaclient') +local InfoMessage = require("ui/widget/infomessage") +local sleep = require("ffi/util").sleep +local _ = require("gettext") + +local WpaSupplicant = {} + +function WpaSupplicant:getNetworkList() + local wcli, _ = WpaClient.new(self.wpa_supplicant.ctrl_interface) + local list = wcli:scanThenGetResults() + wcli:close() + + local saved_networks = self:getAllSavedNetworks() + local curr_network = self:getCurrentNetwork() + + for _,network in ipairs(list) do + network.signal_quality = network:getSignalQuality() + local saved_nw = saved_networks:readSetting(network.ssid) + if saved_nw and saved_nw.flags == network.flags then + network.password = saved_nw.password + end + -- TODO: also verify bssid if it is not set to any + if curr_network and curr_network.ssid == network.ssid then + network.connected = true + network.wpa_supplicant_id = curr_network.id + end + end + return list +end + +function WpaSupplicant:authenticateNetwork(network) + -- TODO: support passwordless network + local err, wcli, nw_id + wcli, err = WpaClient.new(self.wpa_supplicant.ctrl_interface) + + if not wcli then + return false, _("Failed to initialize network control client: ")..err + end + + nw_id, err = wcli:addNetwork() + if err then return false, err end + + wcli:setNetwork(nw_id, "ssid", network.ssid) + wcli:setNetwork(nw_id, "psk", network.password) + wcli:enableNetworkByID(nw_id) + + wcli:attach() + local cnt = 0 + local failure_cnt = 0 + local max_retry = 30 + local info = InfoMessage:new{text = _("Authenticating…")} + local re, msg + UIManager:show(info) + UIManager:forceRePaint() + while cnt < max_retry do + local ev = wcli:readEvent() + if ev ~= nil then + if not ev:isScanEvent() then + UIManager:close(info) + info = InfoMessage:new{text = ev.msg} + UIManager:show(info) + UIManager:forceRePaint() + end + if ev:isAuthSuccessful() then + network.wpa_supplicant_id = nw_id + re = true + break + elseif ev:isAuthFailed() then + failure_cnt = failure_cnt + 1 + if failure_cnt > 3 then + re, msg = false, _('Failed to authenticate') + break + end + end + else + sleep(1) + cnt = cnt + 1 + end + end + if re ~= true then wcli:removeNetwork(nw_id) end + wcli:close() + UIManager:close(info) + UIManager:forceRePaint() + if cnt >= max_retry then + re, msg = false, _('Timed out') + end + return re, msg +end + +function WpaSupplicant:disconnectNetwork(network) + if not network.wpa_supplicant_id then return end + local wcli, _ = WpaClient.new(self.wpa_supplicant.ctrl_interface) + wcli:removeNetwork(network.wpa_supplicant_id) + wcli:close() +end + +function WpaSupplicant:getCurrentNetwork() + local wcli, _ = WpaClient.new(self.wpa_supplicant.ctrl_interface) + local nw = wcli:getCurrentNetwork() + wcli:close() + return nw +end + +function WpaSupplicant.init(network_mgr, options) + network_mgr.wpa_supplicant = {ctrl_interface = options.ctrl_interface} + network_mgr.getNetworkList = WpaSupplicant.getNetworkList + network_mgr.getCurrentNetwork = WpaSupplicant.getCurrentNetwork + network_mgr.authenticateNetwork = WpaSupplicant.authenticateNetwork + network_mgr.disconnectNetwork = WpaSupplicant.disconnectNetwork +end + +return WpaSupplicant diff --git a/frontend/ui/otamanager.lua b/frontend/ui/otamanager.lua index 5231407a4..11e3407a9 100644 --- a/frontend/ui/otamanager.lua +++ b/frontend/ui/otamanager.lua @@ -1,6 +1,6 @@ local InfoMessage = require("ui/widget/infomessage") local ConfirmBox = require("ui/widget/confirmbox") -local NetworkMgr = require("ui/networkmgr") +local NetworkMgr = require("ui/network/manager") local lfs = require("libs/libkoreader-lfs") local DataStorage = require("datastorage") local UIManager = require("ui/uimanager") diff --git a/frontend/ui/widget/confirmbox.lua b/frontend/ui/widget/confirmbox.lua index af075aaa3..e4d1f26af 100644 --- a/frontend/ui/widget/confirmbox.lua +++ b/frontend/ui/widget/confirmbox.lua @@ -1,3 +1,18 @@ +--[[-- +Widget that shows a message and OK/Cancel buttons + +Example: + + UIManager:show(ConfirmBox:new{ + text = _("Save the document?"), + ok_text = _("Save"), -- ok_text defaults to _("OK") + ok_callback = function() + -- save document + end, + }) + +]] + local InputContainer = require("ui/widget/container/inputcontainer") local CenterContainer = require("ui/widget/container/centercontainer") local FrameContainer = require("ui/widget/container/framecontainer") @@ -15,11 +30,7 @@ local DEBUG = require("dbg") local _ = require("gettext") local Blitbuffer = require("ffi/blitbuffer") --- screen ---[[ -Widget that shows a message and OK/Cancel buttons -]] local ConfirmBox = InputContainer:new{ modal = true, text = _("no text"), diff --git a/frontend/ui/widget/imagewidget.lua b/frontend/ui/widget/imagewidget.lua index 9ed5540dd..5e5d3ce0a 100644 --- a/frontend/ui/widget/imagewidget.lua +++ b/frontend/ui/widget/imagewidget.lua @@ -1,3 +1,16 @@ +--[[-- +ImageWidget shows an image from a file + +Example: + + UIManager:show(ImageWidget:new{ + file = "resources/info-i.png", + -- Make sure alpha is set to true if png has transparent background + -- alpha = true, + }) + +]] + local Widget = require("ui/widget/widget") local Screen = require("device").screen local CacheItem = require("cacheitem") @@ -23,9 +36,6 @@ function ImageCacheItem:onFree() end end ---[[ -ImageWidget shows an image from a file ---]] local ImageWidget = Widget:new{ file = nil, image = nil, diff --git a/frontend/ui/widget/inputdialog.lua b/frontend/ui/widget/inputdialog.lua index 014a54133..186097661 100644 --- a/frontend/ui/widget/inputdialog.lua +++ b/frontend/ui/widget/inputdialog.lua @@ -11,6 +11,7 @@ Example: input = "default value", input_hint = "hint text", input_type = "text", + -- text_type = "password", buttons = { { { diff --git a/frontend/ui/widget/inputtext.lua b/frontend/ui/widget/inputtext.lua index 2de3bd35d..7243ea4ac 100644 --- a/frontend/ui/widget/inputtext.lua +++ b/frontend/ui/widget/inputtext.lua @@ -73,23 +73,33 @@ end function InputText:initTextBox(text) self.text = text - self.charlist = util.splitToChars(text) - if self.charpos == nil then - self.charpos = #self.charlist + 1 - end - local fgcolor = Blitbuffer.gray(self.text == "" and 0.5 or 1.0) - - local show_text = self.text - if self.text_type == "password" and show_text ~= "" then - show_text = self.text:gsub("(.-).", function() return "*" end) - show_text = show_text:gsub("(.)$", function() return self.text:sub(-1) end) - elseif show_text == "" then + local fgcolor + local show_charlist + local show_text = text + if show_text == "" or show_text == nil then + -- no preset value, use hint text if set show_text = self.hint + fgcolor = Blitbuffer.COLOR_GREY + self.charlist = {} + self.charpos = 1 + else + fgcolor = Blitbuffer.COLOR_BLACK + if self.text_type == "password" then + show_text = self.text:gsub( + "(.-).", function() return "*" end) + show_text = show_text:gsub( + "(.)$", function() return self.text:sub(-1) end) + end + self.charlist = util.splitToChars(text) + if self.charpos == nil then + self.charpos = #self.charlist + 1 + end end + show_charlist = util.splitToChars(show_text) if self.scroll then self.text_widget = ScrollTextWidget:new{ text = show_text, - charlist = self.charlist, + charlist = show_charlist, charpos = self.charpos, editable = self.focused, face = self.face, @@ -100,7 +110,7 @@ function InputText:initTextBox(text) else self.text_widget = TextBoxWidget:new{ text = show_text, - charlist = self.charlist, + charlist = show_charlist, charpos = self.charpos, editable = self.focused, face = self.face, diff --git a/frontend/ui/widget/keyvaluepage.lua b/frontend/ui/widget/keyvaluepage.lua index 1774133b1..47c0cacdf 100644 --- a/frontend/ui/widget/keyvaluepage.lua +++ b/frontend/ui/widget/keyvaluepage.lua @@ -39,7 +39,7 @@ local Device = require("device") local Screen = Device.screen -local ellipsis, space = "...", " " +local ellipsis, space = "…", " " local ellipsis_width, space_width local function truncateTextByWidth(text, face, max_width, prepend_space) if not ellipsis_width then diff --git a/frontend/ui/widget/listview.lua b/frontend/ui/widget/listview.lua new file mode 100644 index 000000000..760ffc5ab --- /dev/null +++ b/frontend/ui/widget/listview.lua @@ -0,0 +1,123 @@ +--[[-- +Widget compoent that handles pagination for a list of items. + +Example: + + local list_view = ListView:new{ + height = 400, + width = 200, + page_update_cb = function(curr_page_num, total_pages) + -- This callback function will be called whenever there is a + -- page turn event triggered. You can use it to update information + -- on parent widget. + end, + items = { + FrameContainer:new{ + bordersize = 0, + background = Blitbuffer.COLOR_WHITE + TextWidget:new{ + text = "foo", + fact = Font:getFace("cfont"), + } + }, + FrameContainer:new{ + bordersize = 0, + background = Blitbuffer.COLOR_LIGHT_GREY + TextWidget:new{ + text = "bar", + fact = Font:getFace("cfont"), + } + }, + -- You can add as many widgets as you want here... + } + } + +Note that ListView is created mainly to be used as a building block for other +widgets like NetworkSetting so they can share the same pagination code. +]] + +local InputContainer = require("ui/widget/container/inputcontainer") +local FrameContainer = require("ui/widget/container/framecontainer") +local VerticalGroup = require("ui/widget/verticalgroup") +local GestureRange = require("ui/gesturerange") +local Blitbuffer = require("ffi/blitbuffer") +local Device = require("device") +local Screen = Device.screen +local Geom = require("ui/geometry") + +local ListView = InputContainer:new{ + width = nil, + height = nil, + padding = nil, + item_height = nil, + itmes = nil, +} + +function ListView:init() + self.show_page = 1 + self.dimen = Geom:new{w = self.width, h = self.height} + + if Device:isTouchDevice() then + self.ges_events.Swipe = { + GestureRange:new{ + ges = "swipe", + range = self.dimen, + } + } + end + + local padding = self.padding or Screen:scaleBySize(10) + self.item_height = self.item_height or self.items[1]:getSize().h + self.item_width = self.dimen.w - 2 * padding + self.items_per_page = math.floor(self.height / self.item_height) + self.main_content = VerticalGroup:new{} + self:_populateItems() + self[1] = FrameContainer:new{ + height = self.dimen.h, + padding = padding, + bordersize = 0, + background = Blitbuffer.COLOR_WHITE, + self.main_content, + } +end + +-- make sure self.item_height are set before calling this +function ListView:_populateItems() + self.pages = math.ceil(#self.items / self.items_per_page) + self.main_content:clear() + local idx_offset = (self.show_page - 1) * self.items_per_page + for idx = 1, self.items_per_page do + local item = self.items[idx_offset + idx] + if item == nil then break end + table.insert(self.main_content, item) + end + self.page_update_cb(self.show_page, self.pages) +end + +function ListView:nextPage() + local new_page = math.min(self.show_page+1, self.pages) + if new_page > self.show_page then + self.show_page = new_page + self:_populateItems() + end +end + +function ListView:prevPage() + local new_page = math.max(self.show_page-1, 1) + if new_page < self.show_page then + self.show_page = new_page + self:_populateItems() + end +end + +function ListView:onSwipe(arg, ges_ev) + if ges_ev.direction == "west" then + self:nextPage() + return true + elseif ges_ev.direction == "east" then + self:prevPage() + return true + end +end + +return ListView diff --git a/frontend/ui/widget/networksetting.lua b/frontend/ui/widget/networksetting.lua new file mode 100644 index 000000000..bb776483b --- /dev/null +++ b/frontend/ui/widget/networksetting.lua @@ -0,0 +1,466 @@ +--[[-- +Network setting widget. + +Example: + + local network_list = { + { + ssid = "foo", + signal_level = -58, + flags = "[WPA2-PSK-CCMP][ESS]", + signal_quality = 84, + password = "123abc", + connected = true, + }, + { + ssid = "bar", + signal_level = -258, + signal_quality = 44, + flags = "[WEP][ESS]", + }, + } + UIManager:show(require("ui/widget/networksetting"):new{ + network_list = network_list, + connect_callback = function() + -- connect_callback will be called when an connect/disconnect + -- attempt has been made. you can update UI widgets in the + -- callback. + end, + }) + +]] + +local FrameContainer = require("ui/widget/container/framecontainer") +local InputContainer = require("ui/widget/container/inputcontainer") +local LeftContainer = require("ui/widget/container/leftcontainer") +local CenterContainer = require("ui/widget/container/centercontainer") +local RightContainer = require("ui/widget/container/rightcontainer") +local HorizontalGroup = require("ui/widget/horizontalgroup") +local HorizontalSpan = require("ui/widget/horizontalspan") +local VerticalGroup = require("ui/widget/verticalgroup") +local OverlapGroup = require("ui/widget/overlapgroup") +local InfoMessage = require("ui/widget/infomessage") +local InputDialog = require("ui/widget/inputdialog") +local NetworkMgr = require("ui/network/manager") +local ListView = require("ui/widget/listview") +local ImageWidget = require("ui/widget/imagewidget") +local Widget = require("ui/widget/widget") +local TextWidget = require("ui/widget/textwidget") +local GestureRange = require("ui/gesturerange") +local Blitbuffer = require("ffi/blitbuffer") +local UIManager = require("ui/uimanager") +local Geom = require("ui/geometry") +local Device = require("device") +local Screen = Device.screen +local Font = require("ui/font") +local _ = require("gettext") + + +local MinimalPaginator = Widget:new{ + width = nil, + height = nil, + progress = nil, +} + +function MinimalPaginator:getSize() + return Geom:new{w = self.width, h = self.height} +end + +function MinimalPaginator:paintTo(bb, x, y) + self.dimen = self:getSize() + self.dimen.x, self.dimen.y = x, y + -- paint background + bb:paintRoundedRect(x, y, + self.dimen.w, self.dimen.h, + Blitbuffer.COLOR_LIGHT_GREY) + -- paint percentage infill + bb:paintRect(x, y, + math.ceil(self.dimen.w*self.progress), self.dimen.h, + Blitbuffer.COLOR_GREY) +end + +function MinimalPaginator:setProgress(progress) self.progress = progress end + + +local NetworkItem = InputContainer:new{ + dimen = nil, + height = Screen:scaleBySize(44), + width = nil, + info = nil, + background = Blitbuffer.COLOR_WHITE, +} + +function NetworkItem:init() + self.dimen = Geom:new{w = self.width, h = self.height} + if not self.info.ssid then + self.info.ssid = "[hidden]" + end + + local wifi_icon_path + if string.find(self.info.flags, "WPA") then + wifi_icon_path = "resources/icons/koicon.wifi.secure.%d.medium.png" + else + wifi_icon_path = "resources/icons/koicon.wifi.open.%d.medium.png" + end + if self.info.signal_quality == 0 or self.info.signal_quality == 100 then + wifi_icon_path = string.format(wifi_icon_path, self.info.signal_quality) + else + wifi_icon_path = string.format( + wifi_icon_path, + self.info.signal_quality + 25 - self.info.signal_quality % 25) + end + local horizontal_space = HorizontalSpan:new{width = Screen:scaleBySize(8)} + self.content_container = OverlapGroup:new{ + dimen = self.dimen:copy(), + LeftContainer:new{ + dimen = self.dimen:copy(), + HorizontalGroup:new{ + horizontal_space, + ImageWidget:new{ + alpha = true, + file = wifi_icon_path, + }, + horizontal_space, + TextWidget:new{ + text = self.info.ssid, + face = Font:getFace("cfont"), + }, + }, + } + } + self.btn_disconnect = nil + self.btn_edit_nw = nil + if self.info.connected then + self.btn_disconnect = FrameContainer:new{ + bordersize = 0, + padding = 0, + TextWidget:new{ + text = _("disconnect"), + face = Font:getFace("cfont"), + } + } + + table.insert(self.content_container, RightContainer:new{ + dimen = self.dimen:copy(), + HorizontalGroup:new{ + self.btn_disconnect, + horizontal_space, + } + }) + elseif self.info.password then + self.btn_edit_nw = FrameContainer:new{ + bordersize = 0, + padding = 0, + TextWidget:new{ + text = _("edit"), + face = Font:getFace("cfont"), + } + } + + table.insert(self.content_container, RightContainer:new{ + dimen = self.dimen:copy(), + HorizontalGroup:new{ + self.btn_edit_nw, + horizontal_space, + } + }) + end + + self[1] = FrameContainer:new{ + padding = 0, + margin = 0, + background = self.background, + bordersize = 0, + width = self.width, + self.content_container, + } + + if Device:isTouchDevice() then + self.ges_events = { + TapSelect = { + GestureRange:new{ + ges = "tap", + range = self.dimen, + } + } + } + end +end + +function NetworkItem:refresh() + self:init() + UIManager:setDirty(self.setting_ui, function() return "ui", self.dimen end) +end + +function NetworkItem:connect() + local connected_item = self.setting_ui:getConnectedItem(self) + if connected_item then connected_item:disconnect() end + + local success, err_msg = NetworkMgr:authenticateNetwork(self.info) + + local text + if success then + local info = InfoMessage:new{text = _("Obtaining IP address…")} + UIManager:show(info) + UIManager:forceRePaint() + NetworkMgr:obtainIP() + UIManager:close(info) + self.info.connected = true + self.setting_ui:setConnectedItem(self) + text = _("Connected.") + else + text = err_msg + end + + if self.setting_ui.connect_callback then + self.setting_ui.connect_callback() + end + UIManager:show(InfoMessage:new{text = text}) +end + +function NetworkItem:disconnect() + local info = InfoMessage:new{text = _("Disconnecting…")} + UIManager:show(info) + UIManager:forceRePaint() + + NetworkMgr:disconnectNetwork(self.info) + NetworkMgr:releaseIP() + + UIManager:close(info) + self.info.connected = nil + self:refresh() + if self.setting_ui.connect_callback then + self.setting_ui.connect_callback() + end +end + +function NetworkItem:saveAndConnectToNetwork(password_input) + local new_passwd = password_input:getInputText() + if new_passwd == nil or string.len(new_passwd) == 0 then + UIManager:show(InfoMessage:new{ + text = _("Password cannot be empty."), + }) + else + if new_passwd ~= self.info.password then + self.info.password = new_passwd + NetworkMgr:saveNetwork(self.info) + end + self:connect() + self:refresh() + end + + UIManager:close(password_input) +end + +function NetworkItem:onEditNetwork() + local password_input + password_input = InputDialog:new{ + title = self.info.ssid, + input = self.info.password, + input_hint = "password", + input_type = "text", + text_type = "password", + buttons = { + { + { + text = _("Cancel"), + callback = function() + UIManager:close(password_input) + end, + }, + { + text = _("Forget"), + callback = function() + NetworkMgr:deleteNetwork(self.info) + self.info.password = nil + -- remove edit button + table.remove(self.content_container, 2) + UIManager:close(password_input) + self:refresh() + end, + }, + { + text = _("Connect"), + is_enter_default = true, + callback = function() + self:saveAndConnectToNetwork(password_input) + end, + }, + }, + }, + } + password_input:onShowKeyboard() + UIManager:show(password_input) + return true +end + +function NetworkItem:onAddNetwork() + local password_input + password_input = InputDialog:new{ + title = self.info.ssid, + input = "", + input_hint = "password", + input_type = "text", + text_type = "password", + buttons = { + { + { + text = _("Cancel"), + callback = function() + UIManager:close(password_input) + end, + }, + { + text = _("Connect"), + is_enter_default = true, + callback = function() + self:saveAndConnectToNetwork(password_input) + end, + }, + }, + }, + } + password_input:onShowKeyboard() + UIManager:show(password_input) + return true +end + +function NetworkItem:onTapSelect(arg, ges_ev) + if not string.find(self.info.flags, "WPA") then + UIManager:show(InfoMessage:new{ + text = _("Networks without WPA/WPA2 encryption are not supported.") + }) + return + end + if self.btn_disconnect then + -- noop if touch is not on disconnect button + if ges_ev.pos:intersectWith(self.btn_disconnect.dimen) then + self:disconnect() + end + elseif self.info.password then + if self.btn_edit_nw and ges_ev.pos:intersectWith(self.btn_edit_nw.dimen) then + self:onEditNetwork() + else + self:connect() + self:refresh() + end + else + self:onAddNetwork() + end + return true +end + + +local NetworkSetting = InputContainer:new{ + width = nil, + height = nil, + -- sample network_list entry: { + -- bssid = "any", + -- ssid = "foo", + -- signal_level = -58, + -- signal_quality = 84, + -- frequency = 5660, + -- flags = "[WPA2-PSK-CCMP][ESS]", + -- } + network_list = nil, + connect_callback = nil, +} + +function NetworkSetting:init() + self.width = self.width or Screen:getWidth() - Screen:scaleBySize(50) + self.width = math.min(self.width, Screen:scaleBySize(600)) + + local gray_bg = Blitbuffer.gray(0.1) + local items = {} + table.sort(self.network_list, + function(l, r) return l.signal_quality > r.signal_quality end) + for idx,network in ipairs(self.network_list) do + local bg + if idx % 2 == 0 then + bg = gray_bg + else + bg = Blitbuffer.COLOR_WHITE + end + table.insert(items, NetworkItem:new{ + width = self.width, + info = network, + background = bg, + setting_ui = self, + }) + end + + self.status_text = TextWidget:new{ + text = "", + face = Font:getFace("ffont"), + } + self.page_text = TextWidget:new{ + text = "", + face = Font:getFace("ffont"), + } + + self.pagination = MinimalPaginator:new{ + width = self.width, + height = Screen:scaleBySize(8), + percentage = 0, + } + + self.height = self.height or math.min(Screen:getHeight()*3/4, + Screen:scaleBySize(800)) + self.popup = FrameContainer:new{ + background = Blitbuffer.COLOR_WHITE, + padding = 0, + bordersize = 3, + VerticalGroup:new{ + align = "left", + self.pagination, + ListView:new{ + padding = 0, + items = items, + width = self.width, + height = self.height-self.pagination:getSize().h, + page_update_cb = function(curr_page, total_pages) + self.pagination:setProgress(curr_page/total_pages) + -- self.page_text:setText(curr_page .. "/" .. total_pages) + UIManager:setDirty(self, function() + return "ui", self.dimen + end) + end + }, + }, + } + + self[1] = CenterContainer:new{ + dimen = {w = Screen:getWidth(), h = Screen:getHeight()}, + self.popup, + } + + 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 + +function NetworkSetting:setConnectedItem(item) + self.connected_item = item +end + +function NetworkSetting:getConnectedItem() + return self.connected_item +end + +function NetworkSetting:onTapClose(arg, ges_ev) + if ges_ev.pos:notIntersectWith(self.popup.dimen) then + UIManager:close(self) + return true + end +end + +return NetworkSetting diff --git a/frontend/ui/widget/opdsbrowser.lua b/frontend/ui/widget/opdsbrowser.lua index bac7728be..ed4c15abd 100644 --- a/frontend/ui/widget/opdsbrowser.lua +++ b/frontend/ui/widget/opdsbrowser.lua @@ -3,7 +3,7 @@ local ButtonDialog = require("ui/widget/buttondialog") local InfoMessage = require("ui/widget/infomessage") local LoginDialog = require("ui/widget/logindialog") local OPDSParser = require("ui/opdsparser") -local NetworkMgr = require("ui/networkmgr") +local NetworkMgr = require("ui/network/manager") local UIManager = require("ui/uimanager") local CacheItem = require("cacheitem") local Menu = require("ui/widget/menu") diff --git a/frontend/ui/widget/textboxwidget.lua b/frontend/ui/widget/textboxwidget.lua index 9a8170dd0..43181f407 100644 --- a/frontend/ui/widget/textboxwidget.lua +++ b/frontend/ui/widget/textboxwidget.lua @@ -87,9 +87,7 @@ end -- Split the text into logical lines to fit into the text box. function TextBoxWidget:_splitCharWidthList() - self.vertical_string_list = { - {text = self.text, offset = 1, width = 0} -- hint for empty string - } + self.vertical_string_list = {} local idx = 1 local size = #self.char_width_list @@ -178,10 +176,17 @@ end -- Return the position of the cursor corresponding to `self.charpos`, -- Be aware of virtual line number of the scorllTextWidget. function TextBoxWidget:_findCharPos() + if self.text == nil or string.len(self.text) == 0 then + return 0, 0 + end -- Find the line number. local ln = self.height == nil and 1 or self.virtual_line_num while ln + 1 <= #self.vertical_string_list do - if self.vertical_string_list[ln + 1].offset > self.charpos then break else ln = ln + 1 end + if self.vertical_string_list[ln + 1].offset > self.charpos then + break + else + ln = ln + 1 + end end -- Find the offset at the current line. local x = 0 @@ -193,6 +198,12 @@ function TextBoxWidget:_findCharPos() return x + 1, (ln - 1) * self.line_height_px -- offset `x` by 1 to avoid overlap end +function TextBoxWidget:moveCursorToCharpos(charpos) + self.charpos = charpos + local x, y = self:_findCharPos() + self.cursor_line:paintTo(self._bb, x, y) +end + -- Click event: Move the cursor to a new location with (x, y), in pixels. -- Be aware of virtual line number of the scorllTextWidget. function TextBoxWidget:moveCursor(x, y) diff --git a/frontend/ui/widget/touchmenu.lua b/frontend/ui/widget/touchmenu.lua index a3fd72f19..833e51c9e 100644 --- a/frontend/ui/widget/touchmenu.lua +++ b/frontend/ui/widget/touchmenu.lua @@ -564,7 +564,7 @@ function TouchMenu:onMenuSelect(item) -- put stuff in scheduler so we can see -- the effect of inverted menu item UIManager:scheduleIn(0.1, function() - callback() + callback(self) if refresh then self:updateItems() else diff --git a/plugins/evernote.koplugin/main.lua b/plugins/evernote.koplugin/main.lua index c81d617b9..9b102868a 100644 --- a/plugins/evernote.koplugin/main.lua +++ b/plugins/evernote.koplugin/main.lua @@ -1,7 +1,7 @@ local InputContainer = require("ui/widget/container/inputcontainer") local LoginDialog = require("ui/widget/logindialog") local InfoMessage = require("ui/widget/infomessage") -local NetworkMgr = require("ui/networkmgr") +local NetworkMgr = require("ui/network/manager") local DataStorage = require("datastorage") local DocSettings = require("docsettings") local UIManager = require("ui/uimanager") diff --git a/plugins/kosync.koplugin/main.lua b/plugins/kosync.koplugin/main.lua index adef437c8..c697c8115 100644 --- a/plugins/kosync.koplugin/main.lua +++ b/plugins/kosync.koplugin/main.lua @@ -3,7 +3,7 @@ local LoginDialog = require("ui/widget/logindialog") local InfoMessage = require("ui/widget/infomessage") local ConfirmBox = require("ui/widget/confirmbox") local DocSettings = require("docsettings") -local NetworkMgr = require("ui/networkmgr") +local NetworkMgr = require("ui/network/manager") local UIManager = require("ui/uimanager") local Screen = require("device").screen local Device = require("device") diff --git a/resources/icons/koicon.wifi.open.0.medium.png b/resources/icons/koicon.wifi.open.0.medium.png new file mode 100644 index 0000000000000000000000000000000000000000..aa72a3a63c48f65b4564006938343534243d4fa3 GIT binary patch literal 729 zcmV;~0w(>5P)xK~z|U&6jI$5>XI_pP`~yd!s2e+E$}qjQ{`F_`yp+Ys3}; zp_ByV^~0HkWxKnar4pQELUJxM@6J0jXD~_ZQffQ({yzYCQsuCj?E^1>y5Ft))-vF)%PJk2ODCKaSOo2h7DsfT>z|5M!YoHP7v`8kvwWNL&05dxPu7LeVI?l=% zXiK^)1Yl+@cWyZb=D?5;)(J3kg=Ic8fMej0`~;VSxLE;UW(K?g&T=kIfS*9WjLVg; zGOH2t<`wxA{suadlvIK7FyvF9Z5Z>`3e}VL^Y3AKYU5}@-mtF?>c~80-X<>6@|0`v~t#mt_jyO2V7^F7l*EC zX#qXpF0)?Rh+2foz#X=wl54OY`PYxd8SGt}K$S$VMdqX(PY~!b=z<1K@K^>&DM`+n;=}Y=r)mG)9)F-7Tf%~*JeQ&z}2#J_Cu$b>{9YEQg%Jwz^Fd)3?S(*FZ z)&blS#XL5mP9tb?6)Q88NB1`!lQ)FKdV!fs+Arc4pPZ3Nlpu7R<*XYqO6`}W?a zvKg4-o-^~G|CyOH@4OTd=2Lp+HTwU7Kwn+Mlv2W%_!@&)fzK6(c+{qU;IF7#+lZ*t z489%b@fcB4AL3m0?d=$&B5=Ym?llNnrGH}=QY`}5i1CjQxU3WSrXv9y!gK||OI*gj zO!5nSmURs_;~=hJvI1iY``;sgew?jvx{pyT>{QFrK^9{yr++rgpRu5wzySU%8U2gh z=xOVV)w!NO;hLiBpWr%9U?gW|#-|tG;Xw)eA1r&P1XzXV+2A$?+sGYj+pD8b@gtUG zj6wXK_a?Efst2~<4Epo;m}b|ntWU5zV=c(3AFXo@`*5m8fJZe6o>VapstBxAbZi;u z27bV|SkY#`65DVbw`TEsjjw7G$e;(>0PbQ_gIlmcaqnmRk5t*FP9r;|^h>VYfrx13 zwWU~(p{ywfo4|NPOlG@9I1Lf8r!vSKC+REwE*!>-4z+$JH{EO)jY?1gS%^C|nr%j} z$~S2)t@R#=h$+QHtSc`fV!D=PgT`8>NO9md#e^HeI_&i}Lr<~1E^fyrEK5phL@^mJ zM9DFw)PwKwAR>OPjoE}iDc}}t&7apI;$~MsbyHO;$3?}Un$9|fi(O%L5quc0yx;k+ zFx(Wcn^*A;#rvTw{dupEbVFc3G5yMt^4=#Wa2>5#)?0b6nRL+uy^6Pg%iH?5@><6y z_y^B{8AΠV5?IrAs&!5&t#D>UIu%ESdNJhVxF~4WHTpcg16oDgXcg07*qoM6N<$ Ef(KMy?*IS* literal 0 HcmV?d00001 diff --git a/resources/icons/koicon.wifi.open.25.medium.png b/resources/icons/koicon.wifi.open.25.medium.png new file mode 100644 index 0000000000000000000000000000000000000000..cbf4866e5b93e7d7e404d4532c4c1496502f1bce GIT binary patch literal 758 zcmVer}uda=^?sfDK?R=5U^jfU`(7!$~FpGus3n0`;6uvt$Sy zOFGR3z|2;FeV}nm$4Th}ElC%d0L-lE&Mn2j6>v@n>kt^b!XzK+z&fx>dV0RI3cO4L_ zU0>=f7nc9~A#b>J8nUsGUa%fM4YID5cnNh3ea4sf6RWjh9b03AtJW>y2904oG4 zKSrgR%z-svlfaYAYzx>UfBB^WX-n!l&HF%aDynI;`r^LEW5?}2ZG_8K*+69Vs17u_aAXbiOT8qGz)_2ST&D)GxCF~0+h zvaa4+14uY~20ZuAx02qMG9;<60!P3l@XPfQI4WhUg!4nf6|IC0fy1Kq%DjqS0Lv-k z%RW~~$^dAP{$fgdQ~VPE??~?n1D`7t85?mTe9%~h6@Mq2NiDfNvDpc%m4rY07*qoM6N<$g7%6xN#%86*nS{R!k+KrA^Z&^tgC;+GIL2nT$%ja7gn0+d0@6D;2`S&+YyuzB$a@f zEdZB*O3t8Z)&@2uHFFU#vuR)jnEJ!Oaccn^lJ-&wm|4|>TY`flV26;_Hqdpzi5@Dz z956$&z~v-<^${>L16F~hgiUSW2hhx_W#3bojS+Nnk>nC?0X0dAy1+P{@(%D#QlrQ5 zq_2x`#`m@y`-CoN0}V540*k<9`UN}(Yy#cm5|~*TxJF3lF7QQC$DigraDn`7+Xa3C zbxB8NRsya9(}Yrfj#@RYfmvXIP$!w$BCt&U_DdpClhp7suK>G!SxqXdFJ2}*d6sqj za}dEgp`53JrKPk2R1?N`98s;iPPoL0E@m9PRs|ZsUTi$`AgU5N0}r?%X*Y#%d~f_L zsY$9QuFdK!GrK}i`G%w;ADahm0@ob|d~|#`7FQ7ODuAS~sX-*AA|mMD$ACwKgGedZ z0`8~zNQ{v++2dpqc$X14j+a0=kMCUf0Z9kIr#x%jwj~|rTA7EXXW<+n#RA>}4+t8# z0X*a+Lv>&&uit_9fnGDSdjw@ai}$pP?lJIP((~Lti(X32>=tm_f4-9RrjVn&sfv|j z9XJ3E9S6XAAzuZ8uMu9+apkvxwPF1gxfS05$_e9TpBu`G5SSwQ#*}oY_$LHj6W$GR z+xNNQtbhi_f%hITs{g)^jSuUuP#c=r1n>m7>q9Sqr}EeHP`-+u1Aog#{@-xq1bzb& Wn3-Gx+fX+E0000i z!Ll}hwmEeAp#+Qq+i*9ynuygd0U{#6K49FD)Bu)%YOh*$%}OLk&^Nnrmv9C6q^cVJ z3WPtGyajw#)inbZ5jg-%Mf|YM^eylv^7%nk*F>ZWi~)P-Zs1X15on{T*u;v!Brpve z1PV4@25teX1WV}S%fL-w*f0u)Ic>trzj?z43mgIV)9H=#z}FPSasLKfFs!i5V_lrl zFA*VmV9tYJ$y4ARV93dpAm9+tbO1dCZU85NU6J?+;52X>c<#t+0Y|+B80c&S;2m&0 zDb|sD3|NTdUrUPZxfP4ZJ!_y@Rju1t1vo;`u#;BX1@qV8IUph>;4XlwUP>hCGn0Je zIR{)J*ocy9y@EBJ3nNttVjx?9mmbX)`Xiv|gY9)6P}L^z-Y3>lJ_?^0P70cG4bCfA+f zp9pvY)E(n>dza2K^uQ4C$_T^y*KN#s;=G literal 0 HcmV?d00001 diff --git a/resources/icons/koicon.wifi.secure.0.medium.png b/resources/icons/koicon.wifi.secure.0.medium.png new file mode 100644 index 0000000000000000000000000000000000000000..cf79ecd9125c9c3d75565704b0e2c8a4853a6255 GIT binary patch literal 877 zcmV-z1CsoSP)b>rr z`*VVVZr$t9wYv~hq>`3No9U`)yXyMw@Vuk5!|XacTKR?Hg?DD2_dCz?eSVBd;cwE31unyED)lY|ESiN0fyNMb9H+RQ}8%nX;X+&H$_}S17U54 zD)3c_!&}IhSqZp`Ya)%n4_pI{4hKcH`hX!|qz$nRU|v$Q9hoUypmFQ~%fQe0mzY@* z4`Bt@QytgFcFO@~1>DP*Qb^S#&9x&_2Ci|K@~gmtr2P=S;qbK;pf}Yehaa#FtVvo8 zwtIld@IH8G-^ab2rkMd?3K!bgRNSyHHLd>R?tSOFF#$;`@Wy6aeH%2z3@3Os?QXL@b$a?lIZ<5?8$6@V+a@T-!(9G8p%gYi-xs`CgQ zjwoF>Bz-co`@o~H*8rY`wbsd-naCuFmGJ63lyZ9k*UuvG7}wKX;HIPpS?qT^LR@S^ z1YA!g;G3iu0A@A^+{$XaTQbXdnn(4fiF;!OZ-UQ(l9_#ozhq_)B|SaTx}>fyyj|P| z#(_d?{|r9I2zU!j$Td?#rsn&v{*#4!WB#D}ZQ$TfCK+6h@UitDI~ z_pt~!@TqY8uagP5j2E`ZD^Z9HxCgwBJ=-J$@($!Byv`gzQp3!qfjhD7H(`C2$!?zi zCNmqvhp@;i4Ls8#W9!LkoCSHqv`cFvEYsSexxQ3irz1KO++`kB3M2 zNFlrce$Hz!mu3!L_Vn&2%)pYiavM7?nUlt;`tkn*79)fLws$&j00000NkvXXu0mjf DdsC7# literal 0 HcmV?d00001 diff --git a/resources/icons/koicon.wifi.secure.100.medium.png b/resources/icons/koicon.wifi.secure.100.medium.png new file mode 100644 index 0000000000000000000000000000000000000000..d3dbe59c1fd3e852cac8bebfb6be886aae73a382 GIT binary patch literal 846 zcmV-U1F`&xP)K8WaR5J^xt$gj$Q7{0Uz+ri!!Psewz z`#v5%7c4mUoW1|+zt`7ZH&ROGQs&L6^*ez;uXbTX1fIegjA0p;%ZE7E&~Nc$s*P<* zsZ$I13pj%FNR@gPAC$c#18sB|4s6EHLm2JSQ+NqcMF7v@yIC;Y*6;DmKmvFT*E#_H z!KZk+kbD$Niav(*com=FN(aVOymAu(EWrC6n|{F-j1G!r>mYYwYjOW{nD1hKlfXj! z&|35dwqaf)FCH%Od>hB)tiOQc*pKnznf{&;Y{!`v_Gv8rUkLCZ{w@nnVyr>#&BnJH z{S*6ecflCLiSq3Vp6tqj4R{X=%I~d)E5Ej0z_x-lzqo!&O&RuZu)@H(N`eP;^n)$} zPskbD2KoYTU?Y|_*58lk@fN@S|`@0q~LTVYqt11Y7eaz~s_-ie4F{>7q*7>|f?`9685 z2E5h_OYv7Z_1AKTTOoJ5y$z52hRfx73J(mztsY2HN*5zym)sdYOpWY~hf#Y(U?MqJ{ z&+o%GL!-?g2S(&JJLyKOC(FBmBlyslQJ0j?w+AnlSD)ZuN@;3nteFs)O_}rmhI3Bf YI{ZzQCi(z3z5oCK07*qoM6N<$f`gELFaQ7m literal 0 HcmV?d00001 diff --git a/resources/icons/koicon.wifi.secure.25.medium.png b/resources/icons/koicon.wifi.secure.25.medium.png new file mode 100644 index 0000000000000000000000000000000000000000..71531fb8d6951aa8e356ff091e2a76d4467cb4b4 GIT binary patch literal 865 zcmV-n1D^beP)Fh;V#&4c*X1wfL3IpAQzq89K8SWV-yYgf_$PB%wLrtm9JH8TrUAa)*k z8~A8u^+@1BKQH*OU)%J)iK~JZP?xj{Oaaq$3wRt@0Xn}ZkhBjti#yH+uw-U!zneqA zVe*%42lxWi%xp_i2{-|a;;4KVN;R&5GBAnb$s|nyGvqJ7Bp_8Yt9zPffkxMJ2bfEX zoBKgxW*tdYU?6wdD*8$E03&0~BU8%QfUr#bKtB2&0u1b0!X*e>a2%PgEa@nrl zgp!^BPk}qYVC?H_z~$V*@@gQWh1)p61$u~$m@fh2e?$Wjy9w-z{C~>Kp3&z6 zH`s0{7@_;c^g_~S?~nfmfakcoAy^UDa?8NWzN76z0|nrf2NAg5^kap7<7NIXNHX8~ r?gF>`&?4|KT+e;Q`v2o}uL1l7QpwnEL1FBF00000NkvXXu0mjfY=V4u literal 0 HcmV?d00001 diff --git a/resources/icons/koicon.wifi.secure.50.medium.png b/resources/icons/koicon.wifi.secure.50.medium.png new file mode 100644 index 0000000000000000000000000000000000000000..b0feb26cfe294196b6056bf935deba5bb2393fe7 GIT binary patch literal 870 zcmV-s1DX7ZP)1Y)oQ10TTg95J(gh{16qQhJp@3 zYH zk6TJuchRfL0Uu_ufO7^DJS&J_4*`gXz!ls(V9_pq!se)49@|!G#HgN$DI!xdN+(7T26hy=sJf?(m3)eDp zukPkPT-3d6d-xganYkYk(|81Dl~jJ$lxjZ*=5b!hQ%1xET++S#1_4>i%#E7n%h);; z)j?^klP@VdA0>PB_c(y-N;;1?8y3=8EDadHRkg`=uL7QSz>9NL+p>fW-0mA6*@%{u z%1|AyX6Duq!2N6cd&yd6t`D3$s*@s+~*80llw%a-#Qtn@i<^m?817G7;pRMNnL6t3?pOon43< z(M?bgcLkv+DB{9Dpu1LZX>s8~SMEea1l#8o(m-5s5_$r{dP=5i+j5wRCHqW>=)Zh$&UwT(3EW zKjZt%%;7Ewmxg>DKV;^G0vZu<3+}Axk+RZH@k#CXtIWI*5zE+*8*DD%Ih@5nW-1H2 z0*7%Fw_&C{@8fx#SB0>P{)FeSr(nz!%%g>P{~y1@L76yZO2_Lbp4wS^b(av?YJRGsKXl$o#BoZgQIRC~XUcY7hxNw#$- zjEGn9CQhgdv@XYRPiL^MHBhUCM^ptj%=x@35(Ag+kZ&+|S!$pr52;Shq1>98Z<#Qi zL)lIW`ZnK~79(P*=$Ze5z}u=eJCq)-Qyt|WOpLaT8kknK*;O04UM