Merge branch 'dev' into 240430PreviewSetting

This commit is contained in:
DB P 2024-05-18 12:40:07 +09:00 committed by GitHub
commit fa0d42e5ce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 835 additions and 816 deletions

View file

@ -3,3 +3,6 @@ https
ssh ssh
ubuntu ubuntu
runcount runcount
Firefox
Português
Português (Brasil)

View file

@ -74,6 +74,7 @@ WCA_ACCENT_POLICY
HGlobal HGlobal
dopusrt dopusrt
firefox firefox
Firefox
msedge msedge
svgc svgc
ime ime

View file

@ -27,8 +27,8 @@ namespace Flow.Launcher.Infrastructure.UserSettings
public string SelectNextItemHotkey2 { get; set; } = $""; public string SelectNextItemHotkey2 { get; set; } = $"";
public string SelectPrevItemHotkey { get; set; } = $"Shift + Tab"; public string SelectPrevItemHotkey { get; set; } = $"Shift + Tab";
public string SelectPrevItemHotkey2 { get; set; } = $""; public string SelectPrevItemHotkey2 { get; set; } = $"";
public string SelectNextPageHotkey { get; set; } = $""; public string SelectNextPageHotkey { get; set; } = $"PageUp";
public string SelectPrevPageHotkey { get; set; } = $""; public string SelectPrevPageHotkey { get; set; } = $"PageDown";
public string OpenContextMenuHotkey { get; set; } = $"Ctrl+O"; public string OpenContextMenuHotkey { get; set; } = $"Ctrl+O";
public string SettingWindowHotkey { get; set; } = $"Ctrl+I"; public string SettingWindowHotkey { get; set; } = $"Ctrl+I";

View file

@ -185,9 +185,9 @@
<system:String x:Key="autoCompleteHotkey">Auto Complete</system:String> <system:String x:Key="autoCompleteHotkey">Auto Complete</system:String>
<system:String x:Key="autoCompleteHotkeyToolTip">Runs autocomplete for the selected items.</system:String> <system:String x:Key="autoCompleteHotkeyToolTip">Runs autocomplete for the selected items.</system:String>
<system:String x:Key="SelectNextItemHotkey">Select Next Item</system:String> <system:String x:Key="SelectNextItemHotkey">Select Next Item</system:String>
<system:String x:Key="SelectPrevItemHotkey">Select Prev Item</system:String> <system:String x:Key="SelectPrevItemHotkey">Select Previous Item</system:String>
<system:String x:Key="SelectNextPageHotkey">Next Page</system:String> <system:String x:Key="SelectNextPageHotkey">Next Page</system:String>
<system:String x:Key="SelectPrevPageHotkey">Prev Page</system:String> <system:String x:Key="SelectPrevPageHotkey">Previous Page</system:String>
<system:String x:Key="OpenContextMenuHotkey">Open Context Menu</system:String> <system:String x:Key="OpenContextMenuHotkey">Open Context Menu</system:String>
<system:String x:Key="SettingWindowHotkey">Open Setting Window</system:String> <system:String x:Key="SettingWindowHotkey">Open Setting Window</system:String>
<system:String x:Key="CopyFilePathHotkey">Copy File Path</system:String> <system:String x:Key="CopyFilePathHotkey">Copy File Path</system:String>
@ -195,6 +195,7 @@
<system:String x:Key="ToggleHistoryHotkey">Toggle History</system:String> <system:String x:Key="ToggleHistoryHotkey">Toggle History</system:String>
<system:String x:Key="OpenContainFolderHotkey">Open Containing Folder</system:String> <system:String x:Key="OpenContainFolderHotkey">Open Containing Folder</system:String>
<system:String x:Key="RunAsAdminHotkey">Run As Admin</system:String> <system:String x:Key="RunAsAdminHotkey">Run As Admin</system:String>
<system:String x:Key="RequeryHotkey">Refresh Search Results</system:String>
<system:String x:Key="ReloadPluginHotkey">Reload Plugins Data</system:String> <system:String x:Key="ReloadPluginHotkey">Reload Plugins Data</system:String>
<system:String x:Key="QuickWidthHotkey">Quick Adjust Window Width</system:String> <system:String x:Key="QuickWidthHotkey">Quick Adjust Window Width</system:String>
<system:String x:Key="QuickHeightHotkey">Quick Adjust Window Height</system:String> <system:String x:Key="QuickHeightHotkey">Quick Adjust Window Height</system:String>

View file

@ -26,6 +26,7 @@
LocationChanged="OnLocationChanged" LocationChanged="OnLocationChanged"
Opacity="{Binding MainWindowOpacity, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" Opacity="{Binding MainWindowOpacity, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
PreviewKeyDown="OnKeyDown" PreviewKeyDown="OnKeyDown"
PreviewKeyUp="OnKeyUp"
ResizeMode="NoResize" ResizeMode="NoResize"
ShowInTaskbar="False" ShowInTaskbar="False"
SizeToContent="Height" SizeToContent="Height"

View file

