diff --git a/cmake/XiphosDependencies.cmake b/cmake/XiphosDependencies.cmake index fea583571..183d4bb53 100644 --- a/cmake/XiphosDependencies.cmake +++ b/cmake/XiphosDependencies.cmake @@ -56,6 +56,11 @@ pkg_check_modules(Sword REQUIRED IMPORTED_TARGET "sword>=1.8.1" ) +# Sqlite3 dependency (used to read AndBible bookmark backups for import) +pkg_check_modules(Sqlite3 REQUIRED IMPORTED_TARGET + "sqlite3" + ) + # core dependencies if(WIN32) diff --git a/src/gtk/bookmarks_menu.c b/src/gtk/bookmarks_menu.c index 7e3a6489f..41e349b46 100644 --- a/src/gtk/bookmarks_menu.c +++ b/src/gtk/bookmarks_menu.c @@ -2,7 +2,7 @@ * Xiphos Bible Study Tool * bookmarks_menu.c - gui for bookmarks using menu * - * Copyright (C) 2003-2026 Xiphos Developer Team + * Copyright (C) 2003-2025 Xiphos Developer Team * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -24,6 +24,7 @@ #endif #include +#include #include #ifndef USE_GTKBUILDER #include @@ -40,6 +41,7 @@ #include "gui/bookmarks_menu.h" #include "gui/bookmarks_treeview.h" #include "gui/export_bookmarks.h" +#include "gui/import_andbible.h" #include "gui/utilities.h" #include "gui/main_window.h" #include "gui/dialog.h" @@ -260,6 +262,195 @@ G_MODULE_EXPORT void bibletime_bookmarks_activate(GtkMenuItem *menuitem, gtk_widget_destroy(dialog); } + +static gboolean andbible_folder_exists(GtkTreeIter *parent) +{ + GtkTreeIter child; + gboolean valid; + + valid = gtk_tree_model_iter_children(GTK_TREE_MODEL(model), &child, parent); + while (valid) { + gchar *caption = NULL; + gboolean match; + + gtk_tree_model_get(GTK_TREE_MODEL(model), &child, + COL_CAPTION, &caption, -1); + match = caption && !strcmp(caption, _("Import AndBible")); + g_free(caption); + if (match) + return TRUE; + valid = gtk_tree_model_iter_next(GTK_TREE_MODEL(model), &child); + } + return FALSE; +} + +/****************************************************************************** + * Name + * remove_existing_andbible_folder + * + * Synopsis + * #include "gui/import_andbible.h" + * + * void remove_existing_andbible_folder(GtkTreeIter * parent) + * + * Description + * The "Import AndBible" top-level folder is entirely owned/managed by + * andbible_bookmarks_activate(): every import wipes it and rebuilds it + * from scratch, rather than trying to patch it in place. That gives + * predictable, duplicate-free behaviour (additions, edits and + * removals on the AndBible side are all reflected simply by + * reimporting) at the cost of not preserving manual edits made + * directly inside that folder in Xiphos - which is why it is kept as + * its own clearly-named folder rather than merged into the user's + * own bookmarks. + * + * Return value + * void + */ + +static void remove_existing_andbible_folder(GtkTreeIter *parent) +{ + GtkTreeIter child; + gboolean valid; + + valid = gtk_tree_model_iter_children(GTK_TREE_MODEL(model), &child, + parent); + while (valid) { + gchar *caption = NULL; + + gtk_tree_model_get(GTK_TREE_MODEL(model), &child, + COL_CAPTION, &caption, -1); + if (caption && !strcmp(caption, _("Import AndBible"))) { + g_free(caption); + gtk_tree_store_remove(GTK_TREE_STORE(model), &child); + return; + } + g_free(caption); + valid = gtk_tree_model_iter_next(GTK_TREE_MODEL(model), + &child); + } +} + +/****************************************************************************** + * Name + * andbible_bookmarks_activate + * + * Synopsis + * #include "gui/import_andbible.h" + * + * void andbible_bookmarks_activate(GtkMenuItem * menuitem, + * gpointer user_data) + * + * Description + * Prompts for an AndBible bookmarks backup (.sqlite3), converts it via + * andbible_import_to_temp_xml() (main/import_andbible.cc - uses + * libsword's own versification support to turn AndBible's ordinal + * verse positions back into references), and merges the result into + * the bookmarks tree the same way bibletime_bookmarks_activate() does. + * + * Return value + * void + */ + +G_MODULE_EXPORT void andbible_bookmarks_activate(GtkMenuItem *menuitem, + gpointer user_data) +{ + GtkTreeIter iter; + GtkTreeIter parent; + GtkWidget *dialog; + GtkFileFilter *filter; + + if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(model), &parent)) + return; + + dialog = gtk_file_chooser_dialog_new( + _("Select AndBible bookmarks backup (.sqlite3)"), + GTK_WINDOW(widgets.app), GTK_FILE_CHOOSER_ACTION_OPEN, +#if GTK_CHECK_VERSION(3, 10, 0) + "_Cancel", GTK_RESPONSE_CANCEL, "_OK", GTK_RESPONSE_ACCEPT, +#else + GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, GTK_STOCK_OK, + GTK_RESPONSE_ACCEPT, +#endif + NULL); + + filter = gtk_file_filter_new(); + gtk_file_filter_set_name(filter, _("AndBible backup (*.sqlite3)")); + gtk_file_filter_add_pattern(filter, "*.sqlite3"); + gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), filter); + + if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) { + gchar *sqlite_path, *tmp_xml = NULL, *summary; + gint n_imported = 0, n_skipped = 0; + GError *error = NULL; + + sqlite_path = + gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog)); + + if (andbible_folder_exists(&parent)) { + GtkWidget *confirm = gtk_message_dialog_new( + GTK_WINDOW(widgets.app), GTK_DIALOG_MODAL, + GTK_MESSAGE_QUESTION, GTK_BUTTONS_YES_NO, "%s", + _("An 'Import AndBible' folder already exists " + "and will be replaced by this import. " + "Continue?")); + gint resp = gtk_dialog_run(GTK_DIALOG(confirm)); + gtk_widget_destroy(confirm); + if (resp != GTK_RESPONSE_YES) { + g_free(sqlite_path); + gtk_widget_destroy(dialog); + return; + } + } + + if (!andbible_import_to_temp_xml(sqlite_path, + _("Import AndBible"), + &tmp_xml, &n_imported, + &n_skipped, &error)) { + GtkWidget *msg = gtk_message_dialog_new( + GTK_WINDOW(widgets.app), GTK_DIALOG_MODAL, + GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, "%s", + error ? error->message : _("Unknown error")); + gtk_dialog_run(GTK_DIALOG(msg)); + gtk_widget_destroy(msg); + g_clear_error(&error); + g_free(sqlite_path); + gtk_widget_destroy(dialog); + return; + } + g_free(sqlite_path); + + /* This folder is fully managed by us: wipe any previous + * import before rebuilding it, so re-importing the same + * (or an updated) AndBible backup never creates duplicates. */ + remove_existing_andbible_folder(&parent); + + gtk_tree_store_append(GTK_TREE_STORE(model), &iter, &parent); + gtk_tree_store_set(GTK_TREE_STORE(model), &iter, + COL_OPEN_PIXBUF, + bm_pixbufs->pixbuf_opened, + COL_CLOSED_PIXBUF, + bm_pixbufs->pixbuf_closed, COL_CAPTION, + _("Import AndBible"), COL_KEY, NULL, + COL_MODULE, NULL, -1); + gui_parse_bookmarks(bookmark_tree, (const xmlChar *)tmp_xml, + &iter); + g_unlink(tmp_xml); + g_free(tmp_xml); + + summary = g_strdup_printf( + _("%d bookmark(s) imported, %d skipped."), n_imported, + n_skipped); + GtkWidget *msg = gtk_message_dialog_new( + GTK_WINDOW(widgets.app), GTK_DIALOG_MODAL, + GTK_MESSAGE_INFO, GTK_BUTTONS_OK, "%s", summary); + gtk_dialog_run(GTK_DIALOG(msg)); + gtk_widget_destroy(msg); + g_free(summary); + } + gtk_widget_destroy(dialog); +} + /****************************************************************************** * Name * on_allow_reordering_activate @@ -1065,6 +1256,7 @@ void gui_create_bookmark_menu(void) menu.delete = UI_GET_ITEM(gxml, "delete_item"); menu.reorder = UI_GET_ITEM(gxml, "allow_reordering"); menu.bibletime = UI_GET_ITEM(gxml, "import_bibletime_bookmarks1"); + menu.andbible = UI_GET_ITEM(gxml, "import_andbible_bookmarks1"); menu.remove = UI_GET_ITEM(gxml, "remove_folder"); menu.set_color = UI_GET_ITEM(gxml, "set_tag_color"); menu.crossref_popup = UI_GET_ITEM(gxml, "crossref_popup"); @@ -1079,6 +1271,7 @@ void gui_create_bookmark_menu(void) gtk_widget_set_sensitive(menu.edit, FALSE); gtk_widget_set_sensitive(menu.delete, FALSE); gtk_widget_set_sensitive(menu.bibletime, TRUE); + gtk_widget_set_sensitive(menu.andbible, TRUE); gtk_widget_set_sensitive(menu.remove, TRUE); gtk_widget_hide(menu.remove); diff --git a/src/gui/bookmarks_menu.h b/src/gui/bookmarks_menu.h index d31fee7b7..ec199902d 100644 --- a/src/gui/bookmarks_menu.h +++ b/src/gui/bookmarks_menu.h @@ -2,7 +2,7 @@ * Xiphos Bible Study Tool * bookmarks_menu.h - gui for bookmarks in a menu * - * Copyright (C) 2003-2026 Xiphos Developer Team + * Copyright (C) 2003-2025 Xiphos Developer Team * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -39,6 +39,7 @@ struct _bookmark_menu GtkWidget *reorder; GtkWidget *save; GtkWidget *bibletime; + GtkWidget *andbible; GtkWidget *rr_submenu; GtkWidget *remove; GtkWidget *set_color; diff --git a/src/gui/import_andbible.h b/src/gui/import_andbible.h new file mode 100644 index 000000000..03c645242 --- /dev/null +++ b/src/gui/import_andbible.h @@ -0,0 +1,85 @@ +/* + * Xiphos Bible Study Tool + * import_andbible.h - import bookmarks from an AndBible sqlite backup + * + * Copyright (C) 2026 Xiphos Developer Team + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Library General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef __IMPORT_ANDBIBLE_H__ +#define __IMPORT_ANDBIBLE_H__ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/****************************************************************************** + * Name + * andbible_import_to_temp_xml + * + * Synopsis + * #include "gui/import_andbible.h" + * + * gboolean andbible_import_to_temp_xml(const gchar *sqlite_path, + * gchar **out_xml_path, + * gint *n_imported, + * gint *n_skipped, + * GError **error) + * + * Description + * Reads an AndBible bookmark backup (the Room sqlite database found in + * AndBible's "bookmarks.sqlite3" export/backup) and converts it into a + * temporary bookmarks.xml file using the same schema + * Xiphos itself uses (see main/xml.c). AndBible bookmarks are stored as + * an ordinal position within a named SWORD versification system + * (KJVA, Catholic, Catholic2, ...); this function uses libsword's own + * VerseKey/versification support to turn that ordinal back into a + * human verse reference, so no separate canon table needs to be + * maintained by Xiphos. + * + * AndBible labels become Folder elements (carrying the label's color + * when available); notes are appended to the bookmark description. + * + * The caller is responsible for g_free()-ing *out_xml_path and + * removing the temporary file (g_unlink()) once it has been consumed, + * e.g. via gui_parse_bookmarks(). + * + * Return value + * TRUE on success (out_xml_path is set); FALSE on failure (error is + * set, if provided). + */ +gboolean andbible_import_to_temp_xml(const gchar *sqlite_path, + const gchar *top_folder_name, + gchar **out_xml_path, + gint *n_imported, + gint *n_skipped, + GError **error); + +/* Menu/GtkBuilder signal handler: prompts for an AndBible sqlite backup + * file, converts it, and merges the result into the bookmarks tree - see + * bibletime_bookmarks_activate() in bookmarks_menu.c for the analogous, + * existing BibleTime import. */ +G_MODULE_EXPORT void andbible_bookmarks_activate(GtkMenuItem *menuitem, + gpointer user_data); + +#ifdef __cplusplus +} +#endif + +#endif /* __IMPORT_ANDBIBLE_H__ */ diff --git a/src/main/CMakeLists.txt b/src/main/CMakeLists.txt index 64621cc78..b95282bd3 100644 --- a/src/main/CMakeLists.txt +++ b/src/main/CMakeLists.txt @@ -26,6 +26,7 @@ add_library (main display.cc export_passage.cc global_ops.cc + import_andbible.cc lists.cc main.c mod_mgr.cc @@ -63,6 +64,7 @@ target_include_directories(main PkgConfig::Soup PkgConfig::Sword PkgConfig::Biblesync + PkgConfig::Sqlite3 ) # specify libraries or flags to use when linking @@ -75,6 +77,7 @@ target_link_libraries(main PkgConfig::Soup PkgConfig::Sword PkgConfig::Biblesync + PkgConfig::Sqlite3 ) IF (DBUS) diff --git a/src/main/import_andbible.cc b/src/main/import_andbible.cc new file mode 100644 index 000000000..2ad4680d9 --- /dev/null +++ b/src/main/import_andbible.cc @@ -0,0 +1,335 @@ +/* + * Xiphos Bible Study Tool + * import_andbible.cc - import bookmarks from an AndBible sqlite backup + * + * Copyright (C) 2026 Xiphos Developer Team + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Library General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include + +#include + +#include +#include +#include +#include +#include + +#include "gui/import_andbible.h" +#include "main/xml.h" + +using sword::VerseKey; + +/* AndBible's system labels are not real, user-visible categories. */ +static const char *const SKIP_LABEL_NAMES[] = { + "__UNLABELED__", "__SPEAK_LABEL__", NULL}; + +struct LabelInfo { + gchar *name; + gchar *color; /* "#RRGGBB", may be NULL */ +}; + +static void label_info_free(gpointer p) { + LabelInfo *li = (LabelInfo *)p; + g_free(li->name); + g_free(li->color); + g_free(li); +} + +static gboolean is_system_label(const gchar *name) { + for (int i = 0; SKIP_LABEL_NAMES[i]; ++i) + if (name && !strcmp(name, SKIP_LABEL_NAMES[i])) + return TRUE; + return FALSE; +} + +/* sqlite BLOB ids (16-byte UUIDs) -> stable hex string, for use as a + * GHashTable key. */ +static gchar *blob_to_hex(const void *data, int len) { + static const char hexch[] = "0123456789abcdef"; + gchar *out = (gchar *)g_malloc(len * 2 + 1); + const guint8 *b = (const guint8 *)data; + for (int i = 0; i < len; ++i) { + out[i * 2] = hexch[b[i] >> 4]; + out[i * 2 + 1] = hexch[b[i] & 0xf]; + } + out[len * 2] = '\0'; + return out; +} + +/* AndBible/Android stores label colors as signed 32-bit ARGB ints + * (Java convention); alpha is discarded. */ +static gchar *argb_to_hex(gint64 argb_signed) { + guint32 v = ((guint32)argb_signed) & 0xFFFFFFu; + return g_strdup_printf("#%06X", v); +} + +/* Decodes an AndBible ordinal range, in a named SWORD versification + * system (e.g. "KJVA", "Catholic2"), into a human verse key such as + * "Romans 3:23-Romans 3:24" - using libsword's own VerseKey rather than + * a hand-maintained canon table, so any versification SWORD knows about + * is supported automatically. Returns NULL (caller should skip the + * bookmark) if the versification name is unknown to this SWORD build. */ +static gchar *ordinal_range_to_key(const gchar *v11n, glong ord_start, + glong ord_end) { + if (!v11n || !*v11n) + return NULL; + + VerseKey vk; + vk.setVersificationSystem(v11n); + if (strcmp(vk.getVersificationSystem(), v11n) != 0) + /* SWORD silently falls back to KJV for unknown systems - + * refuse to produce a misleading reference. */ + return NULL; + + vk.setIndex(ord_start); + if (ord_end == ord_start) { + const char *text = vk.getText(); + return text ? g_strdup(text) : NULL; + } + + VerseKey lb(vk); + VerseKey ub; + ub.setVersificationSystem(v11n); + ub.setIndex(ord_end); + vk.setLowerBound(lb); + vk.setUpperBound(ub); + const char *text = vk.getRangeText(); + return text ? g_strdup(text) : NULL; +} + +/* Returns a newly-allocated GList of label-id hex strings (caller frees + * both the list and its contents) attached to the given bookmark id, + * in AndBible's own display order. */ +static GList *fetch_label_ids(sqlite3_stmt *st, const void *bookmark_id, + int bookmark_id_len) { + GList *result = NULL; + sqlite3_reset(st); + sqlite3_bind_blob(st, 1, bookmark_id, bookmark_id_len, SQLITE_STATIC); + while (sqlite3_step(st) == SQLITE_ROW) { + const void *lidblob = sqlite3_column_blob(st, 0); + int lidlen = sqlite3_column_bytes(st, 0); + result = g_list_append(result, blob_to_hex(lidblob, lidlen)); + } + return result; +} + +static gchar *fetch_note(sqlite3_stmt *st, const void *bookmark_id, + int bookmark_id_len) { + gchar *note = NULL; + sqlite3_reset(st); + sqlite3_bind_blob(st, 1, bookmark_id, bookmark_id_len, SQLITE_STATIC); + if (sqlite3_step(st) == SQLITE_ROW) { + const unsigned char *text = sqlite3_column_text(st, 0); + if (text) + note = g_strdup((const gchar *)text); + } + return note; +} + +extern "C" gboolean andbible_import_to_temp_xml(const gchar *sqlite_path, + const gchar *top_folder_name, + gchar **out_xml_path, + gint *n_imported, + gint *n_skipped, + GError **error) { + g_return_val_if_fail(sqlite_path != NULL, FALSE); + g_return_val_if_fail(out_xml_path != NULL, FALSE); + + if (!top_folder_name || !*top_folder_name) + top_folder_name = _("Import AndBible"); + if (n_imported) + *n_imported = 0; + if (n_skipped) + *n_skipped = 0; + + sqlite3 *db = NULL; + if (sqlite3_open_v2(sqlite_path, &db, SQLITE_OPEN_READONLY, NULL) != + SQLITE_OK) { + g_set_error(error, g_quark_from_static_string("andbible-import"), + 1, _("Unable to open '%s': %s"), sqlite_path, + db ? sqlite3_errmsg(db) : _("unknown error")); + if (db) + sqlite3_close(db); + return FALSE; + } + + /* --- Load labels (small table, load fully up-front). --- */ + GHashTable *labels = g_hash_table_new_full( + g_str_hash, g_str_equal, g_free, label_info_free); + sqlite3_stmt *label_st = NULL; + if (sqlite3_prepare_v2(db, "SELECT id, name, color FROM Label", -1, + &label_st, NULL) == SQLITE_OK) { + while (sqlite3_step(label_st) == SQLITE_ROW) { + const void *idblob = sqlite3_column_blob(label_st, 0); + int idlen = sqlite3_column_bytes(label_st, 0); + const unsigned char *name = + sqlite3_column_text(label_st, 1); + + LabelInfo *li = g_new0(LabelInfo, 1); + li->name = g_strdup(name ? (const gchar *)name : ""); + li->color = argb_to_hex(sqlite3_column_int64(label_st, 2)); + g_hash_table_insert(labels, blob_to_hex(idblob, idlen), + li); + } + } + sqlite3_finalize(label_st); + + sqlite3_stmt *link_st = NULL, *note_st = NULL, *bm_st = NULL; + sqlite3_prepare_v2(db, + "SELECT labelId FROM BibleBookmarkToLabel " + "WHERE bookmarkId = ? ORDER BY orderNumber", + -1, &link_st, NULL); + sqlite3_prepare_v2(db, + "SELECT notes FROM BibleBookmarkNotes " + "WHERE bookmarkId = ?", + -1, ¬e_st, NULL); + + if (sqlite3_prepare_v2( + db, + "SELECT id, v11n, ordinalStart, ordinalEnd, book " + "FROM BibleBookmark", + -1, &bm_st, NULL) != SQLITE_OK) { + g_set_error(error, g_quark_from_static_string("andbible-import"), + 2, _("'%s' does not look like an AndBible " + "bookmarks backup (table BibleBookmark not " + "found)."), + sqlite_path); + g_hash_table_destroy(labels); + sqlite3_finalize(link_st); + sqlite3_finalize(note_st); + sqlite3_close(db); + return FALSE; + } + + /* --- Build the SwordBookmarks XML tree. --- */ + xmlDocPtr doc = xmlNewDoc((const xmlChar *)"1.0"); + xmlNodePtr root = xmlNewNode(NULL, (const xmlChar *)"SwordBookmarks"); + xmlNewProp(root, (const xmlChar *)"syntaxVersion", + (const xmlChar *)"1.0"); + xmlDocSetRootElement(doc, root); + /* Folders are attached directly under the document root; the + * caller (andbible_bookmarks_activate) nests the whole thing under + * a single new tree row of its own, the same way the existing + * BibleTime import does. */ + xmlNodePtr top = root; + (void)top_folder_name; /* now only used as the tree-row caption */ + + GHashTable *folder_by_label = g_hash_table_new(g_str_hash, g_str_equal); + xmlNodePtr unfiled = NULL; + gint imported = 0, skipped = 0; + + while (sqlite3_step(bm_st) == SQLITE_ROW) { + const void *idblob = sqlite3_column_blob(bm_st, 0); + int idlen = sqlite3_column_bytes(bm_st, 0); + const unsigned char *v11n_txt = sqlite3_column_text(bm_st, 1); + glong ord_start = (glong)sqlite3_column_int64(bm_st, 2); + glong ord_end = (glong)sqlite3_column_int64(bm_st, 3); + const unsigned char *module_txt = sqlite3_column_text(bm_st, 4); + + gchar *key = ordinal_range_to_key( + v11n_txt ? (const gchar *)v11n_txt : NULL, ord_start, + ord_end); + if (!key) { + ++skipped; + continue; + } + + gchar *note = fetch_note(note_st, idblob, idlen); + gchar *description = + note ? g_strdup_printf("%s - %s", key, note) + : g_strdup(key); + + GList *label_ids = fetch_label_ids(link_st, idblob, idlen); + GList *targets = NULL; + for (GList *l = label_ids; l; l = l->next) { + const gchar *lid_hex = (const gchar *)l->data; + LabelInfo *li = + (LabelInfo *)g_hash_table_lookup(labels, lid_hex); + if (!li || is_system_label(li->name)) + continue; + + xmlNodePtr folder = (xmlNodePtr)g_hash_table_lookup( + folder_by_label, lid_hex); + if (!folder) { + folder = xml_add_folder_to_parent_colored( + top, li->name, li->color); + g_hash_table_insert(folder_by_label, + g_strdup(lid_hex), folder); + } + targets = g_list_append(targets, folder); + } + if (!targets) { + if (!unfiled) + unfiled = xml_add_folder_to_parent( + top, _("Uncategorized")); + targets = g_list_append(targets, unfiled); + } + + for (GList *t = targets; t; t = t->next) + xml_add_bookmark_to_parent( + (xmlNodePtr)t->data, description, key, + module_txt ? (gchar *)module_txt : (gchar *)"", + NULL); + ++imported; + + g_list_free(targets); + g_list_free_full(label_ids, g_free); + g_free(note); + g_free(description); + g_free(key); + } + + sqlite3_finalize(bm_st); + sqlite3_finalize(link_st); + sqlite3_finalize(note_st); + g_hash_table_destroy(labels); + g_hash_table_destroy(folder_by_label); + sqlite3_close(db); + + /* --- Write to a temp file for gui_parse_bookmarks() to consume. --- */ + gchar *tmp_path = NULL; + gint fd = g_file_open_tmp("xiphos-andbible-import-XXXXXX.xml", + &tmp_path, error); + if (fd < 0) { + xmlFreeDoc(doc); + return FALSE; + } + close(fd); + + if (xmlSaveFormatFileEnc(tmp_path, doc, "UTF-8", 1) < 0) { + g_set_error(error, g_quark_from_static_string("andbible-import"), + 3, _("Could not write temporary file '%s'."), + tmp_path); + g_free(tmp_path); + xmlFreeDoc(doc); + return FALSE; + } + + xmlFreeDoc(doc); + *out_xml_path = tmp_path; + if (n_imported) + *n_imported = imported; + if (n_skipped) + *n_skipped = skipped; + return TRUE; +} diff --git a/ui/xi-menus-popup.gtkbuilder b/ui/xi-menus-popup.gtkbuilder index e5128f09c..b7736cba8 100644 --- a/ui/xi-menus-popup.gtkbuilder +++ b/ui/xi-menus-popup.gtkbuilder @@ -151,6 +151,18 @@ + + + Import AndBible Bookmarks + False + True + False + Import bookmarks from an AndBible sqlite backup file (replaces any previous AndBible import - don't file things by hand into this folder) + True + True + + + Export Folder diff --git a/ui/xi-menus.glade b/ui/xi-menus.glade index dc161a762..2a40be6b9 100644 --- a/ui/xi-menus.glade +++ b/ui/xi-menus.glade @@ -1813,6 +1813,28 @@ + + + True + Import bookmarks from an AndBible sqlite backup file (replaces any previous AndBible import - don't file things by hand into this folder) + Import AndBible Bookmarks + True + + + + + True + gtk-add + 1 + 0.5 + 0.5 + 0 + 0 + + + + + True