Skip to content

Commit 9b70eea

Browse files
committed
Custom browsers enhanced
1 parent 03fa058 commit 9b70eea

File tree

7 files changed

+213
-55
lines changed

7 files changed

+213
-55
lines changed

Plugins/Flow.Launcher.Plugin.BrowserBookmarks/Main.cs

Lines changed: 80 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
using System.Collections.Specialized;
1313
using Flow.Launcher.Plugin.SharedModels;
1414
using System.IO;
15+
using System.Collections.Concurrent;
1516

1617
namespace Flow.Launcher.Plugin.BrowserBookmarks;
1718

@@ -26,8 +27,9 @@ public class Main : ISettingProvider, IPlugin, IAsyncReloadable, IPluginI18n, IC
2627

2728
private List<Bookmark> _bookmarks = new();
2829
private readonly CancellationTokenSource _cancellationTokenSource = new();
30+
private PeriodicTimer? _firefoxBookmarkTimer;
2931

30-
public void Init(PluginInitContext context)
32+
public void Init(PluginInitContext context)
3133
{
3234
Context = context;
3335
_settings = context.API.LoadSettingJsonStorage<Settings>();
@@ -43,6 +45,7 @@ public void Init(PluginInitContext context)
4345

4446
// Fire and forget the initial load to make Flow's UI responsive immediately.
4547
_ = ReloadDataAsync();
48+
StartFirefoxBookmarkTimer();
4649
}
4750

4851
private string SetupTempDirectory()
@@ -63,7 +66,7 @@ private string SetupTempDirectory()
6366
return tempPath;
6467
}
6568