@ -47,6 +47,7 @@ namespace Flow.Launcher
private MainViewModel _viewModel; private MainViewModel _viewModel;
private bool _animating; private bool _animating;
MediaPlayer animationSound = new MediaPlayer(); MediaPlayer animationSound = new MediaPlayer();
private bool isArrowKeyPressed = false;
#endregion #endregion
@ -109,9 +110,11 @@ namespace Flow.Launcher
private void OnInitialized(object sender, EventArgs e) private void OnInitialized(object sender, EventArgs e)
{ {
} }
private void OnLoaded(object sender, RoutedEventArgs _) private void OnLoaded(object sender, RoutedEventArgs _)
{ {
// MouseEventHandler
PreviewMouseMove += MainPreviewMouseMove;
CheckFirstLaunch(); CheckFirstLaunch();
HideStartup(); HideStartup();
// show notify icon when flowlauncher is hidden // show notify icon when flowlauncher is hidden
@ -406,6 +409,7 @@ namespace Flow.Launcher
if (_animating) if (_animating)
return; return;
isArrowKeyPressed = true;
_animating = true; _animating = true;
UpdatePosition(); UpdatePosition();
@ -494,6 +498,7 @@ namespace Flow.Launcher
windowsb.Completed += (_, _) => _animating = false; windowsb.Completed += (_, _) => _animating = false;
_settings.WindowLeft = Left; _settings.WindowLeft = Left;
_settings.WindowTop = Top; _settings.WindowTop = Top;
isArrowKeyPressed = false;
if (QueryTextBox.Text.Length == 0) if (QueryTextBox.Text.Length == 0)
{ {
@ -644,10 +649,12 @@ namespace Flow.Launcher
switch (e.Key) switch (e.Key)
{ {
case Key.Down: case Key.Down:
isArrowKeyPressed = true;
_viewModel.SelectNextItemCommand.Execute(null); _viewModel.SelectNextItemCommand.Execute(null);
e.Handled = true; e.Handled = true;
break; break;
case Key.Up: case Key.Up:
isArrowKeyPressed = true;
_viewModel.SelectPrevItemCommand.Execute(null); _viewModel.SelectPrevItemCommand.Execute(null);
e.Handled = true; e.Handled = true;
break; break;
@ -698,7 +705,21 @@ namespace Flow.Launcher
} }
} }
private void OnKeyUp(object sender, KeyEventArgs e)
{
if (e.Key == Key.Up || e.Key == Key.Down)
{
isArrowKeyPressed = false;
}
}
private void MainPreviewMouseMove(object sender, System.Windows.Input.MouseEventArgs e)
{
if (isArrowKeyPressed)
{
e.Handled = true; // Ignore Mouse Hover when press Arrowkeys
}
}
public void PreviewReset() public void PreviewReset()
{ {
_viewModel.ResetPreview(); _viewModel.ResetPreview();

View file

@ -2802,7 +2802,13 @@
Title="{DynamicResource ToggleGameModeHotkey}" Title="{DynamicResource ToggleGameModeHotkey}"
Icon="&#xe7fc;" Icon="&#xe7fc;"
Type="Inside"> Type="Inside">
<cc:HotkeyDisplay Keys="Ctrl+Shift+C" /> <cc:HotkeyDisplay Keys="Ctrl+F12" />
</cc:Card>
<cc:Card
Title="{DynamicResource RequeryHotkey}"
Icon="&#xe72c;"
Type="Inside">
<cc:HotkeyDisplay Keys="Ctrl+R" />
</cc:Card> </cc:Card>
<cc:Card <cc:Card
Title="{DynamicResource ReloadPluginHotkey}" Title="{DynamicResource ReloadPluginHotkey}"

View file

@ -3,23 +3,22 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
namespace Flow.Launcher.Plugin.BrowserBookmark namespace Flow.Launcher.Plugin.BrowserBookmark;
{
public class ChromeBookmarkLoader : ChromiumBookmarkLoader
{
public override List<Bookmark> GetBookmarks()
{
return LoadChromeBookmarks();
}
private List<Bookmark> LoadChromeBookmarks() public class ChromeBookmarkLoader : ChromiumBookmarkLoader
{ {
var bookmarks = new List<Bookmark>(); public override List<Bookmark> GetBookmarks()
var platformPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); {
bookmarks.AddRange(LoadBookmarks(Path.Combine(platformPath, @"Google\Chrome\User Data"), "Google Chrome")); return LoadChromeBookmarks();
bookmarks.AddRange(LoadBookmarks(Path.Combine(platformPath, @"Google\Chrome SxS\User Data"), "Google Chrome Canary"));
bookmarks.AddRange(LoadBookmarks(Path.Combine(platformPath, @"Chromium\User Data"), "Chromium"));
return bookmarks;
}
} }
}
private List<Bookmark> LoadChromeBookmarks()
{
var bookmarks = new List<Bookmark>();
var platformPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
bookmarks.AddRange(LoadBookmarks(Path.Combine(platformPath, @"Google\Chrome\User Data"), "Google Chrome"));
bookmarks.AddRange(LoadBookmarks(Path.Combine(platformPath, @"Google\Chrome SxS\User Data"), "Google Chrome Canary"));
bookmarks.AddRange(LoadBookmarks(Path.Combine(platformPath, @"Chromium\User Data"), "Chromium"));
return bookmarks;
}
}

View file

@ -4,91 +4,90 @@ using System.IO;
using System.Text.Json; using System.Text.Json;
using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.Logger;
namespace Flow.Launcher.Plugin.BrowserBookmark namespace Flow.Launcher.Plugin.BrowserBookmark;
public abstract class ChromiumBookmarkLoader : IBookmarkLoader
{ {
public abstract class ChromiumBookmarkLoader : IBookmarkLoader public abstract List<Bookmark> GetBookmarks();
protected List<Bookmark> LoadBookmarks(string browserDataPath, string name)
{ {
public abstract List<Bookmark> GetBookmarks(); var bookmarks = new List<Bookmark>();
if (!Directory.Exists(browserDataPath)) return bookmarks;
var paths = Directory.GetDirectories(browserDataPath);
protected List<Bookmark> LoadBookmarks(string browserDataPath, string name) foreach (var profile in paths)
{ {
var bookmarks = new List<Bookmark>(); var bookmarkPath = Path.Combine(profile, "Bookmarks");
if (!Directory.Exists(browserDataPath)) return bookmarks; if (!File.Exists(bookmarkPath))
var paths = Directory.GetDirectories(browserDataPath); continue;
foreach (var profile in paths) Main.RegisterBookmarkFile(bookmarkPath);
{
var bookmarkPath = Path.Combine(profile, "Bookmarks");
if (!File.Exists(bookmarkPath))
continue;
Main.RegisterBookmarkFile(bookmarkPath); var source = name + (Path.GetFileName(profile) == "Default" ? "" : $" ({Path.GetFileName(profile)})");
bookmarks.AddRange(LoadBookmarksFromFile(bookmarkPath, source));
}
var source = name + (Path.GetFileName(profile) == "Default" ? "" : $" ({Path.GetFileName(profile)})"); return bookmarks;
bookmarks.AddRange(LoadBookmarksFromFile(bookmarkPath, source)); }
}
protected List<Bookmark> LoadBookmarksFromFile(string path, string source)
{
var bookmarks = new List<Bookmark>();
if (!File.Exists(path))
return bookmarks; return bookmarks;
}
protected List<Bookmark> LoadBookmarksFromFile(string path, string source) using var jsonDocument = JsonDocument.Parse(File.ReadAllText(path));
{ if (!jsonDocument.RootElement.TryGetProperty("roots", out var rootElement))
var bookmarks = new List<Bookmark>();
if (!File.Exists(path))
return bookmarks;
using var jsonDocument = JsonDocument.Parse(File.ReadAllText(path));
if (!jsonDocument.RootElement.TryGetProperty("roots", out var rootElement))
return bookmarks;
EnumerateRoot(rootElement, bookmarks, source);
return bookmarks; return bookmarks;
} EnumerateRoot(rootElement, bookmarks, source);
return bookmarks;
}
private void EnumerateRoot(JsonElement rootElement, ICollection<Bookmark> bookmarks, string source) private void EnumerateRoot(JsonElement rootElement, ICollection<Bookmark> bookmarks, string source)
{
foreach (var folder in rootElement.EnumerateObject())
{ {
foreach (var folder in rootElement.EnumerateObject()) if (folder.Value.ValueKind != JsonValueKind.Object)
{ continue;
if (folder.Value.ValueKind != JsonValueKind.Object)
continue;
// Fix for Opera. It stores bookmarks slightly different than chrome. See PR and bug report for this change for details. // Fix for Opera. It stores bookmarks slightly different than chrome. See PR and bug report for this change for details.
// If various exceptions start to build up here consider splitting this Loader into multiple separate ones. // If various exceptions start to build up here consider splitting this Loader into multiple separate ones.
if (folder.Name == "custom_root") if (folder.Name == "custom_root")
EnumerateRoot(folder.Value, bookmarks, source); EnumerateRoot(folder.Value, bookmarks, source);
else else
EnumerateFolderBookmark(folder.Value, bookmarks, source); EnumerateFolderBookmark(folder.Value, bookmarks, source);
}
}
private void EnumerateFolderBookmark(JsonElement folderElement, ICollection<Bookmark> bookmarks,
string source)
{
if (!folderElement.TryGetProperty("children", out var childrenElement))
return;
foreach (var subElement in childrenElement.EnumerateArray())
{
if (subElement.TryGetProperty("type", out var type))
{
switch (type.GetString())
{
case "folder":
case "workspace": // Edge Workspace
EnumerateFolderBookmark(subElement, bookmarks, source);
break;
default:
bookmarks.Add(new Bookmark(
subElement.GetProperty("name").GetString(),
subElement.GetProperty("url").GetString(),
source));
break;
}
} }
} else
private void EnumerateFolderBookmark(JsonElement folderElement, ICollection<Bookmark> bookmarks,
string source)
{
if (!folderElement.TryGetProperty("children", out var childrenElement))
return;
foreach (var subElement in childrenElement.EnumerateArray())
{ {
if (subElement.TryGetProperty("type", out var type)) Log.Error(
{ $"ChromiumBookmarkLoader: EnumerateFolderBookmark: type property not found for {subElement.GetString()}");
switch (type.GetString())
{
case "folder":
case "workspace": // Edge Workspace
EnumerateFolderBookmark(subElement, bookmarks, source);
break;
default:
bookmarks.Add(new Bookmark(
subElement.GetProperty("name").GetString(),
subElement.GetProperty("url").GetString(),
source));
break;
}
}
else
{
Log.Error(
$"ChromiumBookmarkLoader: EnumerateFolderBookmark: type property not found for {subElement.GetString()}");
}
} }
} }
} }

View file

@ -4,56 +4,55 @@ using Flow.Launcher.Infrastructure;
using Flow.Launcher.Plugin.BrowserBookmark.Models; using Flow.Launcher.Plugin.BrowserBookmark.Models;
using Flow.Launcher.Plugin.SharedModels; using Flow.Launcher.Plugin.SharedModels;
namespace Flow.Launcher.Plugin.BrowserBookmark.Commands namespace Flow.Launcher.Plugin.BrowserBookmark.Commands;
internal static class BookmarkLoader
{ {
internal static class BookmarkLoader internal static MatchResult MatchProgram(Bookmark bookmark, string queryString)
{ {
internal static MatchResult MatchProgram(Bookmark bookmark, string queryString) var match = StringMatcher.FuzzySearch(queryString, bookmark.Name);
{ if (match.IsSearchPrecisionScoreMet())
var match = StringMatcher.FuzzySearch(queryString, bookmark.Name); return match;
if (match.IsSearchPrecisionScoreMet())
return match;
return StringMatcher.FuzzySearch(queryString, bookmark.Url); return StringMatcher.FuzzySearch(queryString, bookmark.Url);
}
internal static List<Bookmark> LoadAllBookmarks(Settings setting)
{
var allBookmarks = new List<Bookmark>();
if (setting.LoadChromeBookmark)
{
// Add Chrome bookmarks
var chromeBookmarks = new ChromeBookmarkLoader();
allBookmarks.AddRange(chromeBookmarks.GetBookmarks());
} }
internal static List<Bookmark> LoadAllBookmarks(Settings setting) if (setting.LoadFirefoxBookmark)
{ {
var allBookmarks = new List<Bookmark>(); // Add Firefox bookmarks
var mozBookmarks = new FirefoxBookmarkLoader();
if (setting.LoadChromeBookmark) allBookmarks.AddRange(mozBookmarks.GetBookmarks());
{
// Add Chrome bookmarks
var chromeBookmarks = new ChromeBookmarkLoader();
allBookmarks.AddRange(chromeBookmarks.GetBookmarks());
}
if (setting.LoadFirefoxBookmark)
{
// Add Firefox bookmarks
var mozBookmarks = new FirefoxBookmarkLoader();
allBookmarks.AddRange(mozBookmarks.GetBookmarks());
}
if (setting.LoadEdgeBookmark)
{
// Add Edge (Chromium) bookmarks
var edgeBookmarks = new EdgeBookmarkLoader();
allBookmarks.AddRange(edgeBookmarks.GetBookmarks());
}
foreach (var browser in setting.CustomChromiumBrowsers)
{
IBookmarkLoader loader = browser.BrowserType switch
{
BrowserType.Chromium => new CustomChromiumBookmarkLoader(browser),
BrowserType.Firefox => new CustomFirefoxBookmarkLoader(browser),
_ => new CustomChromiumBookmarkLoader(browser),
};
allBookmarks.AddRange(loader.GetBookmarks());
}
return allBookmarks.Distinct().ToList();
} }
if (setting.LoadEdgeBookmark)
{
// Add Edge (Chromium) bookmarks
var edgeBookmarks = new EdgeBookmarkLoader();
allBookmarks.AddRange(edgeBookmarks.GetBookmarks());
}
foreach (var browser in setting.CustomChromiumBrowsers)
{
IBookmarkLoader loader = browser.BrowserType switch
{
BrowserType.Chromium => new CustomChromiumBookmarkLoader(browser),
BrowserType.Firefox => new CustomFirefoxBookmarkLoader(browser),
_ => new CustomChromiumBookmarkLoader(browser),
};
allBookmarks.AddRange(loader.GetBookmarks());
}
return allBookmarks.Distinct().ToList();
} }
} }

View file

@ -1,19 +1,18 @@
using Flow.Launcher.Plugin.BrowserBookmark.Models; using Flow.Launcher.Plugin.BrowserBookmark.Models;
using System.Collections.Generic; using System.Collections.Generic;
namespace Flow.Launcher.Plugin.BrowserBookmark namespace Flow.Launcher.Plugin.BrowserBookmark;
{
public class CustomChromiumBookmarkLoader : ChromiumBookmarkLoader
{
public CustomChromiumBookmarkLoader(CustomBrowser browser)
{
BrowserName = browser.Name;
BrowserDataPath = browser.DataDirectoryPath;
}
public string BrowserDataPath { get; init; }
public string BookmarkFilePath { get; init; }
public string BrowserName { get; init; }
public override List<Bookmark> GetBookmarks() => BrowserDataPath != null ? LoadBookmarks(BrowserDataPath, BrowserName) : LoadBookmarksFromFile(BookmarkFilePath, BrowserName); public class CustomChromiumBookmarkLoader : ChromiumBookmarkLoader
{
public CustomChromiumBookmarkLoader(CustomBrowser browser)
{
BrowserName = browser.Name;
BrowserDataPath = browser.DataDirectoryPath;
} }
} public string BrowserDataPath { get; init; }
public string BookmarkFilePath { get; init; }
public string BrowserName { get; init; }
public override List<Bookmark> GetBookmarks() => BrowserDataPath != null ? LoadBookmarks(BrowserDataPath, BrowserName) : LoadBookmarksFromFile(BookmarkFilePath, BrowserName);
}

View file

@ -2,26 +2,25 @@
using System.IO; using System.IO;
using Flow.Launcher.Plugin.BrowserBookmark.Models; using Flow.Launcher.Plugin.BrowserBookmark.Models;
namespace Flow.Launcher.Plugin.BrowserBookmark namespace Flow.Launcher.Plugin.BrowserBookmark;
{
public class CustomFirefoxBookmarkLoader : FirefoxBookmarkLoaderBase
{
public CustomFirefoxBookmarkLoader(CustomBrowser browser)
{
BrowserName = browser.Name;
BrowserDataPath = browser.DataDirectoryPath;
}
/// <summary>
/// Path to places.sqlite
/// </summary>
public string BrowserDataPath { get; init; }
public string BrowserName { get; init; }
public override List<Bookmark> GetBookmarks() public class CustomFirefoxBookmarkLoader : FirefoxBookmarkLoaderBase
{ {
return GetBookmarksFromPath(Path.Combine(BrowserDataPath, "places.sqlite")); public CustomFirefoxBookmarkLoader(CustomBrowser browser)
} {
BrowserName = browser.Name;
BrowserDataPath = browser.DataDirectoryPath;
}
/// <summary>
/// Path to places.sqlite
/// </summary>
public string BrowserDataPath { get; init; }
public string BrowserName { get; init; }
public override List<Bookmark> GetBookmarks()
{
return GetBookmarksFromPath(Path.Combine(BrowserDataPath, "places.sqlite"));
} }
} }

View file

@ -3,21 +3,20 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
namespace Flow.Launcher.Plugin.BrowserBookmark namespace Flow.Launcher.Plugin.BrowserBookmark;
{
public class EdgeBookmarkLoader : ChromiumBookmarkLoader
{
private List<Bookmark> LoadEdgeBookmarks()
{
var bookmarks = new List<Bookmark>();
var platformPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
bookmarks.AddRange(LoadBookmarks(Path.Combine(platformPath, @"Microsoft\Edge\User Data"), "Microsoft Edge"));
bookmarks.AddRange(LoadBookmarks(Path.Combine(platformPath, @"Microsoft\Edge Dev\User Data"), "Microsoft Edge Dev"));
bookmarks.AddRange(LoadBookmarks(Path.Combine(platformPath, @"Microsoft\Edge SxS\User Data"), "Microsoft Edge Canary"));
return bookmarks; public class EdgeBookmarkLoader : ChromiumBookmarkLoader
} {
private List<Bookmark> LoadEdgeBookmarks()
public override List<Bookmark> GetBookmarks() => LoadEdgeBookmarks(); {
var bookmarks = new List<Bookmark>();
var platformPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
bookmarks.AddRange(LoadBookmarks(Path.Combine(platformPath, @"Microsoft\Edge\User Data"), "Microsoft Edge"));
bookmarks.AddRange(LoadBookmarks(Path.Combine(platformPath, @"Microsoft\Edge Dev\User Data"), "Microsoft Edge Dev"));
bookmarks.AddRange(LoadBookmarks(Path.Combine(platformPath, @"Microsoft\Edge SxS\User Data"), "Microsoft Edge Canary"));
return bookmarks;
} }
}
public override List<Bookmark> GetBookmarks() => LoadEdgeBookmarks();
}

View file

@ -5,140 +5,133 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
namespace Flow.Launcher.Plugin.BrowserBookmark namespace Flow.Launcher.Plugin.BrowserBookmark;
public abstract class FirefoxBookmarkLoaderBase : IBookmarkLoader
{ {
public abstract class FirefoxBookmarkLoaderBase : IBookmarkLoader public abstract List<Bookmark> GetBookmarks();
{
public abstract List<Bookmark> GetBookmarks();
private const string queryAllBookmarks = @"SELECT moz_places.url, moz_bookmarks.title private const string QueryAllBookmarks = """
FROM moz_places SELECT moz_places.url, moz_bookmarks.title
INNER JOIN moz_bookmarks ON ( FROM moz_places
INNER JOIN moz_bookmarks ON (
moz_bookmarks.fk NOT NULL AND moz_bookmarks.title NOT NULL AND moz_bookmarks.fk = moz_places.id moz_bookmarks.fk NOT NULL AND moz_bookmarks.title NOT NULL AND moz_bookmarks.fk = moz_places.id
) )
ORDER BY moz_places.visit_count DESC ORDER BY moz_places.visit_count DESC
"; """;
private const string dbPathFormat = "Data Source ={0}"; private const string DbPathFormat = "Data Source ={0}";
protected static List<Bookmark> GetBookmarksFromPath(string placesPath) protected static List<Bookmark> GetBookmarksFromPath(string placesPath)
{ {
// Return empty list if the places.sqlite file cannot be found // Return empty list if the places.sqlite file cannot be found
if (string.IsNullOrEmpty(placesPath) || !File.Exists(placesPath)) if (string.IsNullOrEmpty(placesPath) || !File.Exists(placesPath))
return new List<Bookmark>(); return new List<Bookmark>();
var bookmarkList = new List<Bookmark>(); Main.RegisterBookmarkFile(placesPath);
Main.RegisterBookmarkFile(placesPath); // create the connection string and init the connection
string dbPath = string.Format(DbPathFormat, placesPath);
using var dbConnection = new SqliteConnection(dbPath);
// Open connection to the database file and execute the query
dbConnection.Open();
var reader = new SqliteCommand(QueryAllBookmarks, dbConnection).ExecuteReader();
// create the connection string and init the connection // return results in List<Bookmark> format
string dbPath = string.Format(dbPathFormat, placesPath); return reader
using var dbConnection = new SqliteConnection(dbPath); .Select(
// Open connection to the database file and execute the query x => new Bookmark(
dbConnection.Open(); x["title"] is DBNull ? string.Empty : x["title"].ToString(),
var reader = new SqliteCommand(queryAllBookmarks, dbConnection).ExecuteReader(); x["url"].ToString()
)
// return results in List<Bookmark> format )
bookmarkList = reader.Select( .ToList();
x => new Bookmark(x["title"] is DBNull ? string.Empty : x["title"].ToString(), }
x["url"].ToString()) }
).ToList();
return bookmarkList;
} public class FirefoxBookmarkLoader : FirefoxBookmarkLoaderBase
{
/// <summary>
/// Searches the places.sqlite db and returns all bookmarks
/// </summary>
public override List<Bookmark> GetBookmarks()
{
return GetBookmarksFromPath(PlacesPath);
} }
/// <summary>
public class FirefoxBookmarkLoader : FirefoxBookmarkLoaderBase /// Path to places.sqlite
/// </summary>
private string PlacesPath
{ {
/// <summary> get
/// Searches the places.sqlite db and returns all bookmarks
/// </summary>
public override List<Bookmark> GetBookmarks()
{ {
return GetBookmarksFromPath(PlacesPath); var profileFolderPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), @"Mozilla\Firefox");
} var profileIni = Path.Combine(profileFolderPath, @"profiles.ini");
/// <summary>
/// Path to places.sqlite
/// </summary>
private string PlacesPath
{
get
{
var profileFolderPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), @"Mozilla\Firefox");
var profileIni = Path.Combine(profileFolderPath, @"profiles.ini");
if (!File.Exists(profileIni)) if (!File.Exists(profileIni))
return string.Empty; return string.Empty;
// get firefox default profile directory from profiles.ini // get firefox default profile directory from profiles.ini
string ini; using var sReader = new StreamReader(profileIni);
using (var sReader = new StreamReader(profileIni)) var ini = sReader.ReadToEnd();
{
ini = sReader.ReadToEnd();
}
/* /*
Current profiles.ini structure example as of Firefox version 69.0.1 Current profiles.ini structure example as of Firefox version 69.0.1
[Install736426B0AF4A39CB]
Default=Profiles/7789f565.default-release <== this is the default profile this plugin will get the bookmarks from. When opened Firefox will load the default profile
Locked=1
[Profile2] [Install736426B0AF4A39CB]
Name=newblahprofile Default=Profiles/7789f565.default-release <== this is the default profile this plugin will get the bookmarks from. When opened Firefox will load the default profile
IsRelative=0 Locked=1
Path=C:\t6h2yuq8.newblahprofile <== Note this is a custom location path for the profile user can set, we need to cater for this in code.
[Profile1] [Profile2]
Name=default Name=newblahprofile
IsRelative=1 IsRelative=0
Path=Profiles/cydum7q4.default Path=C:\t6h2yuq8.newblahprofile <== Note this is a custom location path for the profile user can set, we need to cater for this in code.
Default=1
[Profile0] [Profile1]
Name=default-release Name=default
IsRelative=1 IsRelative=1
Path=Profiles/7789f565.default-release Path=Profiles/cydum7q4.default
Default=1
[General] [Profile0]
StartWithLastProfile=1 Name=default-release
Version=2 IsRelative=1
*/ Path=Profiles/7789f565.default-release
var lines = ini.Split(new string[] [General]
{ StartWithLastProfile=1
"\r\n" Version=2
}, StringSplitOptions.None).ToList(); */
var lines = ini.Split("\r\n").ToList();
var defaultProfileFolderNameRaw = lines.Where(x => x.Contains("Default=") && x != "Default=1").FirstOrDefault() ?? string.Empty; var defaultProfileFolderNameRaw = lines.FirstOrDefault(x => x.Contains("Default=") && x != "Default=1") ?? string.Empty;
if (string.IsNullOrEmpty(defaultProfileFolderNameRaw)) if (string.IsNullOrEmpty(defaultProfileFolderNameRaw))
return string.Empty; return string.Empty;
var defaultProfileFolderName = defaultProfileFolderNameRaw.Split('=').Last(); var defaultProfileFolderName = defaultProfileFolderNameRaw.Split('=').Last();
var indexOfDefaultProfileAtttributePath = lines.IndexOf("Path=" + defaultProfileFolderName); var indexOfDefaultProfileAttributePath = lines.IndexOf("Path=" + defaultProfileFolderName);
// Seen in the example above, the IsRelative attribute is always above the Path attribute // Seen in the example above, the IsRelative attribute is always above the Path attribute
var relativeAttribute = lines[indexOfDefaultProfileAtttributePath - 1]; var relativeAttribute = lines[indexOfDefaultProfileAttributePath - 1];
return relativeAttribute == "0" // See above, the profile is located in a custom location, path is not relative, so IsRelative=0 return relativeAttribute == "0" // See above, the profile is located in a custom location, path is not relative, so IsRelative=0
? defaultProfileFolderName + @"\places.sqlite" ? defaultProfileFolderName + @"\places.sqlite"
: Path.Combine(profileFolderPath, defaultProfileFolderName) + @"\places.sqlite"; : Path.Combine(profileFolderPath, defaultProfileFolderName) + @"\places.sqlite";
} }
} }
} }
public static class Extensions public static class Extensions
{ {
public static IEnumerable<T> Select<T>(this SqliteDataReader reader, Func<SqliteDataReader, T> projection) public static IEnumerable<T> Select<T>(this SqliteDataReader reader, Func<SqliteDataReader, T> projection)
{ {
while (reader.Read()) while (reader.Read())
{ {
yield return projection(reader); yield return projection(reader);
}
} }
} }
} }

View file

@ -1,10 +1,9 @@
using Flow.Launcher.Plugin.BrowserBookmark.Models; using Flow.Launcher.Plugin.BrowserBookmark.Models;
using System.Collections.Generic; using System.Collections.Generic;
namespace Flow.Launcher.Plugin.BrowserBookmark namespace Flow.Launcher.Plugin.BrowserBookmark;
public interface IBookmarkLoader
{ {
public interface IBookmarkLoader public List<Bookmark> GetBookmarks();
{ }
public List<Bookmark> GetBookmarks();
}
}

View file

@ -1,7 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Plugin.BrowserBookmark.Commands; using Flow.Launcher.Plugin.BrowserBookmark.Commands;
@ -12,233 +11,234 @@ using System.Threading.Channels;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Threading; using System.Threading;
namespace Flow.Launcher.Plugin.BrowserBookmark namespace Flow.Launcher.Plugin.BrowserBookmark;
public class Main : ISettingProvider, IPlugin, IReloadable, IPluginI18n, IContextMenu, IDisposable
{ {
public class Main : ISettingProvider, IPlugin, IReloadable, IPluginI18n, IContextMenu, IDisposable private static PluginInitContext _context;
private static List<Bookmark> _cachedBookmarks = new List<Bookmark>();
private static Settings _settings;
private static bool _initialized = false;
public void Init(PluginInitContext context)
{ {
private static PluginInitContext context; _context = context;
private static List<Bookmark> cachedBookmarks = new List<Bookmark>(); _settings = context.API.LoadSettingJsonStorage<Settings>();
private static Settings _settings; LoadBookmarksIfEnabled();
}
private static bool initialized = false; private static void LoadBookmarksIfEnabled()
{
public void Init(PluginInitContext context) if (_context.CurrentPluginMetadata.Disabled)
{ {
Main.context = context; // Don't load or monitor files if disabled
return;
}
_settings = context.API.LoadSettingJsonStorage<Settings>(); _cachedBookmarks = BookmarkLoader.LoadAllBookmarks(_settings);
_ = MonitorRefreshQueueAsync();
_initialized = true;
}
public List<Result> Query(Query query)
{
// For when the plugin being previously disabled and is now re-enabled
if (!_initialized)
{
LoadBookmarksIfEnabled(); LoadBookmarksIfEnabled();
} }
private static void LoadBookmarksIfEnabled() string param = query.Search.TrimStart();
// Should top results be returned? (true if no search parameters have been passed)
var topResults = string.IsNullOrEmpty(param);
if (!topResults)
{ {
if (context.CurrentPluginMetadata.Disabled) // Since we mixed chrome and firefox bookmarks, we should order them again
{ return _cachedBookmarks
// Don't load or monitor files if disabled .Select(
return; c => new Result
}
cachedBookmarks = BookmarkLoader.LoadAllBookmarks(_settings);
_ = MonitorRefreshQueueAsync();
initialized = true;
}
public List<Result> Query(Query query)
{
// For when the plugin being previously disabled and is now renabled
if (!initialized)
{
LoadBookmarksIfEnabled();
}
string param = query.Search.TrimStart();
// Should top results be returned? (true if no search parameters have been passed)
var topResults = string.IsNullOrEmpty(param);
if (!topResults)
{
// Since we mixed chrome and firefox bookmarks, we should order them again
var returnList = cachedBookmarks.Select(c => new Result()
{
Title = c.Name,
SubTitle = c.Url,
IcoPath = @"Images\bookmark.png",
Score = BookmarkLoader.MatchProgram(c, param).Score,
Action = _ =>
{ {
context.API.OpenUrl(c.Url); Title = c.Name,
SubTitle = c.Url,
return true; IcoPath = @"Images\bookmark.png",
}, Score = BookmarkLoader.MatchProgram(c, param).Score,
ContextData = new BookmarkAttributes Action = _ =>
{
Url = c.Url
}
}).Where(r => r.Score > 0);
return returnList.ToList();
}
else
{
return cachedBookmarks.Select(c => new Result()
{
Title = c.Name,
SubTitle = c.Url,
IcoPath = @"Images\bookmark.png",
Score = 5,
Action = _ =>
{
context.API.OpenUrl(c.Url);
return true;
},
ContextData = new BookmarkAttributes
{
Url = c.Url
}
}).ToList();
}
}
private static Channel<byte> refreshQueue = Channel.CreateBounded<byte>(1);
private static SemaphoreSlim fileMonitorSemaphore = new(1, 1);
private static async Task MonitorRefreshQueueAsync()
{
if (fileMonitorSemaphore.CurrentCount < 1)
{
return;
}
await fileMonitorSemaphore.WaitAsync();
var reader = refreshQueue.Reader;
while (await reader.WaitToReadAsync())
{
if (reader.TryRead(out _))
{
ReloadAllBookmarks(false);
}
}
fileMonitorSemaphore.Release();
}
private static readonly List<FileSystemWatcher> Watchers = new();
internal static void RegisterBookmarkFile(string path)
{
var directory = Path.GetDirectoryName(path);
if (!Directory.Exists(directory) || !File.Exists(path))
{
return;
}
if (Watchers.Any(x => x.Path.Equals(directory, StringComparison.OrdinalIgnoreCase)))
{
return;
}
var watcher = new FileSystemWatcher(directory!);
watcher.Filter = Path.GetFileName(path);
watcher.NotifyFilter = NotifyFilters.FileName |
NotifyFilters.LastWrite |
NotifyFilters.Size;
watcher.Changed += static (_, _) =>
{
refreshQueue.Writer.TryWrite(default);
};
watcher.Renamed += static (_, _) =>
{
refreshQueue.Writer.TryWrite(default);
};
watcher.EnableRaisingEvents = true;
Watchers.Add(watcher);
}
public void ReloadData()
{
ReloadAllBookmarks();
}
public static void ReloadAllBookmarks(bool disposeFileWatchers = true)
{
cachedBookmarks.Clear();
if (disposeFileWatchers)
DisposeFileWatchers();
LoadBookmarksIfEnabled();
}
public string GetTranslatedPluginTitle()
{
return context.API.GetTranslation("flowlauncher_plugin_browserbookmark_plugin_name");
}
public string GetTranslatedPluginDescription()
{
return context.API.GetTranslation("flowlauncher_plugin_browserbookmark_plugin_description");
}
public Control CreateSettingPanel()
{
return new SettingsControl(_settings);
}
public List<Result> LoadContextMenus(Result selectedResult)
{
return new List<Result>()
{
new Result
{
Title = context.API.GetTranslation("flowlauncher_plugin_browserbookmark_copyurl_title"),
SubTitle = context.API.GetTranslation("flowlauncher_plugin_browserbookmark_copyurl_subtitle"),
Action = _ =>
{
try
{ {
context.API.CopyToClipboard(((BookmarkAttributes)selectedResult.ContextData).Url); _context.API.OpenUrl(c.Url);
return true; return true;
} },
catch (Exception e) ContextData = new BookmarkAttributes { Url = c.Url }
}
)
.Where(r => r.Score > 0)
.ToList();
}
else
{
return _cachedBookmarks
.Select(
c => new Result
{
Title = c.Name,
SubTitle = c.Url,
IcoPath = @"Images\bookmark.png",
Score = 5,
Action = _ =>
{ {
var message = "Failed to set url in clipboard"; _context.API.OpenUrl(c.Url);
Log.Exception("Main", message, e, "LoadContextMenus"); return true;
},
context.API.ShowMsg(message); ContextData = new BookmarkAttributes { Url = c.Url }
}
return false; )
} .ToList();
},
IcoPath = "Images\\copylink.png",
Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\ue8c8")
}
};
}
internal class BookmarkAttributes
{
internal string Url { get; set; }
}
public void Dispose()
{
DisposeFileWatchers();
}
private static void DisposeFileWatchers()
{
foreach (var watcher in Watchers)
{
watcher.Dispose();
}
Watchers.Clear();
} }
} }
private static Channel<byte> _refreshQueue = Channel.CreateBounded<byte>(1);
private static SemaphoreSlim _fileMonitorSemaphore = new(1, 1);
private static async Task MonitorRefreshQueueAsync()
{
if (_fileMonitorSemaphore.CurrentCount < 1)
{
return;
}
await _fileMonitorSemaphore.WaitAsync();
var reader = _refreshQueue.Reader;
while (await reader.WaitToReadAsync())
{
if (reader.TryRead(out _))
{
ReloadAllBookmarks(false);
}
}
_fileMonitorSemaphore.Release();
}
private static readonly List<FileSystemWatcher> Watchers = new();
internal static void RegisterBookmarkFile(string path)
{
var directory = Path.GetDirectoryName(path);
if (!Directory.Exists(directory) || !File.Exists(path))
{
return;
}
if (Watchers.Any(x => x.Path.Equals(directory, StringComparison.OrdinalIgnoreCase)))
{
return;
}
var watcher = new FileSystemWatcher(directory!);
watcher.Filter = Path.GetFileName(path);
watcher.NotifyFilter = NotifyFilters.FileName |
NotifyFilters.LastWrite |
NotifyFilters.Size;
watcher.Changed += static (_, _) =>
{
_refreshQueue.Writer.TryWrite(default);
};
watcher.Renamed += static (_, _) =>
{
_refreshQueue.Writer.TryWrite(default);
};
watcher.EnableRaisingEvents = true;
Watchers.Add(watcher);
}
public void ReloadData()
{
ReloadAllBookmarks();
}
public static void ReloadAllBookmarks(bool disposeFileWatchers = true)
{
_cachedBookmarks.Clear();
if (disposeFileWatchers)
DisposeFileWatchers();
LoadBookmarksIfEnabled();
}
public string GetTranslatedPluginTitle()
{
return _context.API.GetTranslation("flowlauncher_plugin_browserbookmark_plugin_name");
}
public string GetTranslatedPluginDescription()
{
return _context.API.GetTranslation("flowlauncher_plugin_browserbookmark_plugin_description");
}
public Control CreateSettingPanel()
{
return new SettingsControl(_settings);
}
public List<Result> LoadContextMenus(Result selectedResult)
{
return new List<Result>()
{
new Result
{
Title = _context.API.GetTranslation("flowlauncher_plugin_browserbookmark_copyurl_title"),
SubTitle = _context.API.GetTranslation("flowlauncher_plugin_browserbookmark_copyurl_subtitle"),
Action = _ =>
{
try
{
_context.API.CopyToClipboard(((BookmarkAttributes)selectedResult.ContextData).Url);
return true;
}
catch (Exception e)
{
var message = "Failed to set url in clipboard";
Log.Exception("Main", message, e, "LoadContextMenus");
_context.API.ShowMsg(message);
return false;
}
},
IcoPath = @"Images\copylink.png",
Glyph = new GlyphInfo(FontFamily: "/Resources/#Segoe Fluent Icons", Glyph: "\ue8c8")
}
};
}
internal class BookmarkAttributes
{
internal string Url { get; set; }
}
public void Dispose()
{
DisposeFileWatchers();
}
private static void DisposeFileWatchers()
{
foreach (var watcher in Watchers)
{
watcher.Dispose();
}
Watchers.Clear();
}
} }

