Skip to main content

max / balanced_breakfast

v0.3.0: Beta-ready milestone — OTA updater, plugin authoring docs, theme fixes, UI improvements
Co-Authored-By
Claude Opus 4.6 <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-03-17 02:58 UTC
Commit: 6c977d7b3521fc6f9bfa29753670ec3759bbeaa7
Parent: d6f00ba
20 files changed, +565 insertions, -57 deletions
M Cargo.lock +185 -2
@@ -115,6 +115,15 @@
115 115 source = "registry+https://github.com/rust-lang/crates.io-index"
116 116 checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea"
117 117
118 + [[package]]
119 + name = "arbitrary"
120 + version = "1.4.2"
121 + source = "registry+https://github.com/rust-lang/crates.io-index"
122 + checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
123 + dependencies = [
124 + "derive_arbitrary",
125 + ]
126 +
118 127 [[package]]
119 128 name = "argon2"
120 129 version = "0.5.3"
@@ -193,6 +202,7 @@
193 202 "tauri-build",
194 203 "tauri-plugin-dialog",
195 204 "tauri-plugin-shell",
205 + "tauri-plugin-updater",
196 206 "tauri-plugin-window-state",
197 207 "tokio",
198 208 "toml 0.8.2",
@@ -851,6 +861,17 @@
851 861 "serde_core",
852 862 ]
853 863
864 + [[package]]
865 + name = "derive_arbitrary"
866 + version = "1.4.2"
867 + source = "registry+https://github.com/rust-lang/crates.io-index"
868 + checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
869 + dependencies = [
870 + "proc-macro2",
871 + "quote",
872 + "syn 2.0.114",
873 + ]
874 +
854 875 [[package]]
855 876 name = "derive_more"
856 877 version = "0.99.20"
@@ -1103,6 +1124,17 @@
1103 1124 "rustc_version",
1104 1125 ]
1105 1126
1127 + [[package]]
1128 + name = "filetime"
1129 + version = "0.2.27"
1130 + source = "registry+https://github.com/rust-lang/crates.io-index"
1131 + checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db"
1132 + dependencies = [
1133 + "cfg-if",
1134 + "libc",
1135 + "libredox",
1136 + ]
1137 +
1106 1138 [[package]]
1107 1139 name = "find-msvc-tools"
1108 1140 version = "0.1.9"
@@ -2464,6 +2496,12 @@
2464 2496 source = "registry+https://github.com/rust-lang/crates.io-index"
2465 2497 checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
2466 2498
2499 + [[package]]
2500 + name = "minisign-verify"
2501 + version = "0.2.5"
2502 + source = "registry+https://github.com/rust-lang/crates.io-index"
2503 + checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e"
2504 +
2467 2505 [[package]]
2468 2506 name = "miniz_oxide"
2469 2507 version = "0.8.9"
@@ -2818,6 +2856,18 @@
2818 2856 "objc2-core-foundation",
2819 2857 ]
2820 2858
2859 + [[package]]
2860 + name = "objc2-osa-kit"
2861 + version = "0.3.2"
2862 + source = "registry+https://github.com/rust-lang/crates.io-index"
2863 + checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0"
2864 + dependencies = [
2865 + "bitflags 2.10.0",
2866 + "objc2",
2867 + "objc2-app-kit",
2868 + "objc2-foundation",
2869 + ]
2870 +
2821 2871 [[package]]
2822 2872 name = "objc2-quartz-core"
2823 2873 version = "0.3.2"
@@ -2956,6 +3006,20 @@
2956 3006 "windows-sys 0.61.2",
2957 3007 ]
2958 3008
3009 + [[package]]
3010 + name = "osakit"
3011 + version = "0.3.1"
3012 + source = "registry+https://github.com/rust-lang/crates.io-index"
3013 + checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b"
3014 + dependencies = [
3015 + "objc2",
3016 + "objc2-foundation",
3017 + "objc2-osa-kit",
3018 + "serde",
3019 + "serde_json",
3020 + "thiserror 2.0.18",
3021 + ]
3022 +
2959 3023 [[package]]
2960 3024 name = "pango"
2961 3025 version = "0.18.3"
@@ -3641,15 +3705,20 @@
3641 3705 "http-body",
3642 3706 "http-body-util",
3643 3707 "hyper",
3708 + "hyper-rustls",
3644 3709 "hyper-util",
3645 3710 "js-sys",
3646 3711 "log",
3647 3712 "percent-encoding",
3648 3713 "pin-project-lite",
3714 + "rustls",
3715 + "rustls-pki-types",
3716 + "rustls-platform-verifier",
3649 3717 "serde",
3650 3718 "serde_json",
3651 3719 "sync_wrapper",
3652 3720 "tokio",
3721 + "tokio-rustls",
3653 3722 "tokio-util",
3654 3723 "tower",
3655 3724 "tower-http",
@@ -3792,6 +3861,18 @@
3792 3861 "zeroize",
3793 3862 ]
3794 3863
3864 + [[package]]
3865 + name = "rustls-native-certs"
3866 + version = "0.8.3"
3867 + source = "registry+https://github.com/rust-lang/crates.io-index"
3868 + checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63"
3869 + dependencies = [
3870 + "openssl-probe",
3871 + "rustls-pki-types",
3872 + "schannel",
3873 + "security-framework",
3874 + ]
3875 +
3795 3876 [[package]]
3796 3877 name = "rustls-pki-types"
3797 3878 version = "1.14.0"
@@ -3801,6 +3882,33 @@
3801 3882 "zeroize",
3802 3883 ]
3803 3884
3885 + [[package]]
3886 + name = "rustls-platform-verifier"
3887 + version = "0.6.2"
3888 + source = "registry+https://github.com/rust-lang/crates.io-index"
3889 + checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784"
3890 + dependencies = [
3891 + "core-foundation 0.10.1",
3892 + "core-foundation-sys",
3893 + "jni",
3894 + "log",
3895 + "once_cell",
3896 + "rustls",
3897 + "rustls-native-certs",
3898 + "rustls-platform-verifier-android",
3899 + "rustls-webpki",
3900 + "security-framework",
3901 + "security-framework-sys",
3902 + "webpki-root-certs",
3903 + "windows-sys 0.61.2",
3904 + ]
3905 +
3906 + [[package]]
3907 + name = "rustls-platform-verifier-android"
3908 + version = "0.1.1"
3909 + source = "registry+https://github.com/rust-lang/crates.io-index"
3910 + checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
3911 +
3804 3912 [[package]]
3805 3913 name = "rustls-webpki"
3806 3914 version = "0.103.9"
@@ -4680,13 +4788,13 @@
4680 4788 "reqwest 0.12.28",
4681 4789 "serde",
4682 4790 "serde_json",
4683 - "sha2",
4684 - "thiserror 1.0.69",
4791 + "thiserror 2.0.18",
4685 4792 "tokio",
4686 4793 "tracing",
4687 4794 "unicode-normalization",
4688 4795 "urlencoding",
4689 4796 "uuid",
4797 + "zeroize",
4690 4798 ]
4691 4799
4692 4800 [[package]]
@@ -4785,6 +4893,17 @@
4785 4893 "syn 2.0.114",
4786 4894 ]
4787 4895
4896 + [[package]]
4897 + name = "tar"
4898 + version = "0.4.44"
4899 + source = "registry+https://github.com/rust-lang/crates.io-index"
4900 + checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a"
4901 + dependencies = [
4902 + "filetime",
4903 + "libc",
4904 + "xattr",
4905 + ]
4906 +
4788 4907 [[package]]
4789 4908 name = "target-lexicon"
4790 4909 version = "0.12.16"
@@ -4983,6 +5102,39 @@
4983 5102 "tokio",
4984 5103 ]
4985 5104
5105 + [[package]]
5106 + name = "tauri-plugin-updater"
5107 + version = "2.10.0"
5108 + source = "registry+https://github.com/rust-lang/crates.io-index"
5109 + checksum = "3fe8e9bebd88fc222938ffdfbdcfa0307081423bd01e3252fc337d8bde81fc61"
5110 + dependencies = [
5111 + "base64 0.22.1",
5112 + "dirs",
5113 + "flate2",
5114 + "futures-util",
5115 + "http",
5116 + "infer",
5117 + "log",
5118 + "minisign-verify",
5119 + "osakit",
5120 + "percent-encoding",
5121 + "reqwest 0.13.2",
5122 + "rustls",
5123 + "semver",
5124 + "serde",
5125 + "serde_json",
5126 + "tar",
5127 + "tauri",
5128 + "tauri-plugin",
5129 + "tempfile",
5130 + "thiserror 2.0.18",
5131 + "time",
5132 + "tokio",
5133 + "url",
5134 + "windows-sys 0.60.2",
5135 + "zip",
5136 + ]
5137 +
4986 5138 [[package]]
4987 5139 name = "tauri-plugin-window-state"
4988 5140 version = "2.4.1"
@@ -5972,6 +6124,15 @@
5972 6124 "system-deps",
5973 6125 ]
5974 6126
6127 + [[package]]
6128 + name = "webpki-root-certs"
6129 + version = "1.0.6"
6130 + source = "registry+https://github.com/rust-lang/crates.io-index"
6131 + checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca"
6132 + dependencies = [
6133 + "rustls-pki-types",
6134 + ]
6135 +
5975 6136 [[package]]
5976 6137 name = "webpki-roots"
5977 6138 version = "0.26.11"
@@ -6653,6 +6814,16 @@
6653 6814 "pkg-config",
6654 6815 ]
6655 6816
6817 + [[package]]
6818 + name = "xattr"
6819 + version = "1.6.1"
6820 + source = "registry+https://github.com/rust-lang/crates.io-index"
6821 + checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
6822 + dependencies = [
6823 + "libc",
6824 + "rustix",
6825 + ]
6826 +
6656 6827 [[package]]
6657 6828 name = "yoke"
6658 6829 version = "0.8.1"
@@ -6756,6 +6927,18 @@
6756 6927 "syn 2.0.114",
6757 6928 ]
6758 6929
6930 + [[package]]
6931 + name = "zip"
6932 + version = "4.6.1"
6933 + source = "registry+https://github.com/rust-lang/crates.io-index"
6934 + checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1"
6935 + dependencies = [
6936 + "arbitrary",
6937 + "crc32fast",
6938 + "indexmap 2.13.0",
6939 + "memchr",
6940 + ]
6941 +
6759 6942 [[package]]
6760 6943 name = "zmij"
6761 6944 version = "1.0.19"
M Cargo.toml +2 -1
@@ -10,7 +10,7 @@
10 10 default-members = ["src-tauri"]
11 11
12 12 [workspace.package]
13 - version = "0.2.1"
13 + version = "0.3.0"
14 14 edition = "2021"
15 15 authors = ["BalancedBreakfast Contributors"]
16 16 license-file = "LICENSE"
@@ -50,3 +50,4 @@
50 50 bb-feed = { path = "crates/bb-feed" }
51 51 bb-db = { path = "crates/bb-db" }
52 52 synckit-client = { path = "../synckit-client" }
53 + tauri-plugin-updater = "2"
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "balanced-breakfast-desktop"
3 - version = "0.2.1"
3 + version = "0.3.0"
4 4 edition = "2021"
5 5
6 6 [[bin]]
@@ -62,6 +62,7 @@
62 62 [target.'cfg(not(any(target_os = "ios", target_os = "android")))'.dependencies]
63 63 tauri-plugin-shell = "2.3.5"
64 64 tauri-plugin-window-state = "2.4.1"
65 + tauri-plugin-updater = { workspace = true }
65 66
66 67 [dev-dependencies]
67 68 bb-core.workspace = true
@@ -1,7 +1,7 @@
1 1 {
2 2 "$schema": "https://schema.tauri.app/config/2",
3 3 "productName": "BalancedBreakfast",
4 - "version": "0.1.0",
4 + "version": "0.3.0",
5 5 "identifier": "com.balancedbreakfast.app",
6 6 "build": {
7 7 "frontendDist": "../src-tauri/frontend"
@@ -40,5 +40,13 @@
40 40 "iOS": {
41 41 "developmentTeam": "93C54W92UP"
42 42 }
43 + },
44 + "plugins": {
45 + "updater": {
46 + "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEJBOTU1QzIxQzdFRDI1MUMKUldRY0plM0hJVnlWdWhmekNSOThnb0FxejRNZ3NsS1BBWlE0aWNZbW92dXltOEExbVhnRmI2LzAK",
47 + "endpoints": [
48 + "https://makenot.work/api/sync/ota/balanced-breakfast/{{target}}/{{arch}}/{{current_version}}"
49 + ]
50 + }
43 51 }
44 52 }
@@ -22,6 +22,7 @@
22 22 </select>
23 23 <button id="refresh-btn" class="btn btn-primary" title="Refresh all feeds">Refresh</button>
24 24 <button id="add-feed-btn" class="btn btn-success" title="Add a new feed source">+ Add Feed</button>
25 + <button class="btn btn-small" title="Settings" onclick="BB.app.showSettings()">&#9881;</button>
25 26 </div>
26 27 </header>
27 28
@@ -86,6 +87,7 @@
86 87 <script src="js/detail.js"></script>
87 88 <script src="js/feeds.js"></script>
88 89 <script src="js/settings-sync.js"></script>
90 + <script src="js/updater.js"></script>
89 91 <script src="js/app.js"></script>
90 92 </body>
91 93 </html>
@@ -14,7 +14,7 @@
14 14
15 15 use state::AppState;
16 16 use std::sync::Arc;
17 - use tauri::Manager;
17 + use tauri::{Emitter, Manager};
18 18
19 19 /// Build the Tauri app with all commands registered.
20 20 pub fn build_app() -> tauri::Builder<tauri::Wry> {
@@ -25,7 +25,8 @@
25 25 {
26 26 builder = builder
27 27 .plugin(tauri_plugin_shell::init())
28 - .plugin(tauri_plugin_window_state::Builder::new().build());
28 + .plugin(tauri_plugin_window_state::Builder::new().build())
29 + .plugin(tauri_plugin_updater::Builder::new().build());
29 30 }
30 31
31 32 builder
@@ -49,6 +50,16 @@
49 50 sync_scheduler::start_sync_scheduler(app_handle);
50 51 });
51 52
53 + // Check for OTA updates after a short delay (desktop only)
54 + #[cfg(not(any(target_os = "ios", target_os = "android")))]
55 + {
56 + let update_handle = app.handle().clone();
57 + tauri::async_runtime::spawn(async move {
58 + tokio::time::sleep(std::time::Duration::from_secs(10)).await;
59 + check_for_updates(update_handle).await;
60 + });
61 + }
62 +
52 63 Ok(())
53 64 })
54 65 .invoke_handler(tauri::generate_handler![
@@ -93,3 +104,35 @@
93 104 commands::extract_reader_view,
94 105 ])
95 106 }
107 +
108 + /// Check for OTA updates and emit an event to the frontend if one is available.
109 + #[cfg(not(any(target_os = "ios", target_os = "android")))]
110 + async fn check_for_updates(app: tauri::AppHandle) {
111 + use tauri_plugin_updater::UpdaterExt;
112 +
113 + let updater = match app.updater() {
114 + Ok(u) => u,
115 + Err(e) => {
116 + tracing::warn!("Failed to initialize updater: {e}");
117 + return;
118 + }
119 + };
120 + match updater.check().await {
121 + Ok(Some(update)) => {
122 + tracing::info!("Update available: v{}", update.version);
123 + let _ = app.emit(
124 + "update-available",
125 + serde_json::json!({
126 + "version": update.version,
127 + "body": update.body.unwrap_or_default(),
128 + }),
129 + );
130 + }
131 + Ok(None) => {
132 + tracing::info!("App is up to date");
133 + }
134 + Err(e) => {
135 + tracing::warn!("Update check failed: {e}");
136 + }
137 + }
138 + }
@@ -84,6 +84,8 @@
84 84 "Help",
85 85 true,
86 86 &[
87 + &MenuItem::with_id(app, "check_updates", "Check for Updates...", true, None::<&str>)?,
88 + &PredefinedMenuItem::separator(app)?,
87 89 &MenuItem::with_id(app, "about", "About Balanced Breakfast", true, None::<&str>)?,
88 90 ],
89 91 )?;
@@ -115,12 +115,11 @@
115 115 cursor: pointer;
116 116 font-weight: 500;
117 117 font-size: 0.875rem;
118 - transition: all 0.2s;
118 + transition: background-color 0.2s, border-color 0.2s;
119 119 }
120 120 .btn:hover {
121 121 background-color: var(--bg-tertiary);
122 122 border-color: var(--border-dark);
123 - box-shadow: 0 1px 3px var(--shadow);
124 123 }
125 124 .btn-primary {
126 125 background-color: var(--accent);
@@ -826,6 +825,16 @@
826 825 border-top: 1px solid var(--border);
827 826 }
828 827
828 + /* Settings modal */
829 + .settings-content .form-group { margin-bottom: 1rem; }
830 + .settings-content .form-group label {
831 + display: block;
832 + margin-bottom: 0.35rem;
833 + color: var(--text-secondary);
834 + font-size: 0.8rem;
835 + font-weight: 500;
836 + }
837 +
829 838 /* Scrollbar */
830 839 ::-webkit-scrollbar { width: 6px; height: 6px; }
831 840 ::-webkit-scrollbar-track { background: var(--bg-secondary); }
@@ -16,7 +16,6 @@
16 16 async function init() {
17 17 // Load theme before rendering content
18 18 await BB.themes.init();
19 - await BB.themes.buildSelector();
20 19
21 20 // Load data
22 21 await BB.sources.load();
@@ -34,7 +33,7 @@
34 33 const searchSpinner = document.getElementById('search-spinner');
35 34 searchInput.addEventListener('input', BB.utils.debounce(async () => {
36 35 BB.state.set('currentSearch', searchInput.value);
37 - BB.state.set('currentPage', 0);
36 + BB.state.resetPagination();
38 37 searchSpinner.classList.add('active');
39 38 await BB.items.load();
40 39 searchSpinner.classList.remove('active');
@@ -43,7 +42,7 @@
43 42 // Sort select
44 43 document.getElementById('sort-select').addEventListener('change', (e) => {
45 44 BB.state.set('currentOrder', e.target.value);
46 - BB.state.set('currentPage', 0);
45 + BB.state.resetPagination();
47 46 BB.items.load();
48 47 });
49 48
@@ -165,24 +164,44 @@
165 164 listen('menu:export_opml', () => BB.feeds.exportOpml());
166 165 listen('menu:view_all', () => {
167 166 BB.state.set('currentSource', '');
168 - BB.state.set('currentPage', 0);
167 + BB.state.resetPagination();
169 168 BB.sources.render(BB.state.sources);
170 169 BB.items.load();
171 170 });
172 171 listen('menu:view_unread', () => {
173 172 BB.state.set('currentOrder', 'unread');
174 173 document.getElementById('sort-select').value = 'unread';
175 - BB.state.set('currentPage', 0);
174 + BB.state.resetPagination();
176 175 BB.items.load();
177 176 });
178 177 listen('menu:view_starred', () => {
179 178 BB.state.set('currentOrder', 'starred');
180 179 document.getElementById('sort-select').value = 'starred';
181 - BB.state.set('currentPage', 0);
180 + BB.state.resetPagination();
182 181 BB.items.load();
183 182 });
184 183 }
185 184
185 + /** Show the settings modal with theme selector. */
186 + function showSettings() {
187 + const body = document.getElementById('modal-body');
188 + const title = document.getElementById('modal-title');
189 + title.textContent = 'Settings';
190 + body.innerHTML = `
191 + <div class="settings-content">
192 + <div class="form-group">
193 + <label>Theme</label>
194 + <div id="settings-theme-container"></div>
195 + </div>
196 + <div class="form-actions">
197 + <button class="btn" onclick="BB.ui.closeModal();">Close</button>
198 + </div>
199 + </div>
200 + `;
201 + BB.themes.buildSelector(document.getElementById('settings-theme-container'));
202 + BB.ui.openModal();
203 + }
204 +
186 205 /** Show the keyboard shortcuts help modal. */
187 206 function showHelp() {
188 207 const body = document.getElementById('modal-body');
@@ -225,13 +244,13 @@
225 244 <div class="welcome-content">
226 245 <p>Your personal feed reader for RSS, Atom, podcasts, and more.</p>
227 246 <h3>Getting Started</h3>
228 - <p>Click <strong>+ Add Feed</strong> to subscribe to your first source. You can also import existing subscriptions via <strong>File &gt; Import OPML</strong>.</p>
247 + <p>Subscribe to feeds with the <strong>+ Add Feed</strong> button, or import existing subscriptions via <strong>File &gt; Import OPML</strong>.</p>
229 248 <h3>Keyboard Shortcuts</h3>
230 - <p>Press <kbd>?</kbd> anytime to see all shortcuts. Use <kbd>j</kbd>/<kbd>k</kbd> to navigate, <kbd>s</kbd> to star, <kbd>r</kbd> to toggle read.</p>
231 - <h3>Themes</h3>
232 - <p>Balanced Breakfast supports multiple themes. Switch via the menu bar.</p>
249 + <p>Press <kbd>?</kbd> anytime to see all shortcuts. Navigate with <kbd>j</kbd>/<kbd>k</kbd>, star with <kbd>s</kbd>, toggle read with <kbd>r</kbd>.</p>
250 + <h3>Themes &amp; Settings</h3>
251 + <p>Click the gear icon (<strong>&#9881;</strong>) in the header to change themes and configure preferences.</p>
233 252 <div class="welcome-cta">
234 - <button class="btn btn-success" onclick="BB.ui.closeModal(); BB.feeds.openAddFeed();">Add Your First Feed</button>
253 + <button class="btn btn-success" onclick="BB.feeds.openAddFeed();">Add Your First Feed</button>
235 254 <button class="btn" onclick="BB.ui.closeModal();">Explore First</button>
236 255 </div>
237 256 </div>
@@ -240,7 +259,7 @@
240 259 invoke('set_config', { key: 'bb-welcomed', value: '1' });
241 260 }
242 261
243 - BB.app = { init, showHelp, showWelcome };
262 + BB.app = { init, showHelp, showSettings, showWelcome };
244 263
245 264 // Auto-init when DOM ready
246 265 if (document.readyState === 'loading') {
@@ -174,5 +174,17 @@
174 174 if (firstInput) firstInput.focus();
175 175 }
176 176
177 - BB.ui = { showToast, showProgress, openModal, closeModal, openFormModal };
177 + /**
178 + * Show an error toast with a Retry button.
179 + * @param {string} message - Error message.
180 + * @param {function} retryFn - Called when Retry is clicked.
181 + */
182 + function showErrorWithRetry(message, retryFn) {
183 + showToast(message, 'error', {
184 + action: { label: 'Retry', fn: retryFn },
185 + duration: 5000,
186 + });
187 + }
188 +
189 + BB.ui = { showToast, showProgress, openModal, closeModal, openFormModal, showErrorWithRetry };
178 190 })();
@@ -30,10 +30,7 @@
30 30 panel.style.display = 'flex';
31 31 renderDetail(item);
32 32 } catch (err) {
33 - BB.ui.showToast('Failed to load item: ' + err, 'error', {
34 - action: { label: 'Retry', fn: () => load(id) },
35 - duration: 5000,
36 - });
33 + BB.ui.showErrorWithRetry('Failed to load item: ' + err, () => load(id));
37 34 }
38 35 }
39 36