support open browser
This commit is contained in:
119
src-tauri/src/browsers.rs
Normal file
119
src-tauri/src/browsers.rs
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
use std::{env, path::PathBuf};
|
||||||
|
|
||||||
|
use crate::models::BrowserDefinition;
|
||||||
|
|
||||||
|
pub fn browser_definitions() -> Vec<BrowserDefinition> {
|
||||||
|
vec![
|
||||||
|
BrowserDefinition {
|
||||||
|
id: "chrome",
|
||||||
|
name: "Google Chrome",
|
||||||
|
local_app_data_segments: &["Google", "Chrome", "User Data"],
|
||||||
|
executable_candidates: &[
|
||||||
|
ExecutableCandidate::ProgramFiles(&[
|
||||||
|
"Google",
|
||||||
|
"Chrome",
|
||||||
|
"Application",
|
||||||
|
"chrome.exe",
|
||||||
|
]),
|
||||||
|
ExecutableCandidate::ProgramFilesX86(&[
|
||||||
|
"Google",
|
||||||
|
"Chrome",
|
||||||
|
"Application",
|
||||||
|
"chrome.exe",
|
||||||
|
]),
|
||||||
|
ExecutableCandidate::LocalAppData(&[
|
||||||
|
"Google",
|
||||||
|
"Chrome",
|
||||||
|
"Application",
|
||||||
|
"chrome.exe",
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
BrowserDefinition {
|
||||||
|
id: "edge",
|
||||||
|
name: "Microsoft Edge",
|
||||||
|
local_app_data_segments: &["Microsoft", "Edge", "User Data"],
|
||||||
|
executable_candidates: &[
|
||||||
|
ExecutableCandidate::ProgramFiles(&[
|
||||||
|
"Microsoft",
|
||||||
|
"Edge",
|
||||||
|
"Application",
|
||||||
|
"msedge.exe",
|
||||||
|
]),
|
||||||
|
ExecutableCandidate::ProgramFilesX86(&[
|
||||||
|
"Microsoft",
|
||||||
|
"Edge",
|
||||||
|
"Application",
|
||||||
|
"msedge.exe",
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
BrowserDefinition {
|
||||||
|
id: "brave",
|
||||||
|
name: "Brave",
|
||||||
|
local_app_data_segments: &["BraveSoftware", "Brave-Browser", "User Data"],
|
||||||
|
executable_candidates: &[
|
||||||
|
ExecutableCandidate::ProgramFiles(&[
|
||||||
|
"BraveSoftware",
|
||||||
|
"Brave-Browser",
|
||||||
|
"Application",
|
||||||
|
"brave.exe",
|
||||||
|
]),
|
||||||
|
ExecutableCandidate::ProgramFilesX86(&[
|
||||||
|
"BraveSoftware",
|
||||||
|
"Brave-Browser",
|
||||||
|
"Application",
|
||||||
|
"brave.exe",
|
||||||
|
]),
|
||||||
|
ExecutableCandidate::LocalAppData(&[
|
||||||
|
"BraveSoftware",
|
||||||
|
"Brave-Browser",
|
||||||
|
"Application",
|
||||||
|
"brave.exe",
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn browser_definition_by_id(browser_id: &str) -> Option<BrowserDefinition> {
|
||||||
|
browser_definitions()
|
||||||
|
.into_iter()
|
||||||
|
.find(|definition| definition.id == browser_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resolve_browser_executable(browser_id: &str) -> Option<PathBuf> {
|
||||||
|
let definition = browser_definition_by_id(browser_id)?;
|
||||||
|
definition
|
||||||
|
.executable_candidates
|
||||||
|
.iter()
|
||||||
|
.find_map(resolve_executable_candidate)
|
||||||
|
.filter(|path| path.is_file())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_executable_candidate(candidate: &ExecutableCandidate) -> Option<PathBuf> {
|
||||||
|
match candidate {
|
||||||
|
ExecutableCandidate::ProgramFiles(segments) => env::var_os("ProgramFiles")
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.map(|root| join_segments(root, segments)),
|
||||||
|
ExecutableCandidate::ProgramFilesX86(segments) => env::var_os("ProgramFiles(x86)")
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.map(|root| join_segments(root, segments)),
|
||||||
|
ExecutableCandidate::LocalAppData(segments) => env::var_os("LOCALAPPDATA")
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.map(|root| join_segments(root, segments)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn join_segments(mut root: PathBuf, segments: &[&str]) -> PathBuf {
|
||||||
|
for segment in segments {
|
||||||
|
root.push(segment);
|
||||||
|
}
|
||||||
|
root
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum ExecutableCandidate {
|
||||||
|
ProgramFiles(&'static [&'static str]),
|
||||||
|
ProgramFilesX86(&'static [&'static str]),
|
||||||
|
LocalAppData(&'static [&'static str]),
|
||||||
|
}
|
||||||
@@ -1,6 +1,64 @@
|
|||||||
use crate::{models::ScanResponse, scanner};
|
use std::{path::PathBuf, process::Command};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
browsers::{browser_definition_by_id, resolve_browser_executable},
|
||||||
|
models::ScanResponse,
|
||||||
|
scanner,
|
||||||
|
utils::local_app_data_dir,
|
||||||
|
};
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn scan_browsers() -> Result<ScanResponse, String> {
|
pub fn scan_browsers() -> Result<ScanResponse, String> {
|
||||||
scanner::scan_browsers()
|
scanner::scan_browsers()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn open_browser_profile(browser_id: String, profile_id: String) -> Result<(), String> {
|
||||||
|
let definition = browser_definition_by_id(&browser_id)
|
||||||
|
.ok_or_else(|| format!("Unsupported browser: {browser_id}"))?;
|
||||||
|
let executable_path = resolve_browser_executable(&browser_id)
|
||||||
|
.ok_or_else(|| format!("Unable to locate executable for browser: {browser_id}"))?;
|
||||||
|
let local_app_data = local_app_data_dir().ok_or_else(|| {
|
||||||
|
"Unable to resolve the LOCALAPPDATA directory for the current user.".to_string()
|
||||||
|
})?;
|
||||||
|
let user_data_dir = definition
|
||||||
|
.local_app_data_segments
|
||||||
|
.iter()
|
||||||
|
.fold(local_app_data, |path, segment| path.join(segment));
|
||||||
|
let profile_directory = user_data_dir.join(&profile_id);
|
||||||
|
|
||||||
|
if !user_data_dir.is_dir() {
|
||||||
|
return Err(format!(
|
||||||
|
"User data directory does not exist: {}",
|
||||||
|
user_data_dir.display()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !profile_directory.is_dir() {
|
||||||
|
return Err(format!(
|
||||||
|
"Profile directory does not exist: {}",
|
||||||
|
profile_directory.display()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
spawn_browser_process(executable_path, user_data_dir, profile_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_browser_process(
|
||||||
|
executable_path: PathBuf,
|
||||||
|
user_data_dir: PathBuf,
|
||||||
|
profile_id: String,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
Command::new(&executable_path)
|
||||||
|
.arg(format!("--user-data-dir={}", user_data_dir.display()))
|
||||||
|
.arg(format!("--profile-directory={profile_id}"))
|
||||||
|
.arg("https://www.google.com")
|
||||||
|
.spawn()
|
||||||
|
.map(|_| ())
|
||||||
|
.map_err(|error| {
|
||||||
|
format!(
|
||||||
|
"Failed to open browser profile with executable {}: {error}",
|
||||||
|
executable_path.display()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
mod browsers;
|
||||||
mod commands;
|
mod commands;
|
||||||
mod models;
|
mod models;
|
||||||
mod scanner;
|
mod scanner;
|
||||||
@@ -7,7 +8,10 @@ mod utils;
|
|||||||
pub fn run() {
|
pub fn run() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
.invoke_handler(tauri::generate_handler![commands::scan_browsers])
|
.invoke_handler(tauri::generate_handler![
|
||||||
|
commands::scan_browsers,
|
||||||
|
commands::open_browser_profile
|
||||||
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ pub struct BrowserDefinition {
|
|||||||
pub id: &'static str,
|
pub id: &'static str,
|
||||||
pub name: &'static str,
|
pub name: &'static str,
|
||||||
pub local_app_data_segments: &'static [&'static str],
|
pub local_app_data_segments: &'static [&'static str],
|
||||||
|
pub executable_candidates: &'static [crate::browsers::ExecutableCandidate],
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TempExtension {
|
pub struct TempExtension {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use std::{
|
|||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
browsers::browser_definitions,
|
||||||
models::{
|
models::{
|
||||||
BookmarkSummary, BrowserDefinition, BrowserStats, BrowserView, ExtensionSummary,
|
BookmarkSummary, BrowserDefinition, BrowserStats, BrowserView, ExtensionSummary,
|
||||||
ProfileSummary, ScanResponse, TempBookmark, TempExtension,
|
ProfileSummary, ScanResponse, TempBookmark, TempExtension,
|
||||||
@@ -30,26 +31,6 @@ pub fn scan_browsers() -> Result<ScanResponse, String> {
|
|||||||
Ok(ScanResponse { browsers })
|
Ok(ScanResponse { browsers })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn browser_definitions() -> Vec<BrowserDefinition> {
|
|
||||||
vec![
|
|
||||||
BrowserDefinition {
|
|
||||||
id: "chrome",
|
|
||||||
name: "Google Chrome",
|
|
||||||
local_app_data_segments: &["Google", "Chrome", "User Data"],
|
|
||||||
},
|
|
||||||
BrowserDefinition {
|
|
||||||
id: "edge",
|
|
||||||
name: "Microsoft Edge",
|
|
||||||
local_app_data_segments: &["Microsoft", "Edge", "User Data"],
|
|
||||||
},
|
|
||||||
BrowserDefinition {
|
|
||||||
id: "brave",
|
|
||||||
name: "Brave",
|
|
||||||
local_app_data_segments: &["BraveSoftware", "Brave-Browser", "User Data"],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn scan_browser(local_app_data: &Path, definition: BrowserDefinition) -> Option<BrowserView> {
|
fn scan_browser(local_app_data: &Path, definition: BrowserDefinition) -> Option<BrowserView> {
|
||||||
let root = definition
|
let root = definition
|
||||||
.local_app_data_segments
|
.local_app_data_segments
|
||||||
|
|||||||
23
src/App.vue
23
src/App.vue
@@ -14,7 +14,10 @@ const {
|
|||||||
extensionMonogram,
|
extensionMonogram,
|
||||||
extensionProfilesExpanded,
|
extensionProfilesExpanded,
|
||||||
extensionSortKey,
|
extensionSortKey,
|
||||||
|
isOpeningProfile,
|
||||||
loading,
|
loading,
|
||||||
|
openProfileError,
|
||||||
|
openBrowserProfile,
|
||||||
profileSortKey,
|
profileSortKey,
|
||||||
scanBrowsers,
|
scanBrowsers,
|
||||||
sectionCount,
|
sectionCount,
|
||||||
@@ -109,6 +112,10 @@ const {
|
|||||||
|
|
||||||
<div class="content-scroll-area">
|
<div class="content-scroll-area">
|
||||||
<section v-if="activeSection === 'profiles'" class="content-section">
|
<section v-if="activeSection === 'profiles'" class="content-section">
|
||||||
|
<div v-if="openProfileError" class="inline-error">
|
||||||
|
{{ openProfileError }}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="sort-bar">
|
<div class="sort-bar">
|
||||||
<SortDropdown
|
<SortDropdown
|
||||||
v-model="profileSortKey"
|
v-model="profileSortKey"
|
||||||
@@ -138,7 +145,21 @@ const {
|
|||||||
<div class="profile-body">
|
<div class="profile-body">
|
||||||
<div class="profile-topline">
|
<div class="profile-topline">
|
||||||
<h4>{{ profile.name }}</h4>
|
<h4>{{ profile.name }}</h4>
|
||||||
<span class="badge neutral">{{ profile.id }}</span>
|
<div class="profile-actions">
|
||||||
|
<button
|
||||||
|
class="card-action-button"
|
||||||
|
:disabled="isOpeningProfile(currentBrowser.browserId, profile.id)"
|
||||||
|
type="button"
|
||||||
|
@click="openBrowserProfile(currentBrowser.browserId, profile.id)"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
isOpeningProfile(currentBrowser.browserId, profile.id)
|
||||||
|
? "Opening..."
|
||||||
|
: "Open"
|
||||||
|
}}
|
||||||
|
</button>
|
||||||
|
<span class="badge neutral">{{ profile.id }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="profile-email">{{ profile.email || "No email found" }}</p>
|
<p class="profile-email">{{ profile.email || "No email found" }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import type {
|
|||||||
export function useBrowserAssistant() {
|
export function useBrowserAssistant() {
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const error = ref("");
|
const error = ref("");
|
||||||
|
const openProfileError = ref("");
|
||||||
|
const openingProfileKey = ref("");
|
||||||
const response = ref<ScanResponse>({ browsers: [] });
|
const response = ref<ScanResponse>({ browsers: [] });
|
||||||
const selectedBrowserId = ref("");
|
const selectedBrowserId = ref("");
|
||||||
const activeSection = ref<ActiveSection>("profiles");
|
const activeSection = ref<ActiveSection>("profiles");
|
||||||
@@ -81,6 +83,30 @@ export function useBrowserAssistant() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function openBrowserProfile(browserId: string, profileId: string) {
|
||||||
|
const profileKey = `${browserId}:${profileId}`;
|
||||||
|
openingProfileKey.value = profileKey;
|
||||||
|
openProfileError.value = "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
await invoke("open_browser_profile", {
|
||||||
|
browserId,
|
||||||
|
profileId,
|
||||||
|
});
|
||||||
|
} catch (openError) {
|
||||||
|
openProfileError.value =
|
||||||
|
openError instanceof Error
|
||||||
|
? openError.message
|
||||||
|
: "Failed to open the selected browser profile.";
|
||||||
|
} finally {
|
||||||
|
openingProfileKey.value = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isOpeningProfile(browserId: string, profileId: string) {
|
||||||
|
return openingProfileKey.value === `${browserId}:${profileId}`;
|
||||||
|
}
|
||||||
|
|
||||||
function browserMonogram(browserId: string) {
|
function browserMonogram(browserId: string) {
|
||||||
if (browserId === "chrome") return "CH";
|
if (browserId === "chrome") return "CH";
|
||||||
if (browserId === "edge") return "ED";
|
if (browserId === "edge") return "ED";
|
||||||
@@ -144,6 +170,9 @@ export function useBrowserAssistant() {
|
|||||||
extensionProfilesExpanded,
|
extensionProfilesExpanded,
|
||||||
extensionSortKey,
|
extensionSortKey,
|
||||||
loading,
|
loading,
|
||||||
|
isOpeningProfile,
|
||||||
|
openProfileError,
|
||||||
|
openBrowserProfile,
|
||||||
profileSortKey,
|
profileSortKey,
|
||||||
scanBrowsers,
|
scanBrowsers,
|
||||||
sectionCount,
|
sectionCount,
|
||||||
|
|||||||
@@ -289,6 +289,16 @@ button {
|
|||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.inline-error {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid rgba(239, 68, 68, 0.18);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgba(254, 242, 242, 0.92);
|
||||||
|
color: #b42318;
|
||||||
|
font-size: 0.86rem;
|
||||||
|
}
|
||||||
|
|
||||||
.sort-bar {
|
.sort-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
@@ -524,6 +534,12 @@ button {
|
|||||||
line-height: 1.35;
|
line-height: 1.35;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.profile-actions {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.profile-email,
|
.profile-email,
|
||||||
.meta-line,
|
.meta-line,
|
||||||
.bookmark-url {
|
.bookmark-url {
|
||||||
@@ -561,6 +577,36 @@ button {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-action-button {
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(241, 245, 249, 0.92));
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.24);
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
border-color 160ms ease,
|
||||||
|
background 160ms ease,
|
||||||
|
box-shadow 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-action-button:hover {
|
||||||
|
border-color: rgba(100, 116, 139, 0.36);
|
||||||
|
background: linear-gradient(180deg, rgba(255, 255, 255, 1), rgba(226, 232, 240, 0.9));
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-action-button:disabled {
|
||||||
|
cursor: default;
|
||||||
|
opacity: 0.7;
|
||||||
|
border-color: rgba(148, 163, 184, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-action-button:active {
|
||||||
|
box-shadow: inset 0 2px 4px rgba(148, 163, 184, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
.disclosure-panel {
|
.disclosure-panel {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|||||||
Reference in New Issue
Block a user