66-
public List<Result> Query(Query query)
69+
public List<Result> Query(Query query)
6770
{
6871
var search = query.Search.Trim();
6972
var bookmarks = _bookmarks; // use a local copy
@@ -118,19 +121,92 @@ public async Task ReloadDataAsync()
118121

119122
private void OnSettingsPropertyChanged(object? sender, PropertyChangedEventArgs e)
120123
{
124+
if (e.PropertyName is nameof(Settings.LoadFirefoxBookmark))
125+
{
126+
StartFirefoxBookmarkTimer();
127+
}
121128
_ = ReloadDataAsync();
122129
}
123130

124131
private void OnCustomBrowsersChanged(object? sender, NotifyCollectionChangedEventArgs e)
125132
{
133+
StartFirefoxBookmarkTimer();
126134
_ = ReloadDataAsync();
127135
}
128136

129137
private void OnBookmarkFileChanged()
130138
{
131139
_ = ReloadDataAsync();
132140
}
133-
141+
142+
private void StartFirefoxBookmarkTimer()
143+
{
144+
_firefoxBookmarkTimer?.Dispose();
145+
146+
if (!_settings.LoadFirefoxBookmark && !_settings.CustomBrowsers.Any(x => x.BrowserType == BrowserType.Firefox))
147+
return;
148+
149+
_firefoxBookmarkTimer = new PeriodicTimer(TimeSpan.FromHours(3));
150+
151+
_ = Task.Run(async () =>
152+
{
153+
while (await _firefoxBookmarkTimer.WaitForNextTickAsync(_cancellationTokenSource.Token))
154+
{
155+
await ReloadFirefoxBookmarksAsync();
156+
}
157+
}, _cancellationTokenSource.Token);
158+
}
159+
160+
private async Task ReloadFirefoxBookmarksAsync()
161+
{
162+
Context.API.LogInfo(nameof(Main), "Starting periodic reload of Firefox bookmarks.");
163+
164+
var firefoxLoaders = _bookmarkLoader.GetFirefoxBookmarkLoaders().ToList();
165+
if (!firefoxLoaders.Any())
166+
{
167+
Context.API.LogInfo(nameof(Main), "No Firefox bookmark loaders enabled, skipping reload.");
168+
return;
169+
}
170+
171+
var firefoxBookmarks = new ConcurrentBag<Bookmark>();
172+
var tasks = firefoxLoaders.Select(async loader =>
173+
{
174+
try
175+
{
176+
await foreach (var bookmark in loader.GetBookmarksAsync(_cancellationTokenSource.Token))
177+
{
178+
firefoxBookmarks.Add(bookmark);
179+
}
180+
}
181+
catch (OperationCanceledException) { } // Task was cancelled, swallow exception
182+
catch (Exception e)
183+
{
184+
Context.API.LogException(nameof(Main), $"Failed to load bookmarks from {loader.Name}.", e);
185+
}
186+
});
187+
188+
await Task.WhenAll(tasks);
189+
190+
if (firefoxBookmarks.IsEmpty)
191+
{
192+
Context.API.LogInfo(nameof(Main), "No Firefox bookmarks found during periodic reload.");
193+
return;
194+
}
195+
196+
var currentBookmarks = Volatile.Read(ref _bookmarks);
197+
198+
var firefoxLoaderNames = firefoxLoaders.Select(l => l.Name).ToHashSet();
199+
var otherBookmarks = currentBookmarks.Where(b => !firefoxLoaderNames.Any(name => b.Source.StartsWith(name, StringComparison.OrdinalIgnoreCase)));
200+
201+
var newBookmarkList = otherBookmarks.Concat(firefoxBookmarks).Distinct().ToList();
202+
203+
Volatile.Write(ref _bookmarks, newBookmarkList);
204+
205+
Context.API.LogInfo(nameof(Main), $"Periodic reload complete. Loaded {firefoxBookmarks.Count} Firefox bookmarks.");
206+
207+
_ = _faviconService.ProcessBookmarkFavicons(firefoxBookmarks.ToList(), _cancellationTokenSource.Token);
208+
}
209+
134210
public Control CreateSettingPanel()
135211
{
136212
return new Views.SettingsControl(_settings);
@@ -181,6 +257,7 @@ public void Dispose()
181257
{
182258
_settings.PropertyChanged -= OnSettingsPropertyChanged;
183259
_settings.CustomBrowsers.CollectionChanged -= OnCustomBrowsersChanged;
260+
_firefoxBookmarkTimer?.Dispose();
184261
_cancellationTokenSource.Cancel();
185262
_cancellationTokenSource.Dispose();
186263
_faviconService.Dispose();

Plugins/Flow.Launcher.Plugin.BrowserBookmarks/Models/CustomBrowser.cs

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ public class CustomBrowser : BaseModel
88
{
99
private string _name;
1010
private string _dataDirectoryPath;
11-
private BrowserType _browserType = BrowserType.Chromium;
11+
private BrowserType _browserType = BrowserType.Unknown;
1212

1313
public string Name
1414
{
@@ -36,13 +36,6 @@ public string DataDirectoryPath
3636
}
3737
}
3838

39-
// WORKAROUND: Manually create the list for the ComboBox to resolve the CS0246 source generator error.
40-
// This provides the necessary data structure without relying on the auto-generated 'BrowserTypeLocalized' type.
41-
public List<BrowserTypeDisplay> AllBrowserTypes { get; } =
42-
System.Enum.GetValues<BrowserType>()
43-
.Select(e => new BrowserTypeDisplay(e.ToString(), e))
44-
.ToList();
45-
4639
public BrowserType BrowserType
4740
{
4841
get => _browserType;
@@ -63,6 +56,9 @@ public record BrowserTypeDisplay(string Display, BrowserType Value);
6356
[EnumLocalize]
6457
public enum BrowserType
6558
{
59+
[EnumLocalizeValue("Unknown")]
60+
Unknown,
61+
6662
[EnumLocalizeValue("Chromium")]
6763
Chromium,
6864

Plugins/Flow.Launcher.Plugin.BrowserBookmarks/Services/BookmarkLoaderService.cs

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -56,31 +56,49 @@ public async Task<List<Bookmark>> LoadBookmarksAsync(CancellationToken cancellat
5656
return bookmarks.Distinct().ToList();
5757
}
5858

59-
private IEnumerable<IBookmarkLoader> GetBookmarkLoaders()
59+
public IEnumerable<IBookmarkLoader> GetBookmarkLoaders()
60+
{
61+
return GetChromiumBookmarkLoaders().Concat(GetFirefoxBookmarkLoaders());
62+
}
63+
64+
public IEnumerable<IBookmarkLoader> GetChromiumBookmarkLoaders()
6065
{
6166
var logAction = (string tag, string msg, Exception? ex) => _context.API.LogException(tag, msg, ex);
6267

6368
if (_settings.LoadChromeBookmark)
6469
{
6570
var path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Google\Chrome\User Data");
66-
if(Directory.Exists(path))
71+
if (Directory.Exists(path))
6772
yield return new ChromiumBookmarkLoader("Google Chrome", path, logAction, DiscoveredBookmarkFiles);
6873
}
6974

7075
if (_settings.LoadEdgeBookmark)
7176
{
7277
var path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Microsoft\Edge\User Data");
73-
if(Directory.Exists(path))
78+
if (Directory.Exists(path))
7479
yield return new ChromiumBookmarkLoader("Microsoft Edge", path, logAction, DiscoveredBookmarkFiles);
7580
}
7681

7782
if (_settings.LoadChromiumBookmark)
7883
{
7984
var path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Chromium\User Data");
80-
if(Directory.Exists(path))
85+
if (Directory.Exists(path))
8186
yield return new ChromiumBookmarkLoader("Chromium", path, logAction, DiscoveredBookmarkFiles);
8287
}
8388

89+
foreach (var browser in _settings.CustomBrowsers.Where(b => b.BrowserType == BrowserType.Chromium))
90+
{
91+
if (string.IsNullOrEmpty(browser.Name) || string.IsNullOrEmpty(browser.DataDirectoryPath) || !Directory.Exists(browser.DataDirectoryPath))
92+
continue;
93+
94+
yield return new ChromiumBookmarkLoader(browser.Name, browser.DataDirectoryPath, logAction, DiscoveredBookmarkFiles);
95+
}
96+
}
97+
98+
public IEnumerable<IBookmarkLoader> GetFirefoxBookmarkLoaders()
99+
{
100+
var logAction = (string tag, string msg, Exception? ex) => _context.API.LogException(tag, msg, ex);
101+
84102
if (_settings.LoadFirefoxBookmark)
85103
{
86104
string? placesPath = null;
@@ -96,7 +114,7 @@ private IEnumerable<IBookmarkLoader> GetBookmarkLoaders()
96114
{
97115
yield return new FirefoxBookmarkLoader("Firefox", placesPath, _tempPath, logAction);
98116
}
99-
117+
100118
string? msixPlacesPath = null;
101119
try
102120
{
@@ -112,18 +130,12 @@ private IEnumerable<IBookmarkLoader> GetBookmarkLoaders()
112130
}
113131
}
114132

115-
foreach (var browser in _settings.CustomBrowsers)
133+
foreach (var browser in _settings.CustomBrowsers.Where(b => b.BrowserType == BrowserType.Firefox))
116134
{
117135
if (string.IsNullOrEmpty(browser.Name) || string.IsNullOrEmpty(browser.DataDirectoryPath) || !Directory.Exists(browser.DataDirectoryPath))
118136
continue;
119137

120-
IBookmarkLoader loader = browser.BrowserType switch
121-
{
122-
BrowserType.Chromium => new ChromiumBookmarkLoader(browser.Name, browser.DataDirectoryPath, logAction, DiscoveredBookmarkFiles),
123-
BrowserType.Firefox => CreateCustomFirefoxLoader(browser.Name, browser.DataDirectoryPath),
124-
_ => new ChromiumBookmarkLoader(browser.Name, browser.DataDirectoryPath, logAction, DiscoveredBookmarkFiles)
125-
};
126-
yield return loader;
138+
yield return CreateCustomFirefoxLoader(browser.Name, browser.DataDirectoryPath);
127139
}
128140
}
129141

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#nullable enable
2+
using Flow.Launcher.Plugin.BrowserBookmarks.Models;
3+
using System.IO;
4+
using System.Linq;
5+
6+
namespace Flow.Launcher.Plugin.BrowserBookmarks.Services;
7+
8+
public static class BrowserDetector
9+
{
10+
public static BrowserType DetectBrowserType(string dataDirectoryPath)
11+
{
12+
if (string.IsNullOrEmpty(dataDirectoryPath) || !Directory.Exists(dataDirectoryPath))
13+
return BrowserType.Unknown;
14+
15+
// Check for Chromium-based browsers by looking for the 'Bookmarks' file.
16+
// This includes checking common profile subdirectories.
17+
var profileDirectories = Directory.EnumerateDirectories(dataDirectoryPath, "Profile *").ToList();
18+
var defaultProfile = Path.Combine(dataDirectoryPath, "Default");
19+
if (Directory.Exists(defaultProfile))
20+
profileDirectories.Add(defaultProfile);
21+
22+
// Also check the root directory itself, as some browsers use it directly.
23+
profileDirectories.Add(dataDirectoryPath);
24+
25+
if (profileDirectories.Any(p => File.Exists(Path.Combine(p, "Bookmarks"))))
26+
{
27+
return BrowserType.Chromium;
28+
}
29+
30+
// Check for Firefox-based browsers by looking for 'places.sqlite'.
31+
// This leverages the existing FirefoxProfileFinder logic.
32+
if (File.Exists(Path.Combine(dataDirectoryPath, "places.sqlite")) || !string.IsNullOrEmpty(FirefoxProfileFinder.GetPlacesPathFromProfileDir(dataDirectoryPath)))
33+
{
34+
return BrowserType.Firefox;
35+
}
36+
37+
return BrowserType.Unknown;
38+
}
39+
}

Plugins/Flow.Launcher.Plugin.BrowserBookmarks/ViewModels/CustomBrowserSettingViewModel.cs

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@
22
using CommunityToolkit.Mvvm.ComponentModel;
33
using CommunityToolkit.Mvvm.Input;
44
using Flow.Launcher.Plugin.BrowserBookmarks.Models;
5+
using Flow.Launcher.Plugin.BrowserBookmarks.Services;
56
using System;
7+
using System.ComponentModel;
8+
using System.IO;
69
using System.Windows.Forms;
10+
using Flow.Launcher.Plugin.SharedCommands;
711

812
namespace Flow.Launcher.Plugin.BrowserBookmarks.ViewModels;
913

@@ -15,19 +19,57 @@ public partial class CustomBrowserSettingViewModel : ObservableObject
1519
[ObservableProperty]
1620
private CustomBrowser _editableBrowser;
1721

22+
public string DetectedEngineText
23+
{
24+
get
25+
{
26+
if (string.IsNullOrEmpty(EditableBrowser.DataDirectoryPath))
27+
{
28+
return Localize.flowlauncher_plugin_browserbookmark_engine_detection_select_directory();
29+
}
30+
31+
return EditableBrowser.BrowserType switch
32+
{
33+
BrowserType.Unknown => Localize.flowlauncher_plugin_browserbookmark_engine_detection_invalid(),
34+
BrowserType.Chromium => Localize.flowlauncher_plugin_browserbookmark_engine_detection_chromium(),
35+
BrowserType.Firefox => Localize.flowlauncher_plugin_browserbookmark_engine_detection_firefox(),
36+
_ => string.Empty
37+
};
38+
}
39+
}
40+
41+
public bool IsValidPath => EditableBrowser.BrowserType != BrowserType.Unknown;
42+
1843
public CustomBrowserSettingViewModel(CustomBrowser browser, Action<bool> closeAction)
1944
{
2045
_originalBrowser = browser;
2146
_closeAction = closeAction;
2247
EditableBrowser = new CustomBrowser
2348
{
2449
Name = browser.Name,
25-
DataDirectoryPath = browser.DataDirectoryPath,
26-
BrowserType = browser.BrowserType
50+
DataDirectoryPath = browser.DataDirectoryPath
2751
};
52+
EditableBrowser.PropertyChanged += EditableBrowser_PropertyChanged;
53+
DetectEngineType();
2854
}
2955

30-
[RelayCommand]
56+
private void EditableBrowser_PropertyChanged(object? sender, PropertyChangedEventArgs e)
57+
{
58+
if (e.PropertyName == nameof(CustomBrowser.DataDirectoryPath))
59+
{
60+
DetectEngineType();
61+
}
62+
}
63+
64+
private void DetectEngineType()
65+
{
66+
EditableBrowser.BrowserType = BrowserDetector.DetectBrowserType(EditableBrowser.DataDirectoryPath);
67+
OnPropertyChanged(nameof(DetectedEngineText));
68+
OnPropertyChanged(nameof(IsValidPath));
69+
SaveCommand.NotifyCanExecuteChanged();
70+
}
71+
72+
[RelayCommand(CanExecute = nameof(IsValidPath))]
3173
private void Save()
3274
{
3375
_originalBrowser.Name = EditableBrowser.Name;
@@ -46,6 +88,11 @@ private void Cancel()
4688
private void BrowseDataDirectory()
4789
{
4890
var dialog = new FolderBrowserDialog();
91+
if (!string.IsNullOrEmpty(EditableBrowser.DataDirectoryPath) && Directory.Exists(EditableBrowser.DataDirectoryPath))
92+
{
93+
dialog.SelectedPath = EditableBrowser.DataDirectoryPath;
94+
}
95+
4996
if (dialog.ShowDialog() == DialogResult.OK)
5097
{
5198
EditableBrowser.DataDirectoryPath = dialog.SelectedPath;

0 commit comments

Comments
 (0)