View file

@ -1,22 +1,21 @@
using System.Collections.Generic; using System.Collections.Generic;
namespace Flow.Launcher.Plugin.BrowserBookmark.Models namespace Flow.Launcher.Plugin.BrowserBookmark.Models;
// Source may be important in the future
public record Bookmark(string Name, string Url, string Source = "")
{ {
// Source may be important in the future public override int GetHashCode()
public record Bookmark(string Name, string Url, string Source = "")
{ {
public override int GetHashCode() var hashName = Name?.GetHashCode() ?? 0;
{ var hashUrl = Url?.GetHashCode() ?? 0;
var hashName = Name?.GetHashCode() ?? 0; return hashName ^ hashUrl;
var hashUrl = Url?.GetHashCode() ?? 0;
return hashName ^ hashUrl;
}
public virtual bool Equals(Bookmark other)
{
return other != null && Name == other.Name && Url == other.Url;
}
public List<CustomBrowser> CustomBrowsers { get; set; }= new();
} }
}
public virtual bool Equals(Bookmark other)
{
return other != null && Name == other.Name && Url == other.Url;
}
public List<CustomBrowser> CustomBrowsers { get; set; } = new();
}

View file

@ -1,45 +1,44 @@
namespace Flow.Launcher.Plugin.BrowserBookmark.Models namespace Flow.Launcher.Plugin.BrowserBookmark.Models;
{
public class CustomBrowser : BaseModel
{
private string _name;
private string _dataDirectoryPath;
private BrowserType browserType = BrowserType.Chromium;
public string Name public class CustomBrowser : BaseModel
{
private string _name;
private string _dataDirectoryPath;
private BrowserType _browserType = BrowserType.Chromium;
public string Name
{
get => _name;
set
{ {
get => _name; _name = value;
set OnPropertyChanged();
{
_name = value;
OnPropertyChanged(nameof(Name));
}
}
public string DataDirectoryPath
{
get => _dataDirectoryPath;
set
{
_dataDirectoryPath = value;
OnPropertyChanged(nameof(DataDirectoryPath));
}
}
public BrowserType BrowserType
{
get => browserType;
set
{
browserType = value;
OnPropertyChanged(nameof(BrowserType));
}
} }
} }
public enum BrowserType public string DataDirectoryPath
{ {
Chromium, get => _dataDirectoryPath;
Firefox, set
{
_dataDirectoryPath = value;
OnPropertyChanged();
}
}
public BrowserType BrowserType
{
get => _browserType;
set
{
_browserType = value;
OnPropertyChanged();
}
} }
} }
public enum BrowserType
{
Chromium,
Firefox,
}

