diff --git a/Makefile b/Makefile index 4ed0aa8..c1647c2 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ CPP_MODS = ../cpp-mods include $(CPP_MODS)/config.mk OBJDIR := obj/ -CFLAGS += -g -Wall -I$(CPP_MODS) -Wno-switch +CFLAGS += -g -O2 -Wall -I$(CPP_MODS) -Wno-switch #CFLAGS += -O2 SRCDIR := src/ #LDFLAGS += -pg @@ -44,7 +44,7 @@ include $(CPP_MODS)/crypto/module.mk TARGET := chipmachine LOCAL_FILES += main.cpp ChipMachine.cpp ChipMachine_config.cpp ChipMachine_keys.cpp MusicDatabase.cpp PlaylistDatabase.cpp SearchIndex.cpp MusicPlayerList.cpp MusicPlayer.cpp TelnetInterface.cpp -LOCAL_FILES += state_machine.cpp +LOCAL_FILES += renderable.cpp renderset.cpp state_machine.cpp LIBS += -lz include $(CPP_MODS)/build.mk diff --git a/README.md b/README.md index f8e127b..6e08173 100644 --- a/README.md +++ b/README.md @@ -29,11 +29,15 @@ Chipmachine binary; * Type words separated by spaces for incremental search * ENTER to play, *SHIFT-ENTER* to enque * *F1* = Player screen, *F2* = Search screen -* *F6* = Next Song, *ESC* = Clear search field, *F8* = Clear playlist +* *F6* = Next Song +* *ESC* = Clear search field +* *F8* = Clear playlist +* *F7* = Toggle Favorite +* *F9* = Save playlist to server ## Configuring the application -* Start with *-f* for fullscreen +* Start with *-w* for window mode or *-d* for debug output * Checkout `lua/screen.lua` for GUI layout ## Raspberry PI notes diff --git a/src/ChipMachine.cpp b/src/ChipMachine.cpp index cd01604..134c737 100644 --- a/src/ChipMachine.cpp +++ b/src/ChipMachine.cpp @@ -103,6 +103,9 @@ ChipMachine::ChipMachine() : currentScreen(0), eq(SpectrumAnalyzer::eq_slots), s renderSet.add(toastField); starEffect.fadeIn(); + File f { ".login" }; + if(f.exists()) + userName = f.read(); } @@ -195,6 +198,9 @@ void ChipMachine::update() { scrollEffect.set("scrolltext", m); } + if(currentDialog && currentDialog->getParent() == nullptr) + currentDialog = nullptr; + update_keys(); auto state = player.getState(); @@ -358,16 +364,16 @@ void ChipMachine::render(uint32_t delta) { } if(currentScreen == MAIN_SCREEN) { - mainScreen.render(screen, delta); + mainScreen.render(delta); if(isFavorite) screen.draw(favTexture, favPos.x, favPos.y, favPos.w, favPos.h, nullptr); } else { - searchScreen.render(screen, delta); + searchScreen.render(delta); songList.render(); } - renderSet.render(screen, delta); + renderSet.render(delta); font.update_cache(); listFont.update_cache(); diff --git a/src/ChipMachine.h b/src/ChipMachine.h index ca2a2c3..1ff1bf8 100644 --- a/src/ChipMachine.h +++ b/src/ChipMachine.h @@ -34,12 +34,16 @@ class LineEdit : public TextField { cursorColor = grappix::Color::WHITE; } - LineEdit(const grappix::Font &font, const std::string &text, float x, float y, float sc, uint32_t col) : TextField(font, text, x, y, sc, col) { + LineEdit(const grappix::Font &font, const std::string &text = "", float x = 0.F, float y = 0.F, float sc = 1.0F, uint32_t col = 0xffffffff) : TextField(font, "", x, y, sc, col) { cursorColor = grappix::Color::WHITE;//grappix::Color(col)/2.0F; - + this->text = prompt + text; //tween::make_tween().sine().repeating().to(cursorColor, grappix::Color::WHITE).seconds(1.7); } + void on_ok(std::function cb) { + onOk = cb; + } + void on_key(uint32_t key) { if(key < 0x100) text = text + (char)key; @@ -66,21 +70,67 @@ class LineEdit : public TextField { prompt = p; } - virtual void render(grappix::RenderTarget &target, uint32_t delta) override { - TextField::render(target, delta); + virtual void render(uint32_t delta) override { + TextField::render(delta); getWidth(); //LOGD("REC %s %d %d", text, tsize.x, tsize.y); - target.rectangle(pos.x + tsize.x + 2, pos.y + 2, 10, tsize.y - 4, cursorColor); + target->rectangle(pos.x + tsize.x + 2, pos.y + 2, 10, tsize.y - 4, cursorColor); } grappix::Color cursorColor; + std::function onOk; + std::string prompt = ">"; }; class Dialog : public Renderable { - virtual void render(grappix::RenderTarget &target, uint32_t delta) override { +public: + Dialog(std::shared_ptr target, const grappix::Font &font, const std::string &text, float scale = 1.0F) : Renderable(target), font(font), textField(font, text), lineEdit(font) { + auto size = font.get_size(text, scale); + bounds.w = size.x + 20; + bounds.h = size.y * 3; + bounds.x = (target->width() - bounds.w) / 2; + bounds.y = (target->height() - bounds.h) / 2; + textField.pos = { bounds.x + 10, bounds.y + 10 }; + lineEdit.pos = { bounds.x + 10, bounds.y + size.y + 20 }; + } + void on_ok(std::function cb) { + //lineEdit.on_ok(cb); + onOk = cb; + } + + void on_key(uint32_t key) { + + if(key == grappix::Window::ENTER) { + if(onOk) + onOk(lineEdit.getText()); + remove(); + } else { + lineEdit.on_key(key); + } } + virtual void setTarget(std::shared_ptr target) { + textField.setTarget(target); + lineEdit.setTarget(target); + this->target = target; + } + + virtual void render(uint32_t delta) override { + target->rectangle(bounds, 0x80ffffff); + textField.render(delta); + lineEdit.render(delta); + } + + std::function onOk; + + grappix::Font font; + std::string text; + + grappix::Rectangle bounds; + TextField textField; + LineEdit lineEdit; + }; class ChipMachine : public grappix::VerticalList::Renderer { @@ -205,6 +255,7 @@ class ChipMachine : public grappix::VerticalList::Renderer { bool onSearchScreen; bool onMainScreen; bool haveSearchChars; + bool dialogOpen; statemachine::StateMachine smac; @@ -212,6 +263,10 @@ class ChipMachine : public grappix::VerticalList::Renderer { std::string editPlaylistName; bool playlistEdit = false; + + std::shared_ptr currentDialog; + + std::string userName; }; } diff --git a/src/ChipMachine_keys.cpp b/src/ChipMachine_keys.cpp index 49a979b..90ed319 100644 --- a/src/ChipMachine_keys.cpp +++ b/src/ChipMachine_keys.cpp @@ -27,6 +27,7 @@ enum ChipAction { SELECT_PLAYLIST, EDIT_PLAYLIST, ADD_COMMAND_CHAR, + ADD_DIALOG_CHAR, CANCEL_COMMAND, LAST_ACTION }; @@ -43,6 +44,9 @@ void ChipMachine::setup_rules() { smac.add(Window::F5, if_equals(currentScreen, MAIN_SCREEN), PLAY_PAUSE); smac.add("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz._0123456789 ", if_true(playlistEdit), ADD_COMMAND_CHAR); + smac.add("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz._0123456789 ", if_true(dialogOpen), ADD_DIALOG_CHAR); + smac.add({Window::ENTER, Window::ESCAPE}, if_true(dialogOpen), ADD_DIALOG_CHAR); + smac.add({ Window::BACKSPACE, Window::LEFT, Window::RIGHT, Window::HOME, Window::END }, if_true(playlistEdit), ADD_COMMAND_CHAR); smac.add("abcdefghijklmnopqrstuvwxyz0123456789 ", ADD_SEARCH_CHAR); smac.add(Window::BACKSPACE, DEL_SEARCH_CHAR); @@ -95,6 +99,7 @@ void ChipMachine::update_keys() { onMainScreen = (currentScreen == MAIN_SCREEN); onSearchScreen = (currentScreen == SEARCH_SCREEN); haveSearchChars = (iquery.getString().length() > 0); + dialogOpen = (currentDialog != nullptr); bool searchUpdated = false; auto last_selection = songList.selected(); @@ -104,10 +109,35 @@ void ChipMachine::update_keys() { // return; if(key == Window::F9) { - auto plist = PlaylistDatabase::getInstance().getPlaylist(currentPlaylistName); - PlayTracker::getInstance().sendList(plist.songs, plist.name); - } + if(userName == "") { + currentDialog = make_shared(screenptr, font, "Login with handle:"); + currentDialog->on_ok([=](const string &text) { + //toast(text, 1); + PlayTracker::getInstance().login(text, [=](int rc) { + LOGD("LOGIN"); + userName = text; + if(rc) + toast("Login successful", 2); + File f { ".login" }; + f.write(userName); + f.close(); + auto plist = PlaylistDatabase::getInstance().getPlaylist(currentPlaylistName); + PlayTracker::getInstance().sendList(plist.songs, plist.name); + }); + }); + renderSet.add(currentDialog); + } else { + auto plist = PlaylistDatabase::getInstance().getPlaylist(currentPlaylistName); + PlayTracker::getInstance().sendList(plist.songs, plist.name); + } + } +/* + if(key == Window::ENTER && dialogOpen) { + renderSet.remove(currentDialog); + currentDialog = nullptr; + } +*/ if(currentScreen == SEARCH_SCREEN) songList.on_key(key); @@ -157,6 +187,9 @@ void ChipMachine::update_keys() { case ADD_COMMAND_CHAR: commandField->on_key(action.event); break; + case ADD_DIALOG_CHAR: + currentDialog->on_key(action.event); + break; case CANCEL_COMMAND: commandField->visible(false); playlistEdit = false; @@ -195,6 +228,7 @@ void ChipMachine::update_keys() { break; case SHOW_SEARCH: show_search(); + searchUpdated = true; break; case ADD_SEARCH_CHAR: if(currentScreen == MAIN_SCREEN && action.event != ' ') diff --git a/src/Dialog.h b/src/Dialog.h new file mode 100644 index 0000000..e69de29 diff --git a/src/LineEdit.h b/src/LineEdit.h new file mode 100644 index 0000000..e69de29 diff --git a/src/MusicDatabase.cpp b/src/MusicDatabase.cpp index c0ba2c0..8cd23f2 100644 --- a/src/MusicDatabase.cpp +++ b/src/MusicDatabase.cpp @@ -282,6 +282,21 @@ int MusicDatabase::search(const string &query, vector &result, unsigned int return result.size(); } +SongInfo MusicDatabase::lookup(const std::string &path) { + auto q = db.query("SELECT title, game, composer, format, collection.url, collection.localdir FROM song, collection WHERE song.path=? AND song.collection = collection.id", path); + + SongInfo song; + if(q.step()) { + string url, localdir; + tie(song.title, song.game, song.composer, song.format, url, localdir) = q.get_tuple(); + song.path = localdir + path; + LOGD("LOCAL FILE: %s", song.path); + if(!File::exists(song.path)) + song.path = url + path; + } + return song; +} + string MusicDatabase::getFullString(int id) const { id++; @@ -403,21 +418,26 @@ static uint8_t formatToByte(const std::string &f) { MusicDatabase::Collection MusicDatabase::stripCollectionPath(string &path) { vector colls; - auto q = db.query("SELECT ROWID,name,url FROM collection"); + auto q = db.query("SELECT ROWID,name,url,localdir FROM collection"); while(q.step()) { colls.push_back(q.get()); } for(const auto &c : colls) { - if(startsWith(path, c.url)) { + LOGD("COMPARE %s to %s/%s", path, c.url, c.local_dir); + if(c.url != "" && startsWith(path, c.url)) { path = path.substr(c.url.length()); return c; } + if(c.local_dir != "" && startsWith(path, c.local_dir)) { + path = path.substr(c.local_dir.length()); + return c; + } } return Collection(); } MusicDatabase::Collection MusicDatabase::getCollection(int id) { - auto q = db.query("SELECT ROWID,name,url FROM collection WHERE ROWID=?", id); + auto q = db.query("SELECT ROWID,name,url,localdir FROM collection WHERE ROWID=?", id); LOGD("ID %d", id); if(q.step()) { LOGD("####### FOUND"); diff --git a/src/MusicDatabase.h b/src/MusicDatabase.h index 06c4773..cbeb5d0 100644 --- a/src/MusicDatabase.h +++ b/src/MusicDatabase.h @@ -64,14 +64,17 @@ class MusicDatabase : public SearchProvider { } struct Collection { - Collection(int id = -1, const std::string &name = "", const std::string url = "") : id(id), name(name), url(url) {} + Collection(int id = -1, const std::string &name = "", const std::string url = "", const std::string local_dir = "") : id(id), name(name), url(url), local_dir(local_dir) {} int id; std::string name; std::string url; + std::string local_dir; }; Collection stripCollectionPath(std::string &path); + SongInfo lookup(const std::string &path); + Collection getCollection(int id); Collection getCollection(const std::string &name); diff --git a/src/PlayTracker.h b/src/PlayTracker.h index 94dc532..235ecd1 100644 --- a/src/PlayTracker.h +++ b/src/PlayTracker.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -13,7 +14,7 @@ namespace chipmachine { //wired-height-596.appspot.com class PlayTracker { public: - PlayTracker() : rpc("http://localhost:8080/"), done(true) { + PlayTracker() : rpc("http://wired-height-596.appspot.com/"), done(true) { utils::File f { ".trackid" }; if(!f.exists()) { std::random_device rd; @@ -72,7 +73,8 @@ class PlayTracker { rpc.call("get_lists", [=](const std::string &result) { std::vector lists; LOGD("GET LISTS:%s", result); - for(auto pl : JSon::parse(result)) { + JSon json = JSon::parse(result); + for(auto pl : json("lists")) { std::string user = pl("user"); std::string name = pl("name"); lists.emplace_back((std::string)pl("name") + ":" + user); @@ -81,6 +83,16 @@ class PlayTracker { }); } + void login(const std::string &name, std::function f) { + JSon json; + json.add("uid", std::to_string(trackid)); + json.add("name", name); + rpc.post("login", json.to_string(), [=](const std::string &result) { + LOGD("LOGIN: " + result); + f(0); + }); + } + void getList(const std::string &name, std::function&)> f) { auto parts = utils::split(name, ":"); diff --git a/src/PlaylistDatabase.cpp b/src/PlaylistDatabase.cpp index bb9416b..0d0d69e 100644 --- a/src/PlaylistDatabase.cpp +++ b/src/PlaylistDatabase.cpp @@ -47,9 +47,15 @@ PlaylistDatabase::PlaylistDatabase() : db("play.db") { while(q.step()) { SongInfo song; int cid; - tie(song.path, song.game, song.title, song.composer, song.format, cid) = q.get_tuple(); + string path; + tie(path, song.game, song.title, song.composer, song.format, cid) = q.get_tuple(); auto collection = MusicDatabase::getInstance().getCollection(cid); - song.path = collection.url + song.path; + + song.path = collection.local_dir + path; + LOGD("LOCAL PATH: %s", song.path); + if(!File::exists(song.path)) + song.path = collection.url + path; + pl.songs.push_back(song); } id++; @@ -127,8 +133,10 @@ Playlist PlaylistDatabase::getPlaylist(const std::string &name) { if(!pl) return; for(const auto &l : list) { auto parts = split(l, ":"); - auto collection = MusicDatabase::getInstance().getCollection(parts[1]); - pl->songs.emplace_back(collection.url + parts[0]); + SongInfo song = MusicDatabase::getInstance().lookup(parts[0]); + pl->songs.push_back(song); + //auto collection = MusicDatabase::getInstance().getCollection(parts[1]); + //pl->songs.emplace_back(collection.url + parts[0]); } cb(*pl); }); diff --git a/src/TextField.h b/src/TextField.h index ccff5a6..616fb4d 100644 --- a/src/TextField.h +++ b/src/TextField.h @@ -11,10 +11,10 @@ class TextField : public Renderable { public: - TextField() : pos(0, 0), scale(1.0), color(0xffffffff), add(0), f {&pos.x, &pos.y, &scale, &color.r, &color.g, &color.b, &color.a, &add}, text(""), tsize(-1, -1) { + TextField() : Renderable(grappix::screenptr), pos(0, 0), scale(1.0), color(0xffffffff), add(0), f {&pos.x, &pos.y, &scale, &color.r, &color.g, &color.b, &color.a, &add}, text(""), tsize(-1, -1) { } - TextField(const grappix::Font &font, const std::string &text = "", float x = 0.0, float y = 0.0, float sc = 1.0, uint32_t col = 0xffffffff) : pos(x, y), scale(sc), color(col), add(0), f {&pos.x, &pos.y, &scale, &color.r, &color.g, &color.b, &color.a, &add}, text(text), tsize(-1, -1), font(font) { + TextField(const grappix::Font &font, const std::string &text = "", float x = 0.0, float y = 0.0, float sc = 1.0, uint32_t col = 0xffffffff) : Renderable(grappix::screenptr), pos(x, y), scale(sc), color(col), add(0), f {&pos.x, &pos.y, &scale, &color.r, &color.g, &color.b, &color.a, &add}, text(text), tsize(-1, -1), font(font) { } utils::vec2f pos; @@ -44,7 +44,7 @@ class TextField : public Renderable { font = f; } - virtual void render(grappix::RenderTarget &target, uint32_t delta) override { + virtual void render(uint32_t delta) override { if(color.a == 0.0) return; auto x = pos.x; @@ -53,7 +53,7 @@ class TextField : public Renderable { tsize = font.get_size(text, scale); //if(x < 0) x = grappix::screen.width() - tlen + x; //if(y < 0) y = grappix::screen.height() + y; - grappix::screen.text(font, text, x, y, color + add, scale); + target->text(font, text, x, y, color + add, scale); } protected: diff --git a/src/renderable.cpp b/src/renderable.cpp new file mode 100644 index 0000000..4563d7a --- /dev/null +++ b/src/renderable.cpp @@ -0,0 +1,9 @@ +#include "renderable.h" +#include "renderset.h" + +void Renderable::remove() { + if(parent) + parent->remove(this); + parent = nullptr; +// do_remove = true; +} diff --git a/src/renderable.h b/src/renderable.h index f38b4cc..d76b9e1 100644 --- a/src/renderable.h +++ b/src/renderable.h @@ -3,15 +3,34 @@ #include //class grappix::RenderTarget; - +class RenderSet; class Renderable { public: - virtual void render(grappix::RenderTarget &target, uint32_t delta) = 0; + Renderable(std::shared_ptr target) : target(target) {} + + virtual void render(uint32_t delta) = 0; virtual void visible(bool b) { is_visible = b; } virtual bool visible() { return is_visible; } -protected: + virtual void setTarget(std::shared_ptr target) { + this->target = target; + } + + void setParent(RenderSet *p) { + parent = p; + } + + RenderSet *getParent() { + return parent; + } + + void remove(); + +//protected: bool is_visible = true; + //bool do_remove = false; + RenderSet *parent; + std::shared_ptr target; }; diff --git a/src/renderset.cpp b/src/renderset.cpp new file mode 100644 index 0000000..88c6b9d --- /dev/null +++ b/src/renderset.cpp @@ -0,0 +1,9 @@ +#include "renderset.h" + +using namespace std; + +void RenderSet::add(shared_ptr r) { + r->setTarget(target); + fields.push_back(r); + r->setParent(this); +} diff --git a/src/renderset.h b/src/renderset.h index 809fe8e..c66ff54 100644 --- a/src/renderset.h +++ b/src/renderset.h @@ -10,27 +10,41 @@ class RenderSet { public: - void add(std::shared_ptr r) { - fields.push_back(r); - } + RenderSet(std::shared_ptr target = grappix::screenptr) : target(target) {} + + void add(std::shared_ptr r); void remove(std::shared_ptr r) { auto it = fields.begin(); while(it != fields.end()) { - if(r.get() == it->get()) + if(r.get() == it->get()) { + (*it)->setParent(nullptr); + it = fields.erase(it); + } else + it++; + } + } + + void remove(Renderable *r) { + auto it = fields.begin(); + while(it != fields.end()) { + if(r == it->get()) { + (*it)->setParent(nullptr); it = fields.erase(it); - else + } else it++; } } - void render(grappix::RenderTarget &target, uint32_t delta) { - for(auto &r : fields) + void render(uint32_t delta) { + for(auto &r : fields) { if(r->visible()) - r->render(target, delta); + r->render(delta); + } } std::vector> fields; + std::shared_ptr target; };