Lagrange [work/v1.17]

Importing all links on page into bookmark folders

=> a24657899111ce21e13bbae0b8dfdd0feba497a3

diff --git a/po/en.po b/po/en.po
index 8977fb17..ce5ec830 100644
--- a/po/en.po
+++ b/po/en.po
@@ -954,6 +954,12 @@ msgstr "Error Saving File"
 msgid "heading.import.bookmarks"
 msgstr "Import Bookmarks"
 
+msgid "dlg.import.intofolder"
+msgstr "Add to Folder:"
+
+msgid "dlg.import.headings"
+msgstr "Subfolders from Headings:"
+
 #, c-format
 msgid "dlg.import.found"
 msgid_plural "dlg.import.found.n"
@@ -969,6 +975,9 @@ msgstr[1] "%sAdd %d Bookmarks"
 msgid "dlg.import.notnew"
 msgstr "All links on this page are already bookmarked."
 
+msgid "dlg.import.notfound"
+msgstr "There are no links on this page."
+
 msgid "heading.autoreload"
 msgstr "Auto-Reload"
 
diff --git a/res/lang/cs.bin b/res/lang/cs.bin
index 1b8c0e08..01bd8529 100644
Binary files a/res/lang/cs.bin and b/res/lang/cs.bin differ
diff --git a/res/lang/de.bin b/res/lang/de.bin
index d6d47f51..fe140b86 100644
Binary files a/res/lang/de.bin and b/res/lang/de.bin differ
diff --git a/res/lang/en.bin b/res/lang/en.bin
index f8b4ffca..09229bfe 100644
Binary files a/res/lang/en.bin and b/res/lang/en.bin differ
diff --git a/res/lang/eo.bin b/res/lang/eo.bin
index 865910c0..0a5dd9e8 100644
Binary files a/res/lang/eo.bin and b/res/lang/eo.bin differ
diff --git a/res/lang/es.bin b/res/lang/es.bin
index c42c7c70..8ed2fa37 100644
Binary files a/res/lang/es.bin and b/res/lang/es.bin differ
diff --git a/res/lang/es_MX.bin b/res/lang/es_MX.bin
index e9bafe94..a6288c90 100644
Binary files a/res/lang/es_MX.bin and b/res/lang/es_MX.bin differ
diff --git a/res/lang/eu.bin b/res/lang/eu.bin
index 67bfccbc..4472a069 100644
Binary files a/res/lang/eu.bin and b/res/lang/eu.bin differ
diff --git a/res/lang/fi.bin b/res/lang/fi.bin
index 802860a4..c0d480c8 100644
Binary files a/res/lang/fi.bin and b/res/lang/fi.bin differ
diff --git a/res/lang/fr.bin b/res/lang/fr.bin
index 99bdc5c5..8505078e 100644
Binary files a/res/lang/fr.bin and b/res/lang/fr.bin differ
diff --git a/res/lang/gl.bin b/res/lang/gl.bin
index 5acfecfc..03b09939 100644
Binary files a/res/lang/gl.bin and b/res/lang/gl.bin differ
diff --git a/res/lang/hu.bin b/res/lang/hu.bin
index f07665ca..f19f2e96 100644
Binary files a/res/lang/hu.bin and b/res/lang/hu.bin differ
diff --git a/res/lang/ia.bin b/res/lang/ia.bin
index 1e95f965..b23b441a 100644
Binary files a/res/lang/ia.bin and b/res/lang/ia.bin differ
diff --git a/res/lang/ie.bin b/res/lang/ie.bin
index 94220e80..57439c4c 100644
Binary files a/res/lang/ie.bin and b/res/lang/ie.bin differ
diff --git a/res/lang/isv.bin b/res/lang/isv.bin
index e290eb93..e06b83c2 100644
Binary files a/res/lang/isv.bin and b/res/lang/isv.bin differ
diff --git a/res/lang/it.bin b/res/lang/it.bin
index 3b1bd5d3..9894f60c 100644
Binary files a/res/lang/it.bin and b/res/lang/it.bin differ
diff --git a/res/lang/ja.bin b/res/lang/ja.bin
index d66a9f07..b74b54d8 100644
Binary files a/res/lang/ja.bin and b/res/lang/ja.bin differ
diff --git a/res/lang/nl.bin b/res/lang/nl.bin
index 944e0414..d05f2ac8 100644
Binary files a/res/lang/nl.bin and b/res/lang/nl.bin differ
diff --git a/res/lang/pl.bin b/res/lang/pl.bin
index 95d963fa..03cc3f7a 100644
Binary files a/res/lang/pl.bin and b/res/lang/pl.bin differ
diff --git a/res/lang/ru.bin b/res/lang/ru.bin
index c3ed24ad..2b719706 100644
Binary files a/res/lang/ru.bin and b/res/lang/ru.bin differ
diff --git a/res/lang/sk.bin b/res/lang/sk.bin
index 7f03e531..d0dc7df9 100644
Binary files a/res/lang/sk.bin and b/res/lang/sk.bin differ
diff --git a/res/lang/sr.bin b/res/lang/sr.bin
index 77786e29..edc6e4a9 100644
Binary files a/res/lang/sr.bin and b/res/lang/sr.bin differ
diff --git a/res/lang/tok.bin b/res/lang/tok.bin
index 8a857f68..3f2b05f4 100644
Binary files a/res/lang/tok.bin and b/res/lang/tok.bin differ
diff --git a/res/lang/tr.bin b/res/lang/tr.bin
index 5a10e7c4..0db87aab 100644
Binary files a/res/lang/tr.bin and b/res/lang/tr.bin differ
diff --git a/res/lang/uk.bin b/res/lang/uk.bin
index 26da3aad..15c0c180 100644
Binary files a/res/lang/uk.bin and b/res/lang/uk.bin differ
diff --git a/res/lang/zh_Hans.bin b/res/lang/zh_Hans.bin
index 7cb820a4..4c3ae410 100644
Binary files a/res/lang/zh_Hans.bin and b/res/lang/zh_Hans.bin differ
diff --git a/res/lang/zh_Hant.bin b/res/lang/zh_Hant.bin
index 24753c7e..607a8c98 100644
Binary files a/res/lang/zh_Hant.bin and b/res/lang/zh_Hant.bin differ
diff --git a/src/app.c b/src/app.c
index fd4d53ff..f84d0acc 100644
--- a/src/app.c
+++ b/src/app.c
@@ -5051,7 +5051,7 @@ iBool handleCommand_App(const char *cmd) {
         iArchive *zip = iClob(new_Archive());
         if (openFile_Archive(zip, path)) {
             if (!arg_Command(cmd)) {
-                makeUserDataImporter_Dialog(path);
+                makeUserDataImporter_Widget(path);
                 return iTrue;
             }
             const int bookmarks = argLabel_Command(cmd, "bookmarks");
diff --git a/src/bookmarks.c b/src/bookmarks.c
index bfc47783..f01c9c54 100644
--- a/src/bookmarks.c
+++ b/src/bookmarks.c
@@ -163,7 +163,7 @@ struct Impl_Bookmarks {
     int       idEnum;
     iHash     bookmarks; /* bookmark ID is the hash key */
     uint32_t  recentFolderId; /* recently interacted with */
-    iPtrArray remoteRequests;   
+    iPtrArray remoteRequests;
 };
 
 iDefineTypeConstruction(Bookmarks)
@@ -252,7 +252,7 @@ static void loadOldFormat_Bookmarks(iBookmarks *d, const char *dirPath) {
 /*----------------------------------------------------------------------------------------------*/
 
 iDeclareType(BookmarkLoader)
-    
+
 struct Impl_BookmarkLoader {
     iTomlParser       *toml;
     iBookmarks        *bookmarks;
@@ -368,7 +368,7 @@ static void load_BookmarkLoader(iBookmarkLoader *d, iStream *stream) {
 }
 
 iDefineTypeConstructionArgs(BookmarkLoader, (iBookmarks *b), b)
-    
+
 /*----------------------------------------------------------------------------------------------*/
 
 static iBool isMatchingParent_Bookmark_(void *context, const iBookmark *bm) {
@@ -522,6 +522,11 @@ static iRangei orderRange_Bookmarks_(const iBookmarks *d) {
 
 uint32_t add_Bookmarks(iBookmarks *d, const iString *url, const iString *title, const iString *tags,
                        iChar icon) {
+    return addToFolder_Bookmarks(d, url, title, tags, icon, 0);
+}
+
+uint32_t addToFolder_Bookmarks(iBookmarks *d, const iString *url, const iString *title,
+                               const iString *tags, iChar icon, uint32_t folderId) {
     lock_Mutex(d->mtx);
     iBookmark *bm = new_Bookmark();
     if (url) {
@@ -540,6 +545,7 @@ uint32_t add_Bookmarks(iBookmarks *d, const iString *url, const iString *title,
     else {
         bm->order = ord.start - 1; /* First in lists. */
     }
+    bm->parentId = folderId;
     insert_Bookmarks_(d, bm);
     unlock_Mutex(d->mtx);
     return id_Bookmark(bm);
@@ -563,7 +569,7 @@ iBool updateBookmarkIcon_Bookmarks(iBookmarks *d, const iString *url, iChar icon
     iBool changed = iFalse;
     lock_Mutex(d->mtx);
     iForEach(Hash, i, &d->bookmarks) {
-        iBookmark *bm = (iBookmark *) i.value;    
+        iBookmark *bm = (iBookmark *) i.value;
         if (~bm->flags & remote_BookmarkFlag && ~bm->flags & userIcon_BookmarkFlag) {
             if (equalCase_String(&bm->url, url) && icon != bm->icon) {
                 bm->icon = icon;
@@ -640,10 +646,10 @@ uint32_t findUrl_Bookmarks(const iBookmarks *d, const iString *url) {
 /*----------------------------------------------------------------------------------------------*/
 
 iDeclareType(MatchUrlArgs)
-    
+
 struct Impl_MatchUrlArgs {
     const iString *url;
-    const iString *identityFp;    
+    const iString *identityFp;
 };
 
 static iBool matchUrlAndIdent_(iMatchUrlArgs *args, const iBookmark *bm) {
@@ -664,7 +670,7 @@ uint32_t findUrlIdent_Bookmarks(const iBookmarks *d, const iString *url, const i
     if (isEmpty_PtrArray(found)) {
         return 0;
     }
-    return id_Bookmark(constFront_PtrArray(found));    
+    return id_Bookmark(constFront_PtrArray(found));
 }
 
 /*----------------------------------------------------------------------------------------------*/
diff --git a/src/bookmarks.h b/src/bookmarks.h
index bdc8f600..b4b3517d 100644
--- a/src/bookmarks.h
+++ b/src/bookmarks.h
@@ -32,7 +32,7 @@ iDeclareType(GmRequest)
 
 iDeclareType(Bookmark)
 iDeclareTypeConstruction(Bookmark)
-    
+
 /* These values are not serialized as-is in bookmarks.ini. Instead, they are included in `tags`
    with a dot prefix. This helps retain backwards and forwards compatibility. */
 enum iBookmarkFlags {
@@ -88,6 +88,8 @@ void        deserialize_Bookmarks       (iBookmarks *, iStream *ins, enum iImpor
 
 uint32_t    add_Bookmarks               (iBookmarks *, const iString *url, const iString *title,
                                          const iString *tags, iChar icon);
+uint32_t    addToFolder_Bookmarks       (iBookmarks *, const iString *url, const iString *title,
+                                         const iString *tags, iChar icon, uint32_t folderId);
 iBool       remove_Bookmarks            (iBookmarks *, uint32_t id);
 iBookmark * get_Bookmarks               (iBookmarks *, uint32_t id);
 void        reorder_Bookmarks           (iBookmarks *, uint32_t id, int newOrder);
diff --git a/src/gmdocument.c b/src/gmdocument.c
index 0aaf5868..043b93e7 100644
--- a/src/gmdocument.c
+++ b/src/gmdocument.c
@@ -2687,6 +2687,10 @@ static const iGmLink *link_GmDocument_(const iGmDocument *d, iGmLinkId id) {
     return NULL;
 }
 
+size_t numLinks_GmDocument(const iGmDocument *d) {
+    return size_PtrArray(&d->links);
+}
+
 const iString *linkUrl_GmDocument(const iGmDocument *d, iGmLinkId linkId) {
     const iGmLink *link = link_GmDocument_(d, linkId);
     return link ? &link->url : NULL;
diff --git a/src/gmdocument.h b/src/gmdocument.h
index 96db60db..34e8358a 100644
--- a/src/gmdocument.h
+++ b/src/gmdocument.h
@@ -248,6 +248,7 @@ enum iGmLinkPart {
 const iGmRun *  findRun_GmDocument      (const iGmDocument *, iInt2 pos);
 iRangecc        findLoc_GmDocument      (const iGmDocument *, iInt2 pos);
 const iGmRun *  findRunAtLoc_GmDocument (const iGmDocument *, const char *loc);
+size_t          numLinks_GmDocument     (const iGmDocument *); /* link IDs: 1...numLinks (inclusive) */
 const iString * linkUrl_GmDocument      (const iGmDocument *, iGmLinkId linkId);
 iRangecc        linkUrlRange_GmDocument (const iGmDocument *, iGmLinkId linkId);
 iRangecc        linkLabel_GmDocument    (const iGmDocument *, iGmLinkId linkId);
diff --git a/src/macos.m b/src/macos.m
index 15481c55..d42bdaf7 100644
--- a/src/macos.m
+++ b/src/macos.m
@@ -853,15 +853,23 @@ static NSMenuItem *makeMenuItems_(NSMenu *menu, MenuCommands *commands, int atIn
                 if (!subwidget) {
                     subwidget = findWidget_Root(submenuId);
                 }
-                iAssert(subwidget);
-                const iArray *items = userData_Object(subwidget);
-                iAssert(items);
-                makeMenuItems_(sub, commands, 0, isBookmarksMenu, constData_Array(items), size_Array(items));
-                [item setSubmenu:sub];
-                if (isBookmarksMenu) {
-#if defined (__MAC_10_13)
-                    [item setImage:[NSImage imageWithSystemSymbolName:@"folder" accessibilityDescription:nil]];
-#endif
+                if (subwidget) {
+                    const iArray *items = userData_Object(subwidget);
+                    iAssert(items);
+                    makeMenuItems_(sub, commands, 0, isBookmarksMenu, constData_Array(items),
+                                   size_Array(items));
+                    [item setSubmenu:sub];
+                    if (isBookmarksMenu) {
+    #if defined (__MAC_10_13)
+                        [item setImage:[NSImage imageWithSystemSymbolName:@"folder"
+                                                 accessibilityDescription:nil]];
+    #endif
+                    }
+                }
+                else {
+                    [sub release];
+                    [item release];
+                    continue;
                 }
                 [sub release];
             }
@@ -869,7 +877,8 @@ static NSMenuItem *makeMenuItems_(NSMenu *menu, MenuCommands *commands, int atIn
                 item.action = (hasCommand ? @selector(postMenuItemCommand:) : nil);
                 if (isBookmarksMenu && hasCommand && startsWith_CStr(items[i].command, "!open ")) {
 #if defined (__MAC_10_13)
-                    [item setImage:[NSImage imageWithSystemSymbolName:@"bookmark.fill" accessibilityDescription:nil]];
+                    [item setImage:[NSImage imageWithSystemSymbolName:@"bookmark.fill"
+                                             accessibilityDescription:nil]];
 #endif
                 }
             }
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c
index 4229989d..42711739 100644
--- a/src/ui/documentwidget.c
+++ b/src/ui/documentwidget.c
@@ -71,6 +71,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -2510,13 +2511,6 @@ static const iString *saveToDownloads_(const iString *url, const iString *mime,
     return savePath;
 }
 
-static void addAllLinks_(void *context, const iGmRun *run) {
-    iPtrArray *links = context;
-    if (~run->flags & decoration_GmRunFlag && run->linkId) {
-        pushBack_PtrArray(links, run);
-    }
-}
-
 static iBool handlePinch_DocumentWidget_(iDocumentWidget *d, const char *cmd) {
     if (equal_Command(cmd, "pinch.began")) {
         d->pinchZoomInitial = d->pinchZoomPosted = prefs_App()->zoomPercent;
@@ -3593,51 +3587,113 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
         return iTrue;
     }
     else if (equal_Command(cmd, "bookmark.links") && document_App() == d) {
-        iPtrArray *links = collectNew_PtrArray();
-        render_GmDocument(d->view->doc, (iRangei){ 0, size_GmDocument(d->view->doc).y }, addAllLinks_, links);
+        iIntSet *linkIds = collectNew_IntSet();
         /* Find links that aren't already bookmarked. */
-        iForEach(PtrArray, i, links) {
-            const iGmRun *run = i.ptr;
-            uint32_t      bmid;
-            if ((bmid = findUrl_Bookmarks(bookmarks_App(),
-                                          linkUrl_GmDocument(d->view->doc, run->linkId))) != 0) {
+        const iGmDocument *doc = d->view->doc;
+        for (size_t linkId = 1; linkId <= numLinks_GmDocument(doc); linkId++) {
+            uint32_t bmid;
+            if ((bmid = findUrl_Bookmarks(bookmarks_App(), linkUrl_GmDocument(doc, linkId))) != 0) {
                 const iBookmark *bm = get_Bookmarks(bookmarks_App(), bmid);
                 /* We can import local copies of remote bookmarks. */
                 if (~bm->flags & remote_BookmarkFlag) {
-                    remove_PtrArrayIterator(&i);
+                    continue; /* This one is bookmarked. */
                 }
             }
+            insert_IntSet(linkIds, linkId);
         }
-        if (!isEmpty_PtrArray(links)) {
+        if (!isEmpty_IntSet(linkIds)) {
             if (argLabel_Command(cmd, "confirm")) {
-                const size_t count = size_PtrArray(links);
-                makeQuestion_Widget(
-                    uiHeading_ColorEscape "${heading.import.bookmarks}",
-                    formatCStrs_Lang("dlg.import.found.n", count),
-                    (iMenuItem[]){ { "${cancel}" },
-                                   { format_CStr(cstrCount_Lang("dlg.import.add.n", (int) count),
-                                                 uiTextAction_ColorEscape,
-                                                 count),
-                                     0,
-                                     0,
-                                     "bookmark.links" } },
-                    2);
+                const size_t count = size_IntSet(linkIds);
+                makeLinkImporter_Widget(count);
             }
             else {
-                iConstForEach(PtrArray, j, links) {
-                    const iGmRun *run = j.ptr;
-                    add_Bookmarks(bookmarks_App(),
-                                  linkUrl_GmDocument(d->view->doc, run->linkId),
-                                  collect_String(newRange_String(run->text)),
-                                  NULL,
-                                  0x1f588 /* pin */);
+                const uint32_t intoFolder   = argLabel_Command(cmd, "folder");
+                const iBool    withHeadings = argLabel_Command(cmd, "headings");
+                /* We need to prepare some auxiliary bookkeeping to keep track of the folders
+                   that are created for each section of the page. */
+                uint32_t parentId = intoFolder;
+                uint32_t hierarchy[] = { intoFolder, 0, 0, 0, 0, 0 };
+                const iPtrArray  *headings = headings_GmDocument(doc);
+                const iGmHeading *head = isEmpty_Array(headings) ? NULL : constData_Array(headings);
+                const iGmHeading *headFirst = head;
+                const iGmHeading *headEnd   = isEmpty_Array(headings) ? NULL : constEnd_Array(headings);
+                uint32_t *headingBookmarkIds = calloc(size_Array(headings), sizeof(uint32_t));
+                /* We will create folders as we go and afterwards delete the ones that
+                   didn't end up containing any links. */
+                iDeclareType(InfoNode);
+                struct Impl_InfoNode {
+                    iHashNode node;
+                    size_t numChildren;
+                };
+                iHash *folderInfo = new_Hash();
+                /* `linkIds` only contains the new links that need to be bookmarked. */
+                iConstForEach(IntSet, j, linkIds) {
+                    const iGmLinkId linkId = *j.value;
+                    iRangecc linkRange = linkUrlRange_GmDocument(doc, linkId);
+                    /* Advance in the headings until we reach the one that this link is under. */
+                    while (withHeadings && head < headEnd && linkRange.start > head->text.start) {
+                        if (!headingBookmarkIds[head - headFirst]) {
+                            const int hlev = head->level + 1;
+                            parentId       = addToFolder_Bookmarks(bookmarks_App(),
+                                                             NULL,
+                                                             collectNewRange_String(head->text),
+                                                             NULL,
+                                                             0,
+                                                             hierarchy[hlev - 1]);
+                            hierarchy[hlev] = parentId;
+                            iInfoNode *info = iMalloc(InfoNode); {
+                                info->node.key = parentId;
+                                info->numChildren = 0;
+                            }
+                            insert_Hash(folderInfo, &info->node);
+                            headingBookmarkIds[head - headFirst] = parentId;
+                            parentId = parentId;
+                            /* Keep track of the hierarchy so we know at any time the parent
+                               of each heading level. */
+                            for (int k = 1; k < hlev; k++) {
+                                if (hierarchy[k] == 0) {
+                                    hierarchy[k] = parentId;
+                                }
+                            }
+                            hierarchy[hlev + 1] = parentId;
+                            hierarchy[hlev + 2] = parentId;
+                        }
+                        head++;
+                    }
+                    addToFolder_Bookmarks(bookmarks_App(),
+                                          linkUrl_GmDocument(doc, linkId),
+                                          collectNewRange_String(linkLabel_GmDocument(doc, linkId)),
+                                          NULL,
+                                          0x1f588 /* pin */,
+                                          withHeadings ? parentId : intoFolder);
+                    /* Count children. */
+                    if (withHeadings) {
+                        for (uint32_t pid = parentId;
+                             pid && pid != intoFolder;
+                             pid = get_Bookmarks(bookmarks_App(), pid)->parentId) {
+                            iInfoNode *n = (iInfoNode *) value_Hash(folderInfo, pid);
+                            iAssert(n);
+                            n->numChildren++;
+                        }
+                    }
+                }
+                iForEach(Hash, iter, folderInfo) {
+                    iInfoNode *n = (iInfoNode *) iter.value;
+                    if (n->numChildren == 0) {
+                        /* This folder was not needed. */
+                        remove_Bookmarks(bookmarks_App(), n->node.key);
+                    }
+                    free(remove_HashIterator(&iter));
                 }
+                delete_Hash(folderInfo);
+                free(headingBookmarkIds);
                 postCommand_App("bookmarks.changed");
             }
         }
         else {
             makeSimpleMessage_Widget(uiHeading_ColorEscape "${heading.import.bookmarks}",
-                                     "${dlg.import.notnew}");
+                                     numLinks_GmDocument(doc) == 0 ? "${dlg.import.notfound}"
+                                                                   : "${dlg.import.notnew}");
         }
         return iTrue;
     }
diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c
index d6870278..3378671a 100644
--- a/src/ui/sidebarwidget.c
+++ b/src/ui/sidebarwidget.c
@@ -1790,6 +1790,7 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
                         postCommand_App("bookmarks.changed");
                     }
                     else {
+                        setFocus_Widget(NULL);
                         const size_t numBookmarks = numBookmarks_(list);
                         makeQuestion_Widget(uiHeading_ColorEscape "${heading.confirm.bookmarks.delete}",
                                             formatCStrs_Lang("dlg.confirm.bookmarks.delete.n", numBookmarks),
@@ -2276,6 +2277,7 @@ static void draw_SidebarItem_(const iSidebarItem *d, iPaint *p, iRect itemRect,
                               const iListWidget *list) {
     const iSidebarWidget *sidebar = findParentClass_Widget(constAs_Widget(list),
                                                            &Class_SidebarWidget);
+    const iBool isListFocus  = isFocused_Widget(list);
     const iBool isMenuVisible = isVisible_Widget(sidebar->menu);
     const iBool isDragging   = constDragItem_ListWidget(list) == d;
     const iBool isEditing    = sidebar->isEditing; /* only on mobile */
@@ -2530,6 +2532,10 @@ static void draw_SidebarItem_(const iSidebarItem *d, iPaint *p, iRect itemRect,
         }
         iEndCollect();
     }
+    if (isListFocus && isHover && !isTerminal_Platform()) {
+        /* Visualize the keyboard cursor. */
+        drawRect_Paint(p, shrunk_Rect(itemRect, one_I2()), uiTextAction_ColorId);
+    }
 }
 
 iBeginDefineSubclass(SidebarWidget, Widget)
diff --git a/src/ui/util.c b/src/ui/util.c
index 296d0280..59866400 100644
--- a/src/ui/util.c
+++ b/src/ui/util.c
@@ -2948,10 +2948,12 @@ static void addPrefsInputWithHeading_(iWidget *headings, iWidget *values,
     addDialogInputWithHeading_(headings, values, format_CStr("${%s}", id), id, input);
 }
 
-static void addDialogToggle_(iWidget *headings, iWidget *values,
+static iWidget *addDialogToggle_(iWidget *headings, iWidget *values,
                              const char *heading, const char *toggleId) {
+    iWidget *toggle;
     addChild_Widget(headings, iClob(makeHeading_Widget(heading)));
-    addChild_Widget(values, iClob(makeToggle_Widget(toggleId)));
+    addChild_Widget(values, toggle = iClob(makeToggle_Widget(toggleId)));
+    return toggle;
 }
 
 static void addDialogToggleGroup_(iWidget *headings, iWidget *values, const char *title,
@@ -4367,6 +4369,74 @@ iWidget *makeSnippetCreation_Widget(void) {
 
 /*----------------------------------------------------------------------------------------------*/
 
+static iBool handleLinkImporterCommands_(iWidget *dlg, const char *cmd) {
+    if (equalWidget_Command(cmd, dlg, "cancel")) {
+        setupSheetTransition_Mobile(dlg, dialogTransitionDir_Widget(dlg));
+        destroy_Widget(dlg);
+        return iTrue;
+    }
+    else if (equalWidget_Command(cmd, dlg, "dlg.import.intofolder")) {
+        updateDropdownSelection_LabelWidget(findChild_Widget(dlg, "dlg.import.intofolder"),
+                                            format_CStr(" arg:%d", arg_Command(cmd)));
+        return iTrue;
+    }
+    else if (equalWidget_Command(cmd, dlg, "dlg.import.accept")) {
+        const char *intoFolder =
+            selectedDropdownCommand_LabelWidget(findChild_Widget(dlg, "dlg.import.intofolder"));
+        const iBool headings = isSelected_Widget(findChild_Widget(dlg, "dlg.import.headings"));
+        postCommandf_App("bookmark.links folder:%d headings:%d", arg_Command(intoFolder), headings);
+        setupSheetTransition_Mobile(dlg, dialogTransitionDir_Widget(dlg));
+        destroy_Widget(dlg);
+        return iTrue;
+    }
+    return iFalse;
+}
+
+iWidget *makeLinkImporter_Widget(size_t count) {
+    const iMenuItem actions[] = {
+        { "${cancel}" },
+        { format_CStr(
+              cstrCount_Lang("dlg.import.add.n", (int) count), uiTextAction_ColorEscape, count),
+          0,
+          0,
+          "dlg.import.accept" },
+    };
+    iWidget *dlg = NULL;
+    if (isUsingPanelLayout_Mobile()) {
+        /* TODO */
+    }
+    else {
+        iWidget *headings, *values;
+        dlg = makeSheet_Widget("linkbookmarking");
+        addDialogTitle_(dlg, "${heading.import.bookmarks}", "heading.import.bookmarks");
+        addWrappedLabel_Widget(dlg, formatCStrs_Lang("dlg.import.found.n", count), NULL);
+        addChild_Widget(dlg, iClob(makePadding_Widget(gap_UI)));
+        addChild_Widget(dlg, iClob(makeTwoColumns_Widget(&headings, &values)));
+        const iArray *folders = makeBookmarkFolderActions_MenuItem("dlg.import.intofolder", iTrue, 0);
+        iLabelWidget *intoFolder = addDialogDropMenu_(headings,
+                           values,
+                           "${dlg.import.intofolder}",
+                           constData_Array(folders),
+                           iInvalidSize,
+                           "dlg.import.intofolder");
+        updateDropdownSelection_LabelWidget(
+            intoFolder, format_CStr(" arg:%zu", recentFolder_Bookmarks(bookmarks_App())));
+        setToggle_Widget(
+            addDialogToggle_(headings, values, "${dlg.import.headings}", "dlg.import.headings"),
+            iTrue);
+        addChild_Widget(dlg, iClob(makePadding_Widget(gap_UI)));
+        addChild_Widget(dlg, iClob(makeDialogButtons_Widget(actions, iElemCount(actions))));
+        addChild_Widget(get_Root()->widget, iClob(dlg));
+        arrange_Widget(dlg);
+        arrange_Widget(dlg);
+    }
+    setCommandHandler_Widget(dlg, handleLinkImporterCommands_);
+    setupSheetTransition_Mobile(dlg, incoming_TransitionFlag | dialogTransitionDir_Widget(dlg));
+    return dlg;
+}
+
+/*----------------------------------------------------------------------------------------------*/
+
 iWidget *makeIdentityCreation_Widget(void) {
     const iMenuItem actions[] = { { "${dlg.newident.more}", 0, 0, "ident.showmore" },
                                   { "---" },
@@ -4731,7 +4801,7 @@ static iBool handleUserDataImporterCommands_(iWidget *dlg, const char *cmd) {
     return iFalse;
 }
 
-iWidget *makeUserDataImporter_Dialog(const iString *archivePath) {
+iWidget *makeUserDataImporter_Widget(const iString *archivePath) {
     iWidget *dlg;
     const iMenuItem actions[] = {
         { "${menu.selectall}", 0, 0, "importer.selectall" },
diff --git a/src/ui/util.h b/src/ui/util.h
index 6d23ad00..88835167 100644
--- a/src/ui/util.h
+++ b/src/ui/util.h
@@ -407,7 +407,8 @@ iWidget *   makeSiteSpecificSettings_Widget (const iString *url);
 iWidget *   makeSnippetCreation_Widget      (void);
 iWidget *   makeTranslation_Widget          (iWidget *parent);
 iWidget *   makeGlyphFinder_Widget          (void);
-iWidget *   makeUserDataImporter_Dialog     (const iString *archivePath);
+iWidget *   makeUserDataImporter_Widget     (const iString *archivePath);
+iWidget *   makeLinkImporter_Widget         (size_t count);
 
 const char *    languageId_String   (const iString *menuItemLabel);
 int             languageIndex_CStr  (const char *langId);
Proxy Information
Original URL
gemini://git.skyjake.fi/lagrange/work%2Fv1.17/cdiff/a24657899111ce21e13bbae0b8dfdd0feba497a3
Status Code
Success (20)
Meta
text/gemini; charset=utf-8
Capsule Response Time
111.673145 milliseconds
Gemini-to-HTML Time
1.498448 milliseconds

This content has been proxied by September (3851b).