View file

@ -1,17 +1,16 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
namespace Flow.Launcher.Plugin.BrowserBookmark.Models namespace Flow.Launcher.Plugin.BrowserBookmark.Models;
public class Settings : BaseModel
{ {
public class Settings : BaseModel public bool OpenInNewBrowserWindow { get; set; } = true;
{
public bool OpenInNewBrowserWindow { get; set; } = true;
public string BrowserPath { get; set; } public string BrowserPath { get; set; }
public bool LoadChromeBookmark { get; set; } = true; public bool LoadChromeBookmark { get; set; } = true;
public bool LoadFirefoxBookmark { get; set; } = true; public bool LoadFirefoxBookmark { get; set; } = true;
public bool LoadEdgeBookmark { get; set; } = true; public bool LoadEdgeBookmark { get; set; } = true;
public ObservableCollection<CustomBrowser> CustomChromiumBrowsers { get; set; } = new(); public ObservableCollection<CustomBrowser> CustomChromiumBrowsers { get; set; } = new();
} }
}

View file

@ -63,7 +63,6 @@
<StackPanel Margin="26,0,26,0"> <StackPanel Margin="26,0,26,0">
<StackPanel Margin="0,0,0,12"> <StackPanel Margin="0,0,0,12">
<TextBlock <TextBlock
Grid.Column="0"
Margin="0,0,0,0" Margin="0,0,0,0"
FontSize="20" FontSize="20"
FontWeight="SemiBold" FontWeight="SemiBold"

