diff --git a/CMakeLists.txt b/CMakeLists.txt index abfaac1..519ba36 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,7 +28,9 @@ find_package(SDL2 REQUIRED) add_executable(${PROJECT_NAME} src/main.cpp src/lib/debug/debugScreen.c - src/lib/subsonic.hpp + src/subsonic.hpp + src/settings.hpp + src/lib/dr_mp3.h src/lib/dr_flac.h src/lib/audio.cpp @@ -42,6 +44,7 @@ target_link_libraries(${PROJECT_NAME} SceDisplay_stub SceAudio_stub SceTouch_stub + SceIme_stub ${CURL_LIBRARIES} ${OPENSSL_LIBRARIES} c diff --git a/emulator.sh b/emulator.sh new file mode 100755 index 0000000..888b969 --- /dev/null +++ b/emulator.sh @@ -0,0 +1 @@ +/home/deck/Downloads/ubuntu-latest/Vita3K make/subsonic.vpk diff --git a/src/main.cpp b/src/main.cpp index 895c49e..a4fdb28 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,8 +1,11 @@ #include #include #include +#include #include +#include #include +#include #include #include #include @@ -11,9 +14,14 @@ #include -#include "lib/subsonic.hpp" +#include "subsonic.hpp" #include "lib/audio.hpp" - +#include "settings.hpp" +#include +#include // for fabsf if you want +#include +#include +#include #define SCREEN_W 960 #define SCREEN_H 544 @@ -44,6 +52,14 @@ struct AudioQueueEntry { std::string id; bool isFlac; }; +struct InputBox { + int x, y, w, h; + std::string text; + bool focused = false; + int caretPos = 0; // caret index + Uint32 lastBlink = 0; // for caret blinking + bool caretVisible = true; +}; std::queue gArtRequestQ; std::queue gArtResultQ; @@ -68,18 +84,16 @@ int lastY = 0; float velocity = 0.0f; // current scrolling speed float friction = 0.92f; // deceleration factor (lower = more friction) bool inertial = false; // true after releasing a drag -std::string selectedAlbumId = ""; +bool settingsOpened = false; + std::vector gPlayQueue; // holds Subsonic track IDs int gQueueIndex = -1; // index in queue; -1 = not playing + const int LEFT_X = 0; const int LEFT_Y = 60; // below the tabs const int LEFT_W = 350; const int LEFT_H = SCREEN_H - LEFT_Y; -#include -#include // for fabsf if you want - -// Simple cache: coverArt ID -> SDL_Texture* std::map gArtCache; void log(SDL_Window* win, const char* fmt, ...) { @@ -253,6 +267,204 @@ void renderLeftList( SDL_Rect dst = {LEFT_X, LEFT_Y, LEFT_W, LEFT_H}; SDL_RenderCopy(rd, listTex, NULL, &dst); } + +void drawInputBox(SDL_Renderer* rd, TTF_Font* font, InputBox& b) +{ + // background + if (b.focused) + SDL_SetRenderDrawColor(rd, 80, 80, 120, 255); // focused color + else + SDL_SetRenderDrawColor(rd, 60, 60, 90, 255); + + SDL_Rect r = { b.x, b.y, b.w, b.h }; + SDL_RenderFillRect(rd, &r); + + // text texture + SDL_Texture* tt = nullptr; + int tw = 0, th = 0; + if (!b.text.empty()) { + tt = text(rd, font, b.text.c_str(), {255,255,255}); + SDL_QueryTexture(tt, NULL, NULL, &tw, &th); + } + + int textX = r.x + 6; // left padding + int textY = r.y + (r.h - th) / 2; + + if (tt) { + SDL_Rect h = SDL_Rect{ textX, textY, tw, th }; + SDL_RenderCopy(rd, tt, NULL, &h); + } + if (tt) + SDL_DestroyTexture(tt); + + // caret blinking + Uint32 now = SDL_GetTicks(); + if (now - b.lastBlink > 500) { + b.caretVisible = !b.caretVisible; + b.lastBlink = now; + } + + // caret drawing + if (b.focused && b.caretVisible) { + // measure text up to caret + std::string left = b.text.substr(0, b.caretPos); + int caretX = textX; + + if (!left.empty()) { + TTF_SizeUTF8(font, left.c_str(), &caretX, NULL); + caretX += textX; + } + + SDL_SetRenderDrawColor(rd, 255, 255, 255, 255); + SDL_RenderDrawLine(rd, caretX, r.y + 5, caretX, r.y + r.h - 5); + } +} +void utf8_to_utf16(SceWChar16* dst, const char* src, size_t max) +{ + size_t di = 0; + for (size_t si = 0; src[si] && di + 1 < max; ) + { + unsigned char c = src[si++]; + + // 1-byte ASCII fast path + if (c < 0x80) { + dst[di++] = c; + continue; + } + + // 2-byte UTF-8 → UTF-16 + if ((c & 0xE0) == 0xC0) { + unsigned char c2 = src[si++]; + dst[di++] = ((c & 0x1F) << 6) | (c2 & 0x3F); + continue; + } + + // 3-byte UTF-8 → UTF-16 + if ((c & 0xF0) == 0xE0) { + unsigned char c2 = src[si++]; + unsigned char c3 = src[si++]; + dst[di++] = ((c & 0x0F) << 12) | + ((c2 & 0x3F) << 6) | + (c3 & 0x3F); + continue; + } + + // Unsupported (skip) + } + + dst[di] = 0; +} +void utf16_to_utf8(const SceWChar16* src, uint8_t* dst) +{ + size_t di = 0; + + for (size_t si = 0; src[si]; si++) + { + uint16_t v = src[si]; + + if (v < 0x80) { + dst[di++] = (uint8_t)v; + } + else if (v < 0x800) { + dst[di++] = 0xC0 | (v >> 6); + dst[di++] = 0x80 | (v & 0x3F); + } + else { + dst[di++] = 0xE0 | (v >> 12); + dst[di++] = 0x80 | ((v >> 6) & 0x3F); + dst[di++] = 0x80 | (v & 0x3F); + } + } + + dst[di] = 0; +} + +// Global input buffer (UTF-16, where IME writes text) +static SceWChar16 gImeBuf[SCE_IME_DIALOG_MAX_TEXT_LENGTH + 1]; + +// Open IME dialog (set title + current text) +static void ShowIME(const char* titleUtf8, const char* initUtf8) +{ + SceImeDialogParam p; + sceImeDialogParamInit(&p); + + static SceWChar16 title[64]; + static SceWChar16 init[256]; + + utf8_to_utf16(title, titleUtf8, 63); + utf8_to_utf16(init, initUtf8, 255); + + p.supportedLanguages = 0; + p.languagesForced = SCE_FALSE; + p.type = SCE_IME_TYPE_DEFAULT; + p.option = 0; + p.textBoxMode = SCE_IME_DIALOG_TEXTBOX_MODE_WITH_CLEAR; + p.maxTextLength = SCE_IME_DIALOG_MAX_TEXT_LENGTH; + p.title = title; + p.initialText = init; + p.inputTextBuffer = gImeBuf; + + sceImeDialogInit(&p); +} + +// Poll IME; returns true when finished and fills utf8Out +static bool PollIME(char* utf8Out, int maxlen) +{ + if (sceImeDialogGetStatus() != SCE_COMMON_DIALOG_STATUS_FINISHED) + return false; + + SceImeDialogResult r; + SDL_memset(&r, 0, sizeof(r)); + sceImeDialogGetResult(&r); + + uint8_t tmp[512]; + utf16_to_utf8(gImeBuf, tmp); // <-- text is in gImeBuf, not in SceImeDialogResult + + SDL_strlcpy(utf8Out, (char*)tmp, maxlen); + + // if you care about which button: + // if (r.button == SCE_IME_DIALOG_BUTTON_ENTER) { ... } + + sceImeDialogTerm(); + return true; +} + +void handleInputBoxEvent(InputBox& box, const SDL_Event& e) +{ + // Click = focus + open IME keyboard + if (e.type == SDL_FINGERDOWN) { + int mx = e.tfinger.x * SCREEN_W; + int my = e.tfinger.y * SCREEN_H; + + bool inside = (mx >= box.x && mx <= box.x + box.w && + my >= box.y && my <= box.y + box.h); + + if (inside) { + box.focused = true; + ShowIME("testing.. testing..", box.text.c_str()); + char* result; + while(true) { + if(PollIME(result, 256)) { + // Replace the entire input content + box.text = result; + + box.caretPos = box.text.size(); + box.lastBlink = SDL_GetTicks(); + box.caretVisible = true; + + box.focused = false; + break; + } + } + } else { + box.focused = false; + } + } + + if (!box.focused) + return; +} + void renderRightPanel( SDL_Renderer* rd, TTF_Font* fontBig, TTF_Font* fontMed, @@ -309,14 +521,6 @@ void drawButton(SDL_Renderer* rd, TTF_Font* font, Button& b) { SDL_DestroyTexture(tt); } -void renderControls(SDL_Renderer* rd, TTF_Font* fontSmall, - Button& b1, Button& b2, Button& b3) -{ - drawButton(rd, fontSmall, b1); - drawButton(rd, fontSmall, b2); - drawButton(rd, fontSmall, b3); -} - void renderTabs(SDL_Renderer* rd, TTF_Font* fontMed, Tab tabs[3]) { for (int i=0;i<3;i++){ SDL_Rect r = {tabs[i].x,tabs[i].y,tabs[i].w,tabs[i].h}; @@ -381,32 +585,31 @@ void loadTracks( if (maxScroll < 0) maxScroll = 0; } - -void loadAlbums( - std::vector& items, - const SubsonicClient& ss) -{ +void loadArtists( + std::vector& items, + const SubsonicClient& ss +) { std::string err; - auto allAlbums = ss.getAllAlbums(&err); + auto allArtists = ss.getArtists(&err); if (!err.empty()) - SDL_Log("getAlbum error: %s", err.c_str()); + SDL_Log("getArtists error: %s", err.c_str()); items.clear(); - items.reserve(allAlbums.size()); + items.reserve(allArtists.size()); const int artSize = 80; const int rowH = 100; const int baseY = 70; - for (int k = 0; k < allAlbums.size(); k++) { - const auto& a = allAlbums[k]; + for (int k = 0; k < allArtists.size(); k++) { + const auto& a = allArtists[k]; Item it; int y = baseY + k * rowH; it.art = {20, y, artSize, artSize}; it.title = a.name; - it.artist = a.artist; + it.artist = ""; it.coverId = a.coverArt; it.id = a.id; it.isTrack = false; @@ -421,19 +624,70 @@ void loadAlbums( maxScroll = items.size() * rowH - LEFT_H; if (maxScroll < 0) maxScroll = 0; } +void loadAlbums( + std::vector& items, + const SubsonicClient& ss, + std::string artist = "" +) { + std::string err; + auto allAlbums = ss.getAllAlbums(&err); + if (!err.empty()) + SDL_Log("getAlbum error: %s", err.c_str()); + + items.clear(); + + const int artSize = 80; + const int rowH = 100; + const int baseY = 70; + + int k2 = 0; + + for (int k = 0; k < allAlbums.size(); k++) { + const auto& a = allAlbums[k]; + + if(!artist.empty()) { + if(a.artistId != artist) { + continue; + } + } + + Item it; + int y = baseY + k2 * rowH; + + it.art = {20, y, artSize, artSize}; + it.title = a.name; + it.artist = a.artist; + it.coverId = a.coverArt; + it.id = a.id; + it.isTrack = false; + + it.titleRect = {20 + artSize + 15, y + 10, 200, 20}; + it.artistRect = {20 + artSize + 15, y + 35, 200, 20}; + + items.push_back(it); + + k2=k2+1; + } + + scrollOffset = 0; + maxScroll = items.size() * rowH - LEFT_H; + if (maxScroll < 0) maxScroll = 0; +} int main() { + Settings settings; + settings.load(); + SubsonicClient ss( - "http://192.168.8.102:4747", - "admin", - "admin", + settings.server, + settings.user, + settings.pass, "vita-player", "1.16.1" ); - SDL_setenv("VITA_DISABLE_TOUCH_BACK", "1", 1); Audio_Init(); @@ -491,9 +745,31 @@ int main() int controlsCenter = sliderBar.x + sliderBar.w/2; - Button btnBack = { controlsCenter - 150, 340, 80, 50, "back" }; - Button btnPlay = { controlsCenter - 40, 340, 80, 50, "play" }; - Button btnNext = { controlsCenter + 70, 340, 80, 50, "next" }; + Button btnBack = { controlsCenter - 150, 340, 80, 50, "Back" }; + Button btnPlay = { controlsCenter - 40, 340, 80, 50, "Play" }; + Button btnNext = { controlsCenter + 70, 340, 80, 50, "Next" }; + Button btnSettings = { SCREEN_W-80, 10, 70, 50, "Settings" }; + + + Button btnSettingsBack = { 10, 10, 80, 50, "Back" }; + + InputBox serverUrl { + 10,10+50+10,350,30, + settings.server + }; + + InputBox password { + 10,10+50+10+30+10,350,30, + settings.user + }; + InputBox username { + 10,10+50+10+30+10+30+10,350,30, + settings.pass + }; + + Button btnSettingsSave = { + 10, 10+50+10+30+10+30+10+30+20,80,50,"Save" + }; bool running=true; @@ -504,6 +780,11 @@ int main() bool touch=false; while(SDL_PollEvent(&e)){ + if(settingsOpened) { + handleInputBoxEvent(serverUrl, e); + handleInputBoxEvent(password, e); + handleInputBoxEvent(username, e); + } if(e.type==SDL_QUIT) running=false; if (e.type == SDL_FINGERMOTION && dragging) { int newY = e.tfinger.y * SCREEN_H; @@ -542,102 +823,158 @@ int main() if (touch) { - // Tabs - for (int i=0;i<3;i++){ - SDL_Rect r = {tabs[i].x,tabs[i].y,tabs[i].w,tabs[i].h}; - if (hit(tx,ty,r)){ - for(int j=0;j<3;j++) tabs[j].active=false; - tabs[i].active=true; - if (strcmp(tabs[i].label, "albums") == 0) { - loadAlbums(items, ss); - } + if(settingsOpened) { + SDL_Rect rB = {btnSettingsBack.x,btnSettingsBack.y,btnSettingsBack.w,btnSettingsBack.h}; + SDL_Rect rS = {btnSettingsSave.x,btnSettingsSave.y,btnSettingsSave.w,btnSettingsSave.h}; + if(hit(tx, ty,rB)) { + settingsOpened = false; } - } - for (int i = 0; i < items.size(); i++) { - SDL_Rect r = items[i].art; - r.y -= scrollOffset; - if (hit(tx, ty, {LEFT_X + r.x, r.y, r.w, r.h})) { - if (tabs[0].active) { - selectedAlbumId = items[i].id; - loadTracks(items, ss, selectedAlbumId, win); + if(hit(tx,ty,rS)) { + settings.pass = password.text; + settings.server = serverUrl.text; + settings.user = username.text; - // switch to Tracks tab - for (int j = 0; j < 3; j++) tabs[j].active = false; - tabs[2].active = true; - } else if(tabs[2].active) { - gPlayQueue.clear(); - for (int t = i; t < items.size(); t++) { - if (items[t].isTrack) - gPlayQueue.push_back( AudioQueueEntry{ - items[t].id, - items[t].contentType=="audio/flac"||items[t].contentType=="audio/x-flac", - }); - } - - gQueueIndex = 0; // start at selected track - sel = items[i]; - auto q = gPlayQueue[gQueueIndex]; - if(q.isFlac) { - Audio_Play(ss.streamUrl(q.id).c_str()); - } else { - Audio_PlayMP3(ss.streamUrl(q.id).c_str()); - } - btnPlay.label = "Pause"; - } - break; + settings.save(); } - } + } else { + // Tabs - // Slider - if (hit(tx,ty,sliderBar)){ - float rel = (tx - sliderBar.x) / (float)sliderBar.w; - if (rel<0) rel=0; - if (rel>1) rel=1; - sliderVal = rel; - } + // THE LAST TAB IS ONLY NAVIGATED TO IN CODE!! + for (int i=0;i<2;i++){ + SDL_Rect r = {tabs[i].x,tabs[i].y,tabs[i].w,tabs[i].h}; + if (hit(tx,ty,r)){ + for(int j=0;j<3;j++) tabs[j].active=false; + tabs[i].active=true; + if (strcmp(tabs[i].label, "albums") == 0) { + loadAlbums(items, ss); + } + if (strcmp(tabs[i].label, "artists") == 0) { + loadArtists(items, ss); + } + } + } - // Controls - SDL_Rect rB = {btnBack.x,btnBack.y,btnBack.w,btnBack.h}; - SDL_Rect rP = {btnPlay.x,btnPlay.y,btnPlay.w,btnPlay.h}; - SDL_Rect rN = {btnNext.x,btnNext.y,btnNext.w,btnNext.h}; + for (int i = 0; i < items.size(); i++) { + SDL_Rect r = items[i].art; + r.y -= scrollOffset; + if (hit(tx, ty, {LEFT_X + r.x, r.y, r.w, r.h})) { + if (tabs[0].active) { + loadTracks(items, ss, items[i].id, win); - if (hit(tx,ty,rB)) { - // Previous track in queue - if (gQueueIndex > 0) { - gQueueIndex--; - auto prevId = gPlayQueue[gQueueIndex]; + // switch to Tracks tab + for (int j = 0; j < 3; j++) tabs[j].active = false; + tabs[2].active = true; + } else if(tabs[1].active) { + loadAlbums(items, ss, items[i].id); - // Update UI selection - for (auto &it : items) - if (it.id == prevId.id) - sel = it; + for (int j = 0; j < 3; j++) tabs[j].active = false; + tabs[0].active = true; + } else if(tabs[2].active) { + gPlayQueue.clear(); + for (int t = i; t < items.size(); t++) { + if (items[t].isTrack) + gPlayQueue.push_back( AudioQueueEntry{ + items[t].id, + items[t].contentType=="audio/flac"||items[t].contentType=="audio/x-flac", + }); + } + + gQueueIndex = 0; // start at selected track + sel = items[i]; + auto q = gPlayQueue[gQueueIndex]; + if(q.isFlac) { + Audio_Play(ss.streamUrl(q.id).c_str()); + } else { + Audio_PlayMP3(ss.streamUrl(q.id).c_str()); + } + btnPlay.label = "Pause"; + } + break; + } + } + + // Slider + if (hit(tx,ty,sliderBar)){ + float rel = (tx - sliderBar.x) / (float)sliderBar.w; + if (rel<0) rel=0; + if (rel>1) rel=1; + sliderVal = rel; + } + + // Controls + SDL_Rect rB = {btnBack.x,btnBack.y,btnBack.w,btnBack.h}; + SDL_Rect rP = {btnPlay.x,btnPlay.y,btnPlay.w,btnPlay.h}; + SDL_Rect rN = {btnNext.x,btnNext.y,btnNext.w,btnNext.h}; + SDL_Rect rS = {btnSettings.x,btnSettings.y,btnSettings.w,btnSettings.h}; + if(hit(tx, ty,rS)) { + settingsOpened = true; + } + + if (hit(tx,ty,rB)) { + // Previous track in queue + if (gQueueIndex > 0) { + gQueueIndex--; + auto prevId = gPlayQueue[gQueueIndex]; + + // Update UI selection + for (auto &it : items) + if (it.id == prevId.id) + sel = it; if(prevId.isFlac) { - Audio_Play(ss.streamUrl(prevId.id).c_str()); - } else { - Audio_PlayMP3(ss.streamUrl(prevId.id).c_str()); - } - btnPlay.label = "Pause"; - } - } - if (hit(tx,ty,rP)) { - if(!sel.id.empty()) { - if (strcmp(btnPlay.label, "Pause") == 0) { - Audio_Pause(); - btnPlay.label = "Play"; - } else if(Audio_IsPaused()) { + Audio_Play(ss.streamUrl(prevId.id).c_str()); + } else { + Audio_PlayMP3(ss.streamUrl(prevId.id).c_str()); + } + btnPlay.label = "Pause"; + } + } + if (hit(tx,ty,rP)) { + if(!sel.id.empty()) { + if (strcmp(btnPlay.label, "Pause") == 0) { + Audio_Pause(); + btnPlay.label = "Play"; + } else if(Audio_IsPaused()) { + btnPlay.label = "Pause"; + Audio_Resume(); + } + } + } + if (hit(tx,ty,rN)) { + if (gQueueIndex >= 0 && gQueueIndex + 1 < gPlayQueue.size()) { + gQueueIndex++; + auto nextId = gPlayQueue[gQueueIndex]; + + for (auto &it : items) + if (it.id == nextId.id) + sel = it; + + if(nextId.isFlac) { + Audio_Play(ss.streamUrl(nextId.id).c_str()); + } else { + Audio_PlayMP3(ss.streamUrl(nextId.id).c_str()); + } btnPlay.label = "Pause"; - Audio_Resume(); } } } - if (hit(tx,ty,rN)) { + } + + if(!settingsOpened) { + float prog = Audio_GetProgress(); + + if (Audio_IsPlaying()) { + sliderVal = prog; + } else { + // Track finished → advance queue if (gQueueIndex >= 0 && gQueueIndex + 1 < gPlayQueue.size()) { + gQueueIndex++; auto nextId = gPlayQueue[gQueueIndex]; + // update UI selection for (auto &it : items) if (it.id == nextId.id) sel = it; @@ -648,87 +985,74 @@ int main() Audio_PlayMP3(ss.streamUrl(nextId.id).c_str()); } btnPlay.label = "Pause"; - } - } - } - - float prog = Audio_GetProgress(); - - if (Audio_IsPlaying()) { - sliderVal = prog; - } else { - // Track finished → advance queue - if (gQueueIndex >= 0 && gQueueIndex + 1 < gPlayQueue.size()) { - - gQueueIndex++; - auto nextId = gPlayQueue[gQueueIndex]; - - // update UI selection - for (auto &it : items) - if (it.id == nextId.id) - sel = it; - - if(nextId.isFlac) { - Audio_Play(ss.streamUrl(nextId.id).c_str()); } else { - Audio_PlayMP3(ss.streamUrl(nextId.id).c_str()); + // queue finished + btnPlay.label = "Play"; + gQueueIndex = -1; + sel = {}; } - btnPlay.label = "Pause"; - } else { - // queue finished - btnPlay.label = "Play"; - gQueueIndex = -1; - sel = {}; } } - // ------------------------- // DRAW UI // ------------------------- SDL_SetRenderDrawColor(rd,25,25,35,255); SDL_RenderClear(rd); + processPendingArt(rd); - renderTabs(rd, fontMed, tabs); + if(settingsOpened) { + drawButton(rd, fontSmall, btnSettingsBack); + drawInputBox(rd, fontSmall, serverUrl); + drawInputBox(rd, fontSmall, username); + drawInputBox(rd, fontSmall, password); - // INERTIA SCROLLING UPDATE - if (inertial && !dragging) { + drawButton(rd, fontSmall, btnSettingsSave); + } else { + renderTabs(rd, fontMed, tabs); - // apply inertia - scrollOffset += (int)velocity; + renderLeftList(rd, listTex, albumTex, fontSmall, + items, scrollOffset, ss); - // decay the velocity - velocity *= friction; - - // stop when very slow - if (fabs(velocity) < 0.3f) - inertial = false; - - // clamp - if (scrollOffset < 0) { - scrollOffset = 0; - inertial = false; - velocity = 0; + if (!sel.id.empty()) { + renderRightPanel(rd, fontBig, fontMed, + selArt,albumTex, ss); } - if (scrollOffset > maxScroll) { - scrollOffset = maxScroll; - inertial = false; - velocity = 0; + // Slider + renderSlider(rd, sliderBar, sliderVal); + + // Controls + drawButton(rd, fontSmall, btnBack); + drawButton(rd, fontSmall, btnPlay); + drawButton(rd, fontSmall, btnNext); + drawButton(rd, fontSmall, btnSettings); + + // INERTIA SCROLLING UPDATE + if (inertial && !dragging) { + + // apply inertia + scrollOffset += (int)velocity; + + // decay the velocity + velocity *= friction; + + // stop when very slow + if (fabs(velocity) < 0.3f) + inertial = false; + + // clamp + if (scrollOffset < 0) { + scrollOffset = 0; + inertial = false; + velocity = 0; + } + if (scrollOffset > maxScroll) { + scrollOffset = maxScroll; + inertial = false; + velocity = 0; + } } } - renderLeftList(rd, listTex, albumTex, fontSmall, - items, scrollOffset, ss); - - if (!sel.id.empty()) { - renderRightPanel(rd, fontBig, fontMed, - selArt,albumTex, ss); - } - // Slider - renderSlider(rd, sliderBar, sliderVal); - - // Controls - renderControls(rd, fontSmall, btnBack, btnPlay, btnNext); - SDL_RenderPresent(rd); SDL_Delay(16); } diff --git a/src/settings.hpp b/src/settings.hpp new file mode 100644 index 0000000..9a8017d --- /dev/null +++ b/src/settings.hpp @@ -0,0 +1,76 @@ +#pragma once +#include +#include +#include +#include +#include + +class Settings { +public: + // ---- Stored values ---- + int volume = 80; + std::string server = "http://192.168.8.102:4747"; + std::string user = "admin"; + std::string pass = "admin"; + + // ---- File paths ---- + static constexpr const char* PATH_DIR = "ux0:/data/subsonic_vita"; + static constexpr const char* PATH_FILE = "ux0:/data/subsonic_vita/settings.json"; + + Settings() {} + + // ================================================================ + // Save to JSON file + // ================================================================ + void save() const + { + sceIoMkdir(PATH_DIR, 0777); + + Json::Value v; + v["volume"] = volume; + v["server"] = server; + v["user"] = user; + v["pass"] = pass; + + Json::StreamWriterBuilder b; + std::string out = Json::writeString(b, v); + + SceUID fd = sceIoOpen(PATH_FILE, + SCE_O_WRONLY | SCE_O_CREAT | SCE_O_TRUNC, + 0666); + if (fd >= 0) { + sceIoWrite(fd, out.c_str(), out.size()); + sceIoClose(fd); + } + } + + // ================================================================ + // Load from JSON file + // ================================================================ + void load() + { + SceUID fd = sceIoOpen(PATH_FILE, SCE_O_RDONLY, 0); + if (fd < 0) + return; // no file → defaults stay + + char buf[4096]; + int n = sceIoRead(fd, buf, sizeof(buf) - 1); + sceIoClose(fd); + if (n <= 0) + return; + buf[n] = 0; + + Json::Value root; + Json::CharReaderBuilder b; + std::string errs; + + std::istringstream ss(buf); + if (!Json::parseFromStream(b, ss, &root, &errs)) + return; + + if (root.isMember("volume")) volume = root["volume"].asInt(); + if (root.isMember("server")) server = root["server"].asString(); + if (root.isMember("user")) user = root["user"].asString(); + if (root.isMember("pass")) pass = root["pass"].asString(); + } +}; diff --git a/src/lib/subsonic.hpp b/src/subsonic.hpp similarity index 100% rename from src/lib/subsonic.hpp rename to src/subsonic.hpp