This is an automated email from the git hooks/post-receive script.
git pushed a commit to branch main
in repository eradio.
View the commit online.
commit 66eb83100f8491361a8cd1d0f09ce46948beb5be
Author: politebot <[email protected]>
AuthorDate: Fri Oct 10 18:07:52 2025 -0500
faves
---
src/Makefile.am | 2 +-
src/appdata.h | 4 +
src/favorites.c | 257 +++++++++++++++++++++++++++++++++++++++++++++++++++++
src/favorites.h | 24 +++++
src/http.c | 2 +
src/main.c | 4 +
src/station_list.c | 53 ++++++++++-
src/ui.c | 17 ++++
8 files changed, 361 insertions(+), 2 deletions(-)
diff --git a/src/Makefile.am b/src/Makefile.am
index 6005fc8..d0da069 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -1,6 +1,6 @@
bin_PROGRAMS = eradio
-eradio_SOURCES = main.c ui.c radio_player.c station_list.c http.c
+eradio_SOURCES = main.c ui.c radio_player.c station_list.c http.c favorites.c
eradio_CFLAGS = $(EFL_CFLAGS) $(LIBXML_CFLAGS)
eradio_LDADD = $(EFL_LIBS) $(LIBXML_LIBS)
\ No newline at end of file
diff --git a/src/appdata.h b/src/appdata.h
index b7706ce..8531af1 100644
--- a/src/appdata.h
+++ b/src/appdata.h
@@ -10,6 +10,7 @@ typedef struct _Station
const char *url;
const char *favicon;
const char *stationuuid;
+ Eina_Bool favorite;
} Station;
typedef struct _AppData
@@ -24,4 +25,7 @@ typedef struct _AppData
Evas_Object *search_btn;
Eina_List *stations;
Eina_Bool playing;
+ Eina_Hash *favorites;
+ Eina_List *favorites_stations;
+ Evas_Object *favorites_btn;
} AppData;
diff --git a/src/favorites.c b/src/favorites.c
new file mode 100644
index 0000000..87749cf
--- /dev/null
+++ b/src/favorites.c
@@ -0,0 +1,257 @@
+#include <libxml/parser.h>
+#include <libxml/tree.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <errno.h>
+#include <string.h>
+#include <stdlib.h>
+
+#include "favorites.h"
+
+typedef struct {
+ char *key; // uuid or url
+ char *uuid;
+ char *url;
+ char *name;
+} FavEntry;
+
+static void _fav_entry_free(void *data)
+{
+ FavEntry *e = data;
+ if (!e) return;
+ free(e->key);
+ free(e->uuid);
+ free(e->url);
+ free(e->name);
+ free(e);
+}
+
+static char *
+_favorites_dir_path(void)
+{
+ const char *home = getenv("HOME");
+ if (!home) return NULL;
+ size_t len = strlen(home) + strlen("/.config/eradio") + 1;
+ char *p = malloc(len);
+ if (!p) return NULL;
+ snprintf(p, len, "%s/.config/eradio", home);
+ return p;
+}
+
+static char *
+_favorites_file_path(void)
+{
+ const char *home = getenv("HOME");
+ if (!home) return NULL;
+ size_t len = strlen(home) + strlen("/.config/eradio/favorites.xml") + 1;
+ char *p = malloc(len);
+ if (!p) return NULL;
+ snprintf(p, len, "%s/.config/eradio/favorites.xml", home);
+ return p;
+}
+
+static Eina_Bool
+_ensure_dir_exists(const char *path)
+{
+ struct stat st;
+ if (stat(path, &st) == 0)
+ return S_ISDIR(st.st_mode) ? EINA_TRUE : EINA_FALSE;
+
+ if (errno == ENOENT)
+ {
+ if (mkdir(path, 0700) == 0)
+ return EINA_TRUE;
+ }
+ return EINA_FALSE;
+}
+
+void
+favorites_init(AppData *ad)
+{
+ if (!ad->favorites)
+ ad->favorites = eina_hash_string_superfast_new(_fav_entry_free);
+}
+
+static void
+_favorites_hash_add_entry(AppData *ad, const char *key, const char *uuid, const char *url, const char *name)
+{
+ if (!key || !key[0]) return;
+ FavEntry *e = calloc(1, sizeof(FavEntry));
+ if (!e) return;
+ e->key = key ? strdup(key) : NULL;
+ e->uuid = uuid ? strdup(uuid) : NULL;
+ e->url = "" ? strdup(url) : NULL;
+ e->name = name ? strdup(name) : NULL;
+ eina_hash_add(ad->favorites, e->key, e);
+}
+
+void
+favorites_shutdown(AppData *ad)
+{
+ if (!ad->favorites) return;
+ eina_hash_free(ad->favorites);
+ ad->favorites = NULL;
+}
+
+void
+favorites_load(AppData *ad)
+{
+ char *dir = _favorites_dir_path();
+ char *path = _favorites_file_path();
+ if (!dir || !path)
+ goto end;
+
+ _ensure_dir_exists(dir);
+
+ xmlDocPtr doc = xmlParseFile(path);
+ if (!doc)
+ goto end;
+
+ xmlNodePtr root = xmlDocGetRootElement(doc);
+ for (xmlNodePtr cur = root ? root->children : NULL; cur; cur = cur->next)
+ {
+ if (cur->type != XML_ELEMENT_NODE) continue;
+ if (xmlStrcmp(cur->name, (xmlChar *)"station") != 0) continue;
+
+ xmlChar *uuid = xmlGetProp(cur, (xmlChar *)"uuid");
+ xmlChar *url = "" (xmlChar *)"url");
+ xmlChar *name = xmlGetProp(cur, (xmlChar *)"name");
+ const char *key = NULL;
+ if (uuid && uuid[0]) key = (const char *)uuid;
+ else if (url && url[0]) key = (const char *)url;
+ _favorites_hash_add_entry(ad, key, (const char *)uuid, (const char *)url, (const char *)name);
+ if (uuid) xmlFree(uuid);
+ if (url) xmlFree(url);
+ if (name) xmlFree(name);
+ }
+
+ xmlFreeDoc(doc);
+
+end:
+ if (dir) free(dir);
+ if (path) free(path);
+}
+
+void
+favorites_apply_to_stations(AppData *ad)
+{
+ Eina_List *l;
+ Station *st;
+ EINA_LIST_FOREACH(ad->stations, l, st)
+ {
+ const char *key = st->stationuuid ? st->stationuuid : st->url;
+ st->favorite = EINA_FALSE;
+ if (key && eina_hash_find(ad->favorites, key))
+ st->favorite = EINA_TRUE;
+ }
+}
+
+static Eina_Bool _favorites_save_cb(const Eina_Hash *hash EINA_UNUSED, const void *key EINA_UNUSED, void *data, void *fdata)
+{
+ xmlNodePtr root = fdata;
+ FavEntry *e = data;
+ if (!e) return EINA_TRUE;
+ if ((!e->uuid || !e->uuid[0]) && (!e->url || !e->url[0]))
+ return EINA_TRUE;
+ xmlNodePtr sn = xmlNewChild(root, NULL, (xmlChar *)"station", NULL);
+ if (e->uuid && e->uuid[0]) xmlNewProp(sn, (xmlChar *)"uuid", (xmlChar *)e->uuid);
+ if (e->name && e->name[0]) xmlNewProp(sn, (xmlChar *)"name", (xmlChar *)e->name);
+ if (e->url && e->url[0]) xmlNewProp(sn, (xmlChar *)"url", (xmlChar *)e->url);
+ return EINA_TRUE;
+}
+
+void favorites_save(AppData *ad)
+{
+ char *dir = _favorites_dir_path();
+ char *path = _favorites_file_path();
+ if (!dir || !path)
+ goto end;
+
+ if (!_ensure_dir_exists(dir))
+ goto end;
+
+ size_t tmplen = strlen(path) + 5;
+ char *tmp = malloc(tmplen);
+ if (!tmp) goto end;
+ snprintf(tmp, tmplen, "%s.tmp", path);
+
+ xmlDocPtr doc = xmlNewDoc((xmlChar *)"1.0");
+ xmlNodePtr root = xmlNewNode(NULL, (xmlChar *)"favorites");
+ xmlNewProp(root, (xmlChar *)"version", (xmlChar *)"1");
+ xmlDocSetRootElement(doc, root);
+
+ eina_hash_foreach(ad->favorites, _favorites_save_cb, root);
+
+ xmlSaveFormatFileEnc(tmp, doc, "UTF-8", 1);
+ xmlFreeDoc(doc);
+
+ rename(tmp, path);
+ free(tmp);
+
+end:
+ if (dir) free(dir);
+ if (path) free(path);
+}
+
+void favorites_set(AppData *ad, Station *st, Eina_Bool on)
+{
+ if (!ad || !st) return;
+ const char *key = st->stationuuid ? st->stationuuid : st->url;
+ if (!key || !key[0]) return;
+ if (on)
+ {
+ FavEntry *existing = eina_hash_find(ad->favorites, key);
+ if (existing)
+ {
+ // Update metadata
+ if (st->name) {
+ free(existing->name);
+ existing->name = strdup(st->name);
+ }
+ if (st->url) {
+ free(existing->url);
+ existing->url = ""
+ }
+ if (st->stationuuid) {
+ free(existing->uuid);
+ existing->uuid = strdup(st->stationuuid);
+ }
+ }
+ else
+ {
+ _favorites_hash_add_entry(ad, key, st->stationuuid, st->url, st->name);
+ }
+ }
+ else
+ {
+ FavEntry *existing = eina_hash_find(ad->favorites, key);
+ if (existing)
+ eina_hash_del(ad->favorites, key, existing);
+ }
+}
+
+static Eina_Bool _favorites_rebuild_cb(const Eina_Hash *hash EINA_UNUSED, const void *key EINA_UNUSED, void *data, void *fdata)
+{
+ AppData *ad = fdata;
+ FavEntry *e = data;
+ if (!ad || !e) return EINA_TRUE;
+ Station *st = calloc(1, sizeof(Station));
+ if (!st) return EINA_TRUE;
+ if (e->name) st->name = eina_stringshare_add(e->name);
+ if (e->url) st->url = ""
+ if (e->uuid) st->stationuuid = eina_stringshare_add(e->uuid);
+ st->favorite = EINA_TRUE;
+ ad->favorites_stations = eina_list_append(ad->favorites_stations, st);
+ return EINA_TRUE;
+}
+
+void favorites_rebuild_station_list(AppData *ad)
+{
+ // free previous list items (shallow as per existing patterns)
+ if (ad->favorites_stations)
+ {
+ eina_list_free(ad->favorites_stations);
+ ad->favorites_stations = NULL;
+ }
+ eina_hash_foreach(ad->favorites, _favorites_rebuild_cb, ad);
+}
\ No newline at end of file
diff --git a/src/favorites.h b/src/favorites.h
new file mode 100644
index 0000000..23b166f
--- /dev/null
+++ b/src/favorites.h
@@ -0,0 +1,24 @@
+#pragma once
+
+#include "appdata.h"
+
+// Initialize favorites storage (hash) in AppData
+void favorites_init(AppData *ad);
+
+// Load favorites from XML (~/.config/eradio/favorites.xml) into AppData
+void favorites_load(AppData *ad);
+
+// Apply loaded favorites to current stations list (sets Station.favorite)
+void favorites_apply_to_stations(AppData *ad);
+
+// Save current favorites (from stations list) to XML
+void favorites_save(AppData *ad);
+
+// Shutdown and free any favorites-related resources
+void favorites_shutdown(AppData *ad);
+
+// Update favorites hash from a station toggle (add/remove)
+void favorites_set(AppData *ad, Station *st, Eina_Bool on);
+
+// Rebuild an in-memory list of favorite stations from the favorites hash
+void favorites_rebuild_station_list(AppData *ad);
\ No newline at end of file
diff --git a/src/http.c b/src/http.c
index fc23964..e59bed8 100644
--- a/src/http.c
+++ b/src/http.c
@@ -4,6 +4,7 @@
#include "http.h"
#include "station_list.h"
+#include "favorites.h"
typedef enum _Download_Type
{
@@ -234,6 +235,7 @@ _handle_station_list_complete(Ecore_Con_Event_Url_Complete *ev)
ad->stations = eina_list_append(ad->stations, st);
}
+ favorites_apply_to_stations(ad);
station_list_populate(ad, ad->stations);
xmlXPathFreeObject(xpathObj);
diff --git a/src/main.c b/src/main.c
index 4edf67d..5545215 100644
--- a/src/main.c
+++ b/src/main.c
@@ -2,6 +2,7 @@
#include "ui.h"
#include "radio_player.h"
#include "http.h"
+#include "favorites.h"
EAPI_MAIN int
elm_main(int argc, char **argv)
@@ -11,6 +12,8 @@ elm_main(int argc, char **argv)
elm_policy_set(ELM_POLICY_QUIT, ELM_POLICY_QUIT_LAST_WINDOW_CLOSED);
ui_create(&ad);
+ favorites_init(&ad);
+ favorites_load(&ad);
http_init(&ad);
radio_player_init(&ad);
@@ -18,6 +21,7 @@ elm_main(int argc, char **argv)
http_shutdown();
radio_player_shutdown();
+ favorites_shutdown(&ad);
return 0;
}
diff --git a/src/station_list.c b/src/station_list.c
index f08c4a3..7ccab08 100644
--- a/src/station_list.c
+++ b/src/station_list.c
@@ -1,6 +1,9 @@
#include "station_list.h"
#include "radio_player.h"
#include "http.h"
+#include "favorites.h"
+
+static void _favorite_btn_clicked_cb(void *data, Evas_Object *obj, void *event_info);
static void
_station_click_counter_request(Station *st)
@@ -49,9 +52,27 @@ station_list_populate(AppData *ad, Eina_List *stations)
{
Evas_Object *icon = elm_icon_add(ad->win);
elm_icon_standard_set(icon, "radio");
- Elm_Object_Item *li = elm_list_item_append(ad->list, st->name, icon, NULL, _list_item_selected_cb, ad);
+ Evas_Object *fav_btn = elm_button_add(ad->win);
+ evas_object_size_hint_min_set(fav_btn, 40, 40);
+ evas_object_propagate_events_set(fav_btn, EINA_FALSE);
+ if (st->favorite)
+ elm_object_text_set(fav_btn, "★");
+ else
+ elm_object_text_set(fav_btn, "☆");
+
+ Elm_Object_Item *li = elm_list_item_append(ad->list, st->name, icon, fav_btn, _list_item_selected_cb, ad);
elm_object_item_data_set(li, st);
+ // Attach callback with context so we can save favorites on toggle
+ typedef struct {
+ AppData *ad;
+ Elm_Object_Item *li;
+ } FavCtx;
+ FavCtx *ctx = calloc(1, sizeof(FavCtx));
+ ctx->ad = ad;
+ ctx->li = li;
+ evas_object_smart_callback_add(fav_btn, "clicked", _favorite_btn_clicked_cb, ctx);
+
if (st->favicon && st->favicon[0])
{
http_download_icon(ad, li, st->favicon);
@@ -59,3 +80,33 @@ station_list_populate(AppData *ad, Eina_List *stations)
}
elm_list_go(ad->list);
}
+
+static void
+_favorite_btn_clicked_cb(void *data, Evas_Object *obj, void *event_info)
+{
+ typedef struct {
+ AppData *ad;
+ Elm_Object_Item *li;
+ } FavCtx;
+
+ FavCtx *ctx = data;
+ if (!ctx) return;
+ Station *st = elm_object_item_data_get(ctx->li);
+ if (!st) { free(ctx); return; }
+
+ st->favorite = !st->favorite;
+
+ // Update button text to reflect state; use star characters for a clear fallback
+ if (st->favorite)
+ elm_object_text_set(obj, "★");
+ else
+ elm_object_text_set(obj, "☆");
+
+ favorites_set(ctx->ad, st, st->favorite);
+ favorites_save(ctx->ad);
+
+ // Ensure the list item doesn't get selected when clicking the favorite button
+ evas_object_propagate_events_set(obj, EINA_FALSE);
+
+ free(ctx);
+}
diff --git a/src/ui.c b/src/ui.c
index 0ba08b8..80bef67 100644
--- a/src/ui.c
+++ b/src/ui.c
@@ -1,5 +1,7 @@
#include "ui.h"
#include "appdata.h"
+#include "favorites.h"
+#include "station_list.h"
static void _win_del_cb(void *data, Evas_Object *obj, void *event_info);
static void _app_exit_cb(void *data, Evas_Object *obj, void *event_info);
@@ -11,6 +13,7 @@ void _stop_btn_clicked_cb(void *data, Evas_Object *obj, void *event_info);
void _search_btn_clicked_cb(void *data, Evas_Object *obj, void *event_info);
void _search_entry_activated_cb(void *data, Evas_Object *obj, void *event_info);
void _list_item_selected_cb(void *data, Evas_Object *obj, void *event_info);
+static void _favorites_btn_clicked_cb(void *data, Evas_Object *obj, void *event_info);
static void
@@ -91,6 +94,11 @@ ui_create(AppData *ad)
elm_box_pack_end(search_hbox, ad->search_btn);
evas_object_show(ad->search_btn);
+ ad->favorites_btn = elm_button_add(ad->win);
+ elm_object_text_set(ad->favorites_btn, "Favorites");
+ elm_box_pack_end(search_hbox, ad->favorites_btn);
+ evas_object_show(ad->favorites_btn);
+
ad->list = elm_list_add(ad->win);
evas_object_size_hint_weight_set(ad->list, EVAS_HINT_EXPAND, EVAS_HINT_EXPAND);
evas_object_size_hint_align_set(ad->list, EVAS_HINT_FILL, EVAS_HINT_FILL);
@@ -129,7 +137,16 @@ ui_create(AppData *ad)
evas_object_smart_callback_add(ad->search_btn, "clicked", _search_btn_clicked_cb, ad);
evas_object_smart_callback_add(ad->search_entry, "activated", _search_entry_activated_cb, ad);
evas_object_smart_callback_add(ad->list, "selected", _list_item_selected_cb, ad);
+ evas_object_smart_callback_add(ad->favorites_btn, "clicked", _favorites_btn_clicked_cb, ad);
evas_object_resize(ad->win, 400, 600);
evas_object_show(ad->win);
}
+
+static void
+_favorites_btn_clicked_cb(void *data, Evas_Object *obj EINA_UNUSED, void *event_info EINA_UNUSED)
+{
+ AppData *ad = data;
+ favorites_rebuild_station_list(ad);
+ station_list_populate(ad, ad->favorites_stations);
+}
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.