View file

@ -3,55 +3,54 @@ using System.Windows;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Forms; using System.Windows.Forms;
namespace Flow.Launcher.Plugin.BrowserBookmark.Views namespace Flow.Launcher.Plugin.BrowserBookmark.Views;
/// <summary>
/// Interaction logic for CustomBrowserSetting.xaml
/// </summary>
public partial class CustomBrowserSettingWindow : Window
{ {
/// <summary> private CustomBrowser _currentCustomBrowser;
/// Interaction logic for CustomBrowserSetting.xaml public CustomBrowserSettingWindow(CustomBrowser browser)
/// </summary>
public partial class CustomBrowserSettingWindow : Window
{ {
private CustomBrowser currentCustomBrowser; InitializeComponent();
public CustomBrowserSettingWindow(CustomBrowser browser) _currentCustomBrowser = browser;
DataContext = new CustomBrowser
{ {
InitializeComponent(); Name = browser.Name,
currentCustomBrowser = browser; DataDirectoryPath = browser.DataDirectoryPath,
DataContext = new CustomBrowser BrowserType = browser.BrowserType,
{ };
Name = browser.Name, }
DataDirectoryPath = browser.DataDirectoryPath,
BrowserType = browser.BrowserType,
};
}
private void ConfirmEditCustomBrowser(object sender, RoutedEventArgs e)
{
CustomBrowser editBrowser = (CustomBrowser)DataContext;
currentCustomBrowser.Name = editBrowser.Name;
currentCustomBrowser.DataDirectoryPath = editBrowser.DataDirectoryPath;
currentCustomBrowser.BrowserType = editBrowser.BrowserType;
DialogResult = true;
Close();
}
private void CancelEditCustomBrowser(object sender, RoutedEventArgs e) private void ConfirmEditCustomBrowser(object sender, RoutedEventArgs e)
{ {
Close(); CustomBrowser editBrowser = (CustomBrowser)DataContext;
} _currentCustomBrowser.Name = editBrowser.Name;
_currentCustomBrowser.DataDirectoryPath = editBrowser.DataDirectoryPath;
_currentCustomBrowser.BrowserType = editBrowser.BrowserType;
DialogResult = true;
Close();
}
private void WindowKeyDown(object sender, System.Windows.Input.KeyEventArgs e) private void CancelEditCustomBrowser(object sender, RoutedEventArgs e)
{ {
if (e.Key == Key.Enter) Close();
{ }
ConfirmEditCustomBrowser(sender, e);
}
}
private void OnSelectPathClick(object sender, RoutedEventArgs e) private void WindowKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
{
if (e.Key == Key.Enter)
{ {
var dialog = new FolderBrowserDialog(); ConfirmEditCustomBrowser(sender, e);
dialog.ShowDialog();
CustomBrowser editBrowser = (CustomBrowser)DataContext;
editBrowser.DataDirectoryPath = dialog.SelectedPath;
} }
} }
private void OnSelectPathClick(object sender, RoutedEventArgs e)
{
var dialog = new FolderBrowserDialog();
dialog.ShowDialog();
CustomBrowser editBrowser = (CustomBrowser)DataContext;
editBrowser.DataDirectoryPath = dialog.SelectedPath;
}
} }

