//
// MusicApp.c
//
//
// Created by Chris Galzerano on 2/26/26.
//
#include "main.h"
#include "MusicApp.h"
/*
// --- Music Player Globals ---
extern CCLabel* uiMusicTitleLbl;
extern CCLabel* uiMusicArtistLbl;
extern CCView* uiMusicProgressFill;
extern CCLabel* uiMusicPlayBtnLbl;
// Tag Constants
#define TAG_MUSIC_PREV 20
#define TAG_MUSIC_PLAY 21
#define TAG_MUSIC_NEXT 22
#define TAG_MUSIC_PROGRESS_BAR 23
// Layout Constants
#define SCREEN_W 320
#define MAIN_PADDING 20
#define ART_SIZE 280
// --- Music Player Globals ---
CCLabel* uiMusicTitleLbl = NULL;
CCLabel* uiMusicArtistLbl = NULL;
CCView* uiMusicProgressFill = NULL;
CCLabel* uiMusicPlayBtnLbl = NULL;
void setup_music_player_ui(void) {
// 1. Reset Root View
currentView = CurrentViewMusic;
//if (mainWindowView) freeViewHierarchy(mainWindowView);
mainWindowView = viewWithFrame(ccRect(0, 0, SCREEN_W, 480));
mainWindowView->backgroundColor = color(0.08, 0.08, 0.12, 1.0);
// --- TIGHTER LAYOUT MATH ---
int topPadding = 30;
int artSize = 220; // Reduced from 280
int currentY = topPadding;
//================================================================
// 2. Album Artwork
//================================================================
// Centered horizontally: (320 - 220) / 2 = 50
int artX = (SCREEN_W - artSize) / 2;
CCView* artView = viewWithFrame(ccRect(artX, currentY, artSize, artSize));
artView->backgroundColor = color(0.2, 0.25, 0.35, 1.0);
layerSetCornerRadius(artView->layer, 20.0);
artView->layer->shadowOpacity = 0.6;
artView->layer->shadowRadius = 15;
artView->layer->shadowOffset = ccPoint(0, 10);
viewAddSubview(mainWindowView, artView);
currentY += artSize + 25; // Gap reduced to 25
//================================================================
// 3. Track Information
//================================================================
// Song Title
uiMusicTitleLbl = labelWithFrame(ccRect(20, currentY, 280, 28));
uiMusicTitleLbl->text = ccs("Song Title Placeholder");
uiMusicTitleLbl->textColor = color(1, 1, 1, 1);
uiMusicTitleLbl->fontSize = 22; // Slightly smaller font
uiMusicTitleLbl->textAlignment = CCTextAlignmentCenter;
viewAddSubview(mainWindowView, uiMusicTitleLbl);
currentY += 28;
// Artist Name
uiMusicArtistLbl = labelWithFrame(ccRect(20, currentY, 280, 20));
uiMusicArtistLbl->text = ccs("Artist Name");
uiMusicArtistLbl->textColor = color(0.7, 0.7, 0.8, 1.0);
uiMusicArtistLbl->fontSize = 14;
uiMusicArtistLbl->textAlignment = CCTextAlignmentCenter;
viewAddSubview(mainWindowView, uiMusicArtistLbl);
currentY += 20 + 20; // Gap reduced to 20
//================================================================
// 4. Progress Section
//================================================================
int progressHeight = 6;
int progressWidth = 280; // 20px padding on each side
int progressX = 20;
// A. Track
CCView* progressTrack = viewWithFrame(ccRect(progressX, currentY, progressWidth, progressHeight));
progressTrack->backgroundColor = color(0.2, 0.2, 0.25, 1.0);
layerSetCornerRadius(progressTrack->layer, progressHeight / 2.0);
progressTrack->tag = TAG_MUSIC_PROGRESS_BAR;
viewAddSubview(mainWindowView, progressTrack);
// B. Fill
uiMusicProgressFill = viewWithFrame(ccRect(progressX, currentY, 0, progressHeight)); // Width 0 init
uiMusicProgressFill->backgroundColor = color(0.0, 0.8, 1.0, 1.0);
layerSetCornerRadius(uiMusicProgressFill->layer, progressHeight / 2.0);
// uiMusicProgressFill->userInteractionEnabled = false;
viewAddSubview(mainWindowView, uiMusicProgressFill);
currentY += progressHeight + 8;
// C. Timestamps
CCLabel* currTimeLbl = labelWithFrame(ccRect(20, currentY, 60, 14));
currTimeLbl->text = ccs("0:00");
currTimeLbl->textColor = color(0.6, 0.6, 0.7, 1.0);
currTimeLbl->fontSize = 10;
viewAddSubview(mainWindowView, currTimeLbl);
CCLabel* totalTimeLbl = labelWithFrame(ccRect(SCREEN_W - 80, currentY, 60, 14));
totalTimeLbl->text = ccs("-:--");
totalTimeLbl->textColor = color(0.6, 0.6, 0.7, 1.0);
totalTimeLbl->fontSize = 10;
totalTimeLbl->textAlignment = CCTextAlignmentRight;
viewAddSubview(mainWindowView, totalTimeLbl);
currentY += 14 + 20; // Gap reduced to 20
//================================================================
// 5. Playback Controls
//================================================================
// Current Y should be approx 395px.
// We have 85px remaining.
int playBtnSize = 64; // Reduced slightly from 74
int sideBtnSize = 44; // Reduced slightly from 54
int spacing = 30;
int centerX = SCREEN_W / 2;
// We want the CENTER of the buttons to be at a specific Y,
// ensuring they don't hit the bottom edge.
// Let's place the button Top at currentY.
// Button Bottom will be 395 + 64 = 459px. (Safe!)
int controlsCenterY = currentY + (playBtnSize / 2);
// A. Play Button
CCView* playBtn = viewWithFrame(ccRect(centerX - (playBtnSize/2), currentY, playBtnSize, playBtnSize));
playBtn->backgroundColor = color(0.0, 0.8, 1.0, 1.0);
layerSetCornerRadius(playBtn->layer, playBtnSize / 2.0);
playBtn->tag = TAG_MUSIC_PLAY;
// --- SHADOW FIX ---
playBtn->layer->shadowOpacity = 0.4;
playBtn->layer->shadowRadius = 8;
playBtn->layer->shadowOffset = ccPoint(0, 4); // <--- MUST BE SET to allocate the pointer!
// ------------------
uiMusicPlayBtnLbl = labelWithFrame(ccRect(0, 0, playBtnSize, playBtnSize));
uiMusicPlayBtnLbl->text = ccs(">");
uiMusicPlayBtnLbl->textColor = color(1, 1, 1, 1);
uiMusicPlayBtnLbl->fontSize = 28;
uiMusicPlayBtnLbl->textAlignment = CCTextAlignmentCenter;
uiMusicPlayBtnLbl->textVerticalAlignment = CCTextVerticalAlignmentCenter;
viewAddSubview(playBtn, uiMusicPlayBtnLbl);
viewAddSubview(mainWindowView, playBtn);
// B. Prev Button
// Align centers vertically
int prevX = centerX - (playBtnSize/2) - spacing - sideBtnSize;
int sideBtnY = controlsCenterY - (sideBtnSize/2);
CCView* prevBtn = viewWithFrame(ccRect(prevX, sideBtnY, sideBtnSize, sideBtnSize));
prevBtn->backgroundColor = color(0.25, 0.25, 0.3, 1.0);
layerSetCornerRadius(prevBtn->layer, sideBtnSize / 2.0);
prevBtn->tag = TAG_MUSIC_PREV;
CCLabel* prevLbl = labelWithFrame(ccRect(0, 0, sideBtnSize, sideBtnSize));
prevLbl->text = ccs("|<");
prevLbl->textColor = color(0.8, 0.8, 0.9, 1.0);
prevLbl->fontSize = 14;
prevLbl->textAlignment = CCTextAlignmentCenter;
prevLbl->textVerticalAlignment = CCTextVerticalAlignmentCenter;
viewAddSubview(prevBtn, prevLbl);
viewAddSubview(mainWindowView, prevBtn);
// C. Next Button
int nextX = centerX + (playBtnSize/2) + spacing;
CCView* nextBtn = viewWithFrame(ccRect(nextX, sideBtnY, sideBtnSize, sideBtnSize));
nextBtn->backgroundColor = color(0.25, 0.25, 0.3, 1.0);
layerSetCornerRadius(nextBtn->layer, sideBtnSize / 2.0);
nextBtn->tag = TAG_MUSIC_NEXT;
CCLabel* nextLbl = labelWithFrame(ccRect(0, 0, sideBtnSize, sideBtnSize));
nextLbl->text = ccs(">|");
nextLbl->textColor = color(0.8, 0.8, 0.9, 1.0);
nextLbl->fontSize = 14;
nextLbl->textAlignment = CCTextAlignmentCenter;
nextLbl->textVerticalAlignment = CCTextVerticalAlignmentCenter;
viewAddSubview(nextBtn, nextLbl);
viewAddSubview(mainWindowView, nextBtn);
}
// percentage is 0.0 to 1.0
void update_music_progress(float percentage) {
if (uiMusicProgressFill == NULL) return;
// Clamp percentage safety check
if (percentage < 0.0f) percentage = 0.0f;
if (percentage > 1.0f) percentage = 1.0f;
int maxWidth = SCREEN_W - (MAIN_PADDING * 2);
int newWidth = (int)(maxWidth * percentage);
// Update the width of the fill view
uiMusicProgressFill->frame->size->width = newWidth;
// Optimized Redraw: Only redraw the progress track area via its parent
// Assuming the track is the parent, or just update the root if needed.
// If you implemented update_view_area_via_parent:
// update_view_area_via_parent(uiMusicProgressFill);
// Fallback if parent optimization isn't ready:
update_full_ui();
}
*/
// ======================================================================
// 1. UNIVERSAL CONFIG I/O & MP3 PARSERS
// ======================================================================
#include <sys/stat.h>
#include <string.h>
#include <dirent.h>
#include <stdio.h>
// ======================================================================
// MUSIC APP GLOBALS & TAGS
// ======================================================================
// --- UI TAGS (For Touch Routing) ---
// Navigation & Core UI
#define TAG_MUSIC_NAV_LIBRARY 1001
#define TAG_MUSIC_NAV_PLAYLISTS 1002
#define TAG_MUSIC_NAV_ARTISTS 1003 // Replaced Settings
// Player Controls
#define TAG_MUSIC_TOGGLE_LOOP 2001
#define TAG_MUSIC_TOGGLE_SHUFFLE 2002
#define TAG_MUSIC_PLAY 2003
#define TAG_MUSIC_PREV 2004
#define TAG_MUSIC_NEXT 2005
#define TAG_MUSIC_PROGRESS_BAR 2006
// Library & Lists
#define TAG_MUSIC_SEARCH_BAR 500
#define TAG_MUSIC_SCAN_BTN 501
#define TAG_MUSIC_LIB_ROW_START 10000 // e.g., 10005 = Track Index 5
#define TAG_MUSIC_LIB_CHECK_START 20000 // e.g., 20005 = Checkbox Index 5
// Context Menu (Long Press)
#define TAG_MUSIC_CTX_BG 30000
#define TAG_MUSIC_CTX_TRACK_PLAY 30001
#define TAG_MUSIC_CTX_TRACK_ADD 30002
#define TAG_MUSIC_CTX_TRACK_PROP 30003
#define TAG_MUSIC_CTX_TRACK_DEL 30004
#define TAG_MUSIC_CTX_PL_OPEN 30101
#define TAG_MUSIC_CTX_PL_COPY 30102
#define TAG_MUSIC_CTX_PL_DEL 30103
#define TAG_MUSIC_ARTIST_ROW_START 40000 // Tapping an artist name
#define TAG_MUSIC_ALBUM_ROW_START 50000 // Tapping an album name
#define TAG_MUSIC_ARTIST_BACK 60000 // "< Artists" back button
#define TAG_MUSIC_ARTIST_TAB_SONGS 60001 // Sub-tab
#define TAG_MUSIC_ARTIST_TAB_ALBUMS 60002 // Sub-tab
// --- CONTEXT MENU TYPES ---
typedef enum {
MusicContextTypeTrack = 0,
MusicContextTypePlaylist = 1
} MusicContextType;
// --- GLOBAL POINTERS & STATE ---
// 1. Master View Containers
CCView* uiMusicTopBar = NULL;
CCView* uiMusicContentView = NULL;
CCView* currentMusicSubView = NULL; // Tracks active tab for safe memory deletion
// 2. Player Tab Elements
CCImageView* uiMusicLoopIcon = NULL;
CCImageView* uiMusicShuffleIcon = NULL;
CCLabel* uiMusicTitleLbl = NULL;
CCLabel* uiMusicArtistLbl = NULL;
CCLabel* uiMusicPlayBtnLbl = NULL;
CCView* uiMusicProgressFill = NULL;
bool isMusicLooping = false;
bool isMusicShuffling = false;
// 3. Library & Data State
CCArray* globalMusicLibrary = NULL; // Holds CCDictionaries of scanned MP3 metadata
bool isMusicLibrarySelectionMode = false; // Toggles checkboxes when building playlists
// 4. Artists Tab State
CCString* currentSelectedArtist = NULL; // NULL means show master artist list
bool isArtistDetailShowingAlbums = false; // Toggles between "Songs" and "Albums" sub-tabs
// 5. Context Menu State
CCView* uiMusicContextMenu = NULL;
// ======================================================================
// TOUCH STATE TRACKERS
// ======================================================================
static bool is_pressing = false;
static bool long_press_fired = false;
static uint64_t touchStartTime = 0;
static int touchStartX = 0;
static int touchStartY = 0;
// --- PAGINATION GLOBALS ---
int musicRowsPerPage = 6; // Configurable variable for different screens
// --- NEW TAGS ---
#define TAG_MUSIC_PAGE_PREV 70001
#define TAG_MUSIC_PAGE_NEXT 70002
#define TAG_MUSIC_MENU_BTN 70003
CCView* uiMusicMenuOverlay = NULL; // Tracks the top-left menu popup
// --- NEW TAGS ---
#define TAG_MUSIC_PL_ADD_ITEM 70012
#define TAG_MUSIC_PL_ADD_DONE 70013
#define TAG_MUSIC_PL_BACK 70014
// --- TAB STATE ---
typedef enum {
MusicTabLibrary = 0,
MusicTabPlaylists,
MusicTabArtists
} MusicTab;
MusicTab currentMusicTab = MusicTabLibrary;
int currentDataPage = 0; // Replaces currentLibraryPage
int totalDataPages = 1;
// --- PERSISTENT SHELL VIEWS ---
CCView* uiMusicHeaderView = NULL;
CCView* uiMusicFooterView = NULL;
CCView* uiMusicPaginationContainer = NULL; // The invisible wrapper for the buttons
// --- NEW TAGS ---
#define TAG_MUSIC_PL_CREATE 70004
// --- PLAYLISTS GLOBALS ---
CCArray* globalPlaylists = NULL;
CCView* uiNewPlaylistDialog = NULL;
CCLabel* uiNewPlaylistInput = NULL;
CCString* currentSelectedPlaylist = NULL; // For when we build the detail view later
// --- NEW TAGS ---
#define TAG_MUSIC_PL_CREATE_DIALOG_CANCEL 70010
#define TAG_MUSIC_PL_CREATE_DIALOG_CONFIRM 70011
#define TAG_MUSIC_PLAYLIST_ROW_START 80000 // Maps to playlist array index
void ensure_parent_directories(const char* filepath) {
char tmp[256];
strncpy(tmp, filepath, sizeof(tmp));
char* lastSlash = strrchr(tmp, '/');
if (lastSlash) {
*lastSlash = '\0';
mkdir(tmp, 0777);
}
}
// ======================================================================
// 1. UNIVERSAL CONFIG I/O & MP3 PARSERS (CORRECTED API)
// ======================================================================
void saveConfigFile(CCString* relativePath, CCDictionary* config) {
if (!relativePath || !config) return;
// 1. Wrap the Dictionary in a CCJSONObject
CCJSONObject* jsonObj = jsonObjectWithObject(config);
if (!jsonObj) return;
// 2. Generate the JSON String (Using your Compressed style to save SD card space)
generateJsonStringFromObject(jsonObj, CCJSONWriteStyleCompressed); // Assuming this enum is available
// 3. Extract the generated string
CCString* jsonString = jsonObj->jsonString;
if (!jsonString || !cStringOfString(jsonString)) {
// Handle cleanup if needed
return;
}
CCString* sdPath = stringWithFormat("/sdcard/freeos/%s", cStringOfString(relativePath));
CCString* flashPath = stringWithFormat("/spiflash/freeos/%s", cStringOfString(relativePath));
ensure_parent_directories(cStringOfString(sdPath));
FILE* file = fopen(cStringOfString(sdPath), "w");
if (file) {
fwrite(cStringOfString(jsonString), 1, strlen(cStringOfString(jsonString)), file);
fclose(file);
} else {
ensure_parent_directories(cStringOfString(flashPath));
file = fopen(cStringOfString(flashPath), "w");
if (file) {
fwrite(cStringOfString(jsonString), 1, strlen(cStringOfString(jsonString)), file);
fclose(file);
}
}
// NOTE: Depending on your memory model, you may need to free 'jsonObj' and paths here.
}
CCDictionary* loadConfigFile(CCString* relativePath) {
if (!relativePath || !cStringOfString(relativePath)) return NULL;
CCString* flashPath = stringWithFormat("/spiflash/freeos/%s", cStringOfString(relativePath));
CCString* sdPath = stringWithFormat("/sdcard/freeos/%s", cStringOfString(relativePath));
// Try Flash first
CCString* jsonRawText = stringWithContentsOfFile(flashPath);
if (!jsonRawText) {
// Fallback to SD card
jsonRawText = stringWithContentsOfFile(sdPath);
}
if (!jsonRawText) return NULL; // No file found
// 1. Wrap the raw text in your CCJSONObject
CCJSONObject* jsonObj = jsonObjectWithJSONString(jsonRawText);
if (!jsonObj) return NULL;
// 2. Parse the JSON string into ObjectiveCC Dictionaries/Arrays
generateObjectFromJsonString(jsonObj);
// 3. Extract the parsed dictionary
CCDictionary* parsedConfig = (CCDictionary*)jsonObj->jsonObject;
// NOTE: Free 'jsonObj' and 'jsonRawText' wrapper here if your API requires manual cleanup.
return parsedConfig;
}
// --- HELPER FUNCTION ---
CCDictionary* get_current_playlist_dict(void) {
if (!globalPlaylists || !currentSelectedPlaylist) return NULL;
for(int i = 0; i < arrayCount(globalPlaylists); i++) {
CCDictionary* pl = arrayObjectAtIndex(globalPlaylists, i);
CCString* name = dictionaryObjectForKey(pl, ccs("Name"));
if (name && stringEqualsString(name, currentSelectedPlaylist)) return pl;
}
return NULL;
}
int decode_id3_size(unsigned char* size_bytes) {
return (size_bytes[0] << 21) | (size_bytes[1] << 14) | (size_bytes[2] << 7) | size_bytes[3];
}
// 1. Math for the master header and ID3v2.4 frames
int decode_syncsafe_size(unsigned char* bytes) {
return (bytes[0] << 21) | (bytes[1] << 14) | (bytes[2] << 7) | bytes[3];
}
// 2. Math for ID3v2.3 frames
int decode_standard_size(unsigned char* bytes) {
return (bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3];
}
CCDictionary* music_parse_mp3_metadata(const char* filepath) {
CCDictionary* trackData = dictionary();
// Set default fallbacks
dictionarySetObjectForKey(trackData, ccs(filepath), ccs("Path"));
const char* filename = strrchr(filepath, '/');
filename = (filename) ? filename + 1 : filepath;
dictionarySetObjectForKey(trackData, ccs(filename), ccs("Title"));
dictionarySetObjectForKey(trackData, ccs("Unknown Artist"), ccs("Artist"));
dictionarySetObjectForKey(trackData, ccs("Unknown Album"), ccs("Album"));
dictionarySetObjectForKey(trackData, ccs("0:00"), ccs("Duration"));
FILE* file = fopen(filepath, "rb");
if (!file) return trackData;
// Basic CBR duration estimate (minimp3 will fix this for VBR later)
fseek(file, 0, SEEK_END);
long fileSize = ftell(file);
fseek(file, 0, SEEK_SET);
int durationSeconds = (fileSize * 8) / 128000;
char durationStr[16];
snprintf(durationStr, sizeof(durationStr), "%d:%02d", durationSeconds / 60, durationSeconds % 60);
dictionarySetObjectForKey(trackData, ccs(durationStr), ccs("Duration"));
unsigned char header[10];
if (fread(header, 1, 10, file) == 10) {
// Confirm it's an ID3 tag
if (header[0] == 'I' && header[1] == 'D' && header[2] == '3') {
int version = header[3]; // Typically 3 (v2.3) or 4 (v2.4)
int tagSize = decode_syncsafe_size(&header[6]);
int bytesRead = 0;
// Limit scanning to first 4KB to save RAM
int maxRead = (tagSize < 4096) ? tagSize : 4096;
while (bytesRead < maxRead) {
unsigned char frameHeader[10];
if (fread(frameHeader, 1, 10, file) != 10) break;
bytesRead += 10;
if (frameHeader[0] == 0) break; // Reached padding, we're done
// APPLY THE VERSION MATH FIX
int frameSize = 0;
if (version >= 4) {
frameSize = decode_syncsafe_size(&frameHeader[4]);
} else {
frameSize = decode_standard_size(&frameHeader[4]);
}
if (frameSize <= 0 || frameSize > 5000000) break; // Sanity check
// Read up to 256 bytes of the frame body
char frameData[257] = {0};
int readSize = (frameSize > 256) ? 256 : frameSize;
if (fread(frameData, 1, readSize, file) == readSize) {
// Fast-forward the file pointer if the frame was larger than our buffer (e.g. cover art)
if (frameSize > 256) fseek(file, frameSize - 256, SEEK_CUR);
char parsedText[256] = {0};
int encoding = frameData[0];
int outIdx = 0;
// Text starting index: Skip encoding byte
int startIdx = 1;
// Skip UTF-16 Byte Order Marks (BOM)
if (encoding == 1 || encoding == 2) {
if (readSize >= 3 && ((frameData[1] == (char)0xFF && frameData[2] == (char)0xFE) ||
(frameData[1] == (char)0xFE && frameData[2] == (char)0xFF))) {
startIdx = 3;
}
}
// Extract printable characters, skipping nulls and hidden control characters
for (int k = startIdx; k < readSize && outIdx < 255; k++) {
if ((unsigned char)frameData[k] >= 32) { // Only standard/extended printable characters
parsedText[outIdx++] = frameData[k];
}
}
if (outIdx > 0) {
if (strncmp((char*)frameHeader, "TIT2", 4) == 0) {
dictionarySetObjectForKey(trackData, ccs(parsedText), ccs("Title"));
} else if (strncmp((char*)frameHeader, "TPE1", 4) == 0) {
dictionarySetObjectForKey(trackData, ccs(parsedText), ccs("Artist"));
} else if (strncmp((char*)frameHeader, "TALB", 4) == 0) {
dictionarySetObjectForKey(trackData, ccs(parsedText), ccs("Album"));
}
}
} else {
break;
}
bytesRead += frameSize;
}
}
}
fclose(file);
return trackData;
}
void music_scan_directory(const char* dirPath) {
DIR* dir = opendir(dirPath);
if (!dir) {
printf("MUSIC SCAN: Could not open directory -> %s\n", dirPath);
return;
}
printf("MUSIC SCAN: Reading directory -> %s\n", dirPath);
struct dirent* entry;
while ((entry = readdir(dir)) != NULL) {
// Skip hidden files, current dir, and parent dir
if (entry->d_name[0] == '.') continue;
char fullPath[512];
snprintf(fullPath, sizeof(fullPath), "%s/%s", dirPath, entry->d_name);
struct stat path_stat;
stat(fullPath, &path_stat);
if (S_ISDIR(path_stat.st_mode)) {
// It's a directory: Recurse!
music_scan_directory(fullPath);
} else {
// It's a file: Check for .mp3 extension
const char* ext = strrchr(entry->d_name, '.');
if (ext && (strcmp(ext, ".mp3") == 0 || strcmp(ext, ".MP3") == 0)) {
printf("MUSIC SCAN: Found MP3 -> %s\n", entry->d_name);
// Parse the metadata into a CCDictionary
CCDictionary* trackMetadata = music_parse_mp3_metadata(fullPath);
// Add to the global array
arrayAddObject(globalMusicLibrary, trackMetadata);
}
}
}
closedir(dir);
}
void music_trigger_full_scan(void) {
if (globalMusicLibrary) {
freeCCArray(globalMusicLibrary);
}
globalMusicLibrary = array();
printf("=== STARTING FULL MP3 SCAN ===\n");
// NOTE: Verify these are your exact FreeOS mount points!
music_scan_directory("/sdcard");
music_scan_directory("/spiflash");
// music_scan_directory("/internal"); // Uncomment if your flash uses this name
printf("=== SCAN COMPLETE: Found %d tracks ===\n", arrayCount(globalMusicLibrary));
// Wrap and Save
CCDictionary* libraryWrapper = dictionary();
dictionarySetObjectForKey(libraryWrapper, globalMusicLibrary, ccs("tracks"));
saveConfigFile(ccs("music/musicLibrary.json"), libraryWrapper);
}
// ======================================================================
// 2. RELATIONAL DATA EXTRACTORS
// ======================================================================
CCArray* music_get_unique_artists(void) {
CCArray* uniqueArtists = array();
if (!globalMusicLibrary) return uniqueArtists;
int trackCount = arrayCount(globalMusicLibrary);
for (int i = 0; i < trackCount; i++) {
CCDictionary* track = arrayObjectAtIndex(globalMusicLibrary, i);
CCString* artistName = dictionaryObjectForKey(track, ccs("Artist"));
if (!artistName) artistName = ccs("Unknown Artist");
bool found = false;
for (int j = 0; j < arrayCount(uniqueArtists); j++) {
CCString* existing = arrayObjectAtIndex(uniqueArtists, j);
if (stringEqualsString(existing, artistName)) {
found = true;
break;
}
}
if (!found) {
// FIX: Pass a COPY of the string into the temporary array
arrayAddObject(uniqueArtists, copyCCString(artistName));
}
}
return uniqueArtists;
}
CCArray* music_get_albums_for_artist(CCString* targetArtist) {
CCArray* uniqueAlbums = array();
if (!globalMusicLibrary || !targetArtist) return uniqueAlbums;
int trackCount = arrayCount(globalMusicLibrary);
for (int i = 0; i < trackCount; i++) {
CCDictionary* track = arrayObjectAtIndex(globalMusicLibrary, i);
CCString* artistName = dictionaryObjectForKey(track, ccs("Artist"));
if (!artistName) artistName = ccs("Unknown Artist");
if (stringEqualsString(artistName, targetArtist)) {
CCString* albumName = dictionaryObjectForKey(track, ccs("Album"));
if (!albumName) albumName = ccs("Unknown Album");
bool found = false;
for (int j = 0; j < arrayCount(uniqueAlbums); j++) {
CCString* existing = arrayObjectAtIndex(uniqueAlbums, j);
if (stringEqualsString(existing, albumName)) {
found = true;
break;
}
}
if (!found) {
// FIX: Pass a COPY of the string into the temporary array
arrayAddObject(uniqueAlbums, copyCCString(albumName));
}
}
}
return uniqueAlbums;
}
// ======================================================================
// 3. MUSIC APP UI BUILDERS
// ======================================================================
void update_music_pagination_ui(void) {
// 1. THE CONTAINER FIX: Safely wipe the old buttons without any array loops!
if (uiMusicPaginationContainer) {
viewRemoveFromSuperview(uiMusicPaginationContainer);
freeViewHierarchy(uiMusicPaginationContainer);
uiMusicPaginationContainer = NULL;
}
// Create a fresh container that fills the footer
uiMusicPaginationContainer = viewWithFrame(ccRect(0, 0, SCREEN_W, 60));
uiMusicPaginationContainer->backgroundColor = color(0, 0, 0, 0.0); // Transparent
int btnWidth = 80;
int currentY = 10; // Margin from top of footer
// ==========================================
// PREV BUTTON
// ==========================================
if (currentDataPage > 0) {
CCView* prevBtn = viewWithFrame(ccRect(10, currentY, btnWidth, 40));
prevBtn->backgroundColor = color(0.2, 0.2, 0.25, 1.0);
layerSetCornerRadius(prevBtn->layer, 8.0);
prevBtn->tag = TAG_MUSIC_PAGE_PREV;
CCLabel* prevLbl = labelWithFrame(ccRect(0, 0, btnWidth, 40));
prevLbl->text = ccs("< Back");
prevLbl->textColor = color(0.9, 0.9, 0.95, 1.0);
prevLbl->textAlignment = CCTextAlignmentCenter;
prevLbl->textVerticalAlignment = CCTextVerticalAlignmentCenter;
prevLbl->ignoreTouch = true;
viewAddSubview(prevBtn, prevLbl);
viewAddSubview(uiMusicPaginationContainer, prevBtn); // Add to container!
}
// ==========================================
// PAGE INDICATOR
// ==========================================
// FIX: Use snprintf + ccs to guarantee a fresh, isolated heap allocation!
char pageStr[32];
snprintf(pageStr, sizeof(pageStr), "Page %d of %d", currentDataPage + 1, totalDataPages);
CCLabel* pageIndicator = labelWithFrame(ccRect(btnWidth + 10, currentY, SCREEN_W - (btnWidth * 2) - 20, 40));
pageIndicator->text = ccs(pageStr);
pageIndicator->textColor = color(0.6, 0.6, 0.7, 1.0);
pageIndicator->textAlignment = CCTextAlignmentCenter;
pageIndicator->textVerticalAlignment = CCTextVerticalAlignmentCenter;
pageIndicator->ignoreTouch = true;
viewAddSubview(uiMusicPaginationContainer, pageIndicator); // Add to container!
// ==========================================
// NEXT BUTTON
// ==========================================
if (currentDataPage < totalDataPages - 1) {
CCView* nextBtn = viewWithFrame(ccRect(SCREEN_W - btnWidth - 10, currentY, btnWidth, 40));
nextBtn->backgroundColor = color(0.2, 0.2, 0.25, 1.0);
layerSetCornerRadius(nextBtn->layer, 8.0);
nextBtn->tag = TAG_MUSIC_PAGE_NEXT;
CCLabel* nextLbl = labelWithFrame(ccRect(0, 0, btnWidth, 40));
nextLbl->text = ccs("Next >");
nextLbl->textColor = color(0.9, 0.9, 0.95, 1.0);
nextLbl->textAlignment = CCTextAlignmentCenter;
nextLbl->textVerticalAlignment = CCTextVerticalAlignmentCenter;
nextLbl->ignoreTouch = true;
viewAddSubview(nextBtn, nextLbl);
viewAddSubview(uiMusicPaginationContainer, nextBtn); // Add to container!
}
// Finally, attach the fully assembled container to the persistent footer
viewAddSubview(uiMusicFooterView, uiMusicPaginationContainer);
}
void setup_music_app(void) {
currentView = CurrentViewMusic;
if (mainWindowView) freeViewHierarchy(mainWindowView);
mainWindowView = viewWithFrame(ccRect(0, 0, SCREEN_W, 480));
mainWindowView->backgroundColor = color(0.08, 0.08, 0.12, 1.0);
// ==========================================
// 1. TOP NAV BAR
// ==========================================
uiMusicTopBar = viewWithFrame(ccRect(0, 25, SCREEN_W, 40));
uiMusicTopBar->backgroundColor = color(0.12, 0.12, 0.16, 1.0);
CCView* navBorder = viewWithFrame(ccRect(0, 39, SCREEN_W, 1));
navBorder->backgroundColor = color(0.3, 0.3, 0.35, 1.0);
viewAddSubview(uiMusicTopBar, navBorder);
int navBtnW = SCREEN_W / 3;
// Library Tab
CCView* libBtn = viewWithFrame(ccRect(0, 0, navBtnW, 40));
libBtn->tag = TAG_MUSIC_NAV_LIBRARY;
CCLabel* libLbl = labelWithFrame(ccRect(0, 0, navBtnW, 40));
libLbl->text = ccs("Library");
libLbl->textColor = color(0.8, 0.8, 0.9, 1.0);
libLbl->fontSize = 14;
libLbl->textAlignment = CCTextAlignmentCenter;
libLbl->textVerticalAlignment = CCTextVerticalAlignmentCenter;
libLbl->ignoreTouch = true;
viewAddSubview(libBtn, libLbl);
viewAddSubview(uiMusicTopBar, libBtn);
// Playlists Tab
CCView* plBtn = viewWithFrame(ccRect(navBtnW, 0, navBtnW, 40));
plBtn->tag = TAG_MUSIC_NAV_PLAYLISTS;
CCLabel* plLbl = labelWithFrame(ccRect(0, 0, navBtnW, 40));
plLbl->text = ccs("Playlists");
plLbl->textColor = color(0.8, 0.8, 0.9, 1.0);
plLbl->fontSize = 14;
plLbl->textAlignment = CCTextAlignmentCenter;
plLbl->textVerticalAlignment = CCTextVerticalAlignmentCenter;
plLbl->ignoreTouch = true;
viewAddSubview(plBtn, plLbl);
viewAddSubview(uiMusicTopBar, plBtn);
// Artists Tab
CCView* artBtn = viewWithFrame(ccRect(navBtnW * 2, 0, navBtnW, 40));
artBtn->tag = TAG_MUSIC_NAV_ARTISTS;
CCLabel* artLbl = labelWithFrame(ccRect(0, 0, navBtnW, 40));
artLbl->text = ccs("Artists");
artLbl->textColor = color(0.8, 0.8, 0.9, 1.0);
artLbl->fontSize = 14;
artLbl->textAlignment = CCTextAlignmentCenter;
artLbl->textVerticalAlignment = CCTextVerticalAlignmentCenter;
artLbl->ignoreTouch = true;
viewAddSubview(artBtn, artLbl);
viewAddSubview(uiMusicTopBar, artBtn);
viewAddSubview(mainWindowView, uiMusicTopBar);
// ==========================================
// 2. PERSISTENT HEADER (Menu & Search)
// ==========================================
uiMusicHeaderView = viewWithFrame(ccRect(0, afb(uiMusicTopBar), SCREEN_W, 46));
CCImageView* menuIcon = imageViewWithFrame(ccRect(10, 5, 36, 36));
menuIcon->image = imageWithFile(ccs("/spiflash/menu.png"));
menuIcon->ignoreTouch = true;
menuIcon->alpha = 0.7;
viewAddSubview(uiMusicHeaderView, menuIcon);
CCView* menuBtnHitbox = viewWithFrame(ccRect(5, 0, 46, 46));
menuBtnHitbox->tag = TAG_MUSIC_MENU_BTN;
menuBtnHitbox->backgroundColor = color(0, 0, 0, 0.0);
viewAddSubview(uiMusicHeaderView, menuBtnHitbox);
CCView* searchBarView = viewWithFrame(ccRect(56, 5, SCREEN_W - 66, 36));
searchBarView->backgroundColor = color(0.15, 0.15, 0.2, 1.0);
layerSetCornerRadius(searchBarView->layer, 18.0);
searchBarView->tag = TAG_MUSIC_SEARCH_BAR;
CCLabel* searchPlaceholder = labelWithFrame(ccRect(15, 0, 200, 36));
searchPlaceholder->text = ccs("Search...");
searchPlaceholder->textColor = color(0.5, 0.5, 0.6, 1.0);
searchPlaceholder->fontSize = 14;
searchPlaceholder->textVerticalAlignment = CCTextVerticalAlignmentCenter;
searchPlaceholder->ignoreTouch = true;
viewAddSubview(searchBarView, searchPlaceholder);
viewAddSubview(uiMusicHeaderView, searchBarView);
viewAddSubview(mainWindowView, uiMusicHeaderView);
// ==========================================
// 3. PERSISTENT FOOTER (Pagination)
// ==========================================
int footerHeight = 60;
uiMusicFooterView = viewWithFrame(ccRect(0, 480 - footerHeight, SCREEN_W, footerHeight));
uiMusicFooterView->backgroundColor = color(0.08, 0.08, 0.12, 1.0);
viewAddSubview(mainWindowView, uiMusicFooterView);
// (We will populate this dynamically using the update_music_pagination_ui function below)
// ==========================================
// 4. DYNAMIC CONTENT CANVAS
// ==========================================
int contentY = afb(uiMusicHeaderView);
int contentHeight = (480 - footerHeight) - contentY;
uiMusicContentView = viewWithFrame(ccRect(0, contentY, SCREEN_W, contentHeight));
uiMusicContentView->backgroundColor = color(0.08, 0.08, 0.12, 1.0);
viewAddSubview(mainWindowView, uiMusicContentView);
// Initial Load
currentMusicTab = MusicTabLibrary;
currentDataPage = 0;
build_music_library_ui();
}
void show_music_context_menu(MusicContextType type, int index, int touchX, int touchY) {
if (uiMusicContextMenu) {
viewRemoveFromSuperview(uiMusicContextMenu);
freeViewHierarchy(uiMusicContextMenu);
uiMusicContextMenu = NULL;
}
uiMusicContextMenu = viewWithFrame(ccRect(0, 0, SCREEN_W, 480));
uiMusicContextMenu->backgroundColor = color(0, 0, 0, 0.0);
uiMusicContextMenu->tag = TAG_MUSIC_CTX_BG;
int menuW = 180;
int btnH = 44;
int itemCount = (type == MusicContextTypeTrack) ? 4 : 3;
int menuH = itemCount * btnH;
if (touchX + menuW > SCREEN_W - 10) touchX = SCREEN_W - menuW - 10;
if (touchY + menuH > 480 - 10) touchY = 480 - menuH - 10;
CCView* menuBox = viewWithFrame(ccRect(touchX, touchY, menuW, menuH));
menuBox->backgroundColor = color(0.15, 0.15, 0.2, 0.95);
layerSetCornerRadius(menuBox->layer, 12.0);
menuBox->layer->borderWidth = 1.0;
menuBox->layer->borderColor = color(0.3, 0.3, 0.4, 1.0);
menuBox->layer->shadowOpacity = 0.5;
menuBox->layer->shadowRadius = 10;
menuBox->layer->shadowOffset = ccPoint(0, 5);
CCArray* buttonLabels = array();
if (type == MusicContextTypeTrack) {
arrayAddObject(buttonLabels, ccs("Play"));
arrayAddObject(buttonLabels, ccs("Add to Playlist"));
arrayAddObject(buttonLabels, ccs("Properties"));
arrayAddObject(buttonLabels, ccs("Remove from Library"));
} else {
arrayAddObject(buttonLabels, ccs("Open Playlist"));
arrayAddObject(buttonLabels, ccs("Copy Playlist"));
arrayAddObject(buttonLabels, ccs("Delete Playlist"));
}
for (int i = 0; i < itemCount; i++) {
CCView* btn = viewWithFrame(ccRect(0, i * btnH, menuW, btnH));
btn->tag = (type == MusicContextTypeTrack ? TAG_MUSIC_CTX_TRACK_PLAY : TAG_MUSIC_CTX_PL_OPEN) + i + (index * 100000);
CCString* lblStr = arrayObjectAtIndex(buttonLabels, i);
CCLabel* lbl = labelWithFrame(ccRect(15, 0, menuW - 30, btnH));
lbl->text = lblStr;
lbl->textColor = color(0.9, 0.9, 0.95, 1.0);
lbl->fontSize = 14;
lbl->textVerticalAlignment = CCTextVerticalAlignmentCenter;
lbl->ignoreTouch = true;
if (i < itemCount - 1) {
CCView* line = viewWithFrame(ccRect(10, btnH - 1, menuW - 20, 1));
line->backgroundColor = color(0.3, 0.3, 0.4, 0.5);
viewAddSubview(btn, line);
}
viewAddSubview(btn, lbl);
viewAddSubview(menuBox, btn);
}
viewAddSubview(uiMusicContextMenu, menuBox);
viewAddSubview(mainWindowView, uiMusicContextMenu);
freeCCArray(buttonLabels);
}
void build_music_library_ui(void) {
if (currentMusicSubView) {
viewRemoveFromSuperview(currentMusicSubView);
freeViewHierarchy(currentMusicSubView);
currentMusicSubView = NULL;
}
int trackCount = arrayCount(globalMusicLibrary);
totalDataPages = (trackCount > 0) ? ((trackCount + musicRowsPerPage - 1) / musicRowsPerPage) : 1;
if (currentDataPage >= totalDataPages) currentDataPage = totalDataPages - 1;
if (currentDataPage < 0) currentDataPage = 0;
int startIndex = currentDataPage * musicRowsPerPage;
int endIndex = startIndex + musicRowsPerPage;
if (endIndex > trackCount) endIndex = trackCount;
// Canvas size is now just the exact height of the rows!
int rowHeight = 44;
int totalCanvasHeight = 25 + (musicRowsPerPage * rowHeight);
currentMusicSubView = viewWithFrame(ccRect(0, 0, SCREEN_W, totalCanvasHeight));
currentMusicSubView->backgroundColor = color(0.08, 0.08, 0.12, 1.0);
int currentY = 0;
// --- SELECTION MODE "DONE" BUTTON ---
CCDictionary* activePl = NULL;
CCArray* plTracks = NULL;
if (isMusicLibrarySelectionMode) {
activePl = get_current_playlist_dict();
if (activePl) plTracks = dictionaryObjectForKey(activePl, ccs("Tracks"));
CCView* doneHeader = viewWithFrame(ccRect(0, currentY, SCREEN_W, 44));
doneHeader->backgroundColor = color(0.2, 0.6, 1.0, 1.0);
doneHeader->tag = TAG_MUSIC_PL_ADD_DONE;
CCLabel* doneLbl = labelWithFrame(ccRect(0, 0, SCREEN_W, 44));
doneLbl->text = copyCCString(ccs("Done Adding Tracks"));
doneLbl->textColor = color(1, 1, 1, 1);
doneLbl->fontSize = 16;
doneLbl->textAlignment = CCTextAlignmentCenter;
doneLbl->textVerticalAlignment = CCTextVerticalAlignmentCenter;
doneLbl->ignoreTouch = true;
viewAddSubview(doneHeader, doneLbl);
viewAddSubview(currentMusicSubView, doneHeader);
currentY += 44;
}
// ==========================================
// 2. COLUMN HEADERS
// ==========================================
int textStartX = isMusicLibrarySelectionMode ? 45 : 15;
int durationWidth = 45;
int availableTextWidth = SCREEN_W - textStartX - durationWidth - 10;
int titleWidth = (int)(availableTextWidth * 0.55);
int artistWidth = (int)(availableTextWidth * 0.45);
int artistStartX = textStartX + titleWidth;
int durationStartX = SCREEN_W - durationWidth - 10;
CCView* headerRow = viewWithFrame(ccRect(0, currentY, SCREEN_W, 20));
CCLabel* headTitle = labelWithFrame(ccRect(textStartX, 0, titleWidth, 20));
headTitle->text = ccs("TITLE");
headTitle->textColor = color(0.4, 0.4, 0.5, 1.0);
headTitle->fontSize = 10;
headTitle->ignoreTouch = true;
viewAddSubview(headerRow, headTitle);
CCLabel* headArtist = labelWithFrame(ccRect(artistStartX, 0, artistWidth, 20));
headArtist->text = ccs("ARTIST");
headArtist->textColor = color(0.4, 0.4, 0.5, 1.0);
headArtist->fontSize = 10;
headArtist->ignoreTouch = true;
viewAddSubview(headerRow, headArtist);
CCLabel* headTime = labelWithFrame(ccRect(durationStartX, 0, durationWidth, 20));
headTime->text = ccs("TIME");
headTime->textColor = color(0.4, 0.4, 0.5, 1.0);
headTime->fontSize = 10;
headTime->textAlignment = CCTextAlignmentRight;
headTime->ignoreTouch = true;
viewAddSubview(headerRow, headTime);
viewAddSubview(currentMusicSubView, headerRow);
currentY += 20 + 5;
// ==========================================
// 3. PAGINATED TABLE ROWS
// ==========================================
for (int i = startIndex; i < endIndex; i++) {
CCDictionary* track = arrayObjectAtIndex(globalMusicLibrary, i);
CCString* title = dictionaryObjectForKey(track, ccs("Title"));
CCString* artist = dictionaryObjectForKey(track, ccs("Artist"));
CCString* duration = dictionaryObjectForKey(track, ccs("Duration"));
// FIX: Copy the strings from the dictionary so the view hierarchy
// can safely free them later without corrupting the master array!
CCString* safeTitle = title ? copyCCString(title) : ccs("Unknown");
CCString* safeArtist = artist ? copyCCString(artist) : ccs("Unknown Artist");
CCString* safeDuration = duration ? copyCCString(duration) : ccs("-:--");
CCView* rowView = viewWithFrame(ccRect(0, currentY, SCREEN_W, rowHeight));
rowView->tag = TAG_MUSIC_LIB_ROW_START + i;
if (i % 2 == 0) rowView->backgroundColor = color(0.1, 0.1, 0.15, 1.0);
else rowView->backgroundColor = color(0.08, 0.08, 0.12, 0.0);
if (isMusicLibrarySelectionMode) {
int checkSize = 20;
CCView* checkbox = viewWithFrame(ccRect(15, (rowHeight - checkSize) / 2, checkSize, checkSize));
// Check if this track's Path is already in the playlist!
bool isChecked = false;
CCString* trackPath = dictionaryObjectForKey(track, ccs("Path"));
if (plTracks && trackPath) {
for (int t = 0; t < arrayCount(plTracks); t++) {
CCString* savedPath = arrayObjectAtIndex(plTracks, t);
if (stringEqualsString(savedPath, trackPath)) {
isChecked = true; break;
}
}
}
checkbox->backgroundColor = isChecked ? color(0.2, 0.6, 1.0, 1.0) : color(0.08, 0.08, 0.12, 1.0);
checkbox->layer->borderWidth = 1.5;
checkbox->layer->borderColor = color(0.3, 0.3, 0.4, 1.0);
layerSetCornerRadius(checkbox->layer, checkSize / 2.0);
checkbox->tag = TAG_MUSIC_LIB_CHECK_START + i;
viewAddSubview(rowView, checkbox);
}
CCLabel* titleLbl = labelWithFrame(ccRect(textStartX, 0, titleWidth - 10, rowHeight));
titleLbl->text = safeTitle; // Assign the safe copy
titleLbl->textColor = color(0.9, 0.9, 0.95, 1.0);
titleLbl->fontSize = 14;
titleLbl->textVerticalAlignment = CCTextVerticalAlignmentCenter;
titleLbl->ignoreTouch = true;
viewAddSubview(rowView, titleLbl);
CCLabel* artistLbl = labelWithFrame(ccRect(artistStartX, 0, artistWidth - 5, rowHeight));
artistLbl->text = safeArtist; // Assign the safe copy
artistLbl->textColor = color(0.6, 0.6, 0.7, 1.0);
artistLbl->fontSize = 12;
artistLbl->textVerticalAlignment = CCTextVerticalAlignmentCenter;
artistLbl->ignoreTouch = true;
viewAddSubview(rowView, artistLbl);
CCLabel* durLbl = labelWithFrame(ccRect(durationStartX, 0, durationWidth, rowHeight));
durLbl->text = safeDuration; // Assign the safe copy
durLbl->textColor = color(0.5, 0.5, 0.6, 1.0);
durLbl->fontSize = 12;
durLbl->textAlignment = CCTextAlignmentRight;
durLbl->textVerticalAlignment = CCTextVerticalAlignmentCenter;
durLbl->ignoreTouch = true;
viewAddSubview(rowView, durLbl);
viewAddSubview(currentMusicSubView, rowView);
currentY += rowHeight;
}
currentY += 10;
viewAddSubview(uiMusicContentView, currentMusicSubView);
// Tell the footer to refresh its buttons!
update_music_pagination_ui();
}
void show_music_menu_overlay(void) {
if (uiMusicMenuOverlay) {
viewRemoveFromSuperview(uiMusicMenuOverlay);
freeViewHierarchy(uiMusicMenuOverlay);
uiMusicMenuOverlay = NULL;
}
uiMusicMenuOverlay = viewWithFrame(ccRect(0, 0, SCREEN_W, 480));
uiMusicMenuOverlay->backgroundColor = color(0, 0, 0, 0.0);
uiMusicMenuOverlay->tag = TAG_MUSIC_CTX_BG;
int menuW = 210;
int btnH = 44;
int menuX = 10;
int menuY = afb(uiMusicTopBar) + 5; // Perfectly nested under the top bar!
CCView* menuBox = viewWithFrame(ccRect(menuX, menuY, menuW, btnH));
menuBox->backgroundColor = color(0.15, 0.15, 0.2, 0.95);
layerSetCornerRadius(menuBox->layer, 8.0);
menuBox->layer->borderWidth = 1.0;
menuBox->layer->borderColor = color(0.3, 0.3, 0.4, 1.0);
menuBox->ignoreTouch = true;
viewAddSubview(uiMusicMenuOverlay, menuBox);
// Contextual Logic based on current Tab!
CCView* actionBtn = viewWithFrame(ccRect(menuX, menuY, menuW, btnH));
actionBtn->backgroundColor = color(0, 0, 0, 0.0);
CCLabel* actionLbl = labelWithFrame(ccRect(15, 0, menuW - 30, btnH));
actionLbl->textColor = color(0.9, 0.9, 0.95, 1.0);
actionLbl->fontSize = 14;
actionLbl->textVerticalAlignment = CCTextVerticalAlignmentCenter;
actionLbl->ignoreTouch = true;
// Inside show_music_menu_overlay()...
if (currentMusicTab == MusicTabPlaylists) {
if (currentSelectedPlaylist == NULL) {
actionLbl->text = ccs("Create a New Playlist");
actionBtn->tag = TAG_MUSIC_PL_CREATE;
} else {
// We are inside a Playlist Detail View!
actionLbl->text = ccs("Add Item to Playlist");
actionBtn->tag = TAG_MUSIC_PL_ADD_ITEM;
}
} else {
actionLbl->text = ccs("Scan System for MP3s");
actionBtn->tag = TAG_MUSIC_SCAN_BTN;
}
viewAddSubview(actionBtn, actionLbl);
viewAddSubview(uiMusicMenuOverlay, actionBtn);
viewAddSubview(mainWindowView, uiMusicMenuOverlay);
}
void build_music_artists_ui(void) {
if (currentMusicSubView) {
viewRemoveFromSuperview(currentMusicSubView);
freeViewHierarchy(currentMusicSubView);
currentMusicSubView = NULL;
}
int currentY = 0;
int rowHeight = 44;
int itemCount = 0;
CCArray* displayData = NULL;
// ==========================================
// STATE A: MASTER ARTIST LIST
// ==========================================
if (currentSelectedArtist == NULL) {
displayData = music_get_unique_artists();
itemCount = arrayCount(displayData);
int totalCanvasHeight = (itemCount * rowHeight);
if (totalCanvasHeight < 440) totalCanvasHeight = 440;
currentMusicSubView = viewWithFrame(ccRect(0, 0, SCREEN_W, totalCanvasHeight));
currentMusicSubView->backgroundColor = color(0.08, 0.08, 0.12, 1.0);
for (int i = 0; i < itemCount; i++) {
CCString* artistName = arrayObjectAtIndex(displayData, i);
CCView* rowView = viewWithFrame(ccRect(0, currentY, SCREEN_W, rowHeight));
rowView->tag = TAG_MUSIC_ARTIST_ROW_START + i;
if (i % 2 == 0) rowView->backgroundColor = color(0.1, 0.1, 0.15, 1.0);
else rowView->backgroundColor = color(0.08, 0.08, 0.12, 0.0);
CCLabel* nameLbl = labelWithFrame(ccRect(15, 0, SCREEN_W - 50, rowHeight));
// FIX: Pass a distinct copy to the UI!
nameLbl->text = copyCCString(artistName);
nameLbl->textColor = color(0.9, 0.9, 0.95, 1.0);
nameLbl->fontSize = 16;
nameLbl->textVerticalAlignment = CCTextVerticalAlignmentCenter;
nameLbl->ignoreTouch = true;
viewAddSubview(rowView, nameLbl);
CCLabel* chevronLbl = labelWithFrame(ccRect(SCREEN_W - 30, 0, 20, rowHeight));
chevronLbl->text = ccs(">");
chevronLbl->textColor = color(0.4, 0.4, 0.5, 1.0);
chevronLbl->fontSize = 16;
chevronLbl->textVerticalAlignment = CCTextVerticalAlignmentCenter;
chevronLbl->ignoreTouch = true;
viewAddSubview(rowView, chevronLbl);
viewAddSubview(currentMusicSubView, rowView);
currentY += rowHeight;
}
// This is now 100% safe to call, because the UI labels own their own copies!
freeCCArray(displayData);
}
// ==========================================
// STATE B: ARTIST DETAIL VIEW (Songs & Albums)
// ==========================================
else {
if (isArtistDetailShowingAlbums) {
displayData = music_get_albums_for_artist(currentSelectedArtist);
itemCount = arrayCount(displayData);
} else {
itemCount = 0;
for (int i = 0; i < arrayCount(globalMusicLibrary); i++) {
CCDictionary* track = arrayObjectAtIndex(globalMusicLibrary, i);
CCString* trackArtist = dictionaryObjectForKey(track, ccs("Artist"));
if (trackArtist && stringEqualsString(trackArtist, currentSelectedArtist)) {
itemCount++;
}
}
}
int headerHeight = 44 + 40;
int totalCanvasHeight = headerHeight + (itemCount * rowHeight);
if (totalCanvasHeight < 440) totalCanvasHeight = 440;
currentMusicSubView = viewWithFrame(ccRect(0, 0, SCREEN_W, totalCanvasHeight));
currentMusicSubView->backgroundColor = color(0.08, 0.08, 0.12, 1.0);
CCView* backHeader = viewWithFrame(ccRect(0, currentY, SCREEN_W, 44));
backHeader->backgroundColor = color(0.12, 0.12, 0.16, 1.0);
CCView* backBtn = viewWithFrame(ccRect(5, 5, 100, 34));
backBtn->tag = TAG_MUSIC_ARTIST_BACK;
CCLabel* backLbl = labelWithFrame(ccRect(10, 0, 90, 34));
backLbl->text = ccs("< Artists");
backLbl->textColor = color(0.2, 0.6, 1.0, 1.0);
backLbl->fontSize = 14;
backLbl->textVerticalAlignment = CCTextVerticalAlignmentCenter;
backLbl->ignoreTouch = true;
viewAddSubview(backBtn, backLbl);
viewAddSubview(backHeader, backBtn);
CCLabel* titleLbl = labelWithFrame(ccRect(80, 0, SCREEN_W - 160, 44));
// FIX: NEVER assign a global directly to a label, or freeViewHierarchy will destroy the global!
titleLbl->text = copyCCString(currentSelectedArtist);
titleLbl->textColor = color(1, 1, 1, 1);
titleLbl->fontSize = 16;
titleLbl->textAlignment = CCTextAlignmentCenter;
titleLbl->textVerticalAlignment = CCTextVerticalAlignmentCenter;
titleLbl->ignoreTouch = true;
viewAddSubview(backHeader, titleLbl);
viewAddSubview(currentMusicSubView, backHeader);
currentY += 44;
CCView* subTabBar = viewWithFrame(ccRect(0, currentY, SCREEN_W, 40));
subTabBar->backgroundColor = color(0.1, 0.1, 0.14, 1.0);
int tabW = SCREEN_W / 2;
CCView* songsTab = viewWithFrame(ccRect(0, 0, tabW, 40));
songsTab->tag = TAG_MUSIC_ARTIST_TAB_SONGS;
CCLabel* songsLbl = labelWithFrame(ccRect(0, 0, tabW, 40));
songsLbl->text = ccs("Songs");
songsLbl->textColor = isArtistDetailShowingAlbums ? color(0.5, 0.5, 0.6, 1.0) : color(1, 1, 1, 1);
songsLbl->fontSize = 14;
songsLbl->textAlignment = CCTextAlignmentCenter;
songsLbl->textVerticalAlignment = CCTextVerticalAlignmentCenter;
songsLbl->ignoreTouch = true;
viewAddSubview(songsTab, songsLbl);
if (!isArtistDetailShowingAlbums) {
CCView* highlight = viewWithFrame(ccRect(0, 38, tabW, 2));
highlight->backgroundColor = color(0.2, 0.6, 1.0, 1.0);
viewAddSubview(songsTab, highlight);
}
viewAddSubview(subTabBar, songsTab);
CCView* albumsTab = viewWithFrame(ccRect(tabW, 0, tabW, 40));
albumsTab->tag = TAG_MUSIC_ARTIST_TAB_ALBUMS;
CCLabel* albumsLbl = labelWithFrame(ccRect(0, 0, tabW, 40));
albumsLbl->text = ccs("Albums");
albumsLbl->textColor = isArtistDetailShowingAlbums ? color(1, 1, 1, 1) : color(0.5, 0.5, 0.6, 1.0);
albumsLbl->fontSize = 14;
albumsLbl->textAlignment = CCTextAlignmentCenter;
albumsLbl->textVerticalAlignment = CCTextVerticalAlignmentCenter;
albumsLbl->ignoreTouch = true;
viewAddSubview(albumsTab, albumsLbl);
if (isArtistDetailShowingAlbums) {
CCView* highlight = viewWithFrame(ccRect(0, 38, tabW, 2));
highlight->backgroundColor = color(0.2, 0.6, 1.0, 1.0);
viewAddSubview(albumsTab, highlight);
}
viewAddSubview(subTabBar, albumsTab);
viewAddSubview(currentMusicSubView, subTabBar);
currentY += 40;
if (isArtistDetailShowingAlbums) {
// Draw Albums List
for (int i = 0; i < itemCount; i++) {
CCString* albumName = arrayObjectAtIndex(displayData, i);
CCView* rowView = viewWithFrame(ccRect(0, currentY, SCREEN_W, rowHeight));
rowView->tag = TAG_MUSIC_ALBUM_ROW_START + i;
if (i % 2 == 0) rowView->backgroundColor = color(0.1, 0.1, 0.15, 1.0);
CCLabel* nameLbl = labelWithFrame(ccRect(15, 0, SCREEN_W - 30, rowHeight));
// FIX: Copy string
nameLbl->text = copyCCString(albumName);
nameLbl->textColor = color(0.9, 0.9, 0.95, 1.0);
nameLbl->fontSize = 14;
nameLbl->textVerticalAlignment = CCTextVerticalAlignmentCenter;
nameLbl->ignoreTouch = true;
viewAddSubview(rowView, nameLbl);
viewAddSubview(currentMusicSubView, rowView);
currentY += rowHeight;
}
freeCCArray(displayData);
} else {
// Draw Songs List
for (int i = 0; i < arrayCount(globalMusicLibrary); i++) {
CCDictionary* track = arrayObjectAtIndex(globalMusicLibrary, i);
CCString* trackArtist = dictionaryObjectForKey(track, ccs("Artist"));
if (trackArtist && stringEqualsString(trackArtist, currentSelectedArtist)) {
CCString* trackTitle = dictionaryObjectForKey(track, ccs("Title"));
CCString* trackDuration = dictionaryObjectForKey(track, ccs("Duration"));
// FIX: Isolate strings
CCString* safeTitle = trackTitle ? copyCCString(trackTitle) : ccs("Unknown Track");
CCString* safeDuration = trackDuration ? copyCCString(trackDuration) : ccs("-:--");
CCView* rowView = viewWithFrame(ccRect(0, currentY, SCREEN_W, rowHeight));
rowView->tag = TAG_MUSIC_LIB_ROW_START + i;
if (i % 2 == 0) rowView->backgroundColor = color(0.1, 0.1, 0.15, 1.0);
CCLabel* titleLbl = labelWithFrame(ccRect(15, 0, SCREEN_W - 80, rowHeight));
titleLbl->text = safeTitle;
titleLbl->textColor = color(0.9, 0.9, 0.95, 1.0);
titleLbl->fontSize = 14;
titleLbl->textVerticalAlignment = CCTextVerticalAlignmentCenter;
titleLbl->ignoreTouch = true;
viewAddSubview(rowView, titleLbl);
CCLabel* durLbl = labelWithFrame(ccRect(SCREEN_W - 65, 0, 50, rowHeight));
durLbl->text = safeDuration;
durLbl->textColor = color(0.5, 0.5, 0.6, 1.0);
durLbl->fontSize = 12;
durLbl->textAlignment = CCTextAlignmentRight;
durLbl->textVerticalAlignment = CCTextVerticalAlignmentCenter;
durLbl->ignoreTouch = true;
viewAddSubview(rowView, durLbl);
viewAddSubview(currentMusicSubView, rowView);
currentY += rowHeight;
}
}
}
}
viewAddSubview(uiMusicContentView, currentMusicSubView);
}
void build_music_playlists_ui(void) {
if (!globalPlaylists) {
CCDictionary* savedPls = loadConfigFile(ccs("music/playlists.json"));
if (savedPls) {
CCArray* loadedPls = dictionaryObjectForKey(savedPls, ccs("playlists"));
if (loadedPls) globalPlaylists = loadedPls;
}
if (!globalPlaylists) globalPlaylists = array();
}
if (currentMusicSubView) {
viewRemoveFromSuperview(currentMusicSubView);
freeViewHierarchy(currentMusicSubView);
currentMusicSubView = NULL;
}
int rowHeight = 44;
int currentY = 0;
// ==========================================
// STATE A: MASTER PLAYLIST LIST
// ==========================================
if (currentSelectedPlaylist == NULL) {
int plCount = arrayCount(globalPlaylists);
totalDataPages = (plCount > 0) ? ((plCount + musicRowsPerPage - 1) / musicRowsPerPage) : 1;
if (currentDataPage >= totalDataPages) currentDataPage = totalDataPages - 1;
if (currentDataPage < 0) currentDataPage = 0;
int startIndex = currentDataPage * musicRowsPerPage;
int endIndex = startIndex + musicRowsPerPage;
if (endIndex > plCount) endIndex = plCount;
int totalCanvasHeight = 25 + (musicRowsPerPage * rowHeight);
currentMusicSubView = viewWithFrame(ccRect(0, 0, SCREEN_W, totalCanvasHeight));
currentMusicSubView->backgroundColor = color(0.08, 0.08, 0.12, 1.0);
// --- COLUMN HEADERS ---
CCView* headerRow = viewWithFrame(ccRect(0, currentY, SCREEN_W, 20));
CCLabel* headTitle = labelWithFrame(ccRect(15, 0, 200, 20));
headTitle->text = copyCCString(ccs("PLAYLIST NAME"));
headTitle->textColor = color(0.4, 0.4, 0.5, 1.0);
headTitle->fontSize = 10;
headTitle->ignoreTouch = true;
viewAddSubview(headerRow, headTitle);
viewAddSubview(currentMusicSubView, headerRow);
currentY += 25;
// --- PLAYLIST ROWS ---
for (int i = startIndex; i < endIndex; i++) {
CCDictionary* playlist = arrayObjectAtIndex(globalPlaylists, i);
CCString* plName = dictionaryObjectForKey(playlist, ccs("Name"));
CCArray* plTracks = dictionaryObjectForKey(playlist, ccs("Tracks"));
int trackCount = plTracks ? arrayCount(plTracks) : 0;
char countStr[16];
snprintf(countStr, sizeof(countStr), "%d Tracks", trackCount);
CCView* rowView = viewWithFrame(ccRect(0, currentY, SCREEN_W, rowHeight));
rowView->tag = TAG_MUSIC_PLAYLIST_ROW_START + i;
if (i % 2 == 0) rowView->backgroundColor = color(0.1, 0.1, 0.15, 1.0);
CCLabel* titleLbl = labelWithFrame(ccRect(15, 0, SCREEN_W - 100, rowHeight));
titleLbl->text = plName ? copyCCString(plName) : copyCCString(ccs("Unknown"));
titleLbl->textColor = color(0.9, 0.9, 0.95, 1.0);
titleLbl->fontSize = 14;
titleLbl->textVerticalAlignment = CCTextVerticalAlignmentCenter;
titleLbl->ignoreTouch = true;
viewAddSubview(rowView, titleLbl);
CCLabel* countLbl = labelWithFrame(ccRect(SCREEN_W - 85, 0, 70, rowHeight));
countLbl->text = copyCCString(ccs(countStr));
countLbl->textColor = color(0.5, 0.5, 0.6, 1.0);
countLbl->fontSize = 12;
countLbl->textAlignment = CCTextAlignmentRight;
countLbl->textVerticalAlignment = CCTextVerticalAlignmentCenter;
countLbl->ignoreTouch = true;
viewAddSubview(rowView, countLbl);
viewAddSubview(currentMusicSubView, rowView);
currentY += rowHeight;
}
}
// ==========================================
// STATE B: PLAYLIST DETAIL VIEW
// ==========================================
else {
CCDictionary* activePl = get_current_playlist_dict();
CCArray* plTracks = activePl ? dictionaryObjectForKey(activePl, ccs("Tracks")) : NULL;
int trackCount = plTracks ? arrayCount(plTracks) : 0;
totalDataPages = (trackCount > 0) ? ((trackCount + musicRowsPerPage - 1) / musicRowsPerPage) : 1;
if (currentDataPage >= totalDataPages) currentDataPage = totalDataPages - 1;
if (currentDataPage < 0) currentDataPage = 0;
int startIndex = currentDataPage * musicRowsPerPage;
int endIndex = startIndex + musicRowsPerPage;
if (endIndex > trackCount) endIndex = trackCount;
int headerHeight = 44 + 5; // Back button row
int totalCanvasHeight = headerHeight + (musicRowsPerPage * rowHeight);
currentMusicSubView = viewWithFrame(ccRect(0, 0, SCREEN_W, totalCanvasHeight));
currentMusicSubView->backgroundColor = color(0.08, 0.08, 0.12, 1.0);
// --- BACK BUTTON ROW ---
CCView* backHeader = viewWithFrame(ccRect(0, currentY, SCREEN_W, 44));
backHeader->backgroundColor = color(0.12, 0.12, 0.16, 1.0);
CCView* backBtn = viewWithFrame(ccRect(5, 5, 100, 34));
backBtn->tag = TAG_MUSIC_PL_BACK;
CCLabel* backLbl = labelWithFrame(ccRect(10, 0, 90, 34));
backLbl->text = copyCCString(ccs("< Playlists"));
backLbl->textColor = color(0.2, 0.6, 1.0, 1.0);
backLbl->fontSize = 14;
backLbl->textVerticalAlignment = CCTextVerticalAlignmentCenter;
backLbl->ignoreTouch = true;
viewAddSubview(backBtn, backLbl);
viewAddSubview(backHeader, backBtn);
CCLabel* titleLbl = labelWithFrame(ccRect(80, 0, SCREEN_W - 160, 44));
titleLbl->text = copyCCString(currentSelectedPlaylist);
titleLbl->textColor = color(1, 1, 1, 1);
titleLbl->fontSize = 16;
titleLbl->textAlignment = CCTextAlignmentCenter;
titleLbl->textVerticalAlignment = CCTextVerticalAlignmentCenter;
titleLbl->ignoreTouch = true;
viewAddSubview(backHeader, titleLbl);
viewAddSubview(currentMusicSubView, backHeader);
currentY += 44 + 5;
// --- TRACK ROWS ---
for (int i = startIndex; i < endIndex; i++) {
CCString* savedPath = arrayObjectAtIndex(plTracks, i);
// Cross-reference the master library to get Title/Artist
CCString* titleStr = ccs("Unknown");
CCString* durStr = ccs("-:--");
for (int j = 0; j < arrayCount(globalMusicLibrary); j++) {
CCDictionary* libTrack = arrayObjectAtIndex(globalMusicLibrary, j);
CCString* libPath = dictionaryObjectForKey(libTrack, ccs("Path"));
if (libPath && stringEqualsString(libPath, savedPath)) {
CCString* t = dictionaryObjectForKey(libTrack, ccs("Title"));
CCString* d = dictionaryObjectForKey(libTrack, ccs("Duration"));
if (t) titleStr = t;
if (d) durStr = d;
break;
}
}
CCView* rowView = viewWithFrame(ccRect(0, currentY, SCREEN_W, rowHeight));
// We tag it with the playlist track index! (Requires a new start tag later for playback)
rowView->tag = TAG_MUSIC_LIB_ROW_START + i;
if (i % 2 == 0) rowView->backgroundColor = color(0.1, 0.1, 0.15, 1.0);
CCLabel* titleLbl = labelWithFrame(ccRect(15, 0, SCREEN_W - 80, rowHeight));
titleLbl->text = copyCCString(titleStr);
titleLbl->textColor = color(0.9, 0.9, 0.95, 1.0);
titleLbl->fontSize = 14;
titleLbl->textVerticalAlignment = CCTextVerticalAlignmentCenter;
titleLbl->ignoreTouch = true;
viewAddSubview(rowView, titleLbl);
CCLabel* durLbl = labelWithFrame(ccRect(SCREEN_W - 65, 0, 50, rowHeight));
durLbl->text = copyCCString(durStr);
durLbl->textColor = color(0.5, 0.5, 0.6, 1.0);
durLbl->fontSize = 12;
durLbl->textAlignment = CCTextAlignmentRight;
durLbl->textVerticalAlignment = CCTextVerticalAlignmentCenter;
durLbl->ignoreTouch = true;
viewAddSubview(rowView, durLbl);
viewAddSubview(currentMusicSubView, rowView);
currentY += rowHeight;
}
}
viewAddSubview(uiMusicContentView, currentMusicSubView);
update_music_pagination_ui();
}
void show_new_playlist_dialog(void) {
if (uiNewPlaylistDialog) {
viewRemoveFromSuperview(uiNewPlaylistDialog);
freeViewHierarchy(uiNewPlaylistDialog);
uiNewPlaylistDialog = NULL;
}
// Placed near the top so the OS keyboard has room below it
uiNewPlaylistDialog = viewWithFrame(ccRect(20, 30, 280, 130));
uiNewPlaylistDialog->backgroundColor = color(0.18, 0.18, 0.18, 1.0);
layerSetCornerRadius(uiNewPlaylistDialog->layer, 8.0);
// Title
CCLabel* title = labelWithFrame(ccRect(10, 10, 260, 20));
title->text = copyCCString(ccs("Create New Playlist"));
title->fontSize = 16;
title->textColor = color(1.0, 1.0, 1.0, 1.0);
title->ignoreTouch = true;
viewAddSubview(uiNewPlaylistDialog, title);
// Pseudo-Text Field (The Target Label for the OS Keyboard)
uiNewPlaylistInput = labelWithFrame(ccRect(10, 40, 260, 30));
uiNewPlaylistInput->text = copyCCString(ccs("New Playlist"));
uiNewPlaylistInput->fontSize = 16;
uiNewPlaylistInput->textColor = color(0.0, 0.0, 0.0, 1.0);
uiNewPlaylistInput->textVerticalAlignment = CCTextVerticalAlignmentCenter;
uiNewPlaylistInput->ignoreTouch = true;
// Give it a white background so it visually looks like an input box
uiNewPlaylistInput->view->backgroundColor = color(1.0, 1.0, 1.0, 1.0);
viewAddSubview(uiNewPlaylistDialog, uiNewPlaylistInput);
// Cancel Button
CCView* btnCancel = viewWithFrame(ccRect(10, 85, 120, 35));
btnCancel->backgroundColor = color(0.4, 0.4, 0.4, 1.0);
btnCancel->tag = TAG_MUSIC_PL_CREATE_DIALOG_CANCEL;
layerSetCornerRadius(btnCancel->layer, 4.0);
CCLabel* lblCancel = labelWithFrame(ccRect(0, 0, 120, 35));
lblCancel->text = copyCCString(ccs("Cancel"));
lblCancel->fontSize = 16;
lblCancel->textAlignment = CCTextAlignmentCenter;
lblCancel->textVerticalAlignment = CCTextVerticalAlignmentCenter;
lblCancel->textColor = color(1.0, 1.0, 1.0, 1.0);
lblCancel->ignoreTouch = true;
viewAddSubview(btnCancel, lblCancel);
viewAddSubview(uiNewPlaylistDialog, btnCancel);
// Create Button
CCView* btnCreate = viewWithFrame(ccRect(150, 85, 120, 35));
btnCreate->backgroundColor = color(0.0, 0.47, 1.0, 1.0); // iOS Blue
btnCreate->tag = TAG_MUSIC_PL_CREATE_DIALOG_CONFIRM;
layerSetCornerRadius(btnCreate->layer, 4.0);
CCLabel* lblCreate = labelWithFrame(ccRect(0, 0, 120, 35));
lblCreate->text = copyCCString(ccs("Create"));
lblCreate->fontSize = 16;
lblCreate->textAlignment = CCTextAlignmentCenter;
lblCreate->textVerticalAlignment = CCTextVerticalAlignmentCenter;
lblCreate->textColor = color(1.0, 1.0, 1.0, 1.0);
lblCreate->ignoreTouch = true;
viewAddSubview(btnCreate, lblCreate);
viewAddSubview(uiNewPlaylistDialog, btnCreate);
viewAddSubview(mainWindowView, uiNewPlaylistDialog);
// Summon the OS Keyboard and point it at our Label
setup_keyboard_ui(uiNewPlaylistInput); // Uncomment to link to your exact keyboard function
}
void handle_music_touch(int x, int y, int touchState) {
// ==========================================
// FINGER IS TOUCHING THE SCREEN (DOWN / HELD)
// ==========================================
if (touchState == 1) {
FreeOSLogI("handle_music_touch", "Starting setup_files_ui App");
if (!is_pressing) {
// First frame of touch down
is_pressing = true;
long_press_fired = false; // Reset the flag for this new tap
touchStartTime = esp_timer_get_time() / 1000;
touchStartX = x;
touchStartY = y;
} else {
// Finger is held down or dragging
if (abs(x - touchStartX) > 15 || abs(y - touchStartY) > 15) {
touchStartTime = 0; // Dragged too far, cancel the tap
}
// --- REAL-TIME LONG PRESS CHECK ---
if (touchStartTime != 0 && !long_press_fired && !uiMusicContextMenu) {
uint64_t now = esp_timer_get_time() / 1000;
if ((now - touchStartTime) >= 1000) { // EXACTLY 1 SECOND
long_press_fired = true;
CCView* target = find_subview_at_point_recursive(mainWindowView, touchStartX, touchStartY);
// 1. Did they long-press a track row in the library?
if (target && target->tag >= TAG_MUSIC_LIB_ROW_START && target->tag < TAG_MUSIC_LIB_CHECK_START) {
int trackIndex = target->tag - TAG_MUSIC_LIB_ROW_START;
show_music_context_menu(MusicContextTypeTrack, trackIndex, touchStartX, touchStartY);
update_full_ui();
}
// TODO: Add playlist row long press detection here
}
}
}
}
// ==========================================
// FINGER LIFTED (RELEASED)
// ==========================================
if (touchState == 0) {
if (!is_pressing) return; // Ignore if we are already released
is_pressing = false; // Reset state for next tap
// If they scrolled, OR if the 1-second long press already fired, DO NOTHING on release!
if (touchStartTime == 0 || long_press_fired) {
long_press_fired = false; // Clean up
return;
}
// --- If we reach here, it was a valid SHORT TAP! ---
int tapX = touchStartX;
int tapY = touchStartY;
// 0. Handle Context Menu Taps (Highest Z-Index)
if (uiMusicContextMenu) {
CCView* target = find_subview_at_point_recursive(uiMusicContextMenu, tapX, tapY);
if (target && target->tag >= TAG_MUSIC_CTX_TRACK_PLAY && target->tag <= TAG_MUSIC_CTX_TRACK_DEL) {
int baseTag = target->tag % 100000;
int trackIndex = target->tag / 100000;
CCDictionary* trackData = arrayObjectAtIndex(globalMusicLibrary, trackIndex);
if (baseTag == TAG_MUSIC_CTX_TRACK_PLAY) {
CCString* path = dictionaryObjectForKey(trackData, ccs("Path"));
// trigger_i2s_playback(path);
}
}
// Detach and free the context menu regardless of where they tapped
viewRemoveFromSuperview(uiMusicContextMenu);
freeViewHierarchy(uiMusicContextMenu);
uiMusicContextMenu = NULL;
update_full_ui();
return; // Block underneath UI from receiving this tap
}
// 0.5 Handle Top-Left Menu Overlay Taps
if (uiMusicMenuOverlay) {
CCView* target = find_subview_at_point(uiMusicMenuOverlay, tapX, tapY);
if (target) {
if (target->tag == TAG_MUSIC_SCAN_BTN) {
// Destroy the menu first
viewRemoveFromSuperview(uiMusicMenuOverlay);
freeViewHierarchy(uiMusicMenuOverlay);
uiMusicMenuOverlay = NULL;
// Run the heavy SD card scan
music_trigger_full_scan();
// Rebuild the library UI to show new tracks
build_music_library_ui();
update_full_ui();
return;
}
else if (target->tag == TAG_MUSIC_PL_CREATE) {
// Destroy the menu overlay first
viewRemoveFromSuperview(uiMusicMenuOverlay);
freeViewHierarchy(uiMusicMenuOverlay);
uiMusicMenuOverlay = NULL;
// Trigger the dialog!
show_new_playlist_dialog();
update_full_ui();
return;
}
else if (target->tag == TAG_MUSIC_PL_ADD_ITEM) {
viewRemoveFromSuperview(uiMusicMenuOverlay);
freeViewHierarchy(uiMusicMenuOverlay);
uiMusicMenuOverlay = NULL;
isMusicLibrarySelectionMode = true;
currentDataPage = 0;
build_music_library_ui(); // Switch Canvas to Library Mode!
update_full_ui();
return;
}
}
// If they clicked the background (dismiss), just destroy the menu
viewRemoveFromSuperview(uiMusicMenuOverlay);
freeViewHierarchy(uiMusicMenuOverlay);
uiMusicMenuOverlay = NULL;
update_full_ui();
return; // Block underneath UI from receiving this tap
}
// --- 0.7 Handle New Playlist Dialog Taps ---
if (uiNewPlaylistDialog) {
CCView* target = find_subview_at_point(uiNewPlaylistDialog, tapX, tapY);
if (target) {
if (target->tag == TAG_MUSIC_PL_CREATE_DIALOG_CANCEL) {
// Close dialog and keyboard
hide_keyboard_ui();
viewRemoveFromSuperview(uiNewPlaylistDialog);
freeViewHierarchy(uiNewPlaylistDialog);
uiNewPlaylistDialog = NULL;
// Add your keyboard dismiss function here
update_full_ui();
return;
}
else if (target->tag == TAG_MUSIC_PL_CREATE_DIALOG_CONFIRM) {
// 1. Grab the text from the keyboard label
hide_keyboard_ui();
CCString* finalName = uiNewPlaylistInput->text;
// 2. Create the new Playlist Dictionary
CCDictionary* newPlaylist = dictionary();
dictionarySetObjectForKey(newPlaylist, copyCCString(finalName), ccs("Name"));
dictionarySetObjectForKey(newPlaylist, array(), ccs("Tracks")); // Empty tracks array
// 3. Add to globals and save to SD card
if (!globalPlaylists) globalPlaylists = array();
arrayAddObject(globalPlaylists, newPlaylist);
CCDictionary* wrapper = dictionary();
dictionarySetObjectForKey(wrapper, globalPlaylists, ccs("playlists"));
saveConfigFile(ccs("music/playlists.json"), wrapper);
// 4. Cleanup UI
viewRemoveFromSuperview(uiNewPlaylistDialog);
freeViewHierarchy(uiNewPlaylistDialog);
uiNewPlaylistDialog = NULL;
// teardown_keyboard_ui();
// 5. Rebuild the list to show the new playlist
build_music_playlists_ui();
update_full_ui();
return;
}
}
return; // Block underneath UI from receiving taps while dialog is open
}
// 1. Standard Short Tap Routing
CCView* tappedView = find_subview_at_point_recursive(mainWindowView, tapX, tapY);
FreeOSLogI("handle_music_touch", "%d %d %d %d", (tappedView == NULL)? 1 : 0, tappedView->tag, tapX, tapY);
if (!tappedView) return;
switch (tappedView->tag) {
case TAG_MUSIC_NAV_LIBRARY:
currentMusicTab = MusicTabLibrary;
currentDataPage = 0; // Reset pagination!
isMusicLibrarySelectionMode = false;
if (currentSelectedArtist) {
freeCCString(currentSelectedArtist);
currentSelectedArtist = NULL;
}
build_music_library_ui();
update_full_ui();
break;
case TAG_MUSIC_SCAN_BTN:
{
// 1. Change the button text so the user knows it's working
CCView* btn = find_subview_at_point_recursive(mainWindowView, tapX, tapY);
if (btn && btn->subviews->count > 0) {
CCLabel* lbl = (CCLabel*)arrayObjectAtIndex(btn->subviews, 0);
lbl->text = ccs("Scanning drives...");
update_full_ui(); // Force the screen to draw the new text
}
// 2. Run the heavy SD card scan
music_trigger_full_scan();
// 3. Rebuild the list with the new songs
build_music_library_ui();
update_full_ui();
}
break;
case TAG_MUSIC_NAV_PLAYLISTS:
currentMusicTab = MusicTabPlaylists;
currentDataPage = 0;
build_music_playlists_ui(); // Route to the new builder!
update_full_ui();
break;
case TAG_MUSIC_PL_CREATE:
// Triggered from the "Create a New Playlist" overlay menu
FreeOSLogI("TAG_MUSIC_PL_CREATE", "TAG_MUSIC_PL_CREATE");
// Destroy the menu overlay first
if (uiMusicMenuOverlay) {
viewRemoveFromSuperview(uiMusicMenuOverlay);
freeViewHierarchy(uiMusicMenuOverlay);
uiMusicMenuOverlay = NULL;
}
show_new_playlist_dialog();
update_full_ui();
break;
case TAG_MUSIC_NAV_ARTISTS:
currentMusicTab = MusicTabArtists;
currentDataPage = 0;
if (currentSelectedArtist) {
freeCCString(currentSelectedArtist);
currentSelectedArtist = NULL;
}
build_music_artists_ui();
update_full_ui();
break;
case TAG_MUSIC_ARTIST_BACK:
if (currentSelectedArtist) {
freeCCString(currentSelectedArtist);
currentSelectedArtist = NULL;
}
build_music_artists_ui();
update_full_ui();
break;
case TAG_MUSIC_ARTIST_TAB_SONGS:
isArtistDetailShowingAlbums = false;
build_music_artists_ui();
update_full_ui();
break;
case TAG_MUSIC_ARTIST_TAB_ALBUMS:
isArtistDetailShowingAlbums = true;
build_music_artists_ui();
update_full_ui();
break;
case TAG_MUSIC_TOGGLE_LOOP:
isMusicLooping = !isMusicLooping;
uiMusicLoopIcon->alpha = isMusicLooping ? 1.0f : 0.3f;
// updateDisplayArea(uiMusicLoopIcon->frame);
break;
case TAG_MUSIC_TOGGLE_SHUFFLE:
isMusicShuffling = !isMusicShuffling;
uiMusicShuffleIcon->alpha = isMusicShuffling ? 1.0f : 0.3f;
// updateDisplayArea(uiMusicShuffleIcon->frame);
break;
case TAG_MUSIC_MENU_BTN:
show_music_menu_overlay();
update_full_ui();
break;
case TAG_MUSIC_PAGE_PREV:
if (currentDataPage > 0) {
currentDataPage--;
if (currentMusicTab == MusicTabLibrary) build_music_library_ui();
else if (currentMusicTab == MusicTabArtists) build_music_artists_ui();
// else if (currentMusicTab == MusicTabPlaylists) build_music_playlists_ui();
update_full_ui();
}
break;
case TAG_MUSIC_PAGE_NEXT:
if (currentDataPage < totalDataPages - 1) {
currentDataPage++;
if (currentMusicTab == MusicTabLibrary) build_music_library_ui();
else if (currentMusicTab == MusicTabArtists) build_music_artists_ui();
// else if (currentMusicTab == MusicTabPlaylists) build_music_playlists_ui();
update_full_ui();
}
break;
// 2. In the standard short tap switch (tappedView->tag):
case TAG_MUSIC_PLAYLIST_ROW_START ... (TAG_MUSIC_PLAYLIST_ROW_START + 9999):
{
int plIndex = tappedView->tag - TAG_MUSIC_PLAYLIST_ROW_START;
CCDictionary* pl = arrayObjectAtIndex(globalPlaylists, plIndex);
CCString* plName = dictionaryObjectForKey(pl, ccs("Name"));
if (currentSelectedPlaylist) freeCCString(currentSelectedPlaylist);
currentSelectedPlaylist = copyCCString(plName);
currentDataPage = 0;
build_music_playlists_ui();
update_full_ui();
}
break;
case TAG_MUSIC_PL_BACK:
case TAG_MUSIC_PL_ADD_DONE:
// This button brings us back from Selection Mode OR Detail View to the previous list
isMusicLibrarySelectionMode = false;
if (tappedView->tag == TAG_MUSIC_PL_BACK) {
if (currentSelectedPlaylist) freeCCString(currentSelectedPlaylist);
currentSelectedPlaylist = NULL;
}
currentDataPage = 0;
build_music_playlists_ui(); // Always returns to Playlists
update_full_ui();
break;
// 3. Modifying the Checkbox Tap Logic (Dynamic Saving)
case TAG_MUSIC_LIB_CHECK_START ... (TAG_MUSIC_LIB_CHECK_START + 9999):
{
int trackIndex = tappedView->tag - TAG_MUSIC_LIB_CHECK_START;
CCDictionary* trackData = arrayObjectAtIndex(globalMusicLibrary, trackIndex);
CCString* trackPath = dictionaryObjectForKey(trackData, ccs("Path"));
CCDictionary* activePl = get_current_playlist_dict();
if (activePl && trackPath) {
CCArray* tracks = dictionaryObjectForKey(activePl, ccs("Tracks"));
if (!tracks) {
tracks = array();
dictionarySetObjectForKey(activePl, tracks, ccs("Tracks"));
}
// Add or Remove
bool found = false;
for (int i = 0; i < arrayCount(tracks); i++) {
CCString* existing = arrayObjectAtIndex(tracks, i);
if (stringEqualsString(existing, trackPath)) {
arrayRemoveObject(tracks, existing); // Untick
found = true;
break;
}
}
if (!found) arrayAddObject(tracks, copyCCString(trackPath)); // Tick
// Save instantly to JSON!
CCDictionary* wrapper = dictionary();
dictionarySetObjectForKey(wrapper, globalPlaylists, ccs("playlists"));
saveConfigFile(ccs("music/playlists.json"), wrapper);
// Visually toggle without full redraw
tappedView->backgroundColor = found ? color(0.08, 0.08, 0.12, 1.0) : color(0.2, 0.6, 1.0, 1.0);
// updateDisplayArea(tappedView->frame);
}
}
break;
}
// --- Dynamic List Interactions ---
if (tappedView->tag >= TAG_MUSIC_LIB_ROW_START && tappedView->tag < TAG_MUSIC_CTX_BG) {
// 1. Determine which track was tapped, regardless of whether they hit the row or the tiny checkbox
int trackIndex = -1;
if (tappedView->tag >= TAG_MUSIC_LIB_CHECK_START) {
trackIndex = tappedView->tag - TAG_MUSIC_LIB_CHECK_START;
} else {
trackIndex = tappedView->tag - TAG_MUSIC_LIB_ROW_START;
}
// 2. SELECTION MODE ROUTING (Add/Remove from Playlist)
if (isMusicLibrarySelectionMode) {
CCDictionary* trackData = arrayObjectAtIndex(globalMusicLibrary, trackIndex);
CCString* trackPath = dictionaryObjectForKey(trackData, ccs("Path"));
CCDictionary* activePl = get_current_playlist_dict();
if (activePl && trackPath) {
CCArray* tracks = dictionaryObjectForKey(activePl, ccs("Tracks"));
if (!tracks) {
tracks = array();
dictionarySetObjectForKey(activePl, tracks, ccs("Tracks"));
}
// Toggle Logic: Add or Remove
bool found = false;
for (int i = 0; i < arrayCount(tracks); i++) {
CCString* existing = arrayObjectAtIndex(tracks, i);
if (stringEqualsString(existing, trackPath)) {
// It's already in the playlist -> Remove it
arrayRemoveObject(tracks, existing);
found = true;
break;
}
}
// Not in the playlist -> Add it
if (!found) {
arrayAddObject(tracks, copyCCString(trackPath));
}
// Save instantly to JSON so state is permanently locked
CCDictionary* wrapper = dictionary();
dictionarySetObjectForKey(wrapper, globalPlaylists, ccs("playlists"));
saveConfigFile(ccs("music/playlists.json"), wrapper);
// Force a UI rebuild so the checkbox visually updates
// (This is much safer than hunting for the checkbox pointer!)
build_music_library_ui();
update_full_ui();
}
return;
}
// 3. NORMAL MODE ROUTING (Play the Song)
else {
if (tappedView->tag >= TAG_MUSIC_LIB_ROW_START && tappedView->tag < TAG_MUSIC_LIB_CHECK_START) {
CCDictionary* trackData = arrayObjectAtIndex(globalMusicLibrary, trackIndex);
CCString* path = dictionaryObjectForKey(trackData, ccs("Path"));
// trigger_i2s_playback(path);
printf("PLAYING TRACK: %s\n", cStringOfString(path));
}
}
}
else if (tappedView->tag >= TAG_MUSIC_ARTIST_ROW_START && tappedView->tag < TAG_MUSIC_ALBUM_ROW_START) {
int artistIndex = tappedView->tag - TAG_MUSIC_ARTIST_ROW_START;
CCArray* uniqueArtists = music_get_unique_artists();
CCString* tappedArtist = arrayObjectAtIndex(uniqueArtists, artistIndex);
currentSelectedArtist = copyCCString(tappedArtist);
freeCCArray(uniqueArtists);
build_music_artists_ui();
update_full_ui();
}
else if (tappedView->tag >= TAG_MUSIC_LIB_CHECK_START && tappedView->tag < TAG_MUSIC_CTX_BG) {
if (tappedView->backgroundColor->r < 0.5) {
tappedView->backgroundColor = color(0.2, 0.6, 1.0, 1.0); // Active
} else {
tappedView->backgroundColor = color(0.08, 0.08, 0.12, 1.0); // Inactive
}
// updateDisplayArea(tappedView->frame);
}
}
}