diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt index 06bad37e..fa3f2bc1 100644 --- a/src/ui/CMakeLists.txt +++ b/src/ui/CMakeLists.txt @@ -6,7 +6,7 @@ idf_component_register( SRCS "lvgl_task.cpp" "ui_fsm.cpp" "screen_splash.cpp" "screen_menu.cpp" "wheel_encoder.cpp" "screen_track_browser.cpp" "screen_playing.cpp" "themes.cpp" "widget_top_bar.cpp" "screen.cpp" "screen_onboarding.cpp" - "modal_progress.cpp" "modal.cpp" "modal_confirm.cpp" + "modal_progress.cpp" "modal.cpp" "modal_confirm.cpp" "screen_settings.cpp" "splash.c" "font_fusion.c" "font_symbols.c" INCLUDE_DIRS "include" REQUIRES "drivers" "lvgl" "tinyfsm" "events" "system_fsm" "database" "esp_timer") diff --git a/src/ui/include/screen.hpp b/src/ui/include/screen.hpp index 250b3c8d..f93d17a5 100644 --- a/src/ui/include/screen.hpp +++ b/src/ui/include/screen.hpp @@ -52,8 +52,8 @@ class Screen { -> widgets::TopBar*; lv_obj_t* const root_; - lv_obj_t* const content_; - lv_obj_t* const modal_content_; + lv_obj_t* content_; + lv_obj_t* modal_content_; lv_group_t* const group_; lv_group_t* modal_group_; @@ -62,4 +62,9 @@ class Screen { std::unique_ptr top_bar_; }; +class MenuScreen : public Screen { + public: + MenuScreen(const std::string& title, bool show_back_button = true); +}; + } // namespace ui diff --git a/src/ui/include/screen_menu.hpp b/src/ui/include/screen_menu.hpp index e4cc0e78..be2a9493 100644 --- a/src/ui/include/screen_menu.hpp +++ b/src/ui/include/screen_menu.hpp @@ -13,11 +13,12 @@ #include "lvgl.h" #include "screen.hpp" +#include "screen_settings.hpp" namespace ui { namespace screens { -class Menu : public Screen { +class Menu : public MenuScreen { public: explicit Menu(std::vector indexes); ~Menu(); diff --git a/src/ui/include/screen_settings.hpp b/src/ui/include/screen_settings.hpp new file mode 100644 index 00000000..ebc5bf7d --- /dev/null +++ b/src/ui/include/screen_settings.hpp @@ -0,0 +1,70 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once + +#include +#include + +#include "index.hpp" +#include "lvgl.h" + +#include "screen.hpp" + +namespace ui { +namespace screens { + +class Settings : public MenuScreen { + public: + Settings(); + ~Settings(); + private: + std::shared_ptr bluetooth_; + std::shared_ptr headphones_; + std::shared_ptr appearance_; + std::shared_ptr input_method_; + std::shared_ptr storage_; + std::shared_ptr firmware_update_; + std::shared_ptr about_; +}; + +class Bluetooth : public MenuScreen { + public: + Bluetooth(); +}; + +class Headphones : public MenuScreen { + public: + Headphones(); +}; + +class Appearance : public MenuScreen { + public: + Appearance(); +}; + +class InputMethod : public MenuScreen { + public: + InputMethod(); +}; + +class Storage : public MenuScreen { + public: + Storage(); +}; + +class FirmwareUpdate : public MenuScreen { + public: + FirmwareUpdate(); +}; + +class About : public MenuScreen { + public: + About(); +}; + +} // namespace screens +} // namespace ui diff --git a/src/ui/include/ui_events.hpp b/src/ui/include/ui_events.hpp index 759a0879..2f26c854 100644 --- a/src/ui/include/ui_events.hpp +++ b/src/ui/include/ui_events.hpp @@ -9,6 +9,7 @@ #include #include "database.hpp" #include "index.hpp" +#include "screen.hpp" #include "tinyfsm.hpp" namespace ui { @@ -36,6 +37,10 @@ struct IndexSelected : tinyfsm::Event { }; struct BackPressed : tinyfsm::Event {}; +struct ShowNowPlaying : tinyfsm::Event {}; +struct ShowSettingsPage : tinyfsm::Event { + std::shared_ptr screen; +}; struct ModalConfirmPressed : tinyfsm::Event {}; struct ModalCancelPressed : tinyfsm::Event {}; diff --git a/src/ui/include/ui_fsm.hpp b/src/ui/include/ui_fsm.hpp index 4985129a..80c01c68 100644 --- a/src/ui/include/ui_fsm.hpp +++ b/src/ui/include/ui_fsm.hpp @@ -52,6 +52,8 @@ class UiState : public tinyfsm::Fsm { virtual void react(const internal::RecordSelected&) {} virtual void react(const internal::IndexSelected&) {} virtual void react(const internal::BackPressed&) {} + virtual void react(const internal::ShowNowPlaying&){}; + virtual void react(const internal::ShowSettingsPage&){}; virtual void react(const internal::ModalCancelPressed&) { sCurrentModal.reset(); } @@ -104,6 +106,9 @@ class Browse : public UiState { void react(const internal::IndexSelected&) override; void react(const internal::BackPressed&) override; + void react(const internal::ShowNowPlaying&) override; + void react(const internal::ShowSettingsPage&) override; + void react(const system_fsm::StorageMounted&) override; using UiState::react; }; diff --git a/src/ui/modal.cpp b/src/ui/modal.cpp index c0f9b3f5..36376093 100644 --- a/src/ui/modal.cpp +++ b/src/ui/modal.cpp @@ -33,7 +33,6 @@ Modal::Modal(Screen* host) group_(lv_group_create()), host_(host) { lv_obj_set_style_bg_opa(host->modal_content(), LV_OPA_40, 0); - lv_obj_set_style_bg_color(host->modal_content(), lv_color_black(), 0); lv_obj_set_size(root_, 120, LV_SIZE_CONTENT); lv_obj_center(root_); diff --git a/src/ui/screen.cpp b/src/ui/screen.cpp index 039d2439..c9933042 100644 --- a/src/ui/screen.cpp +++ b/src/ui/screen.cpp @@ -57,4 +57,28 @@ auto Screen::CreateTopBar(lv_obj_t* parent, return top_bar_.get(); } +MenuScreen::MenuScreen(const std::string& title, bool show_back_button) + : Screen() { + lv_group_set_wrap(group_, false); + + lv_obj_set_layout(content_, LV_LAYOUT_FLEX); + lv_obj_set_flex_flow(content_, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(content_, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, + LV_FLEX_ALIGN_CENTER); + + widgets::TopBar::Configuration config{ + .show_back_button = show_back_button, + .title = title.c_str(), + }; + CreateTopBar(content_, config); + + content_ = lv_obj_create(content_); + lv_obj_set_flex_grow(content_, 1); + lv_obj_set_width(content_, lv_pct(100)); + lv_obj_set_layout(content_, LV_LAYOUT_FLEX); + lv_obj_set_flex_flow(content_, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(content_, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, + LV_FLEX_ALIGN_START); +} + } // namespace ui diff --git a/src/ui/screen_menu.cpp b/src/ui/screen_menu.cpp index 8c402532..7134a452 100644 --- a/src/ui/screen_menu.cpp +++ b/src/ui/screen_menu.cpp @@ -26,7 +26,16 @@ namespace ui { namespace screens { -static void item_click_cb(lv_event_t* ev) { +static void now_playing_click_cb(lv_event_t* ev) { + events::Ui().Dispatch(internal::ShowNowPlaying{}); +} + +static void settings_click_callback(lv_event_t* ev) { + std::shared_ptr settings{new Settings()}; + events::Ui().Dispatch(internal::ShowSettingsPage{.screen = settings}); +} + +static void index_click_cb(lv_event_t* ev) { if (ev->user_data == NULL) { return; } @@ -36,27 +45,26 @@ static void item_click_cb(lv_event_t* ev) { events::Ui().Dispatch(internal::IndexSelected{.index = *index}); } -Menu::Menu(std::vector indexes) : indexes_(indexes) { - lv_obj_set_layout(content_, LV_LAYOUT_FLEX); - lv_obj_set_flex_flow(content_, LV_FLEX_FLOW_COLUMN); - lv_obj_set_flex_align(content_, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, - LV_FLEX_ALIGN_CENTER); - - widgets::TopBar::Configuration config{ - .show_back_button = false, - .title = "", - }; - CreateTopBar(content_, config); - +Menu::Menu(std::vector indexes) + : MenuScreen(" ", false), indexes_(indexes) { lv_obj_t* list = lv_list_create(content_); - lv_obj_set_size(list, lv_disp_get_hor_res(NULL), lv_disp_get_ver_res(NULL)); - lv_obj_center(list); + lv_obj_set_size(list, lv_pct(100), lv_pct(100)); + + lv_obj_t* now_playing = lv_list_add_btn(list, NULL, "Now Playing"); + lv_obj_add_event_cb(now_playing, now_playing_click_cb, LV_EVENT_CLICKED, + NULL); + lv_group_add_obj(group_, now_playing); for (database::IndexInfo& index : indexes_) { lv_obj_t* item = lv_list_add_btn(list, NULL, index.name.c_str()); - lv_obj_add_event_cb(item, item_click_cb, LV_EVENT_CLICKED, &index); + lv_obj_add_event_cb(item, index_click_cb, LV_EVENT_CLICKED, &index); lv_group_add_obj(group_, item); } + + lv_obj_t* settings = lv_list_add_btn(list, NULL, "Settings"); + lv_obj_add_event_cb(settings, settings_click_callback, LV_EVENT_CLICKED, + NULL); + lv_group_add_obj(group_, settings); } Menu::~Menu() {} diff --git a/src/ui/screen_playing.cpp b/src/ui/screen_playing.cpp index a1f91902..ce0f71d6 100644 --- a/src/ui/screen_playing.cpp +++ b/src/ui/screen_playing.cpp @@ -109,7 +109,7 @@ Playing::Playing(std::weak_ptr db, audio::TrackQueue* queue) lv_obj_set_layout(content_, LV_LAYOUT_FLEX); lv_group_set_wrap(group_, false); - lv_obj_set_size(content_, lv_pct(100), LV_SIZE_CONTENT); + lv_obj_set_size(content_, lv_pct(100), lv_disp_get_ver_res(NULL)); lv_obj_set_flex_flow(content_, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_align(content_, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START); diff --git a/src/ui/screen_settings.cpp b/src/ui/screen_settings.cpp new file mode 100644 index 00000000..1c875700 --- /dev/null +++ b/src/ui/screen_settings.cpp @@ -0,0 +1,214 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include "screen_settings.hpp" + +#include "core/lv_event.h" +#include "core/lv_obj.h" +#include "esp_log.h" + +#include "core/lv_group.h" +#include "core/lv_obj_pos.h" +#include "event_queue.hpp" +#include "extra/layouts/flex/lv_flex.h" +#include "extra/widgets/list/lv_list.h" +#include "extra/widgets/menu/lv_menu.h" +#include "extra/widgets/spinner/lv_spinner.h" +#include "hal/lv_hal_disp.h" +#include "index.hpp" +#include "misc/lv_anim.h" +#include "misc/lv_area.h" +#include "screen.hpp" +#include "ui_events.hpp" +#include "ui_fsm.hpp" +#include "widget_top_bar.hpp" +#include "widgets/lv_bar.h" +#include "widgets/lv_btn.h" +#include "widgets/lv_dropdown.h" +#include "widgets/lv_label.h" +#include "widgets/lv_slider.h" +#include "widgets/lv_switch.h" + +namespace ui { +namespace screens { + +static void open_sub_menu_cb(lv_event_t* e) { + std::shared_ptr* next_screen = + reinterpret_cast*>(e->user_data); + events::Ui().Dispatch(internal::ShowSettingsPage{ + .screen = *next_screen, + }); +} + +static void sub_menu(lv_obj_t* list, + lv_group_t* group, + const std::string& text, + std::shared_ptr* screen) { + lv_obj_t* item = lv_list_add_btn(list, NULL, text.c_str()); + lv_group_add_obj(group, item); + lv_obj_add_event_cb(item, open_sub_menu_cb, LV_EVENT_CLICKED, screen); +} + +Settings::Settings() + : MenuScreen("Settings"), + bluetooth_(new Bluetooth()), + headphones_(new Headphones()), + appearance_(new Appearance()), + input_method_(new InputMethod()), + storage_(new Storage()), + firmware_update_(new FirmwareUpdate()), + about_(new About()) { + lv_obj_t* list = lv_list_create(content_); + lv_obj_set_size(list, lv_pct(100), lv_pct(100)); + + lv_list_add_text(list, "Audio"); + sub_menu(list, group_, "Bluetooth", &bluetooth_); + sub_menu(list, group_, "Headphones", &headphones_); + + lv_list_add_text(list, "Interface"); + sub_menu(list, group_, "Appearance", &appearance_); + sub_menu(list, group_, "Input Method", &input_method_); + + lv_list_add_text(list, "System"); + sub_menu(list, group_, "Storage", &storage_); + sub_menu(list, group_, "Firmware Update", &firmware_update_); + sub_menu(list, group_, "About", &about_); +} + +Settings::~Settings() {} + +static auto settings_container(lv_obj_t* parent) -> lv_obj_t* { + lv_obj_t* res = lv_obj_create(parent); + lv_obj_set_layout(res, LV_LAYOUT_FLEX); + lv_obj_set_size(res, lv_pct(100), LV_SIZE_CONTENT); + lv_obj_set_flex_flow(res, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(res, LV_FLEX_ALIGN_SPACE_EVENLY, LV_FLEX_ALIGN_CENTER, + LV_FLEX_ALIGN_START); + return res; +} + +static auto label_pair(lv_obj_t* parent, + const std::string& left, + const std::string& right) -> lv_obj_t* { + lv_obj_t* container = settings_container(parent); + lv_obj_t* left_label = lv_label_create(container); + lv_label_set_text(left_label, left.c_str()); + lv_obj_t* right_label = lv_label_create(container); + lv_label_set_text(right_label, right.c_str()); + return right_label; +} + +Bluetooth::Bluetooth() : MenuScreen("Bluetooth") { + lv_obj_t* toggle_container = settings_container(content_); + lv_obj_t* toggle_label = lv_label_create(toggle_container); + lv_label_set_text(toggle_label, "Enable"); + lv_obj_set_flex_grow(toggle_label, 1); + lv_obj_t* toggle = lv_switch_create(toggle_container); + lv_group_add_obj(group_, toggle); + + lv_obj_t* devices_label = lv_label_create(content_); + lv_label_set_text(devices_label, "Devices"); + + lv_obj_t* devices_list = lv_list_create(content_); + lv_list_add_text(devices_list, "My Headphones"); + lv_group_add_obj(group_, + lv_list_add_btn(devices_list, NULL, "Something else")); + lv_group_add_obj(group_, lv_list_add_btn(devices_list, NULL, "A car??")); + lv_group_add_obj(group_, + lv_list_add_btn(devices_list, NULL, "OLED TV ANDROID")); + lv_group_add_obj( + group_, lv_list_add_btn(devices_list, NULL, "there could be another")); + lv_group_add_obj(group_, lv_list_add_btn(devices_list, NULL, + "this one has a really long name")); +} + +Headphones::Headphones() : MenuScreen("Headphones") { + lv_obj_t* vol_label = lv_label_create(content_); + lv_label_set_text(vol_label, "Volume Limit"); + lv_obj_t* vol_dropdown = lv_dropdown_create(content_); + lv_dropdown_set_options(vol_dropdown, + "Line Level (-10 dBV)\nPro Level (+4 dBu)\nMax " + "before clipping\nUnlimited\nCustom"); + lv_group_add_obj(group_, vol_dropdown); + + lv_obj_t* warning_label = label_pair( + content_, "!!", "Changing volume limit is for advanced users."); + lv_label_set_long_mode(warning_label, LV_LABEL_LONG_WRAP); + lv_obj_set_flex_grow(warning_label, 1); + + lv_obj_t* balance_label = lv_label_create(content_); + lv_label_set_text(balance_label, "Left/Right Balance"); + lv_obj_t* balance = lv_slider_create(content_); + lv_obj_set_width(balance, lv_pct(100)); + lv_slider_set_range(balance, -10, 10); + lv_slider_set_value(balance, 0, LV_ANIM_OFF); + lv_slider_set_mode(balance, LV_SLIDER_MODE_SYMMETRICAL); + lv_group_add_obj(group_, balance); + lv_obj_t* current_balance_label = lv_label_create(content_); + lv_label_set_text(current_balance_label, "0dB"); + lv_obj_set_size(current_balance_label, lv_pct(100), LV_SIZE_CONTENT); +} + +Appearance::Appearance() : MenuScreen("Appearance") { + lv_obj_t* toggle_container = settings_container(content_); + lv_obj_t* toggle_label = lv_label_create(toggle_container); + lv_obj_set_flex_grow(toggle_label, 1); + lv_label_set_text(toggle_label, "Dark Mode"); + lv_obj_t* toggle = lv_switch_create(toggle_container); + lv_group_add_obj(group_, toggle); +} + +InputMethod::InputMethod() : MenuScreen("Input Method") { + lv_obj_t* wheel_label = lv_label_create(content_); + lv_label_set_text(wheel_label, "What does the wheel do?"); + lv_obj_t* wheel_dropdown = lv_dropdown_create(content_); + lv_dropdown_set_options(wheel_dropdown, "Scroll\nDirectional\nBig Button"); + lv_group_add_obj(group_, wheel_dropdown); + + lv_obj_t* buttons_label = lv_label_create(content_); + lv_label_set_text(buttons_label, "What do the buttons do?"); + lv_obj_t* buttons_dropdown = lv_dropdown_create(content_); + lv_dropdown_set_options(buttons_dropdown, "Volume\nScroll"); + lv_group_add_obj(group_, buttons_dropdown); +} + +Storage::Storage() : MenuScreen("Storage") { + label_pair(content_, "Storage Capacity:", "32 GiB"); + label_pair(content_, "Currently Used:", "6 GiB"); + label_pair(content_, "DB Size:", "1.2 MiB"); + + lv_obj_t* usage_bar = lv_bar_create(content_); + lv_bar_set_range(usage_bar, 0, 32); + lv_bar_set_value(usage_bar, 6, LV_ANIM_OFF); + + lv_obj_t* reset_btn = lv_btn_create(content_); + lv_obj_t* reset_label = lv_label_create(reset_btn); + lv_label_set_text(reset_label, "Reset Database"); + lv_group_add_obj(group_, reset_btn); +} + +FirmwareUpdate::FirmwareUpdate() : MenuScreen("Firmware Update") { + label_pair(content_, "ESP32 FW:", "vIDKLOL"); + label_pair(content_, "SAMD21 FW:", "vIDKLOL"); + + lv_obj_t* flash_esp_btn = lv_btn_create(content_); + lv_obj_t* flash_esp_label = lv_label_create(flash_esp_btn); + lv_label_set_text(flash_esp_label, "Update ESP32"); + lv_group_add_obj(group_, flash_esp_btn); + + lv_obj_t* flash_samd_btn = lv_btn_create(content_); + lv_obj_t* flash_samd_label = lv_label_create(flash_samd_btn); + lv_label_set_text(flash_samd_label, "Update SAMD21"); + lv_group_add_obj(group_, flash_samd_btn); +} + +About::About() : MenuScreen("About") { + lv_obj_t* label = lv_label_create(content_); + lv_label_set_text(label, "Some licenses or whatever"); +} + +} // namespace screens +} // namespace ui diff --git a/src/ui/ui_fsm.cpp b/src/ui/ui_fsm.cpp index 8b2d3a3c..e8660207 100644 --- a/src/ui/ui_fsm.cpp +++ b/src/ui/ui_fsm.cpp @@ -21,6 +21,7 @@ #include "screen.hpp" #include "screen_menu.hpp" #include "screen_playing.hpp" +#include "screen_settings.hpp" #include "screen_splash.hpp" #include "screen_track_browser.hpp" #include "source.hpp" @@ -137,6 +138,14 @@ void Browse::react(const system_fsm::StorageMounted& ev) { PushScreen(std::make_shared(db->GetIndexes())); } +void Browse::react(const internal::ShowNowPlaying& ev) { + transit(); +} + +void Browse::react(const internal::ShowSettingsPage& ev) { + PushScreen(ev.screen); +} + void Browse::react(const internal::RecordSelected& ev) { auto db = sDb.lock(); if (!db) { diff --git a/src/ui/widget_top_bar.cpp b/src/ui/widget_top_bar.cpp index e5d51350..bd146c99 100644 --- a/src/ui/widget_top_bar.cpp +++ b/src/ui/widget_top_bar.cpp @@ -55,7 +55,7 @@ TopBar::TopBar(lv_obj_t* parent, const Configuration& config) { auto TopBar::Update(const State& state) -> void { switch (state.playback_state) { case PlaybackState::kIdle: - lv_label_set_text(playback_, ""); + lv_label_set_text(playback_, "-"); break; case PlaybackState::kPaused: lv_label_set_text(playback_, "");