View file

@ -4,119 +4,116 @@ using System.Windows.Input;
using System.ComponentModel; using System.ComponentModel;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Flow.Launcher.Plugin.BrowserBookmark.Views namespace Flow.Launcher.Plugin.BrowserBookmark.Views;
public partial class SettingsControl : INotifyPropertyChanged
{ {
public partial class SettingsControl : INotifyPropertyChanged public Settings Settings { get; }
public CustomBrowser SelectedCustomBrowser { get; set; }
public bool LoadChromeBookmark
{ {
public Settings Settings { get; } get => Settings.LoadChromeBookmark;
set
public CustomBrowser SelectedCustomBrowser { get; set; }
public bool LoadChromeBookmark
{ {
get => Settings.LoadChromeBookmark; Settings.LoadChromeBookmark = value;
set _ = Task.Run(() => Main.ReloadAllBookmarks());
}
}
public bool LoadFirefoxBookmark
{
get => Settings.LoadFirefoxBookmark;
set
{
Settings.LoadFirefoxBookmark = value;
_ = Task.Run(() => Main.ReloadAllBookmarks());
}
}
public bool LoadEdgeBookmark
{
get => Settings.LoadEdgeBookmark;
set
{
Settings.LoadEdgeBookmark = value;
_ = Task.Run(() => Main.ReloadAllBookmarks());
}
}
public bool OpenInNewBrowserWindow
{
get => Settings.OpenInNewBrowserWindow;
set
{
Settings.OpenInNewBrowserWindow = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(OpenInNewBrowserWindow)));
}
}
public SettingsControl(Settings settings)
{
Settings = settings;
InitializeComponent();
}
public event PropertyChangedEventHandler PropertyChanged;
private void NewCustomBrowser(object sender, RoutedEventArgs e)
{
var newBrowser = new CustomBrowser();
var window = new CustomBrowserSettingWindow(newBrowser);
window.ShowDialog();
if (newBrowser is not
{ {
Settings.LoadChromeBookmark = value; Name: null,
_ = Task.Run(() => Main.ReloadAllBookmarks()); DataDirectoryPath: null
} })
}
public bool LoadFirefoxBookmark
{ {
get => Settings.LoadFirefoxBookmark; Settings.CustomChromiumBrowsers.Add(newBrowser);
set _ = Task.Run(() => Main.ReloadAllBookmarks());
{
Settings.LoadFirefoxBookmark = value;
_ = Task.Run(() => Main.ReloadAllBookmarks());
}
} }
}
public bool LoadEdgeBookmark private void DeleteCustomBrowser(object sender, RoutedEventArgs e)
{
if (CustomBrowsers.SelectedItem is CustomBrowser selectedCustomBrowser)
{ {
get => Settings.LoadEdgeBookmark; Settings.CustomChromiumBrowsers.Remove(selectedCustomBrowser);
set _ = Task.Run(() => Main.ReloadAllBookmarks());
{
Settings.LoadEdgeBookmark = value;
_ = Task.Run(() => Main.ReloadAllBookmarks());
}
} }
}
public bool OpenInNewBrowserWindow private void MouseDoubleClickOnSelectedCustomBrowser(object sender, MouseButtonEventArgs e)
{
EditSelectedCustomBrowser();
}
private void Others_Click(object sender, RoutedEventArgs e)
{
CustomBrowsersList.Visibility = CustomBrowsersList.Visibility switch
{ {
get => Settings.OpenInNewBrowserWindow; Visibility.Collapsed => Visibility.Visible,
set _ => Visibility.Collapsed
{ };
Settings.OpenInNewBrowserWindow = value; }
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(OpenInNewBrowserWindow)));
}
}
public SettingsControl(Settings settings) private void EditCustomBrowser(object sender, RoutedEventArgs e)
{
EditSelectedCustomBrowser();
}
private void EditSelectedCustomBrowser()
{
if (SelectedCustomBrowser is null)
return;
var window = new CustomBrowserSettingWindow(SelectedCustomBrowser);
var result = window.ShowDialog() ?? false;
if (result)
{ {
Settings = settings; _ = Task.Run(() => Main.ReloadAllBookmarks());
InitializeComponent();
}
public event PropertyChangedEventHandler PropertyChanged;
private void NewCustomBrowser(object sender, RoutedEventArgs e)
{
var newBrowser = new CustomBrowser();
var window = new CustomBrowserSettingWindow(newBrowser);
window.ShowDialog();
if (newBrowser is not
{
Name: null,
DataDirectoryPath: null
})
{
Settings.CustomChromiumBrowsers.Add(newBrowser);
_ = Task.Run(() => Main.ReloadAllBookmarks());
}
}
private void DeleteCustomBrowser(object sender, RoutedEventArgs e)
{
if (CustomBrowsers.SelectedItem is CustomBrowser selectedCustomBrowser)
{
Settings.CustomChromiumBrowsers.Remove(selectedCustomBrowser);
_ = Task.Run(() => Main.ReloadAllBookmarks());
}
}
private void MouseDoubleClickOnSelectedCustomBrowser(object sender, MouseButtonEventArgs e)
{
EditSelectedCustomBrowser();
}
private void Others_Click(object sender, RoutedEventArgs e)
{
if (CustomBrowsersList.Visibility == Visibility.Collapsed)
{
CustomBrowsersList.Visibility = Visibility.Visible;
}
else
CustomBrowsersList.Visibility = Visibility.Collapsed;
}
private void EditCustomBrowser(object sender, RoutedEventArgs e)
{
EditSelectedCustomBrowser();
}
private void EditSelectedCustomBrowser()
{
if (SelectedCustomBrowser is null)
return;
var window = new CustomBrowserSettingWindow(SelectedCustomBrowser);
var result = window.ShowDialog() ?? false;
if (result)
{
_ = Task.Run(() => Main.ReloadAllBookmarks());
}
} }
} }
} }

