summaryrefslogtreecommitdiff
path: root/lua
diff options
context:
space:
mode:
Diffstat (limited to 'lua')
-rw-r--r--lua/browser.lua214
-rw-r--r--lua/img/db.pngbin4557 -> 5361 bytes
-rw-r--r--lua/img/enqueue.pngbin590 -> 4782 bytes
-rw-r--r--lua/img/next.pngbin621 -> 4811 bytes
-rw-r--r--lua/img/next_disabled.pngbin1539 -> 0 bytes
-rw-r--r--lua/img/pause.pngbin581 -> 4771 bytes
-rw-r--r--lua/img/play.pngbin617 -> 4813 bytes
-rw-r--r--lua/img/play_small.pngbin593 -> 4780 bytes
-rw-r--r--lua/img/prev.pngbin626 -> 4810 bytes
-rw-r--r--lua/img/prev_disabled.pngbin1533 -> 0 bytes
-rw-r--r--lua/img/repeat.pngbin4786 -> 5023 bytes
-rw-r--r--lua/img/repeat_disabled.pngbin7287 -> 0 bytes
-rw-r--r--lua/img/shuffle.pngbin4809 -> 5055 bytes
-rw-r--r--lua/img/shuffle_disabled.pngbin8706 -> 0 bytes
-rw-r--r--lua/licenses.lua36
-rw-r--r--lua/main.lua24
-rw-r--r--lua/main_menu.lua63
-rw-r--r--lua/playing.lua452
-rw-r--r--lua/settings.lua585
-rw-r--r--lua/styles.lua (renamed from lua/theme.lua)9
-rw-r--r--lua/theme_dark.lua176
-rw-r--r--lua/theme_light.lua174
-rw-r--r--lua/widgets.lua53
23 files changed, 1119 insertions, 667 deletions
diff --git a/lua/browser.lua b/lua/browser.lua
index a7f0c336..924381ea 100644
--- a/lua/browser.lua
+++ b/lua/browser.lua
@@ -4,32 +4,14 @@ local backstack = require("backstack")
local font = require("font")
local queue = require("queue")
local playing = require("playing")
-local theme = require("theme")
+local styles = require("styles")
local playback = require("playback")
+local theme = require("theme")
+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 +20,114 @@ 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),
+ scrollbar_mode = lvgl.SCROLLBAR_MODE.OFF,
+ }
+ theme.set_style(header, "header")
- local back = screen.list:add_btn(nil, "< Back")
- back:onClicked(backstack.pop)
- back:add_style(theme.list_item)
- 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
+ header:Label {
+ text = self.breadcrumb,
+ text_font = font.fusion_10,
+ }
+
+ 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,
+ })
+
+ local back = self.list:add_btn(nil, "< Back")
+ back:onClicked(backstack.pop)
+ back:add_style(styles.list_item)
- return screen
-end
+ 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(styles.list_item)
+ end
-return browser.create
+ for _ = 1, 8 do
+ local val = self.iterator()
+ if not val then break end
+ self.add_item(val)
+ end
+ end
+}
diff --git a/lua/img/db.png b/lua/img/db.png
index 3952ded2..6245fab2 100644
--- a/lua/img/db.png
+++ b/lua/img/db.png
Binary files differ
diff --git a/lua/img/enqueue.png b/lua/img/enqueue.png
index 9f720969..b5136a77 100644
--- a/lua/img/enqueue.png
+++ b/lua/img/enqueue.png
Binary files differ
diff --git a/lua/img/next.png b/lua/img/next.png
index 1b22a509..1f6f044b 100644
--- a/lua/img/next.png
+++ b/lua/img/next.png
Binary files differ
diff --git a/lua/img/next_disabled.png b/lua/img/next_disabled.png
deleted file mode 100644
index c8ff06b2..00000000
--- a/lua/img/next_disabled.png
+++ /dev/null
Binary files differ
diff --git a/lua/img/pause.png b/lua/img/pause.png
index 29fa4b90..e7011821 100644
--- a/lua/img/pause.png
+++ b/lua/img/pause.png
Binary files differ
diff --git a/lua/img/play.png b/lua/img/play.png
index cc10cab5..a3b8a5af 100644
--- a/lua/img/play.png
+++ b/lua/img/play.png
Binary files differ
diff --git a/lua/img/play_small.png b/lua/img/play_small.png
index 3fc7032e..ac29aa98 100644
--- a/lua/img/play_small.png
+++ b/lua/img/play_small.png
Binary files differ
diff --git a/lua/img/prev.png b/lua/img/prev.png
index f17e6162..b445c75a 100644
--- a/lua/img/prev.png
+++ b/lua/img/prev.png
Binary files differ
diff --git a/lua/img/prev_disabled.png b/lua/img/prev_disabled.png
deleted file mode 100644
index accebe23..00000000
--- a/lua/img/prev_disabled.png
+++ /dev/null
Binary files differ
diff --git a/lua/img/repeat.png b/lua/img/repeat.png
index 9a4da7fd..40a7564e 100644
--- a/lua/img/repeat.png
+++ b/lua/img/repeat.png
Binary files differ
diff --git a/lua/img/repeat_disabled.png b/lua/img/repeat_disabled.png
deleted file mode 100644
index 20b6ab59..00000000
--- a/lua/img/repeat_disabled.png
+++ /dev/null
Binary files differ
diff --git a/lua/img/shuffle.png b/lua/img/shuffle.png
index b54e359d..4a65635b 100644
--- a/lua/img/shuffle.png
+++ b/lua/img/shuffle.png
Binary files differ
diff --git a/lua/img/shuffle_disabled.png b/lua/img/shuffle_disabled.png
deleted file mode 100644
index 912d0e95..00000000
--- a/lua/img/shuffle_disabled.png
+++ /dev/null
Binary files differ
diff --git a/lua/licenses.lua b/lua/licenses.lua
index 38e58678..1fa392cf 100644
--- a/lua/licenses.lua
+++ b/lua/licenses.lua
@@ -1,21 +1,24 @@
local backstack = require("backstack")
local widgets = require("widgets")
local font = require("font")
-local theme = require("theme")
+local styles = require("styles")
+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)
@@ -100,7 +103,7 @@ return function()
w = lvgl.PCT(100),
h = lvgl.SIZE_CONTENT,
}
- row:add_style(theme.list_item)
+ row:add_style(styles.list_item)
row:Label { text = name, flex_grow = 1 }
local button = row:Button {}
button:Label { text = license, text_font = font.fusion_10 }
@@ -149,9 +152,6 @@ return function()
library("MillerShuffle", "Apache 2.0", function()
apache("Copyright 2022 Ronald Ross Miller")
end)
- library("miniflac", "BSD", function()
- bsd("Copyright (C) 2022 John Regan <john@jrjrtech.com>")
- end)
library("ogg", "BSD", function()
xiphbsd("Copyright (c) 2002, Xiph.org Foundation")
end)
@@ -178,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.lua b/lua/main.lua
index 5cbbf0a6..dc73c964 100644
--- a/lua/main.lua
+++ b/lua/main.lua
@@ -1,8 +1,17 @@
local font = require("font")
local vol = require("volume")
+local theme = require("theme")
+local controls = require("controls")
+local time = require("time")
+
+local lock_time = time.ticks()
+
+local theme_dark = require("theme_dark")
+theme.set(theme_dark)
-- Set up property bindings that are used across every screen.
GLOBAL_BINDINGS = {
+ -- Show an alert with the current volume whenever the volume changes
vol.current_pct:bind(function(pct)
require("alerts").show(function()
local container = lvgl.Object(nil, {
@@ -14,11 +23,10 @@ GLOBAL_BINDINGS = {
align_items = "center",
align_content = "center",
},
- bg_opa = lvgl.OPA(100),
- bg_color = "#fafafa",
radius = 8,
pad_all = 2,
})
+ theme.set_style(container, "pop_up")
container:Label {
text = string.format("Volume %i%%", pct),
text_font = font.fusion_10
@@ -32,6 +40,18 @@ GLOBAL_BINDINGS = {
container:center()
end)
end),
+ -- When the device has been locked for a while, default to showing the now
+ -- playing screen after unlocking.
+ controls.lock_switch:bind(function(locked)
+ if locked then
+ lock_time = time.ticks()
+ elseif time.ticks() - lock_time > 8000 then
+ local queue = require("queue")
+ if queue.size:get() > 0 then
+ require("playing"):pushIfNotShown()
+ end
+ end
+ end),
}
local backstack = require("backstack")
diff --git a/lua/main_menu.lua b/lua/main_menu.lua
index 1311f8ea..ac9190be 100644
--- a/lua/main_menu.lua
+++ b/lua/main_menu.lua
@@ -4,42 +4,43 @@ local database = require("database")
local backstack = require("backstack")
local browser = require("browser")
local playing = require("playing")
-local theme = require("theme")
+local styles = require("styles")
+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(styles.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(styles.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(styles.list_item)
- return menu
-end
+ return menu
+ end,
+}
diff --git a/lua/playing.lua b/lua/playing.lua
index c6a3f47e..3368f590 100644
--- a/lua/playing.lua
+++ b/lua/playing.lua
@@ -4,227 +4,241 @@ local backstack = require("backstack")
local font = require("font")
local playback = require("playback")
local queue = require("queue")
+local screen = require("screen")
+local theme = require("theme")
local img = {
- play = "//lua/img/play.png",
- pause = "//lua/img/pause.png",
- next = "//lua/img/next.png",
- next_disabled = "//lua/img/next_disabled.png",
- prev = "//lua/img/prev.png",
- prev_disabled = "//lua/img/prev_disabled.png",
- shuffle = "//lua/img/shuffle.png",
- shuffle_disabled = "//lua/img/shuffle_disabled.png",
- repeat_enabled = "//lua/img/repeat.png",
- repeat_disabled = "//lua/img/repeat_disabled.png",
+ play = lvgl.ImgData("//lua/img/play.png"),
+ pause = lvgl.ImgData("//lua/img/pause.png"),
+ next = lvgl.ImgData("//lua/img/next.png"),
+ prev = lvgl.ImgData("//lua/img/prev.png"),
+ shuffle = lvgl.ImgData("//lua/img/shuffle.png"),
+ repeat_src = lvgl.ImgData("//lua/img/repeat.png"), -- repeat is a reserved word
}
-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:Bar {
- w = lvgl.PCT(100),
- h = 5,
- range = { min = 0, max = 100 },
- value = 0,
- }
-
- 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
+
+local icon_enabled_class = "icon_enabled"
+local icon_disabled_class = "icon_disabled"
+
+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_src }
+ theme.set_style(repeat_img, icon_enabled_class)
+
+
+ local prev_btn = controls:Button {}
+ prev_btn:onClicked(queue.previous)
+ local prev_img = prev_btn:Image { src = img.prev }
+ theme.set_style(prev_img, icon_disabled_class)
+
+ 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 }
+ theme.set_style(play_pause_img, icon_enabled_class)
+
+ local next_btn = controls:Button {}
+ next_btn:onClicked(queue.next)
+ local next_img = next_btn:Image { src = img.next }
+ theme.set_style(next_img, icon_disabled_class)
+
+ 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 }
+ theme.set_style(shuffle_img, icon_enabled_class)
+
+ 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) }
+
+ theme.set_style(
+ next_img, pos < queue.size:get() and icon_enabled_class or icon_disabled_class
+ )
+
+ theme.set_style(
+ prev_img, pos > 1 and icon_enabled_class or icon_disabled_class
+ )
+ end),
+ queue.random:bind(function(shuffling)
+ theme.set_style(shuffle_img, shuffling and icon_enabled_class or icon_disabled_class)
+ end),
+ queue.repeat_track:bind(function(en)
+ theme.set_style(repeat_img, en and icon_enabled_class or icon_disabled_class)
+ 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)
- }
- scrubber:set { value = pos }
- 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..d19a6180 100644
--- a/lua/settings.lua
+++ b/lua/settings.lua
@@ -1,14 +1,15 @@
local lvgl = require("lvgl")
local backstack = require("backstack")
local widgets = require("widgets")
-local theme = require("theme")
+local styles = require("styles")
local volume = require("volume")
local display = require("display")
local controls = require("controls")
local bluetooth = require("bluetooth")
+local theme = require("theme")
local database = require("database")
-
-local settings = {}
+local screen = require("screen")
+local usb = require("usb")
local function SettingsScreen(title)
local menu = widgets.MenuScreen {
@@ -23,7 +24,7 @@ local function SettingsScreen(title)
align_items = "flex-start",
align_content = "flex-start",
},
- w = lvgl.PCT(100),
+ w = lvgl.PCT(90),
flex_grow = 1,
pad_left = 4,
pad_right = 4,
@@ -31,158 +32,161 @@ local function SettingsScreen(title)
return menu
end
-function settings.bluetooth()
- local menu = SettingsScreen("Bluetooth")
+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)
- 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 {
+ theme.set_style(self.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)
+ }, "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 {
+ theme.set_style(self.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
+ }, "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")
+
+ theme.set_style(self.menu.content:Label {
+ text = "Maxiumum volume limit",
+ }, "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)
- }
-end
-
-function settings.headphones()
- local menu = SettingsScreen("Headphones")
- menu.content:Label {
- text = "Maximum volume limit",
- }:add_style(theme.settings_title)
-
- 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 {
+ theme.set_style(self.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)
+ }, "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)
- }
- elseif bias > 0 then
- balance_label:set {
- text = string.format("Right %.2fdB", -bias / 4)
+ end),
+ volume.left_bias:bind(function(bias)
+ balance:set {
+ value = bias
}
- else
- balance_label:set { text = "Balanced" }
- end
- end),
- }
-
- return menu
-end
+ 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
+}
-function settings.display()
- local menu = SettingsScreen("Display")
+local DisplaySettings = screen:new {
+ createUi = function(self)
+ self.menu = SettingsScreen("Display")
- local brightness_title = menu.content:Object {
+ local brightness_title = self.menu.content:Object {
flex = {
flex_direction = "row",
justify_content = "flex-start",
@@ -191,96 +195,145 @@ function settings.display()
},
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) .. "%" }
+ theme.set_style(brightness_pct, "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 {
+ theme.set_style(self.menu.content:Label {
text = "Control scheme",
- }:add_style(theme.settings_title)
+ }, "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 {
+ theme.set_style(self.menu.content:Label {
text = "Scroll Sensitivity",
- }:add_style(theme.settings_title)
+ }, "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 MassStorageSettings = screen:new {
+ createUi = function(self)
+ self.menu = SettingsScreen("USB Storage")
+ local version = require("version").samd()
+ if tonumber(version) < 2 then
+ self.menu.content:Label {
+ w = lvgl.PCT(100),
+ text = "Usb Mass Storage requires a SAMD21 firmware version >=2."
+ }
+ return
+ end
- 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)
+ 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 {}
+
+ local bind_switch = function()
+ if usb.msc_enabled:get() then
+ enable_sw:add_state(lvgl.STATE.CHECKED)
+ else
+ enable_sw:clear_state(lvgl.STATE.CHECKED)
+ end
+ end
- return menu
-end
+ enable_sw:onevent(lvgl.EVENT.VALUE_CHANGED, function()
+ usb.msc_enabled:set(enable_sw:enabled())
+ bind_switch()
+ 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))
+ self.bindings = {
+ usb.msc_enabled:bind(bind_switch),
+ }
+ end,
+ canPop = function()
+ return not usb.msc_enabled:get()
+ end
+}
- local actions_container = menu.content:Object {
+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 = {
@@ -292,62 +345,74 @@ function settings.database()
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
+ actions_container:add_style(styles.list_item)
-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 update = actions_container:Button {}
+ update:Label { text = "Update" }
+ update:onClicked(function()
+ database.update()
+ end)
+ 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
+}
- local function section(name)
- menu.list:add_text(name):add_style(theme.list_heading)
+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 submenu(name, fn)
- local item = menu.list:add_btn(nil, name)
- item:onClicked(function()
- backstack.push(fn)
- end)
- item:add_style(theme.list_item)
+ local function section(name)
+ local elem = self.list:Label {
+ text = name,
+ pad_left = 4,
+ }
+ theme.set_style(elem, "settings_title")
end
- section("Audio")
- submenu("Bluetooth", settings.bluetooth)
- submenu("Headphones", settings.headphones)
+ local function submenu(name, class)
+ local item = self.list:add_btn(nil, name)
+ item:onClicked(function()
+ backstack.push(class:new())
+ end)
+ item:add_style(styles.list_item)
+ end
- section("Interface")
- submenu("Display", settings.display)
- submenu("Input Method", settings.input)
+ section("Audio")
+ submenu("Bluetooth", BluetoothSettings)
+ submenu("Headphones", HeadphonesSettings)
- section("System")
- submenu("Database", settings.database)
- submenu("Firmware", settings.firmware)
- submenu("Licenses", function()
- return require("licenses")()
- end)
+ section("Interface")
+ submenu("Display", DisplaySettings)
+ submenu("Input Method", InputSettings)
- return menu
-end
+ section("USB")
+ submenu("Storage", MassStorageSettings)
-return settings
+ section("System")
+ submenu("Database", DatabaseSettings)
+ submenu("Firmware", FirmwareSettings)
+ submenu("Licenses", LicensesScreen)
+ end
+}
diff --git a/lua/theme.lua b/lua/styles.lua
index 9c808946..fd45263e 100644
--- a/lua/theme.lua
+++ b/lua/styles.lua
@@ -1,7 +1,7 @@
local lvgl = require("lvgl")
local font = require("font")
-local theme = {
+local styles = {
list_item = lvgl.Style {
pad_left = 4,
pad_right = 4,
@@ -13,11 +13,6 @@ local theme = {
text_font = font.fusion_10,
text_align = lvgl.ALIGN.CENTER,
},
- settings_title = lvgl.Style {
- pad_top = 2,
- pad_bottom = 4,
- text_font = font.fusion_10,
- }
}
-return theme
+return styles
diff --git a/lua/theme_dark.lua b/lua/theme_dark.lua
new file mode 100644
index 00000000..be5feeaa
--- /dev/null
+++ b/lua/theme_dark.lua
@@ -0,0 +1,176 @@
+local lvgl = require("lvgl")
+local font = require("font")
+
+local background_color = "#5a5474"
+local background_muted = "#464258"
+local text_color = "#eeeeee"
+local highlight_color = "#9773d3"
+local icon_enabled_color = "#eeeeee"
+local icon_disabled_color = "#6d6d69"
+
+local theme_dark = {
+ base = {
+ {lvgl.PART.MAIN, lvgl.Style {
+ bg_opa = lvgl.OPA(0),
+ text_font = font.fusion_12,
+ }},
+ },
+ root = {
+ {lvgl.PART.MAIN, lvgl.Style {
+ bg_opa = lvgl.OPA(100),
+ bg_color = background_color, -- Root background color
+ bg_grad_dir = 1,
+ bg_grad_color = "#1d0e38",
+ text_color = text_color
+ }},
+ },
+ header = {
+ {lvgl.PART.MAIN, lvgl.Style {
+ bg_opa = lvgl.OPA(100),
+ bg_color = background_muted,
+ }},
+ },
+ pop_up = {
+ {lvgl.PART.MAIN, lvgl.Style {
+ bg_opa = lvgl.OPA(100),
+ bg_color = background_muted,
+ }},
+ },
+ button = {
+ {lvgl.PART.MAIN, lvgl.Style {
+ pad_left = 2,
+ pad_right = 2,
+ pad_top = 1,
+ pad_bottom = 1,
+ bg_color = background_color,
+ img_recolor_opa = 180,
+ img_recolor = highlight_color,
+ radius = 5,
+ }},
+ {lvgl.PART.MAIN | lvgl.STATE.FOCUSED, lvgl.Style {
+ bg_opa = lvgl.OPA(100),
+ bg_color = highlight_color,
+ img_recolor_opa = 0,
+ }},
+ },
+ listbutton = {
+ {lvgl.PART.MAIN | lvgl.STATE.FOCUSED, lvgl.Style {
+ bg_opa = lvgl.OPA(100),
+ bg_color = highlight_color,
+ }},
+ },
+ bar = {
+ {lvgl.PART.MAIN, lvgl.Style {
+ bg_opa = lvgl.OPA(100),
+ radius = 32767, -- LV_RADIUS_CIRCLE = 0x7fff
+ }},
+ },
+ slider = {
+ {lvgl.PART.MAIN, lvgl.Style {
+ bg_opa = lvgl.OPA(100),
+ bg_color = background_muted,
+ radius = 32767, -- LV_RADIUS_CIRCLE = 0x7fff
+ }},
+ {lvgl.PART.INDICATOR, lvgl.Style {
+ radius = 32767, -- LV_RADIUS_CIRCLE = 0x7fff
+ bg_color = highlight_color,
+ }},
+ {lvgl.PART.KNOB, lvgl.Style {
+ radius = 32767, -- LV_RADIUS_CIRCLE = 0x7fff
+ pad_all = 2,
+ bg_color = background_muted,
+ shadow_width = 5,
+ shadow_opa = lvgl.OPA(100)
+ }},
+ {lvgl.PART.MAIN | lvgl.STATE.FOCUSED, lvgl.Style {
+ bg_color = background_muted,
+ }},
+ {lvgl.PART.KNOB | lvgl.STATE.FOCUSED, lvgl.Style {
+ bg_color = highlight_color,
+ }},
+ {lvgl.PART.INDICATOR | lvgl.STATE.CHECKED, lvgl.Style {
+ bg_color = highlight_color,
+ }},
+ },
+ switch = {
+ {lvgl.PART.MAIN, lvgl.Style {
+ bg_opa = lvgl.OPA(100),
+ width = 28,
+ height = 8,
+ radius = 32767, -- LV_RADIUS_CIRCLE = 0x7fff
+ bg_color = background_muted,
+ border_color = highlight_color,
+ }},
+ {lvgl.PART.INDICATOR, lvgl.Style {
+ radius = 32767, -- LV_RADIUS_CIRCLE = 0x7fff
+ bg_color = background_muted,
+ }},
+ {lvgl.PART.INDICATOR | lvgl.STATE.CHECKED, lvgl.Style {
+ bg_color = highlight_color,
+ }},
+ {lvgl.PART.KNOB, lvgl.Style {
+ radius = 32767, -- LV_RADIUS_CIRCLE = 0x7fff
+ pad_all = 2,
+ bg_opa = lvgl.OPA(100),
+ bg_color = background_muted,
+ }},
+ {lvgl.PART.KNOB | lvgl.STATE.FOCUSED, lvgl.Style {
+ bg_color = highlight_color,
+ }},
+ },
+ dropdown = {
+ {lvgl.PART.MAIN, lvgl.Style{
+ radius = 2,
+ pad_all = 2,
+ border_width = 1,
+ border_color = background_muted,
+ border_side = 15, -- LV_BORDER_SIDE_FULL
+ bg_color = background_color,
+ }},
+ {lvgl.PART.MAIN | lvgl.STATE.FOCUSED, lvgl.Style {
+ border_color = highlight_color,
+ }},
+ },
+ dropdownlist = {
+ {lvgl.PART.MAIN, lvgl.Style{
+ radius = 2,
+ pad_all = 2,
+ border_width = 1,
+ border_color = highlight_color,
+ bg_opa = lvgl.OPA(100),
+ bg_color = background_color
+ }},
+ {lvgl.PART.SELECTED | lvgl.STATE.CHECKED, lvgl.Style {
+ bg_color = highlight_color,
+ }},
+ },
+ database_indicator = {
+ {lvgl.PART.MAIN, lvgl.Style {
+ img_recolor_opa = 180,
+ img_recolor = highlight_color,
+ }},
+ },
+ settings_title = {
+ {lvgl.PART.MAIN, lvgl.Style {
+ pad_top = 2,
+ pad_bottom = 4,
+ text_font = font.fusion_10,
+ text_color = highlight_color,
+ }},
+ },
+ icon_disabled = {
+ {lvgl.PART.MAIN, lvgl.Style {
+ img_recolor_opa = 180,
+ img_recolor = icon_disabled_color,
+ }},
+ },
+ icon_enabled = {
+ {lvgl.PART.MAIN, lvgl.Style {
+ img_recolor_opa = 180,
+ img_recolor = icon_enabled_color,
+ }},
+ },
+
+}
+
+return theme_dark
diff --git a/lua/theme_light.lua b/lua/theme_light.lua
new file mode 100644
index 00000000..e0a4468f
--- /dev/null
+++ b/lua/theme_light.lua
@@ -0,0 +1,174 @@
+local lvgl = require("lvgl")
+local font = require("font")
+
+local background_color = "#ffffff"
+local background_muted = "#fafafa"
+local text_color = "#000000"
+local highlight_color = "#ce93d8"
+local icon_enabled_color = "#2c2c2c"
+local icon_disabled_color = "#999999"
+
+local theme_light = {
+ base = {
+ {lvgl.PART.MAIN, lvgl.Style {
+ bg_opa = lvgl.OPA(0),
+ text_font = font.fusion_12,
+ }},
+ },
+ root = {
+ {lvgl.PART.MAIN, lvgl.Style {
+ bg_opa = lvgl.OPA(100),
+ bg_color = background_color, -- Root background color
+ text_color = text_color
+ }},
+ },
+ header = {
+ {lvgl.PART.MAIN, lvgl.Style {
+ bg_opa = lvgl.OPA(100),
+ bg_color = background_muted,
+ }},
+ },
+ pop_up = {
+ {lvgl.PART.MAIN, lvgl.Style {
+ bg_opa = lvgl.OPA(100),
+ bg_color = background_muted,
+ }},
+ },
+ button = {
+ {lvgl.PART.MAIN, lvgl.Style {
+ pad_left = 2,
+ pad_right = 2,
+ pad_top = 1,
+ pad_bottom = 1,
+ bg_color = background_color,
+ img_recolor_opa = 180,
+ img_recolor = highlight_color,
+ radius = 5,
+ }},
+ {lvgl.PART.MAIN | lvgl.STATE.FOCUSED, lvgl.Style {
+ bg_opa = lvgl.OPA(100),
+ bg_color = highlight_color,
+ img_recolor_opa = 0,
+ }},
+ },
+ listbutton = {
+ {lvgl.PART.MAIN | lvgl.STATE.FOCUSED, lvgl.Style {
+ bg_opa = lvgl.OPA(100),
+ bg_color = highlight_color,
+ }},
+ },
+ bar = {
+ {lvgl.PART.MAIN, lvgl.Style {
+ bg_opa = lvgl.OPA(100),
+ radius = 32767, -- LV_RADIUS_CIRCLE = 0x7fff
+ }},
+ },
+ slider = {
+ {lvgl.PART.MAIN, lvgl.Style {
+ bg_opa = lvgl.OPA(100),
+ bg_color = background_muted,
+ radius = 32767, -- LV_RADIUS_CIRCLE = 0x7fff
+ }},
+ {lvgl.PART.INDICATOR, lvgl.Style {
+ radius = 32767, -- LV_RADIUS_CIRCLE = 0x7fff
+ bg_color = highlight_color,
+ }},
+ {lvgl.PART.KNOB, lvgl.Style {
+ radius = 32767, -- LV_RADIUS_CIRCLE = 0x7fff
+ pad_all = 2,
+ bg_color = background_muted,
+ shadow_width = 5,
+ shadow_opa = lvgl.OPA(100)
+ }},
+ {lvgl.PART.MAIN | lvgl.STATE.FOCUSED, lvgl.Style {
+ bg_color = background_muted,
+ }},
+ {lvgl.PART.KNOB | lvgl.STATE.FOCUSED, lvgl.Style {
+ bg_color = highlight_color,
+ }},
+ {lvgl.PART.INDICATOR | lvgl.STATE.CHECKED, lvgl.Style {
+ bg_color = highlight_color,
+ }},
+ },
+ switch = {
+ {lvgl.PART.MAIN, lvgl.Style {
+ bg_opa = lvgl.OPA(100),
+ width = 28,
+ height = 8,
+ radius = 32767, -- LV_RADIUS_CIRCLE = 0x7fff
+ bg_color = background_muted,
+ border_color = highlight_color,
+ }},
+ {lvgl.PART.INDICATOR, lvgl.Style {
+ radius = 32767, -- LV_RADIUS_CIRCLE = 0x7fff
+ bg_color = background_muted,
+ }},
+ {lvgl.PART.INDICATOR | lvgl.STATE.CHECKED, lvgl.Style {
+ bg_color = highlight_color,
+ }},
+ {lvgl.PART.KNOB, lvgl.Style {
+ radius = 32767, -- LV_RADIUS_CIRCLE = 0x7fff
+ pad_all = 2,
+ bg_opa = lvgl.OPA(100),
+ bg_color = background_muted,
+ }},
+ {lvgl.PART.KNOB | lvgl.STATE.FOCUSED, lvgl.Style {
+ bg_color = highlight_color,
+ }},
+ },
+ dropdown = {
+ {lvgl.PART.MAIN, lvgl.Style{
+ radius = 2,
+ pad_all = 2,
+ border_width = 1,
+ border_color = background_muted,
+ border_side = 15, -- LV_BORDER_SIDE_FULL
+ bg_color = background_color,
+ }},
+ {lvgl.PART.MAIN | lvgl.STATE.FOCUSED, lvgl.Style {
+ border_color = highlight_color,
+ }},
+ },
+ dropdownlist = {
+ {lvgl.PART.MAIN, lvgl.Style{
+ radius = 2,
+ pad_all = 2,
+ border_width = 1,
+ border_color = highlight_color,
+ bg_opa = lvgl.OPA(100),
+ bg_color = background_color
+ }},
+ {lvgl.PART.SELECTED | lvgl.STATE.CHECKED, lvgl.Style {
+ bg_color = highlight_color,
+ }},
+ },
+ database_indicator = {
+ {lvgl.PART.MAIN, lvgl.Style {
+ img_recolor_opa = 180,
+ img_recolor = highlight_color,
+ }},
+ },
+ settings_title = {
+ {lvgl.PART.MAIN, lvgl.Style {
+ pad_top = 2,
+ pad_bottom = 4,
+ text_font = font.fusion_10,
+ text_color = highlight_color,
+ }},
+ },
+ icon_disabled = {
+ {lvgl.PART.MAIN, lvgl.Style {
+ img_recolor_opa = 180,
+ img_recolor = icon_disabled_color,
+ }},
+ },
+ icon_enabled = {
+ {lvgl.PART.MAIN, lvgl.Style {
+ img_recolor_opa = 180,
+ img_recolor = icon_enabled_color,
+ }},
+ },
+
+}
+
+return theme_light
diff --git a/lua/widgets.lua b/lua/widgets.lua
index 8905fa43..fa991758 100644
--- a/lua/widgets.lua
+++ b/lua/widgets.lua
@@ -3,8 +3,23 @@ local power = require("power")
local bluetooth = require("bluetooth")
local font = require("font")
local backstack = require("backstack")
-local theme = require("theme")
+local styles = require("styles")
local database = require("database")
+local theme = require("theme")
+
+local img = {
+ db = lvgl.ImgData("//lua/img/db.png"),
+ chg = lvgl.ImgData("//lua/img/bat/chg.png"),
+ bat_100 = lvgl.ImgData("//lua/img/bat/100.png"),
+ bat_80 = lvgl.ImgData("//lua/img/bat/80.png"),
+ bat_60 = lvgl.ImgData("//lua/img/bat/60.png"),
+ bat_40 = lvgl.ImgData("//lua/img/bat/40.png"),
+ bat_20 = lvgl.ImgData("//lua/img/bat/20.png"),
+ bat_0 = lvgl.ImgData("//lua/img/bat/0.png"),
+ bat_0chg = lvgl.ImgData("//lua/img/bat/0chg.png"),
+ bt_conn = lvgl.ImgData("//lua/assets/bt_conn.png"),
+ bt = lvgl.ImgData("//lua/assets/bt.png")
+}
local widgets = {}
@@ -41,7 +56,7 @@ function widgets.Row(parent, left, right)
w = lvgl.PCT(100),
h = lvgl.SIZE_CONTENT,
}
- container:add_style(theme.list_item)
+ container:add_style(styles.list_item)
container:Label { text = left, flex_grow = 1 }
container:Label { text = right }
end
@@ -66,10 +81,7 @@ function widgets.StatusBar(parent, opts)
}
if not opts.transparent_bg then
- status_bar.root:set {
- bg_opa = lvgl.OPA(100),
- bg_color = "#fafafa",
- }
+ theme.set_style(status_bar.root, "header");
end
if opts.back_cb then
@@ -93,14 +105,11 @@ function widgets.StatusBar(parent, opts)
status_bar.title:set { text = opts.title }
end
- status_bar.db_updating = status_bar.root:Image {
- src = "//lua/img/db.png"
- }
+ status_bar.db_updating = status_bar.root:Image { src = img.db }
+ theme.set_style(status_bar.db_updating, "database_indicator")
status_bar.bluetooth = status_bar.root:Image {}
status_bar.battery = status_bar.root:Image {}
- status_bar.chg = status_bar.battery:Image {
- src = "//lua/img/bat/chg.png"
- }
+ status_bar.chg = status_bar.battery:Image { src = img.chg }
status_bar.chg:center()
local is_charging = nil
@@ -110,20 +119,20 @@ function widgets.StatusBar(parent, opts)
if is_charging == nil or percent == nil then return end
local src
if percent >= 95 then
- src = "100.png"
+ src = img.bat_100
elseif percent >= 75 then
- src = "80.png"
+ src = img.bat_80
elseif percent >= 55 then
- src = "60.png"
+ src = img.bat_60
elseif percent >= 35 then
- src = "40.png"
+ src = img.bat_40
elseif percent >= 15 then
- src = "20.png"
+ src = img.bat_20
else
if is_charging then
- src = "0chg.png"
+ src = img.bat_0chg
else
- src = "0.png"
+ src = img.bat_0
end
end
if is_charging then
@@ -131,7 +140,7 @@ function widgets.StatusBar(parent, opts)
else
status_bar.chg:add_flag(lvgl.FLAG.HIDDEN)
end
- status_bar.battery:set_src("//lua/img/bat/" .. src)
+ status_bar.battery:set_src(src)
end
status_bar.bindings = {
@@ -159,9 +168,9 @@ function widgets.StatusBar(parent, opts)
end),
bluetooth.connected:bind(function(connected)
if connected then
- status_bar.bluetooth:set_src("//lua/assets/bt_conn.png")
+ status_bar.bluetooth:set_src(img.bt_conn)
else
- status_bar.bluetooth:set_src("//lua/assets/bt.png")
+ status_bar.bluetooth:set_src(img.bt)
end
end),
}