diff --git a/lua/browser.lua b/lua/browser.lua index a7f0c336..5577d4df 100644 --- a/lua/browser.lua +++ b/lua/browser.lua @@ -6,30 +6,11 @@ local queue = require("queue") local playing = require("playing") local theme = require("theme") local playback = require("playback") +local screen = require("screen") -local browser = {} - -function browser.create(opts) - local screen = {} - screen.root = lvgl.Object(nil, { - flex = { - flex_direction = "column", - flex_wrap = "wrap", - justify_content = "flex-start", - align_items = "flex-start", - align_content = "flex-start", - }, - w = lvgl.HOR_RES(), - h = lvgl.VER_RES(), - }) - screen.root:center() - - screen.status_bar = widgets.StatusBar(screen.root, { - title = opts.title, - }) - - if opts.breadcrumb then - local header = screen.root:Object { +return screen:new { + createUi = function(self) + self.root = lvgl.Object(nil, { flex = { flex_direction = "column", flex_wrap = "wrap", @@ -38,100 +19,113 @@ function browser.create(opts) align_content = "flex-start", }, w = lvgl.HOR_RES(), - h = lvgl.SIZE_CONTENT, - pad_left = 4, - pad_right = 4, - pad_bottom = 2, - bg_opa = lvgl.OPA(100), - bg_color = "#fafafa", - scrollbar_mode = lvgl.SCROLLBAR_MODE.OFF, - } - - header:Label { - text = opts.breadcrumb, - text_font = font.fusion_10, - } + h = lvgl.VER_RES(), + }) + self.root:center() - local buttons = header:Object({ - flex = { - flex_direction = "row", - flex_wrap = "wrap", - justify_content = "flex-end", - align_items = "center", - align_content = "center", - }, - w = lvgl.PCT(100), - h = lvgl.SIZE_CONTENT, - pad_column = 4, + self.status_bar = widgets.StatusBar(self.root, { + title = self.title, }) - local original_iterator = opts.iterator:clone() - local enqueue = widgets.IconBtn(buttons, "//lua/img/enqueue.png", "Enqueue") - enqueue:onClicked(function() - queue.add(original_iterator) - playback.playing:set(true) - end) - -- enqueue:add_flag(lvgl.FLAG.HIDDEN) - local play = widgets.IconBtn(buttons, "//lua/img/play_small.png", "Play") - play:onClicked(function() - queue.clear() - queue.add(original_iterator) - playback.playing:set(true) - backstack.push(playing) - end - ) - end - screen.list = lvgl.List(screen.root, { - w = lvgl.PCT(100), - h = lvgl.PCT(100), - flex_grow = 1, - scrollbar_mode = lvgl.SCROLLBAR_MODE.OFF, - }) + if self.breadcrumb then + local header = self.root:Object { + flex = { + flex_direction = "column", + flex_wrap = "wrap", + justify_content = "flex-start", + align_items = "flex-start", + align_content = "flex-start", + }, + w = lvgl.HOR_RES(), + h = lvgl.SIZE_CONTENT, + pad_left = 4, + pad_right = 4, + pad_bottom = 2, + bg_opa = lvgl.OPA(100), + bg_color = "#fafafa", + scrollbar_mode = lvgl.SCROLLBAR_MODE.OFF, + } - local back = screen.list:add_btn(nil, "< Back") - back:onClicked(backstack.pop) - back:add_style(theme.list_item) + header:Label { + text = self.breadcrumb, + text_font = font.fusion_10, + } - screen.focused_item = 0 - screen.last_item = 0 - screen.add_item = function(item) - if not item then return end - screen.last_item = screen.last_item + 1 - local this_item = screen.last_item - local btn = screen.list:add_btn(nil, tostring(item)) - btn:onClicked(function() - local contents = item:contents() - if type(contents) == "userdata" then - backstack.push(function() - return browser.create({ - title = opts.title, - iterator = contents, - breadcrumb = tostring(item), - }) - end) - else + local buttons = header:Object({ + flex = { + flex_direction = "row", + flex_wrap = "wrap", + justify_content = "flex-end", + align_items = "center", + align_content = "center", + }, + w = lvgl.PCT(100), + h = lvgl.SIZE_CONTENT, + pad_column = 4, + }) + local original_iterator = self.iterator:clone() + local enqueue = widgets.IconBtn(buttons, "//lua/img/enqueue.png", "Enqueue") + enqueue:onClicked(function() + queue.add(original_iterator) + playback.playing:set(true) + end) + -- enqueue:add_flag(lvgl.FLAG.HIDDEN) + local play = widgets.IconBtn(buttons, "//lua/img/play_small.png", "Play") + play:onClicked(function() queue.clear() - queue.add(contents) + queue.add(original_iterator) playback.playing:set(true) backstack.push(playing) end - end) - btn:onevent(lvgl.EVENT.FOCUSED, function() - screen.focused_item = this_item - if screen.last_item - 5 < this_item then - screen.add_item(opts.iterator()) - end - end) - btn:add_style(theme.list_item) - end + ) + end - for _ = 1, 8 do - local val = opts.iterator() - if not val then break end - screen.add_item(val) - end + self.list = lvgl.List(self.root, { + w = lvgl.PCT(100), + h = lvgl.PCT(100), + flex_grow = 1, + scrollbar_mode = lvgl.SCROLLBAR_MODE.OFF, + }) - return screen -end + local back = self.list:add_btn(nil, "< Back") + back:onClicked(backstack.pop) + back:add_style(theme.list_item) -return browser.create + self.focused_item = 0 + self.last_item = 0 + self.add_item = function(item) + if not item then return end + self.last_item = self.last_item + 1 + local this_item = self.last_item + local btn = self.list:add_btn(nil, tostring(item)) + btn:onClicked(function() + local contents = item:contents() + if type(contents) == "userdata" then + backstack.push(require("browser"):new { + title = self.title, + iterator = contents, + breadcrumb = tostring(item), + }) + else + queue.clear() + queue.add(contents) + playback.playing:set(true) + backstack.push(playing:new()) + end + end) + btn:onevent(lvgl.EVENT.FOCUSED, function() + self.focused_item = this_item + if self.last_item - 5 < this_item then + self.add_item(self.iterator()) + end + end) + btn:add_style(theme.list_item) + end + + for _ = 1, 8 do + local val = self.iterator() + if not val then break end + self.add_item(val) + end + end +} diff --git a/lua/licenses.lua b/lua/licenses.lua index 83437454..fb0e5702 100644 --- a/lua/licenses.lua +++ b/lua/licenses.lua @@ -2,20 +2,23 @@ local backstack = require("backstack") local widgets = require("widgets") local font = require("font") local theme = require("theme") +local screen = require("screen") local function show_license(text) - backstack.push(function() - local screen = widgets.MenuScreen { - show_back = true, - title = "Licenses", - } - screen.root:Label { - w = lvgl.PCT(100), - h = lvgl.SIZE_CONTENT, - text_font = font.fusion_10, - text = text, - } - end) + backstack.push(screen:new { + createUi = function(self) + self.menu = widgets.MenuScreen { + show_back = true, + title = "Licenses", + } + self.menu.root:Label { + w = lvgl.PCT(100), + h = lvgl.SIZE_CONTENT, + text_font = font.fusion_10, + text = text, + } + end + }) end local function gpl(copyright) @@ -175,4 +178,6 @@ return function() library("tremor", "bsd", function() xiphbsd("Copyright (c) 2002, Xiph.org Foundation") end) + + return menu end diff --git a/lua/main_menu.lua b/lua/main_menu.lua index 1311f8ea..7d47b785 100644 --- a/lua/main_menu.lua +++ b/lua/main_menu.lua @@ -5,41 +5,42 @@ local backstack = require("backstack") local browser = require("browser") local playing = require("playing") local theme = require("theme") +local screen = require("screen") -return function() - local menu = widgets.MenuScreen({}) +return screen:new { + createUi = function() + local menu = widgets.MenuScreen({}) - menu.list = lvgl.List(menu.root, { - w = lvgl.PCT(100), - h = lvgl.PCT(100), - flex_grow = 1, - }) + menu.list = lvgl.List(menu.root, { + w = lvgl.PCT(100), + h = lvgl.PCT(100), + flex_grow = 1, + }) - local now_playing = menu.list:add_btn(nil, "Now Playing") - now_playing:onClicked(function() - backstack.push(playing) - end) - now_playing:add_style(theme.list_item) + local now_playing = menu.list:add_btn(nil, "Now Playing") + now_playing:onClicked(function() + backstack.push(playing:new()) + end) + now_playing:add_style(theme.list_item) - local indexes = database.indexes() - for _, idx in ipairs(indexes) do - local btn = menu.list:add_btn(nil, tostring(idx)) - btn:onClicked(function() - backstack.push(function() - return browser { + local indexes = database.indexes() + for _, idx in ipairs(indexes) do + local btn = menu.list:add_btn(nil, tostring(idx)) + btn:onClicked(function() + backstack.push(browser:new { title = tostring(idx), - iterator = idx:iter() - } + iterator = idx:iter(), + }) end) - end) - btn:add_style(theme.list_item) - end + btn:add_style(theme.list_item) + end - local settings = menu.list:add_btn(nil, "Settings") - settings:onClicked(function() - backstack.push(require("settings").root) - end) - settings:add_style(theme.list_item) + local settings = menu.list:add_btn(nil, "Settings") + settings:onClicked(function() + backstack.push(require("settings"):new()) + end) + settings:add_style(theme.list_item) - return menu -end + return menu + end, +} diff --git a/lua/playing.lua b/lua/playing.lua index 4767e42f..947bdec9 100644 --- a/lua/playing.lua +++ b/lua/playing.lua @@ -4,6 +4,7 @@ local backstack = require("backstack") local font = require("font") local playback = require("playback") local queue = require("queue") +local screen = require("screen") local img = { play = "//lua/img/play.png", @@ -18,219 +19,227 @@ local img = { repeat_disabled = "//lua/img/repeat_disabled.png", } -return function(opts) - local screen = {} - screen.root = lvgl.Object(nil, { - flex = { - flex_direction = "column", - flex_wrap = "wrap", - justify_content = "center", - align_items = "center", - align_content = "center", - }, - w = lvgl.HOR_RES(), - h = lvgl.VER_RES(), - }) - screen.root:center() - - screen.status_bar = widgets.StatusBar(screen.root, { - back_cb = backstack.pop, - transparent_bg = true, - }) - - local info = screen.root:Object { - flex = { - flex_direction = "column", - flex_wrap = "wrap", - justify_content = "center", - align_items = "center", - align_content = "center", - }, - w = lvgl.PCT(100), - h = lvgl.SIZE_CONTENT, - flex_grow = 1, - } - - local artist = info:Label { - w = lvgl.PCT(100), - h = lvgl.SIZE_CONTENT, - text = "", - text_font = font.fusion_10, - text_align = 2, - } - - local title = info:Label { - w = lvgl.PCT(100), - h = lvgl.SIZE_CONTENT, - text = "", - text_align = 2, - } - - local playlist = screen.root:Object { - flex = { - flex_direction = "row", - justify_content = "center", - align_items = "center", - align_content = "center", - }, - w = lvgl.PCT(100), - h = lvgl.SIZE_CONTENT, - } - - playlist:Object({ w = 3, h = 1 }) -- spacer - - local cur_time = playlist:Label { - w = lvgl.SIZE_CONTENT, - h = lvgl.SIZE_CONTENT, - text = "", - text_font = font.fusion_10, - } - - playlist:Object({ flex_grow = 1, h = 1 }) -- spacer - - local playlist_pos = playlist:Label { - text = "", - text_font = font.fusion_10, - } - playlist:Label { - text = "/", - text_font = font.fusion_10, - } - local playlist_total = playlist:Label { - text = "", - text_font = font.fusion_10, - } - - playlist:Object({ flex_grow = 1, h = 1 }) -- spacer - - local end_time = playlist:Label { - w = lvgl.SIZE_CONTENT, - h = lvgl.SIZE_CONTENT, - align = lvgl.ALIGN.RIGHT_MID, - text = "", - text_font = font.fusion_10, - } - playlist:Object({ w = 3, h = 1 }) -- spacer - - local scrubber = screen.root:Slider { - w = lvgl.PCT(100), - h = 5, - range = { min = 0, max = 100 }, - value = 0, - } - - scrubber:onevent(lvgl.EVENT.RELEASED, function() - playback.position:set(scrubber:value()) - end) - - local controls = screen.root:Object { - flex = { - flex_direction = "row", - justify_content = "center", - align_items = "center", - align_content = "center", - }, - w = lvgl.PCT(100), - h = lvgl.SIZE_CONTENT, - pad_column = 8, - pad_all = 2, - } - - - controls:Object({ flex_grow = 1, h = 1 }) -- spacer - - local repeat_btn = controls:Button {} - repeat_btn:onClicked(function() - queue.repeat_track:set(not queue.repeat_track:get()) - end) - local repeat_img = repeat_btn:Image { src = img.repeat_enabled } - - local prev_btn = controls:Button {} - prev_btn:onClicked(queue.previous) - local prev_img = prev_btn:Image { src = img.prev_disabled } - - local play_pause_btn = controls:Button {} - play_pause_btn:onClicked(function() - playback.playing:set(not playback.playing:get()) - end) - play_pause_btn:focus() - local play_pause_img = play_pause_btn:Image { src = img.pause } - - local next_btn = controls:Button {} - next_btn:onClicked(queue.next) - local next_img = next_btn:Image { src = img.next_disabled } - - local shuffle_btn = controls:Button {} - shuffle_btn:onClicked(function() - queue.random:set(not queue.random:get()) - end) - local shuffle_img = shuffle_btn:Image { src = img.shuffle } - - controls:Object({ flex_grow = 1, h = 1 }) -- spacer - - - local format_time = function(time) - return string.format("%d:%02d", time // 60, time % 60) +local is_now_playing_shown = false + +return screen:new { + createUi = function(self) + self.root = lvgl.Object(nil, { + flex = { + flex_direction = "column", + flex_wrap = "wrap", + justify_content = "center", + align_items = "center", + align_content = "center", + }, + w = lvgl.HOR_RES(), + h = lvgl.VER_RES(), + }) + self.root:center() + + self.status_bar = widgets.StatusBar(self.root, { + back_cb = backstack.pop, + transparent_bg = true, + }) + + local info = self.root:Object { + flex = { + flex_direction = "column", + flex_wrap = "wrap", + justify_content = "center", + align_items = "center", + align_content = "center", + }, + w = lvgl.PCT(100), + h = lvgl.SIZE_CONTENT, + flex_grow = 1, + } + + local artist = info:Label { + w = lvgl.PCT(100), + h = lvgl.SIZE_CONTENT, + text = "", + text_font = font.fusion_10, + text_align = 2, + } + + local title = info:Label { + w = lvgl.PCT(100), + h = lvgl.SIZE_CONTENT, + text = "", + text_align = 2, + } + + local playlist = self.root:Object { + flex = { + flex_direction = "row", + justify_content = "center", + align_items = "center", + align_content = "center", + }, + w = lvgl.PCT(100), + h = lvgl.SIZE_CONTENT, + } + + playlist:Object({ w = 3, h = 1 }) -- spacer + + local cur_time = playlist:Label { + w = lvgl.SIZE_CONTENT, + h = lvgl.SIZE_CONTENT, + text = "", + text_font = font.fusion_10, + } + + playlist:Object({ flex_grow = 1, h = 1 }) -- spacer + + local playlist_pos = playlist:Label { + text = "", + text_font = font.fusion_10, + } + playlist:Label { + text = "/", + text_font = font.fusion_10, + } + local playlist_total = playlist:Label { + text = "", + text_font = font.fusion_10, + } + + playlist:Object({ flex_grow = 1, h = 1 }) -- spacer + + local end_time = playlist:Label { + w = lvgl.SIZE_CONTENT, + h = lvgl.SIZE_CONTENT, + align = lvgl.ALIGN.RIGHT_MID, + text = "", + text_font = font.fusion_10, + } + playlist:Object({ w = 3, h = 1 }) -- spacer + + local scrubber = self.root:Slider { + w = lvgl.PCT(100), + h = 5, + range = { min = 0, max = 100 }, + value = 0, + } + + scrubber:onevent(lvgl.EVENT.RELEASED, function() + playback.position:set(scrubber:value()) + end) + + local controls = self.root:Object { + flex = { + flex_direction = "row", + justify_content = "center", + align_items = "center", + align_content = "center", + }, + w = lvgl.PCT(100), + h = lvgl.SIZE_CONTENT, + pad_column = 8, + pad_all = 2, + } + + + controls:Object({ flex_grow = 1, h = 1 }) -- spacer + + local repeat_btn = controls:Button {} + repeat_btn:onClicked(function() + queue.repeat_track:set(not queue.repeat_track:get()) + end) + local repeat_img = repeat_btn:Image { src = img.repeat_enabled } + + local prev_btn = controls:Button {} + prev_btn:onClicked(queue.previous) + local prev_img = prev_btn:Image { src = img.prev_disabled } + + local play_pause_btn = controls:Button {} + play_pause_btn:onClicked(function() + playback.playing:set(not playback.playing:get()) + end) + play_pause_btn:focus() + local play_pause_img = play_pause_btn:Image { src = img.pause } + + local next_btn = controls:Button {} + next_btn:onClicked(queue.next) + local next_img = next_btn:Image { src = img.next_disabled } + + local shuffle_btn = controls:Button {} + shuffle_btn:onClicked(function() + queue.random:set(not queue.random:get()) + end) + local shuffle_img = shuffle_btn:Image { src = img.shuffle } + + controls:Object({ flex_grow = 1, h = 1 }) -- spacer + + + local format_time = function(time) + return string.format("%d:%02d", time // 60, time % 60) + end + + self.bindings = { + playback.playing:bind(function(playing) + if playing then + play_pause_img:set_src(img.pause) + else + play_pause_img:set_src(img.play) + end + end), + playback.position:bind(function(pos) + if not pos then return end + cur_time:set { + text = format_time(pos) + } + if not scrubber:is_dragged() then + scrubber:set { value = pos } + end + end), + playback.track:bind(function(track) + if not track then return end + end_time:set { + text = format_time(track.duration) + } + title:set { text = track.title } + artist:set { text = track.artist } + scrubber:set { + range = { min = 0, max = track.duration } + } + end), + queue.position:bind(function(pos) + if not pos then return end + playlist_pos:set { text = tostring(pos) } + + next_img:set_src( + pos < queue.size:get() and img.next or img.next_disabled + ) + prev_img:set_src( + pos > 1 and img.prev or img.prev_disabled + ) + end), + queue.random:bind(function(shuffling) + if shuffling then + shuffle_img:set_src(img.shuffle) + else + shuffle_img:set_src(img.shuffle_disabled) + end + end), + queue.repeat_track:bind(function(en) + if en then + repeat_img:set_src(img.repeat_enabled) + else + repeat_img:set_src(img.repeat_disabled) + end + end), + queue.size:bind(function(num) + if not num then return end + playlist_total:set { text = tostring(num) } + end), + } + end, + onShown = function() is_now_playing_shown = true end, + onHidden = function() is_now_playing_shown = false end, + pushIfNotShown = function(self) + if not is_now_playing_shown then + backstack.push(self:new()) + end end - - screen.bindings = { - playback.playing:bind(function(playing) - if playing then - play_pause_img:set_src(img.pause) - else - play_pause_img:set_src(img.play) - end - end), - playback.position:bind(function(pos) - if not pos then return end - cur_time:set { - text = format_time(pos) - } - if not scrubber:is_dragged() then - scrubber:set { value = pos } - end - end), - playback.track:bind(function(track) - if not track then return end - end_time:set { - text = format_time(track.duration) - } - title:set { text = track.title } - artist:set { text = track.artist } - scrubber:set { - range = { min = 0, max = track.duration } - } - end), - queue.position:bind(function(pos) - if not pos then return end - playlist_pos:set { text = tostring(pos) } - - next_img:set_src( - pos < queue.size:get() and img.next or img.next_disabled - ) - prev_img:set_src( - pos > 1 and img.prev or img.prev_disabled - ) - end), - queue.random:bind(function(shuffling) - if shuffling then - shuffle_img:set_src(img.shuffle) - else - shuffle_img:set_src(img.shuffle_disabled) - end - end), - queue.repeat_track:bind(function(en) - if en then - repeat_img:set_src(img.repeat_enabled) - else - repeat_img:set_src(img.repeat_disabled) - end - end), - queue.size:bind(function(num) - if not num then return end - playlist_total:set { text = tostring(num) } - end), - } - - return screen -end +} diff --git a/lua/settings.lua b/lua/settings.lua index 952292e4..9d9ccf2d 100644 --- a/lua/settings.lua +++ b/lua/settings.lua @@ -7,8 +7,7 @@ local display = require("display") local controls = require("controls") local bluetooth = require("bluetooth") local database = require("database") - -local settings = {} +local screen = require("screen") local function SettingsScreen(title) local menu = widgets.MenuScreen { @@ -31,323 +30,331 @@ local function SettingsScreen(title) return menu end -function settings.bluetooth() - local menu = SettingsScreen("Bluetooth") - - local enable_container = menu.content:Object { - flex = { - flex_direction = "row", - justify_content = "flex-start", - align_items = "content", - align_content = "flex-start", - }, - w = lvgl.PCT(100), - h = lvgl.SIZE_CONTENT, - pad_bottom = 1, - } - enable_container:Label { text = "Enable", flex_grow = 1 } - local enable_sw = enable_container:Switch {} - enable_sw:onevent(lvgl.EVENT.VALUE_CHANGED, function() - local enabled = enable_sw:enabled() - bluetooth.enabled:set(enabled) - end) - - menu.content:Label { - text = "Paired Device", - pad_bottom = 1, - }:add_style(theme.settings_title) - - local paired_container = menu.content:Object { - flex = { - flex_direction = "row", - justify_content = "flex-start", - align_items = "flex-start", - align_content = "flex-start", - }, - w = lvgl.PCT(100), - h = lvgl.SIZE_CONTENT, - pad_bottom = 2, - } - - local paired_device = paired_container:Label { - flex_grow = 1, - } - local clear_paired = paired_container:Button {} - clear_paired:Label { text = "x" } - clear_paired:onClicked(function() - bluetooth.paired_device:set() - end) - - menu.content:Label { - text = "Nearby Devices", - pad_bottom = 1, - }:add_style(theme.settings_title) - - local devices = menu.content:List { - w = lvgl.PCT(100), - h = lvgl.SIZE_CONTENT, - } - - menu.bindings = { - bluetooth.enabled:bind(function(en) - if en then - enable_sw:add_state(lvgl.STATE.CHECKED) - else - enable_sw:clear_state(lvgl.STATE.CHECKED) - end - end), - bluetooth.paired_device:bind(function(device) - if device then - paired_device:set { text = device.name } - clear_paired:clear_flag(lvgl.FLAG.HIDDEN) - else - paired_device:set { text = "None" } - clear_paired:add_flag(lvgl.FLAG.HIDDEN) - end - end), - bluetooth.devices:bind(function(devs) - devices:clean() - for _, dev in pairs(devs) do - devices:add_btn(nil, dev.name):onClicked(function() - bluetooth.paired_device:set(dev) - end) - end +local BluetoothSettings = screen:new { + createUi = function(self) + self.menu = SettingsScreen("Bluetooth") + + local enable_container = self.menu.content:Object { + flex = { + flex_direction = "row", + justify_content = "flex-start", + align_items = "content", + align_content = "flex-start", + }, + w = lvgl.PCT(100), + h = lvgl.SIZE_CONTENT, + pad_bottom = 1, + } + enable_container:Label { text = "Enable", flex_grow = 1 } + local enable_sw = enable_container:Switch {} + enable_sw:onevent(lvgl.EVENT.VALUE_CHANGED, function() + local enabled = enable_sw:enabled() + bluetooth.enabled:set(enabled) end) - } -end -function settings.headphones() - local menu = SettingsScreen("Headphones") + self.menu.content:Label { + text = "Paired Device", + pad_bottom = 1, + }:add_style(theme.settings_title) + + local paired_container = self.menu.content:Object { + flex = { + flex_direction = "row", + justify_content = "flex-start", + align_items = "flex-start", + align_content = "flex-start", + }, + w = lvgl.PCT(100), + h = lvgl.SIZE_CONTENT, + pad_bottom = 2, + } + + local paired_device = paired_container:Label { + flex_grow = 1, + } + local clear_paired = paired_container:Button {} + clear_paired:Label { text = "x" } + clear_paired:onClicked(function() + bluetooth.paired_device:set() + end) - menu.content:Label { - text = "Maximum volume limit", - }:add_style(theme.settings_title) + self.menu.content:Label { + text = "Nearby Devices", + pad_bottom = 1, + }:add_style(theme.settings_title) + + local devices = self.menu.content:List { + w = lvgl.PCT(100), + h = lvgl.SIZE_CONTENT, + } + + self.bindings = { + bluetooth.enabled:bind(function(en) + if en then + enable_sw:add_state(lvgl.STATE.CHECKED) + else + enable_sw:clear_state(lvgl.STATE.CHECKED) + end + end), + bluetooth.paired_device:bind(function(device) + if device then + paired_device:set { text = device.name } + clear_paired:clear_flag(lvgl.FLAG.HIDDEN) + else + paired_device:set { text = "None" } + clear_paired:add_flag(lvgl.FLAG.HIDDEN) + end + end), + bluetooth.devices:bind(function(devs) + devices:clean() + for _, dev in pairs(devs) do + devices:add_btn(nil, dev.name):onClicked(function() + bluetooth.paired_device:set(dev) + end) + end + end) + } + end +} + +local HeadphonesSettings = screen:new { + createUi = function(self) + self.menu = SettingsScreen("Headphones") + + self.menu.content:Label { + text = "Maximum volume limit", + }:add_style(theme.settings_title) + + local volume_chooser = self.menu.content:Dropdown { + options = "Line Level (-10 dB)\nCD Level (+6 dB)\nMaximum (+10dB)", + selected = 1, + } + local limits = { -10, 6, 10 } + volume_chooser:onevent(lvgl.EVENT.VALUE_CHANGED, function() + -- luavgl dropdown binding uses 0-based indexing :( + local selection = volume_chooser:get('selected') + 1 + volume.limit_db:set(limits[selection]) + end) - local volume_chooser = menu.content:Dropdown { - options = "Line Level (-10 dB)\nCD Level (+6 dB)\nMaximum (+10dB)", - selected = 1, - } - local limits = { -10, 6, 10 } - volume_chooser:onevent(lvgl.EVENT.VALUE_CHANGED, function() - -- luavgl dropdown binding uses 0-based indexing :( - local selection = volume_chooser:get('selected') + 1 - volume.limit_db:set(limits[selection]) - end) - - menu.content:Label { - text = "Left/Right balance", - }:add_style(theme.settings_title) - - local balance = menu.content:Slider { - w = lvgl.PCT(100), - h = 5, - range = { min = -100, max = 100 }, - value = 0, - } - balance:onevent(lvgl.EVENT.VALUE_CHANGED, function() - volume.left_bias:set(balance:value()) - end) + self.menu.content:Label { + text = "Left/Right balance", + }:add_style(theme.settings_title) + + local balance = self.menu.content:Slider { + w = lvgl.PCT(100), + h = 5, + range = { min = -100, max = 100 }, + value = 0, + } + balance:onevent(lvgl.EVENT.VALUE_CHANGED, function() + volume.left_bias:set(balance:value()) + end) - local balance_label = menu.content:Label {} + local balance_label = self.menu.content:Label {} - menu.bindings = { - volume.limit_db:bind(function(limit) - for i = 1, #limits do - if limits[i] == limit then - volume_chooser:set { selected = i - 1 } + self.bindings = { + volume.limit_db:bind(function(limit) + for i = 1, #limits do + if limits[i] == limit then + volume_chooser:set { selected = i - 1 } + end end - end - end), - volume.left_bias:bind(function(bias) - balance:set { - value = bias - } - if bias < 0 then - balance_label:set { - text = string.format("Left %.2fdB", bias / 4) + end), + volume.left_bias:bind(function(bias) + balance:set { + value = bias } - elseif bias > 0 then - balance_label:set { - text = string.format("Right %.2fdB", -bias / 4) - } - else - balance_label:set { text = "Balanced" } - end - end), - } - - return menu -end - -function settings.display() - local menu = SettingsScreen("Display") - - local brightness_title = menu.content:Object { - flex = { - flex_direction = "row", - justify_content = "flex-start", - align_items = "flex-start", - align_content = "flex-start", - }, - w = lvgl.PCT(100), - h = lvgl.SIZE_CONTENT, - } - brightness_title:Label { text = "Brightness", flex_grow = 1 } - local brightness_pct = brightness_title:Label {} - brightness_pct:add_style(theme.settings_title) - - local brightness = menu.content:Slider { - w = lvgl.PCT(100), - h = 5, - range = { min = 0, max = 100 }, - value = display.brightness:get(), - } - brightness:onevent(lvgl.EVENT.VALUE_CHANGED, function() - display.brightness:set(brightness:value()) - end) - - menu.bindings = { - display.brightness:bind(function(b) - brightness_pct:set { text = tostring(b) .. "%" } + if bias < 0 then + balance_label:set { + text = string.format("Left %.2fdB", bias / 4) + } + elseif bias > 0 then + balance_label:set { + text = string.format("Right %.2fdB", -bias / 4) + } + else + balance_label:set { text = "Balanced" } + end + end), + } + end +} + +local DisplaySettings = screen:new { + createUi = function(self) + self.menu = SettingsScreen("Display") + + local brightness_title = self.menu.content:Object { + flex = { + flex_direction = "row", + justify_content = "flex-start", + align_items = "flex-start", + align_content = "flex-start", + }, + w = lvgl.PCT(100), + h = lvgl.SIZE_CONTENT, + } + brightness_title:Label { text = "Brightness", flex_grow = 1 } + local brightness_pct = brightness_title:Label {} + brightness_pct:add_style(theme.settings_title) + + local brightness = self.menu.content:Slider { + w = lvgl.PCT(100), + h = 5, + range = { min = 0, max = 100 }, + value = display.brightness:get(), + } + brightness:onevent(lvgl.EVENT.VALUE_CHANGED, function() + display.brightness:set(brightness:value()) end) - } - return menu -end + self.bindings = { + display.brightness:bind(function(b) + brightness_pct:set { text = tostring(b) .. "%" } + end) + } + end +} -function settings.input() - local menu = SettingsScreen("Input Method") +local InputSettings = screen:new { + createUi = function(self) + self.menu = SettingsScreen("Input Method") - menu.content:Label { - text = "Control scheme", - }:add_style(theme.settings_title) + self.menu.content:Label { + text = "Control scheme", + }:add_style(theme.settings_title) - local schemes = controls.schemes() - local option_to_scheme = {} - local scheme_to_option = {} + local schemes = controls.schemes() + local option_to_scheme = {} + local scheme_to_option = {} - local option_idx = 0 - local options = "" + local option_idx = 0 + local options = "" - for i, v in pairs(schemes) do - option_to_scheme[option_idx] = i - scheme_to_option[i] = option_idx - if option_idx > 0 then - options = options .. "\n" + for i, v in pairs(schemes) do + option_to_scheme[option_idx] = i + scheme_to_option[i] = option_idx + if option_idx > 0 then + options = options .. "\n" + end + options = options .. v + option_idx = option_idx + 1 end - options = options .. v - option_idx = option_idx + 1 - end - - local controls_chooser = menu.content:Dropdown { - options = options, - } - menu.bindings = { - controls.scheme:bind(function(s) - local option = scheme_to_option[s] - controls_chooser:set({ selected = option }) + local controls_chooser = self.menu.content:Dropdown { + options = options, + } + + self.bindings = { + controls.scheme:bind(function(s) + local option = scheme_to_option[s] + controls_chooser:set({ selected = option }) + end) + } + + controls_chooser:onevent(lvgl.EVENT.VALUE_CHANGED, function() + local option = controls_chooser:get('selected') + local scheme = option_to_scheme[option] + controls.scheme:set(scheme) end) - } - - controls_chooser:onevent(lvgl.EVENT.VALUE_CHANGED, function() - local option = controls_chooser:get('selected') - local scheme = option_to_scheme[option] - controls.scheme:set(scheme) - end) - - menu.content:Label { - text = "Scroll Sensitivity", - }:add_style(theme.settings_title) - - local slider_scale = 4; -- Power steering - local sensitivity = menu.content:Slider { - w = lvgl.PCT(90), - h = 5, - range = { min = 0, max = 255/slider_scale }, - value = controls.scroll_sensitivity:get()/slider_scale, - } - sensitivity:onevent(lvgl.EVENT.VALUE_CHANGED, function() - controls.scroll_sensitivity:set(sensitivity:value()*slider_scale) - end) - - return menu -end - -function settings.database() - local menu = SettingsScreen("Database") - local db = require("database") - widgets.Row(menu.content, "Schema version", db.version()) - widgets.Row(menu.content, "Size on disk", string.format("%.1f KiB", db.size() / 1024)) - local actions_container = menu.content:Object { - w = lvgl.PCT(100), - h = lvgl.SIZE_CONTENT, - flex = { - flex_direction = "row", - justify_content = "center", - align_items = "space-evenly", - align_content = "center", - }, - pad_top = 4, - pad_column = 4, - } - actions_container:add_style(theme.list_item) - - local update = actions_container:Button {} - update:Label { text = "Update" } - update:onClicked(function() - database.update() - end) -end - -function settings.firmware() - local menu = SettingsScreen("Firmware") - local version = require("version") - widgets.Row(menu.content, "ESP32", version.esp()) - widgets.Row(menu.content, "SAMD21", version.samd()) - widgets.Row(menu.content, "Collator", version.collator()) -end - -function settings.root() - local menu = widgets.MenuScreen { - show_back = true, - title = "Settings", - } - menu.list = menu.root:List { - w = lvgl.PCT(100), - h = lvgl.PCT(100), - flex_grow = 1, - } - - local function section(name) - menu.list:add_text(name):add_style(theme.list_heading) + self.menu.content:Label { + text = "Scroll Sensitivity", + }:add_style(theme.settings_title) + + local slider_scale = 4; -- Power steering + local sensitivity = self.menu.content:Slider { + w = lvgl.PCT(90), + h = 5, + range = { min = 0, max = 255 / slider_scale }, + value = controls.scroll_sensitivity:get() / slider_scale, + } + sensitivity:onevent(lvgl.EVENT.VALUE_CHANGED, function() + controls.scroll_sensitivity:set(sensitivity:value() * slider_scale) + end) end - - local function submenu(name, fn) - local item = menu.list:add_btn(nil, name) - item:onClicked(function() - backstack.push(fn) +} + +local DatabaseSettings = screen:new { + createUi = function(self) + self.menu = SettingsScreen("Database") + local db = require("database") + widgets.Row(self.menu.content, "Schema version", db.version()) + widgets.Row(self.menu.content, "Size on disk", string.format("%.1f KiB", db.size() / 1024)) + + local actions_container = self.menu.content:Object { + w = lvgl.PCT(100), + h = lvgl.SIZE_CONTENT, + flex = { + flex_direction = "row", + justify_content = "center", + align_items = "space-evenly", + align_content = "center", + }, + pad_top = 4, + pad_column = 4, + } + actions_container:add_style(theme.list_item) + + local update = actions_container:Button {} + update:Label { text = "Update" } + update:onClicked(function() + database.update() end) - item:add_style(theme.list_item) end +} + +local FirmwareSettings = screen:new { + createUi = function(self) + self.menu = SettingsScreen("Firmware") + local version = require("version") + widgets.Row(self.menu.content, "ESP32", version.esp()) + widgets.Row(self.menu.content, "SAMD21", version.samd()) + widgets.Row(self.menu.content, "Collator", version.collator()) + end +} - section("Audio") - submenu("Bluetooth", settings.bluetooth) - submenu("Headphones", settings.headphones) +local LicensesScreen = screen:new { + createUi = function(self) + self.root = require("licenses")() + end +} + +return screen:new { + createUi = function(self) + self.menu = widgets.MenuScreen { + show_back = true, + title = "Settings", + } + self.list = self.menu.root:List { + w = lvgl.PCT(100), + h = lvgl.PCT(100), + flex_grow = 1, + } + + local function section(name) + self.list:add_text(name):add_style(theme.list_heading) + end - section("Interface") - submenu("Display", settings.display) - submenu("Input Method", settings.input) + local function submenu(name, class) + local item = self.list:add_btn(nil, name) + item:onClicked(function() + backstack.push(class:new()) + end) + item:add_style(theme.list_item) + end - section("System") - submenu("Database", settings.database) - submenu("Firmware", settings.firmware) - submenu("Licenses", function() - return require("licenses")() - end) + section("Audio") + submenu("Bluetooth", BluetoothSettings) + submenu("Headphones", HeadphonesSettings) - return menu -end + section("Interface") + submenu("Display", DisplaySettings) + submenu("Input Method", InputSettings) -return settings + section("System") + submenu("Database", DatabaseSettings) + submenu("Firmware", FirmwareSettings) + submenu("Licenses", LicensesScreen) + end +} diff --git a/src/lua/CMakeLists.txt b/src/lua/CMakeLists.txt index ff0831c9..cee738bd 100644 --- a/src/lua/CMakeLists.txt +++ b/src/lua/CMakeLists.txt @@ -5,6 +5,7 @@ idf_component_register( SRCS "lua_thread.cpp" "bridge.cpp" "property.cpp" "lua_database.cpp" "lua_queue.cpp" "lua_version.cpp" "lua_controls.cpp" "registry.cpp" + "lua_screen.cpp" INCLUDE_DIRS "include" REQUIRES "drivers" "lvgl" "tinyfsm" "events" "system_fsm" "database" "esp_timer" "battery" "esp-idf-lua" "luavgl" "lua-linenoise" "lua-term" diff --git a/src/lua/bridge.cpp b/src/lua/bridge.cpp index a26f74bb..2bef1c30 100644 --- a/src/lua/bridge.cpp +++ b/src/lua/bridge.cpp @@ -19,6 +19,7 @@ #include "lua_controls.hpp" #include "lua_database.hpp" #include "lua_queue.hpp" +#include "lua_screen.hpp" #include "lua_version.hpp" #include "lvgl.h" @@ -84,6 +85,7 @@ auto Bridge::installBaseModules(lua_State* L) -> void { RegisterDatabaseModule(L); RegisterQueueModule(L); RegisterVersionModule(L); + RegisterScreenModule(L); } auto Bridge::installLvgl(lua_State* L) -> void { diff --git a/src/lua/include/lua_screen.hpp b/src/lua/include/lua_screen.hpp new file mode 100644 index 00000000..1c3bed1a --- /dev/null +++ b/src/lua/include/lua_screen.hpp @@ -0,0 +1,15 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once + +#include "lua.hpp" + +namespace lua { + +auto RegisterScreenModule(lua_State*) -> void; + +} // namespace lua diff --git a/src/lua/lua_screen.cpp b/src/lua/lua_screen.cpp new file mode 100644 index 00000000..27843bc7 --- /dev/null +++ b/src/lua/lua_screen.cpp @@ -0,0 +1,75 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include "lua_screen.hpp" + +#include +#include + +#include "lua.hpp" + +#include "esp_log.h" +#include "lauxlib.h" +#include "lua.h" +#include "lvgl.h" + +#include "bridge.hpp" +#include "database.hpp" +#include "event_queue.hpp" +#include "index.hpp" +#include "property.hpp" +#include "service_locator.hpp" +#include "track.hpp" +#include "track_queue.hpp" +#include "ui_events.hpp" + +namespace lua { + +static auto screen_new(lua_State* L) -> int { + // o = o or {} + if (lua_gettop(L) != 2) { + lua_settop(L, 1); + lua_newtable(L); + } + // Swap o and self on the stack. + lua_insert(L, 1); + + lua_pushliteral(L, "__index"); + lua_pushvalue(L, 1); + lua_settable(L, 1); // self.__index = self + + lua_setmetatable(L, 1); // setmetatable(o, self) + + return 1; // return o +} + +static auto screen_noop(lua_State* state) -> int { + return 0; +} + +static const struct luaL_Reg kScreenFuncs[] = {{"new", screen_new}, + {"createUi", screen_noop}, + {"onShown", screen_noop}, + {"onHidden", screen_noop}, + {NULL, NULL}}; + +static auto lua_screen(lua_State* state) -> int { + luaL_newlib(state, kScreenFuncs); + + lua_pushliteral(state, "__index"); + lua_pushvalue(state, -2); + lua_rawset(state, -3); + + return 1; +} + +auto RegisterScreenModule(lua_State* s) -> void { + luaL_requiref(s, "screen", lua_screen, true); + + lua_pop(s, 1); +} + +} // namespace lua diff --git a/src/ui/include/screen.hpp b/src/ui/include/screen.hpp index 60939660..4241c712 100644 --- a/src/ui/include/screen.hpp +++ b/src/ui/include/screen.hpp @@ -27,6 +27,9 @@ class Screen { Screen(); virtual ~Screen(); + virtual auto onShown() -> void {} + virtual auto onHidden() -> void {} + auto root() -> lv_obj_t* { return root_; } auto content() -> lv_obj_t* { return content_; } auto alert() -> lv_obj_t* { return alert_; } diff --git a/src/ui/include/screen_lua.hpp b/src/ui/include/screen_lua.hpp index ee9f6813..0ed3a508 100644 --- a/src/ui/include/screen_lua.hpp +++ b/src/ui/include/screen_lua.hpp @@ -18,6 +18,9 @@ class Lua : public Screen { Lua(); ~Lua(); + auto onShown() -> void override; + auto onHidden() -> void override; + auto SetObjRef(lua_State*) -> void; private: diff --git a/src/ui/screen_lua.cpp b/src/ui/screen_lua.cpp index 5130b4f7..d6c7a26f 100644 --- a/src/ui/screen_lua.cpp +++ b/src/ui/screen_lua.cpp @@ -7,8 +7,10 @@ #include "screen_lua.hpp" #include "core/lv_obj_tree.h" +#include "lua.h" #include "lua.hpp" +#include "lua_thread.hpp" #include "luavgl.h" namespace ui { @@ -22,6 +24,40 @@ Lua::~Lua() { } } +auto Lua::onShown() -> void { + if (!s_ || !obj_ref_) { + return; + } + lua_rawgeti(s_, LUA_REGISTRYINDEX, *obj_ref_); + lua_pushliteral(s_, "onShown"); + + if (lua_gettable(s_, -2) == LUA_TFUNCTION) { + lua_pushvalue(s_, -2); + lua::CallProtected(s_, 1, 0); + } else { + lua_pop(s_, 1); + } + + lua_pop(s_, 1); +} + +auto Lua::onHidden() -> void { + if (!s_ || !obj_ref_) { + return; + } + lua_rawgeti(s_, LUA_REGISTRYINDEX, *obj_ref_); + lua_pushliteral(s_, "onHidden"); + + if (lua_gettable(s_, -2) == LUA_TFUNCTION) { + lua_pushvalue(s_, -2); + lua::CallProtected(s_, 1, 0); + } else { + lua_pop(s_, 1); + } + + lua_pop(s_, 1); +} + auto Lua::SetObjRef(lua_State* s) -> void { assert(s_ == nullptr); s_ = s; diff --git a/src/ui/ui_fsm.cpp b/src/ui/ui_fsm.cpp index d98e435d..5c22e90e 100644 --- a/src/ui/ui_fsm.cpp +++ b/src/ui/ui_fsm.cpp @@ -125,21 +125,24 @@ lua::Property UiState::sPlaybackPlaying{ }}; lua::Property UiState::sPlaybackTrack{}; -lua::Property UiState::sPlaybackPosition{0, [](const lua::LuaValue& val) { - int current_val = std::get(sPlaybackPosition.Get()); - if (!std::holds_alternative(val)) { - return false; - } - int new_val = std::get(val); - if (current_val != new_val) { - auto track = sPlaybackTrack.Get(); - if (!std::holds_alternative(track)) { +lua::Property UiState::sPlaybackPosition{ + 0, [](const lua::LuaValue& val) { + int current_val = std::get(sPlaybackPosition.Get()); + if (!std::holds_alternative(val)) { return false; } - events::Audio().Dispatch(audio::SeekFile{.offset = (uint32_t)new_val, .filename = std::get(track).filepath}); - } - return true; -}}; + int new_val = std::get(val); + if (current_val != new_val) { + auto track = sPlaybackTrack.Get(); + if (!std::holds_alternative(track)) { + return false; + } + events::Audio().Dispatch(audio::SeekFile{ + .offset = (uint32_t)new_val, + .filename = std::get(track).filepath}); + } + return true; + }}; lua::Property UiState::sQueuePosition{0}; lua::Property UiState::sQueueSize{0}; @@ -294,21 +297,29 @@ auto UiState::InitBootSplash(drivers::IGpios& gpios) -> bool { } void UiState::PushScreen(std::shared_ptr screen) { + lv_obj_set_parent(sAlertContainer, screen->alert()); + if (sCurrentScreen) { + sCurrentScreen->onHidden(); sScreens.push(sCurrentScreen); } sCurrentScreen = screen; - lv_obj_set_parent(sAlertContainer, sCurrentScreen->alert()); + sCurrentScreen->onShown(); } int UiState::PopScreen() { if (sScreens.empty()) { return 0; } - sCurrentScreen = sScreens.top(); - lv_obj_set_parent(sAlertContainer, sCurrentScreen->alert()); + lv_obj_set_parent(sAlertContainer, sScreens.top()->alert()); + + sCurrentScreen->onHidden(); + sCurrentScreen = sScreens.top(); sScreens.pop(); + + sCurrentScreen->onShown(); + return sScreens.size(); } @@ -539,7 +550,7 @@ void Lua::entry() { auto Lua::PushLuaScreen(lua_State* s) -> int { // Ensure the arg looks right before continuing. - luaL_checktype(s, 1, LUA_TFUNCTION); + luaL_checktype(s, 1, LUA_TTABLE); // First, create a new plain old Screen object. We will use its root and // group for the Lua screen. Allocate it in external ram so that arbitrarily @@ -554,10 +565,15 @@ auto Lua::PushLuaScreen(lua_State* s) -> int { lv_group_set_default(new_screen->group()); // Call the constructor for this screen. - lua_settop(s, 1); // Make sure the function is actually at top of stack - lua::CallProtected(s, 0, 1); + // lua_settop(s, 1); // Make sure the screen is actually at top of stack + lua_pushliteral(s, "createUi"); + if (lua_gettable(s, 1) == LUA_TFUNCTION) { + lua_pushvalue(s, 1); + lua::CallProtected(s, 1, 0); + } - // Store the reference for the table the constructor returned. + // Store the reference for this screen's table. + lua_settop(s, 1); new_screen->SetObjRef(s); // Finally, push the now-initialised screen as if it were a regular C++ @@ -585,7 +601,7 @@ auto Lua::PopLuaScreen(lua_State* s) -> int { } auto Lua::Ticks(lua_State* s) -> int { - lua_pushinteger(s, esp_timer_get_time()/1000); + lua_pushinteger(s, esp_timer_get_time() / 1000); return 1; }