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.

Reply via email to