202
README.md
View file

@ -16,77 +16,15 @@
</p> </p>
<p align="center"> <p align="center">
Dedicated to making your work flow more seamless. Search everything from applications, files, bookmarks, YouTube, Twitter and more. Flow will continue to evolve, designed to be open and built with the community at heart. A quick file search and app launcher for Windows with community-made plugins.</p>
<p align="center">
Dedicated to making your work flow more seamless. Search everything from applications, files, bookmarks, YouTube, Twitter and more. Flow will continue to evolve, designed to be open and built with the community at heart.</p>
<p align="center"> <sub>Remember to star it, flow will love you more :)</sub></p> <p align="center"> <sub>Remember to star it, flow will love you more :)</sub></p>
<img src="https://user-images.githubusercontent.com/6903107/144858082-8b654daf-60fb-4ee6-89b2-6183b73510d1.png" width="100%"> <img src="https://user-images.githubusercontent.com/6903107/144858082-8b654daf-60fb-4ee6-89b2-6183b73510d1.png" width="100%">
## 🎅 New Features🤶
### Preview Panel
<img src="https://user-images.githubusercontent.com/6903107/207159213-662999d3-2c18-4256-b473-c417efca0069.png" width="400">
- Use the F1 key to open/hide the preview panel.
- Media files will be displayed as large images, otherwise a large icon and entire path will be displayed.
- Turn on preview permanently via Settings (Always Preview).
- Use hotkeys (Ctrl+Plus,Minus / Ctrl+],[) to adjust flow's search window width and height quickly if the preview area is too narrow.
- This feature is currently in its early stages.
### Everything Plugin Merged Into Explorer
<img src="https://user-images.githubusercontent.com/6903107/207171178-78912e76-31c2-4ba2-b3cf-de4b0b5e5691.png" width="300">
- Switch easily between Everything and Windows Search to take advantage of both search engines (remember to remove existing Everything plugin).
- Use features available to both Everything and Explorer plugins
### Date & Time Display In Search Window
<img src="https://user-images.githubusercontent.com/6903107/207159348-8b0c7a2b-0836-4764-916b-e0236087f7f3.png" width="400">
- Display the date and time when the search window is triggered.
### Drag & Drop
<img src="https://user-images.githubusercontent.com/6903107/207159486-1993510f-09f2-4e33-bba7-4ca59ca1bc5a.png" width="500">
- Drag an item to Discord or computer location.
- The target program determines whether the drop is to copy or move the item (can change via CTRL or Alt), and the operation is displayed on the mouse cursor.
### Custom Shortcut
<img src="Flow.Launcher/Images/illustration_02.png" width="300">
<img src="Flow.Launcher/Images/illustration_01.png" width="300">
- New shortcut functionality to set additional action keywords or search terms.
### Improved Program Plugin
- PATH is now indexed
- Support for .url files, flow can now search installed steam/epic games.
- Improved UWP indexing.
### Improved Memory Usage
- Fixed a memory leak and reduced overall memory usage.
### Improved Plugin / Plugin Store
- Search plugins in the Plugin Store and existing plugin tab.
- Categorised sections in Plugin Store to easily see new and updated plugins.
### Improved Non-C# Plugin's Panel Design
<img src="https://user-images.githubusercontent.com/6903107/207166078-871a17aa-5ab4-4808-8d8f-12b7cb66ea89.png" width="450">
- The design has been adjusted to align to the overall look and feel of flow.
- Simplified the information displayed on buttons
🚂[Full Changelogs](https://github.com/Flow-Launcher/Flow.Launcher/releases)
<img src="https://user-images.githubusercontent.com/6903107/144858082-8b654daf-60fb-4ee6-89b2-6183b73510d1.png" width="100%">
<h4 align="center"> <h4 align="center">
<a href="#-getting-started">Getting Started</a> <a href="#-getting-started">Getting Started</a>
<a href="#-features">Features</a> <a href="#-features">Features</a>
@ -104,15 +42,29 @@ Dedicated to making your work flow more seamless. Search everything from applica
### Installation ### Installation
| [Windows 7+ installer](https://github.com/Flow-Launcher/Flow.Launcher/releases/latest/download/Flow-Launcher-Setup.exe) | [Portable](https://github.com/Flow-Launcher/Flow.Launcher/releases/latest/download/Flow-Launcher-Portable.zip) | [Windows 7+ Installer](https://github.com/Flow-Launcher/Flow.Launcher/releases/latest/download/Flow-Launcher-Setup.exe) or [Portable Version](https://github.com/Flow-Launcher/Flow.Launcher/releases/latest/download/Flow-Launcher-Portable.zip)
| :----------------------------------------------------------: | :----------------------------------------------------------: |
| `winget install "Flow Launcher"` | `scoop install Flow-Launcher` | `choco install Flow-Launcher` | #### Winget
| :------------------------------: | :------------------------------: | :------------------------------: |
```
winget install "Flow Launcher"
```
#### Scoop
```
scoop install Flow-Launcher
```
#### Chocolatey
```
choco install Flow-Launcher
```
> When installing for the first time Windows may raise an issue about security due to code not being signed, if you downloaded from this repo then you are good to continue the set up. > When installing for the first time Windows may raise an issue about security due to code not being signed, if you downloaded from this repo then you are good to continue the set up.
And you can download [early access version](https://github.com/Flow-Launcher/Prereleases/releases). Or download the [early access version](https://github.com/Flow-Launcher/Prereleases/releases).
<img src="https://user-images.githubusercontent.com/6903107/144858082-8b654daf-60fb-4ee6-89b2-6183b73510d1.png" width="100%"> <img src="https://user-images.githubusercontent.com/6903107/144858082-8b654daf-60fb-4ee6-89b2-6183b73510d1.png" width="100%">
@ -123,6 +75,7 @@ And you can download [early access version](https://github.com/Flow-Launcher/Pre
<img src="https://user-images.githubusercontent.com/6903107/145332614-74909973-f6eb-47c2-8235-289931e30718.png" width="400"> <img src="https://user-images.githubusercontent.com/6903107/145332614-74909973-f6eb-47c2-8235-289931e30718.png" width="400">
- Search for apps, files or file contents. - Search for apps, files or file contents.
- Supports Everything and Windows Index.
<img src="https://user-images.githubusercontent.com/6903107/145018796-658b7c24-a34f-46b6-98d4-cf4f636d8b60.png" width="400"> <img src="https://user-images.githubusercontent.com/6903107/145018796-658b7c24-a34f-46b6-98d4-cf4f636d8b60.png" width="400">
@ -156,7 +109,7 @@ And you can download [early access version](https://github.com/Flow-Launcher/Pre
<img src="https://user-images.githubusercontent.com/6903107/207142197-9e910147-96a9-466e-bbc4-b1163314ef59.png" width="400"> <img src="https://user-images.githubusercontent.com/6903107/207142197-9e910147-96a9-466e-bbc4-b1163314ef59.png" width="400">
- Run batch and PowerShell commands as Administrator or a different user. - Run batch and PowerShell commands as Administrator or a different user.
- Ctrl+Enter to Run as Administrator. - <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>Enter</kbd> to Run as Administrator.
### Explorer ### Explorer
@ -164,6 +117,13 @@ And you can download [early access version](https://github.com/Flow-Launcher/Pre
- Save file or folder locations for quick access. - Save file or folder locations for quick access.
#### Drag & Drop
<img src="https://user-images.githubusercontent.com/6903107/207159486-1993510f-09f2-4e33-bba7-4ca59ca1bc5a.png" width="500">
- Drag a file/folder to File Exlporer, or even Discord.
- Copy/move behavior can be change via <kbd>Ctrl</kbd> or <kbd>Shift</kbd>, and the operation is displayed on the mouse cursor.
### Windows & Control Panel Settings ### Windows & Control Panel Settings
<img src="https://user-images.githubusercontent.com/6903107/207140658-52c1bea6-5b14-4db8-ae35-acc65e6bda85.png" width="400"> <img src="https://user-images.githubusercontent.com/6903107/207140658-52c1bea6-5b14-4db8-ae35-acc65e6bda85.png" width="400">
@ -176,6 +136,16 @@ And you can download [early access version](https://github.com/Flow-Launcher/Pre
- Prioritise the order of each plugin's results. - Prioritise the order of each plugin's results.
### Preview Panel
<img src="https://user-images.githubusercontent.com/6903107/207159213-662999d3-2c18-4256-b473-c417efca0069.png" width="400">
- Use <kbd>F1</kbd> to toggle the preview panel.
- Media files will be displayed as large images, otherwise a large icon and full path will be displayed.
- Turn on preview permanently via Settings (Always Preview).
- Use <kbd>Ctrl</kbd>+<kbd>+</kbd>/<kbd>-</kbd> and <kbd>Ctrl</kbd>+<kbd>[</kbd>/<kbd>]</kbd> to adjust search window width and height quickly if the preview area is too narrow.
### Customizations ### Customizations
<img src="https://user-images.githubusercontent.com/6903107/144693887-1b92ed16-dca1-4b7e-8644-5e9524cdfb31.gif" width="500"> <img src="https://user-images.githubusercontent.com/6903107/144693887-1b92ed16-dca1-4b7e-8644-5e9524cdfb31.gif" width="500">
@ -187,12 +157,48 @@ And you can download [early access version](https://github.com/Flow-Launcher/Pre
- There are various themes and you also can make your own. - There are various themes and you also can make your own.
### 💬 Language #### Date & Time Display In Search Window
<img src="https://user-images.githubusercontent.com/6903107/207159348-8b0c7a2b-0836-4764-916b-e0236087f7f3.png" width="400">
- Display date and time in search window.
### 💬 Languages
- Supports languages from Chinese to Italian and more. - Supports languages from Chinese to Italian and more.
- Supports Pinyin search. - Supports Pinyin (拼音) search.
- [Crowdin](https://crowdin.com/project/flow-launcher) support for language translations. - [Crowdin](https://crowdin.com/project/flow-launcher) support for language translations.
<details>
<summary>Supported languages</summary>
<ul>
<li>English</li>
<li>中文</li>
<li>中文(繁体)</li>
<li>Українська</li>
<li>Русский</li>
<li>Français</li>
<li>日本語</li>
<li>Dutch</li>
<li>Polski</li>
<li>Dansk</li>
<li>de, Deutsch</li>
<li>ko, 한국어</li>
<li>Srpski</li>
<li>Português</li>
<li>Português (Brasil)</li>
<li>Spanish</li>
<li>es-419, Spanish (Latin America)</li>
<li>Italiano</li>
<li>Norsk Bokmål</li>
<li>Slovenčina</li>
<li>Türkçe</li>
<li>čeština</li>
<li>اللغة العربية</li>
<li>Tiếng Việt</li>
</ul>
</details>
### Portable ### Portable
- Fully portable. - Fully portable.
@ -206,7 +212,7 @@ And you can download [early access version](https://github.com/Flow-Launcher/Pre
<img src="https://user-images.githubusercontent.com/6903107/207144711-0c5f8b2b-4b1b-44c8-b23e-c123f6b05146.png" width="200"> <img src="https://user-images.githubusercontent.com/6903107/207144711-0c5f8b2b-4b1b-44c8-b23e-c123f6b05146.png" width="200">
- Pause hotkey activation when you are playing games. - Pause hotkey activation when you are playing games.
- When in search window use Ctrl+F12 to toggle on/off. - When in search window use <kbd>Ctrl</kbd>+<kbd>F12</kbd> to toggle on/off.
- Type `Toggle Game Mode` - Type `Toggle Game Mode`
<img src="https://user-images.githubusercontent.com/6903107/144858082-8b654daf-60fb-4ee6-89b2-6183b73510d1.png" width="100%"> <img src="https://user-images.githubusercontent.com/6903107/144858082-8b654daf-60fb-4ee6-89b2-6183b73510d1.png" width="100%">
@ -257,6 +263,7 @@ And you can download [early access version](https://github.com/Flow-Launcher/Pre
<img src="https://user-images.githubusercontent.com/6903107/144858082-8b654daf-60fb-4ee6-89b2-6183b73510d1.png" width="100%"> <img src="https://user-images.githubusercontent.com/6903107/144858082-8b654daf-60fb-4ee6-89b2-6183b73510d1.png" width="100%">
### 🛒 Plugin Store ### 🛒 Plugin Store
<img src="https://user-images.githubusercontent.com/6903107/207155616-d559f0d2-ee95-4072-a7bc-3ffcc2faec27.png" width="700"> <img src="https://user-images.githubusercontent.com/6903107/207155616-d559f0d2-ee95-4072-a7bc-3ffcc2faec27.png" width="700">
- You can view the full plugin list or quickly install a plugin via the Plugin Store menu inside Settings - You can view the full plugin list or quickly install a plugin via the Plugin Store menu inside Settings
@ -267,26 +274,29 @@ And you can download [early access version](https://github.com/Flow-Launcher/Pre
## ⌨️ Hotkeys ## ⌨️ Hotkeys
| Hotkey | Description | | Hotkey | Description |
| ------------------------------------------------------------------ | ---------------------------------------------- | | ------------------------------------------------------------------------- | ----------------------------------------------- |
| <kbd>Alt</kbd>+ <kbd>Space</kbd> | Open search window (default and configurable) | | <kbd>Alt</kbd>+<kbd>Space</kbd> | Open search window (default and configurable) |
| <kbd>Enter</kbd> | Execute | | <kbd>Enter</kbd> | Execute |
| <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>Enter</kbd> | Run as admin | | <kbd>Ctrl</kbd>+<kbd>Enter</kbd> | Open containing folder |
| <kbd></kbd><kbd></kbd> | Scroll up & down | | <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>Enter</kbd> | Run as admin |
| <kbd></kbd><kbd></kbd> | Back to result / Open Context Menu | | <kbd></kbd>/<kbd></kbd>, <kbd>Shift</kbd>+<kbd>Tab</kbd>/<kbd>Tab</kbd> | Previous / Next result |
| <kbd>Ctrl</kbd> +<kbd>O</kbd> , <kbd>Shift</kbd> +<kbd>Enter</kbd> | Open Context Menu | | <kbd></kbd>/<kbd></kbd> | Back to result / Open Context Menu |
| <kbd>Tab</kbd> | Autocomplete | | <kbd>Ctrl</kbd>+<kbd>O</kbd> , <kbd>Shift</kbd>+<kbd>Enter</kbd> | Open Context Menu |
| <kbd>F1</kbd> | Toggle Preview Panel (default and configurable)| | <kbd>Ctrl</kbd>+<kbd>Tab</kbd> | Autocomplete |
| <kbd>Esc</kbd> | Back to results / hide search window | | <kbd>F1</kbd> | Toggle Preview Panel (default and configurable) |
| <kbd>Ctrl</kbd> +<kbd>C</kbd> | Copy the actual folder / file | | <kbd>Esc</kbd> | Back to results / hide search window |
| <kbd>Ctrl</kbd> +<kbd>I</kbd> | Open flow's settings | | <kbd>Ctrl</kbd>+<kbd>C</kbd> | Copy folder / file |
| <kbd>Ctrl</kbd> +<kbd>R</kbd> | Run the current query again (refresh results) | | <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>C</kbd> | Copy folder / file path |
| <kbd>F5</kbd> | Reload all plugin data | | <kbd>Ctrl</kbd>+<kbd>I</kbd> | Open Flow's settings |
| <kbd>Ctrl</kbd> + <kbd>F12</kbd> | Toggle Game Mode when in search window | | <kbd>Ctrl</kbd>+<kbd>R</kbd> | Run the current query again (refresh results) |
| <kbd>Ctrl</kbd> + <kbd>+</kbd>,<kbd>-</kbd> | Quickly change maximum results shown | | <kbd>F5</kbd> | Reload all plugin data |
| <kbd>Ctrl</kbd> + <kbd>[</kbd>,<kbd>]</kbd> | Quickly change search window width | | <kbd>Ctrl</kbd>+<kbd>F12</kbd> | Toggle Game Mode when in search window |
| <kbd>Ctrl</kbd> + <kbd>H</kbd> | Open search history | | <kbd>Ctrl</kbd>+<kbd>+</kbd>,<kbd>-</kbd> | Adjust maximum results shown |
| <kbd>Ctrl</kbd> + <kbd>Backspace</kbd> | Back to previous directory | | <kbd>Ctrl</kbd>+<kbd>[</kbd>,<kbd>]</kbd> | Adjust search window width |
| <kbd>Ctrl</kbd>+<kbd>H</kbd> | Open search history |
| <kbd>Ctrl</kbd>+<kbd>Backspace</kbd> | Back to previous directory |
| <kbd>PageUp</kbd>/<kbd>PageDown</kbd> | Previous / Next Page |
## System Command List ## System Command List
@ -313,8 +323,6 @@ And you can download [early access version](https://github.com/Flow-Launcher/Pre
| Flow Launcher UserData Folder | Open the location where Flow Launcher's settings are stored | | Flow Launcher UserData Folder | Open the location where Flow Launcher's settings are stored |
| Toggle Game Mode | Toggle Game Mode | | Toggle Game Mode | Toggle Game Mode |
### 💁‍♂️ Tips ### 💁‍♂️ Tips
- [More tips](https://flowlauncher.com/docs/#/usage-tips) - [More tips](https://flowlauncher.com/docs/#/usage-tips)