[evolution] I#2088 - Import/Export RSS feeds from/to OPML file
- From: Milan Crha <mcrha src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [evolution] I#2088 - Import/Export RSS feeds from/to OPML file
- Date: Thu, 20 Oct 2022 13:43:08 +0000 (UTC)
commit da1a5865b541c2fc83607b27d83f6fab1013f106
Author: Milan Crha <mcrha redhat com>
Date: Thu Oct 20 15:41:58 2022 +0200
I#2088 - Import/Export RSS feeds from/to OPML file
Closes https://gitlab.gnome.org/GNOME/evolution/-/issues/2088
src/modules/rss/evolution/e-rss-preferences.c | 451 ++++++++++++++++++++++++--
1 file changed, 420 insertions(+), 31 deletions(-)
---
diff --git a/src/modules/rss/evolution/e-rss-preferences.c b/src/modules/rss/evolution/e-rss-preferences.c
index 10c695f817..1d0aa023bf 100644
--- a/src/modules/rss/evolution/e-rss-preferences.c
+++ b/src/modules/rss/evolution/e-rss-preferences.c
@@ -122,9 +122,9 @@ e_rss_preferences_create_icon_pixbuf (const gchar *icon_filename)
}
static void
-e_rss_preferences_add_feed (GtkListStore *list_store,
- CamelRssStoreSummary *store_summary,
- const gchar *id)
+e_rss_preferences_add_feed_to_list_store (GtkListStore *list_store,
+ CamelRssStoreSummary *store_summary,
+ const gchar *id)
{
const gchar *href, *display_name, *icon_filename;
CamelRssContentType content_type;
@@ -166,7 +166,7 @@ e_rss_preferences_fill_list_store (GtkListStore *list_store,
for (link = feeds; link; link = g_slist_next (link)) {
const gchar *id = link->data;
- e_rss_preferences_add_feed (list_store, store_summary, id);
+ e_rss_preferences_add_feed_to_list_store (list_store, store_summary, id);
}
g_slist_free_full (feeds, g_free);
@@ -628,6 +628,52 @@ e_rss_preferences_maybe_copy_icon (const gchar *feed_id,
return filename;
}
+static void
+e_rss_preferences_create_feed (CamelService *service,
+ CamelRssStoreSummary *store_summary,
+ const gchar *href,
+ const gchar *display_name,
+ const gchar *icon_filename,
+ CamelRssContentType content_type,
+ const gchar *user_data_dir,
+ gchar **out_new_id,
+ GError **error)
+{
+ const gchar *new_id;
+
+ new_id = camel_rss_store_summary_add (store_summary,
+ href,
+ display_name,
+ icon_filename,
+ content_type);
+
+ if (new_id) {
+ gchar *new_id_copy = g_strdup (new_id);
+ gchar *real_icon_filename;
+
+ real_icon_filename = e_rss_preferences_maybe_copy_icon (new_id_copy, icon_filename,
user_data_dir);
+ if (real_icon_filename) {
+ camel_rss_store_summary_set_icon_filename (store_summary, new_id_copy,
real_icon_filename);
+ g_free (real_icon_filename);
+ }
+
+ if (camel_rss_store_summary_save (store_summary, error)) {
+ CamelFolderInfo *fi;
+
+ fi = camel_rss_store_summary_dup_folder_info (store_summary, new_id_copy);
+
+ camel_store_folder_created (CAMEL_STORE (service), fi);
+
+ camel_folder_info_free (fi);
+ }
+
+ if (out_new_id)
+ *out_new_id = new_id_copy;
+ else
+ g_free (new_id_copy);
+ }
+}
+
static void
e_rss_preferences_save_clicked_cb (GtkWidget *button,
gpointer user_data)
@@ -690,35 +736,14 @@ e_rss_preferences_save_clicked_cb (GtkWidget *button,
g_free (real_icon_filename);
g_free (old_display_name);
} else {
- const gchar *new_id;
-
- new_id = camel_rss_store_summary_add (store_summary,
+ e_rss_preferences_create_feed (service, store_summary,
gtk_entry_get_text (pd->href),
gtk_entry_get_text (pd->name),
icon_filename,
- content_type);
-
- if (new_id) {
- gchar *real_icon_filename;
-
- pd->id = g_strdup (new_id);
-
- real_icon_filename = e_rss_preferences_maybe_copy_icon (pd->id, icon_filename,
user_data_dir);
- if (real_icon_filename) {
- camel_rss_store_summary_set_icon_filename (store_summary, pd->id,
real_icon_filename);
- g_free (real_icon_filename);
- }
-
- if (camel_rss_store_summary_save (store_summary, &error)) {
- CamelFolderInfo *fi;
-
- fi = camel_rss_store_summary_dup_folder_info (store_summary, pd->id);
-
- camel_store_folder_created (CAMEL_STORE (service), fi);
-
- camel_folder_info_free (fi);
- }
- }
+ content_type,
+ user_data_dir,
+ &pd->id,
+ &error);
}
fo = g_slice_new0 (FolderOpts);
@@ -1053,6 +1078,329 @@ e_rss_pereferences_selection_changed_cb (GtkTreeSelection *selection,
gtk_widget_set_sensitive (button, gtk_tree_selection_get_selected (selection, NULL, NULL));
}
+static GFile *
+e_rss_choose_file (gpointer parent,
+ gboolean is_import)
+{
+ GtkFileChooserNative *native;
+ GtkFileFilter *filter;
+ GFile *file = NULL;
+
+ native = gtk_file_chooser_native_new (
+ is_import ? _("Import RSS Feeds") : _("Export RSS Feeds"),
+ GTK_IS_WINDOW (parent) ? GTK_WINDOW (parent) : NULL,
+ is_import ? GTK_FILE_CHOOSER_ACTION_OPEN : GTK_FILE_CHOOSER_ACTION_SAVE,
+ is_import ? _("_Import") : _("Export"),
+ _("_Cancel"));
+
+ filter = gtk_file_filter_new ();
+ gtk_file_filter_set_name (filter, _("OPML files"));
+ gtk_file_filter_add_mime_type (filter, "text/x-opml+xml");
+ gtk_file_chooser_add_filter (GTK_FILE_CHOOSER (native), filter);
+
+ e_util_load_file_chooser_folder (GTK_FILE_CHOOSER (native));
+
+ if (!is_import) {
+ /* Translators: This is a default file name for exported RSS feeds.
+ Keep the extension (".opml") as is, translate only the "feeds" word, if needed. */
+ gtk_file_chooser_set_current_name (GTK_FILE_CHOOSER (native), _("feeds.opml"));
+ }
+
+ if (gtk_native_dialog_run (GTK_NATIVE_DIALOG (native)) == GTK_RESPONSE_ACCEPT) {
+ e_util_save_file_chooser_folder (GTK_FILE_CHOOSER (native));
+ file = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (native));
+ }
+
+ g_object_unref (native);
+
+ return file;
+}
+
+static void
+e_rss_report_text (GtkWindow *parent,
+ const gchar *text)
+{
+ g_return_if_fail (text != NULL);
+
+ e_notice (parent, GTK_MESSAGE_ERROR, "%s", text);
+}
+
+static void
+e_rss_report_error (GtkWindow *parent,
+ const GError *local_error)
+{
+ g_return_if_fail (local_error != NULL);
+
+ e_rss_report_text (parent, local_error->message);
+}
+
+#define EVO_RSS_NS_HREF PACKAGE_URL
+
+static void
+e_rss_export_to_file (GtkWindow *parent,
+ CamelRssStoreSummary *store_summary,
+ GFile *file)
+{
+ EXmlDocument *xml;
+ GSList *feeds, *link;
+ gchar *content;
+ GError *local_error = NULL;
+
+ g_return_if_fail (CAMEL_IS_RSS_STORE_SUMMARY (store_summary));
+ g_return_if_fail (G_IS_FILE (file));
+
+ xml = e_xml_document_new (NULL, "opml");
+ e_xml_document_add_attribute (xml, NULL, "version", "2.0");
+ e_xml_document_add_namespaces (xml, "e", EVO_RSS_NS_HREF, NULL);
+
+ e_xml_document_start_element (xml, NULL, "head");
+ e_xml_document_start_text_element (xml, NULL, "title");
+ if (strlen (VERSION_COMMENT) > 0)
+ e_xml_document_write_string (xml, "Evolution RSS Feeds (" VERSION VERSION_SUBSTRING " "
VERSION_COMMENT ")");
+ else
+ e_xml_document_write_string (xml, "Evolution RSS Feeds (" VERSION VERSION_SUBSTRING ")");
+ e_xml_document_end_element (xml); /* title */
+ e_xml_document_start_text_element (xml, NULL, "dateCreated");
+ e_xml_document_write_time (xml, time (NULL));
+ e_xml_document_end_element (xml); /* dateCreated */
+ e_xml_document_end_element (xml); /* head */
+
+ e_xml_document_start_element (xml, NULL, "body");
+
+ feeds = camel_rss_store_summary_dup_feeds (store_summary);
+ for (link = feeds; link; link = g_slist_next (link)) {
+ const gchar *id = link->data;
+ const gchar *href;
+ const gchar *display_name;
+ CamelRssContentType content_type;
+
+ href = camel_rss_store_summary_get_href (store_summary, id);
+ display_name = camel_rss_store_summary_get_display_name (store_summary, id);
+ content_type = camel_rss_store_summary_get_content_type (store_summary, id);
+
+ e_xml_document_start_element (xml, NULL, "outline");
+ e_xml_document_add_attribute (xml, NULL, "type", "rss");
+ e_xml_document_add_attribute (xml, NULL, "text", display_name);
+ e_xml_document_add_attribute (xml, NULL, "xmlUrl", href);
+ e_xml_document_add_attribute (xml, EVO_RSS_NS_HREF, "contentType",
e_rss_preferences_content_type_to_string (content_type));
+ e_xml_document_end_element (xml); /* outline */
+ }
+
+ e_xml_document_end_element (xml); /* body */
+
+ content = e_xml_document_get_content (xml, NULL);
+
+ if (!g_file_set_contents (g_file_peek_path (file), content, -1, &local_error)) {
+ g_prefix_error_literal (&local_error, _("Failed to export RSS feeds: "));
+ e_rss_report_error (parent, local_error);
+ g_clear_error (&local_error);
+ }
+
+ g_slist_free_full (feeds, g_free);
+ g_clear_object (&xml);
+ g_free (content);
+}
+
+static void
+e_rss_import_from_file (GtkWindow *parent,
+ CamelService *service,
+ CamelRssStoreSummary *store_summary,
+ GFile *file)
+{
+ gchar *content = NULL;
+ gsize length = 0;
+ xmlDoc *doc;
+ GError *local_error = NULL;
+
+ g_return_if_fail (CAMEL_IS_STORE (service));
+ g_return_if_fail (CAMEL_IS_RSS_STORE_SUMMARY (store_summary));
+ g_return_if_fail (G_IS_FILE (file));
+
+ if (!g_file_get_contents (g_file_peek_path (file), &content, &length, &local_error)) {
+ g_prefix_error_literal (&local_error, _("Failed to read file content: "));
+ e_rss_report_error (parent, local_error);
+ g_clear_error (&local_error);
+ return;
+ }
+
+ doc = e_xml_parse_data (content, length);
+ if (doc) {
+ xmlNode *root;
+
+ root = xmlDocGetRootElement (doc);
+ if (root && e_xml_is_element_name (root, NULL, "opml")) {
+ GHashTable *known_feeds;
+ GSList *feeds, *link;
+ xmlNode *node, *next;
+ gsize n_found = 0, n_imported = 0;
+
+ known_feeds = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+ feeds = camel_rss_store_summary_dup_feeds (store_summary);
+ for (link = feeds; link; link = g_slist_next (link)) {
+ const gchar *id = link->data;
+ const gchar *href;
+
+ href = camel_rss_store_summary_get_href (store_summary, id);
+ if (href && *href)
+ g_hash_table_add (known_feeds, g_strdup (href));
+ }
+
+ g_slist_free_full (feeds, g_free);
+
+ /* Find the 'body' element */
+ node = root->children;
+ if (node) {
+ node = e_xml_find_sibling (node, NULL, "body");
+ if (node) {
+ root = node;
+ node = node->children;
+ }
+ }
+
+ while (node != NULL && node != root) {
+ if (e_xml_is_element_name (node, NULL, "outline")) {
+ xmlChar *value;
+
+ value = xmlGetNsProp (node, (const xmlChar *) "type", NULL);
+ if (g_strcmp0 ((const gchar *) value, "rss") == 0) {
+ xmlChar *text, *xml_url, *content_type_str;
+
+ n_found++;
+
+ text = xmlGetNsProp (node, (const xmlChar *) "text", NULL);
+ xml_url = xmlGetNsProp (node, (const xmlChar *) "xmlUrl",
NULL);
+ content_type_str = xmlGetNsProp (node, (const xmlChar *)
"contentType", (const xmlChar *) EVO_RSS_NS_HREF);
+
+ if (text && *text && xml_url && *xml_url &&
+ !g_hash_table_contains (known_feeds, xml_url)) {
+ CamelRssContentType content_type;
+
+ content_type =
e_rss_preferences_content_type_from_string ((const gchar *) content_type_str);
+
+ g_hash_table_add (known_feeds, g_strdup ((const gchar
*) xml_url));
+
+ e_rss_preferences_create_feed (service, store_summary,
+ (const gchar *) xml_url, (const gchar *) text,
+ NULL, content_type, NULL, NULL, &local_error);
+
+ if (local_error) {
+ g_prefix_error_literal (&local_error,
_("Failed to add feed: "));
+ break;
+ }
+
+ n_imported++;
+ }
+
+ g_clear_pointer (&text, xmlFree);
+ g_clear_pointer (&xml_url, xmlFree);
+ g_clear_pointer (&content_type_str, xmlFree);
+ }
+
+ g_clear_pointer (&value, xmlFree);
+ }
+
+ /* traverse the XML structure */
+ next = node->children;
+ if (!next)
+ next = node->next;
+ if (!next) {
+ next = node->parent;
+ if (next == root)
+ next = NULL;
+
+ while (next) {
+ xmlNode *sibl = next->next;
+ if (sibl) {
+ next = sibl;
+ break;
+ } else {
+ next = next->parent;
+ if (next == root)
+ next = NULL;
+ }
+ }
+ }
+ node = next;
+ }
+
+ g_hash_table_destroy (known_feeds);
+
+ if (local_error) {
+ e_rss_report_error (parent, local_error);
+ g_clear_error (&local_error);
+ } else if (!n_found) {
+ e_notice (parent, GTK_MESSAGE_ERROR, "%s", _("No RSS feeds found"));
+ } else if (!n_imported) {
+ e_notice (parent, GTK_MESSAGE_INFO, "%s", _("No new RSS feeds imported"));
+ } else {
+ e_notice (parent, GTK_MESSAGE_INFO, g_dngettext (GETTEXT_PACKAGE,
+ "Imported %" G_GSIZE_FORMAT " feed",
+ "Imported %" G_GSIZE_FORMAT " feeds", n_imported), n_imported);
+ }
+ } else {
+ e_rss_report_text (parent, _("Failed to import data, the file does not contain valid
OPML data."));
+ }
+
+ xmlFreeDoc (doc);
+ } else {
+ e_rss_report_text (parent, _("Failed to parse file content. Expected is an OPML file."));
+ }
+
+ g_free (content);
+}
+
+static void
+e_rss_preferences_export_import (GtkWidget *button,
+ gboolean is_import)
+{
+ CamelService *service;
+ CamelRssStoreSummary *store_summary = NULL;
+ gpointer toplevel;
+ GFile *file;
+
+ service = e_rss_preferences_ref_store (e_shell_get_default ());
+ if (!service) {
+ g_warn_if_reached ();
+ return;
+ }
+
+ g_object_get (service, "summary", &store_summary, NULL);
+
+ if (!store_summary) {
+ g_clear_object (&service);
+ g_warn_if_reached ();
+ return;
+ }
+
+ toplevel = gtk_widget_get_toplevel (button);
+ if (!GTK_IS_WINDOW (toplevel))
+ toplevel = NULL;
+
+ file = e_rss_choose_file (toplevel, is_import);
+ if (file) {
+ if (is_import)
+ e_rss_import_from_file (toplevel, service, store_summary, file);
+ else
+ e_rss_export_to_file (toplevel, store_summary, file);
+ }
+
+ g_clear_object (&store_summary);
+ g_clear_object (&service);
+ g_clear_object (&file);
+}
+
+static void
+e_rss_preferences_export_clicked_cb (GtkWidget *button)
+{
+ e_rss_preferences_export_import (button, FALSE);
+}
+
+static void
+e_rss_preferences_import_clicked_cb (GtkWidget *button)
+{
+ e_rss_preferences_export_import (button, TRUE);
+}
+
static void
e_rss_preferences_map_cb (GtkTreeView *tree_view,
gpointer user_data)
@@ -1128,7 +1476,7 @@ e_rss_preferences_feed_changed_cb (CamelRssStoreSummary *store_summary,
gtk_list_store_remove (list_store, &iter);
}
} else if (camel_rss_store_summary_contains (store_summary, id)) {
- e_rss_preferences_add_feed (list_store, store_summary, id);
+ e_rss_preferences_add_feed_to_list_store (list_store, store_summary, id);
}
}
@@ -1143,6 +1491,28 @@ e_rss_preferences_row_activated_cb (GtkTreeView *tree_view,
e_rss_preferences_edit_clicked_cb (button, tree_view);
}
+static void
+e_rss_preferences_row_deleted_cb (GtkTreeModel *model,
+ GtkTreePath *path,
+ gpointer user_data)
+{
+ GtkWidget *button = user_data;
+ GtkTreeIter iter;
+
+ gtk_widget_set_sensitive (button, gtk_tree_model_get_iter_first (model, &iter));
+}
+
+static void
+e_rss_preferences_row_inserted_cb (GtkTreeModel *model,
+ GtkTreePath *path,
+ GtkTreeIter *iter,
+ gpointer user_data)
+{
+ GtkWidget *button = user_data;
+
+ gtk_widget_set_sensitive (button, TRUE);
+}
+
static GtkWidget *
e_rss_preferences_new (EPreferencesWindow *window)
{
@@ -1394,6 +1764,25 @@ e_rss_preferences_new (EPreferencesWindow *window)
g_signal_connect_object (selection, "changed",
G_CALLBACK (e_rss_pereferences_selection_changed_cb), widget, 0);
+ widget = e_dialog_button_new_with_icon (NULL, _("E_xport"));
+ gtk_widget_set_sensitive (widget, FALSE);
+ gtk_container_add (GTK_CONTAINER (button_box), widget);
+
+ g_signal_connect_object (list_store, "row-deleted",
+ G_CALLBACK (e_rss_preferences_row_deleted_cb), widget, 0);
+
+ g_signal_connect_object (list_store, "row-inserted",
+ G_CALLBACK (e_rss_preferences_row_inserted_cb), widget, 0);
+
+ g_signal_connect (widget, "clicked",
+ G_CALLBACK (e_rss_preferences_export_clicked_cb), NULL);
+
+ widget = e_dialog_button_new_with_icon (NULL, _("_Import"));
+ gtk_container_add (GTK_CONTAINER (button_box), widget);
+
+ g_signal_connect (widget, "clicked",
+ G_CALLBACK (e_rss_preferences_import_clicked_cb), NULL);
+
gtk_grid_attach (grid, scrolled_window, 0, row, 1, 1);
gtk_grid_attach (grid, button_box, 1, row, 1, 1);
row++;
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]