A modern Music Player Daemon based on Rockbox open source high quality audio player
libadwaita audio rust zig deno mpris rockbox mpd
at master 333 lines 9.4 kB view raw
1--[[ 2 __________ __ ___. 3 Open \______ \ ____ ____ | | _\_ |__ _______ ___ 4 Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / 5 Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < 6 Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ 7 \/ \/ \/ \/ \/ 8 $Id$ 9 10 Port of Stopwatch to Lua for touchscreen targets. 11 Original copyright: Copyright (C) 2004 Mike Holden 12 13 Copyright (C) 2009 by Maurus Cuelenaere 14 15 This program is free software; you can redistribute it and/or 16 modify it under the terms of the GNU General Public License 17 as published by the Free Software Foundation; either version 2 18 of the License, or (at your option) any later version. 19 20 This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY 21 KIND, either express or implied. 22 23]]-- 24 25require "actions" 26require "buttons" 27 28STOPWATCH_FILE = rb.PLUGIN_APPS_DATA_DIR .. "/stopwatch.dat" 29 30 31local LapsView = { 32 lapTimes = {}, 33 timer = { 34 counting = false, 35 prevTotal = 0, 36 startAt = 0, 37 current = 0 38 }, 39 vp = { 40 x = 80, 41 y = 0, 42 width = rb.LCD_WIDTH - 80, 43 height = rb.LCD_HEIGHT, 44 font = rb.FONT_UI, 45 fg_pattern = rb.lcd_get_foreground() 46 }, 47 scroll = { 48 prevY = 0, 49 cursorPos = 0 50 } 51} 52 53function LapsView:init() 54 local _, _, h = rb.font_getstringsize("", self.vp.font) 55 56 self.vp.maxLaps = self.vp.height / h 57 self.vp.lapHeight = h 58 59 self:loadState() 60end 61 62function LapsView:display() 63 rb.set_viewport(self.vp) 64 rb.clear_viewport() 65 66 local nrOfLaps = math.min(self.vp.maxLaps, #self.lapTimes) 67 rb.lcd_puts_scroll(0, 0, ticksToString(self.timer.current)) 68 69 for i=1, nrOfLaps do 70 local idx = #self.lapTimes - self.scroll.cursorPos - i + 1 71 if self.lapTimes[idx] ~= nil then 72 rb.lcd_puts_scroll(0, i, ticksToString(self.lapTimes, idx)) 73 end 74 end 75 76 rb.set_viewport(nil) 77end 78 79function LapsView:checkForScroll(btn, x, y) 80 if x > self.vp.x and x < self.vp.x + self.vp.width and 81 y > self.vp.y and y < self.vp.y + self.vp.height then 82 83 if bit.band(btn, rb.buttons.BUTTON_REL) == rb.buttons.BUTTON_REL then 84 self.scroll.prevY = 0 85 else 86 if #self.lapTimes > self.vp.maxLaps and self.scroll.prevY ~= 0 then 87 self.scroll.cursorPos = self.scroll.cursorPos - 88 (y - self.scroll.prevY) / self.vp.lapHeight 89 90 local maxLaps = math.min(self.vp.maxLaps, #self.lapTimes) 91 if self.scroll.cursorPos < 0 then 92 self.scroll.cursorPos = 0 93 elseif self.scroll.cursorPos >= maxLaps then 94 self.scroll.cursorPos = maxLaps 95 end 96 end 97 98 self.scroll.prevY = y 99 end 100 101 return true 102 else 103 return false 104 end 105end 106 107function LapsView:incTimer() 108 if self.timer.counting then 109 self.timer.current = self.timer.prevTotal + rb.current_tick() 110 - self.timer.startAt 111 else 112 self.timer.current = self.timer.prevTotal 113 end 114end 115 116function LapsView:startTimer() 117 self.timer.startAt = rb.current_tick() 118 self.timer.currentLap = self.timer.prevTotal 119 self.timer.counting = true 120end 121 122function LapsView:stopTimer() 123 self.timer.prevTotal = self.timer.prevTotal + rb.current_tick() 124 - self.timer.startAt 125 self.timer.counting = false 126end 127 128function LapsView:newLap() 129 table.insert(self.lapTimes, self.timer.current) 130end 131 132function LapsView:resetTimer() 133 self.lapTimes = {} 134 self.timer.counting = false 135 self.timer.current, self.timer.prevTotal, self.timer.startAt = 0, 0, 0 136 self.scroll.cursorPos = 0 137end 138 139function LapsView:saveState() 140 local fd = assert(io.open(STOPWATCH_FILE, "w")) 141 142 for _, v in ipairs({"current", "startAt", "prevTotal", "counting"}) do 143 assert(fd:write(tostring(self.timer[v]) .. "\n")) 144 end 145 for _, v in ipairs(self.lapTimes) do 146 assert(fd:write(tostring(v) .. "\n")) 147 end 148 149 fd:close() 150end 151 152function LapsView:loadState() 153 local fd = io.open(STOPWATCH_FILE, "r") 154 if fd == nil then return end 155 156 for _, v in ipairs({"current", "startAt", "prevTotal"}) do 157 self.timer[v] = tonumber(fd:read("*line")) 158 end 159 self.timer.counting = toboolean(fd:read("*line")) 160 161 local line = fd:read("*line") 162 while line do 163 table.insert(self.lapTimes, tonumber(line)) 164 line = fd:read("*line") 165 end 166 167 fd:close() 168end 169 170local Button = { 171 x = 0, 172 y = 0, 173 width = 80, 174 height = 50, 175 label = "" 176} 177 178function Button:new(o) 179 local o = o or {} 180 181 if o.label then 182 local _, w, h = rb.font_getstringsize(o.label, LapsView.vp.font) 183 o.width = math.max(5 * w / 4,o.width) 184 o.height = 3 * h / 2 185 end 186 187 setmetatable(o, self) 188 self.__index = self 189 return o 190end 191 192function Button:draw() 193 local _, w, h = rb.font_getstringsize(self.label, LapsView.vp.font) 194 local x, y = (2 * self.x + self.width - w) / 2, (2 * self.y + self.height - h) / 2 195 196 rb.lcd_drawrect(self.x, self.y, self.width, self.height) 197 rb.lcd_putsxy(x, y, self.label) 198end 199 200function Button:isPressed(x, y) 201 return x > self.x and x < self.x + self.width and 202 y > self.y and y < self.y + self.height 203end 204 205-- Helper function 206function ticksToString(laps, lap) 207 local ticks = type(laps) == "table" and laps[lap] or laps 208 lap = lap or 0 209 210 local hours = ticks / (rb.HZ * 3600) 211 ticks = ticks - (rb.HZ * hours * 3600) 212 local minutes = ticks / (rb.HZ * 60) 213 ticks = ticks - (rb.HZ * minutes * 60) 214 local seconds = ticks / rb.HZ 215 ticks = ticks - (rb.HZ * seconds) 216 local cs = ticks 217 218 if (lap == 0) then 219 return string.format("%2d:%02d:%02d.%02d", hours, minutes, seconds, cs) 220 else 221 if (lap > 1) then 222 local last_ticks = laps[lap] - laps[lap-1] 223 local last_hours = last_ticks / (rb.HZ * 3600) 224 last_ticks = last_ticks - (rb.HZ * last_hours * 3600) 225 local last_minutes = last_ticks / (rb.HZ * 60) 226 last_ticks = last_ticks - (rb.HZ * last_minutes * 60) 227 local last_seconds = last_ticks / rb.HZ 228 last_ticks = last_ticks - (rb.HZ * last_seconds) 229 local last_cs = last_ticks 230 231 return string.format("%2d %2d:%02d:%02d.%02d [%2d:%02d:%02d.%02d]", 232 lap, hours, minutes, seconds, cs, last_hours, 233 last_minutes, last_seconds, last_cs) 234 else 235 return string.format("%2d %2d:%02d:%02d.%02d", lap, hours, minutes, seconds, cs) 236 end 237 end 238end 239 240-- Helper function 241function toboolean(v) 242 return v == "true" 243end 244 245function arrangeButtons(btns) 246 local totalWidth, totalHeight, maxWidth, maxHeight, vp = 0, 0, 0, 0 247 local width, row = 0, 0 248 local items, num_rows 249 250 for i, btn in pairs(btns) do 251 maxHeight = math.max(maxHeight, btn.height) 252 totalWidth = totalWidth + btn.width 253 items = i 254 end 255 256 for _, btn in pairs(btns) do 257 btn.height = maxHeight 258 end 259 260 num_rows = totalWidth / rb.LCD_WIDTH 261 262 for _, btn in pairs(btns) do 263 btn.x = width 264 btn.y = rb.LCD_HEIGHT - ((num_rows - row) * maxHeight) 265 width = width + btn.width 266 if (width > rb.LCD_WIDTH - 5) then -- 5 is rounding margin 267 width = 0 268 row = row+1 269 end 270 end 271 vp = { 272 x = 0, 273 y = 0, 274 width = rb.LCD_WIDTH, 275 height = rb.LCD_HEIGHT - num_rows*maxHeight - 2 276 } 277 278 for k, v in pairs(vp) do 279 LapsView.vp[k] = v 280 end 281end 282 283rb.touchscreen_mode(rb.TOUCHSCREEN_POINT) 284 285LapsView:init() 286 287local third = rb.LCD_WIDTH/3 288 289local btns = { 290 Button:new({name = "startTimer", label = "Start", width = third}), 291 Button:new({name = "stopTimer", label = "Stop", width = third}), 292 Button:new({name = "resetTimer", label = "Reset", width = rb.LCD_WIDTH-third*2}), -- correct rounding error 293 Button:new({name = "newLap", label = "New Lap", width = third*2}), 294 Button:new({name = "exitApp", label = "Quit", width = rb.LCD_WIDTH-third*2}) -- correct rounding error 295 } 296 297arrangeButtons(btns) 298 299for _, btn in pairs(btns) do 300 btn:draw() 301end 302 303repeat 304 LapsView:incTimer() 305 306 local action = rb.get_action(rb.contexts.CONTEXT_STD, 0) 307 308 if (action == rb.actions.ACTION_TOUCHSCREEN) then 309 local btn, x, y = rb.action_get_touchscreen_press() 310 311 if LapsView:checkForScroll(btn, x, y) then 312 -- Don't do anything 313 elseif btn == rb.buttons.BUTTON_REL then 314 for _, btn in pairs(btns) do 315 local name = btn.name 316 if (btn:isPressed(x, y)) then 317 if name == "exitApp" then 318 action = rb.actions.ACTION_STD_CANCEL 319 else 320 LapsView[name](LapsView) 321 end 322 end 323 end 324 end 325 end 326 327 LapsView:display() 328 rb.lcd_update() 329 rb.sleep(rb.HZ/50) 330until action == rb.actions.ACTION_STD_CANCEL 331 332LapsView:saveState() 333