I got tired of using Brave just because of bookmark sync, especially on desktop when Trivalent is not missing import/export HTML that Vanadium is missing. So I wrote two scripts to create my own sync. Syncthing is necessary for this to work. This is a one way sync but there is a way to bypass that, I will tell how I solve this in the end (using KDE Connect).
This project is only possible because of GitHub - Baro82/grapheneos-vanadium-bookmarks-helper: A small utility to export Chromium-based browser bookmarks (including favicons) into an HTML+JS format that can be viewed and used directly inside the Vanadium browser on GrapheneOS. and ChatGPT for optimizing my terrible bash and adding regex search to the project U…
I got tired of using Brave just because of bookmark sync, especially on desktop when Trivalent is not missing import/export HTML that Vanadium is missing. So I wrote two scripts to create my own sync. Syncthing is necessary for this to work. This is a one way sync but there is a way to bypass that, I will tell how I solve this in the end (using KDE Connect).
This project is only possible because of GitHub - Baro82/grapheneos-vanadium-bookmarks-helper: A small utility to export Chromium-based browser bookmarks (including favicons) into an HTML+JS format that can be viewed and used directly inside the Vanadium browser on GrapheneOS. and ChatGPT for optimizing my terrible bash and adding regex search to the project UI.
To do this:
On secureblue, change paths and run this command from the home directory:
Script 1
mkdir -p ~/Documents/Bookmarks && cat > ~/Documents/Bookmarks/bookmarks.html <<'EOF'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Bookmarks</title>
<script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
<style>
:root {
--bg: #0f151e;
--panel: #111827;
--muted: #9aa4b2;
--text: #e5e7eb;
--accent: #60a5fa;
--border: #1f2937;
--bgfolder: #192330;
--bgitem: #314258;
}
html, body { height: 100%; }
body {
background-color: var(--bg);
min-height: 100%;
margin: 0px;
font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, sans-serif;
font-size: 14px;
color: var(--text);
}
main { padding: 16px; }
.viewer { padding: 0px; overflow: auto; }
ul.tree { list-style: none; padding-left: 0; margin: 0; }
ul li { padding: 10px 015px; border-radius: 6px; background-color: var(--bgitem); margin: 10px 0px; }
ul li.liRoot { background-color: transparent !important; margin: 0px; padding: 0px; border:none; }
ul li.liFolder { background-color: var(--bgfolder); }
ul li.liFolder:hover { background-color: color-mix(in srgb, var(--bgfolder) 95%, white); cursor: pointer; -webkit-tap-highlight-color: transparent; }
.children { margin-left: 8px; border-left: 1px dashed #263246; padding-left: 13px; }
.caret { user-select: none; font-size: 14px; text-align: center; }
.caret::before { content: '\25B6'; display: inline-block; margin-right: 10px; transform: rotate(0deg); transition: transform 120ms ease; }
.caret.open::before { transform: rotate(90deg); }
.node { display: grid; align-items: center; }
.node:not(.bookmark) { grid-template-columns: 30px 1fr auto; grid-template-rows: auto auto; gap:4px; }
.node:not(.bookmark) .title { font-size: 16px; font-weight: bold; }
.node.folder:not(.bookmark) .count { font-size: 0.75em; color: var(--muted); }
.node.bookmark { grid-template-columns: 35px 1fr; grid-template-rows: auto auto; gap:0px; }
.node.bookmark img { grid-row: 1 / 3; grid-column: 1 / 2; width: 22px; vertical-align: middle; }
.node.bookmark .title { grid-row: 1 / 2; grid-column: 2 / 3; font-size: 0.90em; overflow-wrap: break-word; }
.node.bookmark .count { grid-row: 2 / 3; grid-column: 2 / 3; font-size: 0.75em; overflow-wrap: break-word; color: var(--muted); }
a.link { color: var(--text); text-decoration: none; }
a.link:hover { color: var(--accent); text-decoration: underline; }
.empty { color: var(--muted); padding: 12px; }
</style>
</head>
<body>
<main>
<input
id="searchBox"
placeholder="Regex search (title, url, domain)…"
style="width:100%; padding:10px; margin-bottom:12px;"
>
<section class="viewer">
<ul class="tree" id="tree"></ul>
<div class="empty" id="emptyState">No data loaded.</div>
</section>
</main>
<script src="bookmarks_data.js"></script>
<script>
const $tree = $('#tree');
const $empty = $('#emptyState');
const $totals = $('#totals');
const isFolder = (n) => n && n.type === 'folder';
const isBookmark = (n) => n && (n.type === 'url' || (n.url && !n.children));
function rootsFrom(data) {
if (!data || !data.roots) return [];
return Object.entries(data.roots)
.filter(([_, value]) => Array.isArray(value.children) && value.children.length > 0)
.map(([key, value]) => ({ key, ...value }));
}
function countNodes(node){
let folders=0, links=0;
function walk(n){
if(isFolder(n)){ folders++; (n.children||[]).forEach(walk); }
else if(isBookmark(n)){ links++; }
}
walk(node);
return { folders, links };
}
function buildFolderNode(folder){
const id = 'f_' + Math.random().toString(36).slice(2,9);
const $li = $('<li class="liFolder">');
const { folders, links } = countNodes(folder);
const $row = $('<div class="node folder">');
const $caret = $('<span class="caret" aria-label="toggle" role="button" tabindex="0"></span>');
const $title = $('<span class="title"></span>').text(folder.name || '(folder)');
const $count = $('<span class="count"></span>').text(`${folders-1} folders · ${links} link`);
const $children = $('<ul class="children" hidden></ul>').attr('id', id);
$row.append($caret, $title, $count);
$li.append($row, $children);
$row.on('click keydown', (e)=>{ if(e.type==='click' || e.key==='Enter' || e.key===' '){ $caret.toggleClass('open'); $children.prop('hidden', !$children.prop('hidden')); }});
(folder.children||[]).forEach(child => {
if(isFolder(child)) $children.append(buildFolderNode(child));
else if(isBookmark(child)) $children.append(buildBookmarkNode(child));
});
return $li;
}
function buildBookmarkNode(bm) {
const $li = $('<li>');
const $row = $('<div class="node bookmark">');
const $title = $('<span class="title"></span>');
const $details = $('<span class="count"></span>');
const title = bm.name || bm.url || '(blank title)';
const url = bm.url || '#';
const favicon = bm.favicon || 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAANFJREFUSEtjZKAxYKSx+QwDY8H9+/cNWFhYzpPiuz9//mgqKireQNeD1QePHz/+T4rhMLWysrIY5uG1AJsGbBbDHDRqAc5oGQ0igil2NIiGSRA9efJk+v///zNA3mFkZFwgIyOTCPMa2ZH8588fSWipKoEjnF6ASlEWFpb3IHmSyyI0Q0/8+fPHESTGwsKynYGBwQHdUnItaABqbMRRitYDxRvIKq4ZGRktZWRkThBMQgwMDE+ePLH4////fllZWU6iKhxiDCVWzcDUycS6jhh1AE5lnxnG/RVzAAAAAElFTkSuQmCC';
const $favicon = $(`<img src="${favicon}"/>`);
$title.append($('<a class="link" target="_blank" rel="noopener noreferrer"></a>').attr('href', url).text(title));
try {
const u = new URL(url);
$details.text(u.hostname);
} catch(_) { $details.text(''); }
$row.append($favicon, $title, $details);
$li.append($row);
return $li;
}
function render(data){
$tree.empty();
if(!data){ $empty.show(); return; }
$empty.hide();
const rts = rootsFrom(data);
let totalLinks=0, totalFolders=0;
rts.forEach(rt => {
const {folders, links} = countNodes(rt);
const $section = $('<li class="liRoot">');
const $header = $('<div class="node" style="font-weight:600"></div>');
const label = rt.name;
$header.append('<span class="caret open"></span>');
$header.append($('<span class="title"></span>').text(label));
totalLinks += links; totalFolders += folders;
$header.append($('<span class="count"></span>').text(`${folders-1} folders · ${links} link`));
const $children = $('<ul class="children"></ul>');
(rt.children||[]).forEach(child => {
if(isFolder(child)) $children.append(buildFolderNode(child));
else if(isBookmark(child)) $children.append(buildBookmarkNode(child));
});
$section.append($header, $children);
$tree.append($section);
});
$('#statLinks').text(totalLinks);
$('#statFolders').text(Math.max(0,totalFolders - rootsFrom(data).length));
$('#statRoots').text(rts.length);
$totals.text(`${totalLinks} link · ${Math.max(0,totalFolders - rts.length)} folders`);
}
render(window.BOOKMARKS_JSON);
</script>
<script>
(() => {
const $search = $('#searchBox');
$search.on('input', () => {
let regex;
const value = $search.val().trim();
// RESET: show full tree when search is empty
if (!value) {
$('li').show();
$('.children').each(function () {
const $caret = $(this).siblings('.node').find('.caret');
if (!$caret.hasClass('open')) {
$(this).prop('hidden', true);
}
});
return;
}
try {
regex = new RegExp(value, 'i');
} catch {
return; // invalid regex
}
// Hide everything first
$('li').hide();
// Match bookmarks
$('.node.bookmark').each(function () {
const $bm = $(this);
const text =
$bm.find('.title').text() + ' ' +
($bm.find('.link').attr('href') || '') + ' ' +
$bm.find('.count').text();
if (regex.test(text)) {
const $li = $bm.closest('li');
// Show bookmark
$li.show();
// Show & expand all parents
$li.parents('li').each(function () {
$(this).show();
$(this)
.children('.children')
.prop('hidden', false)
.siblings('.node')
.find('.caret')
.addClass('open');
});
}
});
});
})();
</script>
</body>
</html>
EOF
Then you need open the Bookmarks folder in Documents and periodically run this script from there (Use cron or whatever, I use kde connect which I talk about at the end):
Script 2
rm bookmarks_data.js; cp /home/user/.config/trivalent/Default/Bookmarks /home/user/Documents/Bookmarks && mv Bookmarks bookmarks_data.js && sed -i '1iwindow.BOOKMARKS_JSON = ' bookmarks_data.js && echo ";" >> bookmarks_data.js && echo "" && echo -e "\e[31mThanks to Baro82 for their project: https://github.com/Baro82/grapheneos-vanadium-bookmarks-helper . I just added a search bar to the UI and wrote a bash script instead of python script.\e[0m"
Use syncthing to share this folder between PC and mobile.
Open GrapheneOS:
- Create a folder in Documents called Bookmarks that the PC folder syncs to
- Settings → Apps → Vanadium → Permissions → Photos and Videos → Enable Storage Scopes → Add the created folder
- Add this folder to syncthing and connect it with the desktop folder we created
- Vanadium → Settings → Homepage → Set the custom address to “file:///storage/emulated/0/Documents/Bookmarks/bookmarks.html”
- Enjoy.
Since this only syncs Trivalent → Vanadium, you can use KDE Connect commands to run the second script on demand, and share links to desktop that you want to bookmark. Bookmarking on Vanadium will not sync across, since there is no way built in right now.
Hope this helped. Can someone for whom this works also post this on GrapheneOS forum, I dislike creating multiple accounts, and their forum is aggressive against spammy emails for registration.