mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-04-29 23:27:51 +02:00
feat: add notes
This commit is contained in:
@@ -517,6 +517,7 @@ mod tests {
|
||||
camoufox_config: None,
|
||||
group_id: None,
|
||||
tags: Vec::new(),
|
||||
note: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,8 @@ use browser_runner::{
|
||||
|
||||
use profile::manager::{
|
||||
check_browser_status, create_browser_profile_new, delete_profile, list_browser_profiles,
|
||||
rename_profile, update_camoufox_config, update_profile_proxy, update_profile_tags,
|
||||
rename_profile, update_camoufox_config, update_profile_note, update_profile_proxy,
|
||||
update_profile_tags,
|
||||
};
|
||||
|
||||
use browser_version_manager::{
|
||||
@@ -710,6 +711,7 @@ pub fn run() {
|
||||
get_browser_release_types,
|
||||
update_profile_proxy,
|
||||
update_profile_tags,
|
||||
update_profile_note,
|
||||
check_browser_status,
|
||||
kill_browser_profile,
|
||||
rename_profile,
|
||||
|
||||
@@ -165,6 +165,7 @@ impl ProfileManager {
|
||||
camoufox_config: None,
|
||||
group_id: group_id.clone(),
|
||||
tags: Vec::new(),
|
||||
note: None,
|
||||
};
|
||||
|
||||
match self
|
||||
@@ -207,6 +208,7 @@ impl ProfileManager {
|
||||
camoufox_config: final_camoufox_config,
|
||||
group_id: group_id.clone(),
|
||||
tags: Vec::new(),
|
||||
note: None,
|
||||
};
|
||||
|
||||
// Save profile info
|
||||
@@ -522,6 +524,35 @@ impl ProfileManager {
|
||||
Ok(profile)
|
||||
}
|
||||
|
||||
pub fn update_profile_note(
|
||||
&self,
|
||||
app_handle: &tauri::AppHandle,
|
||||
profile_id: &str,
|
||||
note: Option<String>,
|
||||
) -> Result<BrowserProfile, Box<dyn std::error::Error>> {
|
||||
// Find the profile by ID
|
||||
let profile_uuid =
|
||||
uuid::Uuid::parse_str(profile_id).map_err(|_| format!("Invalid profile ID: {profile_id}"))?;
|
||||
let profiles = self.list_profiles()?;
|
||||
let mut profile = profiles
|
||||
.into_iter()
|
||||
.find(|p| p.id == profile_uuid)
|
||||
.ok_or_else(|| format!("Profile with ID '{profile_id}' not found"))?;
|
||||
|
||||
// Update note (trim whitespace, set to None if empty)
|
||||
profile.note = note.map(|n| n.trim().to_string()).filter(|n| !n.is_empty());
|
||||
|
||||
// Save profile
|
||||
self.save_profile(&profile)?;
|
||||
|
||||
// Emit profile note update event
|
||||
if let Err(e) = app_handle.emit("profiles-changed", ()) {
|
||||
log::warn!("Warning: Failed to emit profiles-changed event: {e}");
|
||||
}
|
||||
|
||||
Ok(profile)
|
||||
}
|
||||
|
||||
pub fn delete_multiple_profiles(
|
||||
&self,
|
||||
app_handle: &tauri::AppHandle,
|
||||
@@ -1445,6 +1476,18 @@ pub fn update_profile_tags(
|
||||
.map_err(|e| format!("Failed to update profile tags: {e}"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn update_profile_note(
|
||||
app_handle: tauri::AppHandle,
|
||||
profile_id: String,
|
||||
note: Option<String>,
|
||||
) -> Result<BrowserProfile, String> {
|
||||
let profile_manager = ProfileManager::instance();
|
||||
profile_manager
|
||||
.update_profile_note(&app_handle, &profile_id, note)
|
||||
.map_err(|e| format!("Failed to update profile note: {e}"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn check_browser_status(
|
||||
app_handle: tauri::AppHandle,
|
||||
|
||||
@@ -22,6 +22,8 @@ pub struct BrowserProfile {
|
||||
pub group_id: Option<String>, // Reference to profile group
|
||||
#[serde(default)]
|
||||
pub tags: Vec<String>, // Free-form tags
|
||||
#[serde(default)]
|
||||
pub note: Option<String>, // User note
|
||||
}
|
||||
|
||||
pub fn default_release_type() -> String {
|
||||
|
||||
@@ -561,6 +561,7 @@ impl ProfileImporter {
|
||||
camoufox_config: None,
|
||||
group_id: None,
|
||||
tags: Vec::new(),
|
||||
note: None,
|
||||
};
|
||||
|
||||
// Save the profile metadata
|
||||
|
||||
+2
-2
@@ -676,8 +676,8 @@ export default function Home() {
|
||||
// Search in profile name
|
||||
if (profile.name.toLowerCase().includes(query)) return true;
|
||||
|
||||
// Search in browser name
|
||||
if (profile.browser.toLowerCase().includes(query)) return true;
|
||||
// Search in note
|
||||
if (profile.note?.toLowerCase().includes(query)) return true;
|
||||
|
||||
// Search in tags
|
||||
if (profile.tags?.some((tag) => tag.toLowerCase().includes(query)))
|
||||
|
||||
@@ -74,7 +74,6 @@ import {
|
||||
DataTableActionBarAction,
|
||||
DataTableActionBarSelection,
|
||||
} from "./data-table-action-bar";
|
||||
import { LoadingButton } from "./loading-button";
|
||||
import MultipleSelector, { type Option } from "./multiple-selector";
|
||||
import { ProxyCheckButton } from "./proxy-check-button";
|
||||
import { Input } from "./ui/input";
|
||||
@@ -103,6 +102,14 @@ type TableMeta = {
|
||||
React.SetStateAction<Record<string, string[]>>
|
||||
>;
|
||||
|
||||
// Note editor state
|
||||
noteOverrides: Record<string, string | null>;
|
||||
openNoteEditorFor: string | null;
|
||||
setOpenNoteEditorFor: React.Dispatch<React.SetStateAction<string | null>>;
|
||||
setNoteOverrides: React.Dispatch<
|
||||
React.SetStateAction<Record<string, string | null>>
|
||||
>;
|
||||
|
||||
// Proxy selector state
|
||||
openProxySelectorFor: string | null;
|
||||
setOpenProxySelectorFor: React.Dispatch<React.SetStateAction<string | null>>;
|
||||
@@ -402,6 +409,243 @@ const TagsCell = React.memo<{
|
||||
|
||||
TagsCell.displayName = "TagsCell";
|
||||
|
||||
const NonHoverableTooltip = React.memo<{
|
||||
children: React.ReactNode;
|
||||
content: React.ReactNode;
|
||||
sideOffset?: number;
|
||||
alignOffset?: number;
|
||||
horizontalOffset?: number;
|
||||
}>(
|
||||
({
|
||||
children,
|
||||
content,
|
||||
sideOffset = 4,
|
||||
alignOffset = 0,
|
||||
horizontalOffset = 0,
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
|
||||
return (
|
||||
<Tooltip open={isOpen} onOpenChange={setIsOpen}>
|
||||
<TooltipTrigger
|
||||
asChild
|
||||
onMouseEnter={() => setIsOpen(true)}
|
||||
onMouseLeave={() => setIsOpen(false)}
|
||||
>
|
||||
{children}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
sideOffset={sideOffset}
|
||||
alignOffset={alignOffset}
|
||||
arrowOffset={horizontalOffset}
|
||||
onPointerEnter={(e) => e.preventDefault()}
|
||||
onPointerLeave={() => setIsOpen(false)}
|
||||
className="pointer-events-none"
|
||||
style={
|
||||
horizontalOffset !== 0
|
||||
? { transform: `translateX(${horizontalOffset}px)` }
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{content}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
NonHoverableTooltip.displayName = "NonHoverableTooltip";
|
||||
|
||||
const NoteCell = React.memo<{
|
||||
profile: BrowserProfile;
|
||||
isDisabled: boolean;
|
||||
noteOverrides: Record<string, string | null>;
|
||||
openNoteEditorFor: string | null;
|
||||
setOpenNoteEditorFor: React.Dispatch<React.SetStateAction<string | null>>;
|
||||
setNoteOverrides: React.Dispatch<
|
||||
React.SetStateAction<Record<string, string | null>>
|
||||
>;
|
||||
}>(
|
||||
({
|
||||
profile,
|
||||
isDisabled,
|
||||
noteOverrides,
|
||||
openNoteEditorFor,
|
||||
setOpenNoteEditorFor,
|
||||
setNoteOverrides,
|
||||
}) => {
|
||||
const effectiveNote: string | null = Object.hasOwn(
|
||||
noteOverrides,
|
||||
profile.id,
|
||||
)
|
||||
? noteOverrides[profile.id]
|
||||
: (profile.note ?? null);
|
||||
|
||||
const onNoteChange = React.useCallback(
|
||||
async (newNote: string | null) => {
|
||||
const trimmedNote = newNote?.trim() || null;
|
||||
setNoteOverrides((prev) => ({ ...prev, [profile.id]: trimmedNote }));
|
||||
try {
|
||||
await invoke<BrowserProfile>("update_profile_note", {
|
||||
profileId: profile.id,
|
||||
note: trimmedNote,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to update note:", error);
|
||||
}
|
||||
},
|
||||
[profile.id, setNoteOverrides],
|
||||
);
|
||||
|
||||
const editorRef = React.useRef<HTMLDivElement | null>(null);
|
||||
const textareaRef = React.useRef<HTMLTextAreaElement | null>(null);
|
||||
const [noteValue, setNoteValue] = React.useState(effectiveNote || "");
|
||||
|
||||
// Update local state when effective note changes (from outside)
|
||||
React.useEffect(() => {
|
||||
if (openNoteEditorFor !== profile.id) {
|
||||
setNoteValue(effectiveNote || "");
|
||||
}
|
||||
}, [effectiveNote, openNoteEditorFor, profile.id]);
|
||||
|
||||
// Auto-resize textarea on open
|
||||
React.useEffect(() => {
|
||||
if (openNoteEditorFor === profile.id && textareaRef.current) {
|
||||
const textarea = textareaRef.current;
|
||||
textarea.style.height = "auto";
|
||||
textarea.style.height = `${Math.min(textarea.scrollHeight, 200)}px`;
|
||||
}
|
||||
}, [openNoteEditorFor, profile.id]);
|
||||
|
||||
const handleTextareaChange = React.useCallback(
|
||||
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const newValue = e.target.value;
|
||||
setNoteValue(newValue);
|
||||
// Auto-resize
|
||||
const textarea = e.target;
|
||||
textarea.style.height = "auto";
|
||||
textarea.style.height = `${Math.min(textarea.scrollHeight, 200)}px`;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (openNoteEditorFor !== profile.id) return;
|
||||
const handleClick = (e: MouseEvent) => {
|
||||
const target = e.target as Node | null;
|
||||
if (
|
||||
editorRef.current &&
|
||||
target &&
|
||||
!editorRef.current.contains(target)
|
||||
) {
|
||||
const currentValue = textareaRef.current?.value || "";
|
||||
void onNoteChange(currentValue);
|
||||
setOpenNoteEditorFor(null);
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", handleClick);
|
||||
return () => document.removeEventListener("mousedown", handleClick);
|
||||
}, [openNoteEditorFor, profile.id, setOpenNoteEditorFor, onNoteChange]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (openNoteEditorFor === profile.id && textareaRef.current) {
|
||||
textareaRef.current.focus();
|
||||
// Move cursor to end
|
||||
const len = textareaRef.current.value.length;
|
||||
textareaRef.current.setSelectionRange(len, len);
|
||||
}
|
||||
}, [openNoteEditorFor, profile.id]);
|
||||
|
||||
const displayNote = effectiveNote || "";
|
||||
const trimmedNote =
|
||||
displayNote.length > 12 ? `${displayNote.slice(0, 12)}...` : displayNote;
|
||||
const showTooltip = displayNote.length > 12 || displayNote.length > 0;
|
||||
|
||||
if (openNoteEditorFor !== profile.id) {
|
||||
return (
|
||||
<div className="w-24 min-h-6">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"flex items-start px-2 py-1 min-h-6 w-full bg-transparent rounded border-none text-left",
|
||||
isDisabled
|
||||
? "opacity-60 cursor-not-allowed"
|
||||
: "cursor-pointer hover:bg-accent/50",
|
||||
)}
|
||||
onClick={() => {
|
||||
if (!isDisabled) {
|
||||
setNoteValue(effectiveNote || "");
|
||||
setOpenNoteEditorFor(profile.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"text-sm wrap-break-word",
|
||||
!effectiveNote && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{effectiveNote ? trimmedNote : "No Note"}
|
||||
</span>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
{showTooltip && (
|
||||
<TooltipContent className="max-w-[320px]">
|
||||
<p className="whitespace-pre-wrap wrap-break-word">
|
||||
{effectiveNote || "No Note"}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"w-24 relative",
|
||||
isDisabled && "opacity-60 pointer-events-none",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
ref={editorRef}
|
||||
className="absolute -top-[15px] -left-px z-50 w-60 min-h-6 bg-popover rounded-md shadow-md border"
|
||||
>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
value={noteValue}
|
||||
onChange={handleTextareaChange}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Escape") {
|
||||
setNoteValue(effectiveNote || "");
|
||||
setOpenNoteEditorFor(null);
|
||||
} else if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
||||
void onNoteChange(noteValue);
|
||||
setOpenNoteEditorFor(null);
|
||||
}
|
||||
}}
|
||||
onBlur={() => {
|
||||
void onNoteChange(noteValue);
|
||||
setOpenNoteEditorFor(null);
|
||||
}}
|
||||
placeholder="Add a note..."
|
||||
className="w-full min-h-6 max-h-[200px] px-2 py-1 text-sm bg-transparent border-0 resize-none focus:outline-none focus:ring-0"
|
||||
style={{
|
||||
overflow: "auto",
|
||||
}}
|
||||
rows={1}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
NoteCell.displayName = "NoteCell";
|
||||
|
||||
interface ProfilesDataTableProps {
|
||||
profiles: BrowserProfile[];
|
||||
onLaunchProfile: (profile: BrowserProfile) => void | Promise<void>;
|
||||
@@ -526,6 +770,12 @@ export function ProfilesDataTable({
|
||||
const [proxyCheckResults, setProxyCheckResults] = React.useState<
|
||||
Record<string, ProxyCheckResult>
|
||||
>({});
|
||||
const [noteOverrides, setNoteOverrides] = React.useState<
|
||||
Record<string, string | null>
|
||||
>({});
|
||||
const [openNoteEditorFor, setOpenNoteEditorFor] = React.useState<
|
||||
string | null
|
||||
>(null);
|
||||
|
||||
// Load cached check results for proxies
|
||||
React.useEffect(() => {
|
||||
@@ -892,6 +1142,12 @@ export function ProfilesDataTable({
|
||||
setOpenTagsEditorFor,
|
||||
setTagsOverrides,
|
||||
|
||||
// Note editor state
|
||||
noteOverrides,
|
||||
openNoteEditorFor,
|
||||
setOpenNoteEditorFor,
|
||||
setNoteOverrides,
|
||||
|
||||
// Proxy selector state
|
||||
openProxySelectorFor,
|
||||
setOpenProxySelectorFor,
|
||||
@@ -940,6 +1196,8 @@ export function ProfilesDataTable({
|
||||
tagsOverrides,
|
||||
allTags,
|
||||
openTagsEditorFor,
|
||||
noteOverrides,
|
||||
openNoteEditorFor,
|
||||
openProxySelectorFor,
|
||||
proxyOverrides,
|
||||
storedProxies,
|
||||
@@ -1021,37 +1279,51 @@ export function ProfilesDataTable({
|
||||
);
|
||||
}
|
||||
|
||||
const browserName = getBrowserDisplayName(browser);
|
||||
|
||||
if (meta.showCheckboxes || isSelected) {
|
||||
return (
|
||||
<span className="flex justify-center items-center w-4 h-4">
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onCheckedChange={(value) =>
|
||||
meta.handleCheckboxChange(profile.id, !!value)
|
||||
}
|
||||
aria-label="Select row"
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
</span>
|
||||
<NonHoverableTooltip
|
||||
content={<p>{browserName}</p>}
|
||||
sideOffset={4}
|
||||
horizontalOffset={8}
|
||||
>
|
||||
<span className="flex justify-center items-center w-4 h-4">
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onCheckedChange={(value) =>
|
||||
meta.handleCheckboxChange(profile.id, !!value)
|
||||
}
|
||||
aria-label="Select row"
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
</span>
|
||||
</NonHoverableTooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="flex relative justify-center items-center w-4 h-4">
|
||||
<button
|
||||
type="button"
|
||||
className="flex justify-center items-center p-0 border-none cursor-pointer"
|
||||
onClick={() => meta.handleIconClick(profile.id)}
|
||||
aria-label="Select profile"
|
||||
>
|
||||
<span className="w-4 h-4 group">
|
||||
{IconComponent && (
|
||||
<IconComponent className="w-4 h-4 group-hover:hidden" />
|
||||
)}
|
||||
<span className="peer border-input dark:bg-input/30 dark:data-[state=checked]:bg-primary size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none w-4 h-4 hidden group-hover:block pointer-events-none items-center justify-center duration-200" />
|
||||
</span>
|
||||
</button>
|
||||
</span>
|
||||
<NonHoverableTooltip
|
||||
content={<p>{browserName}</p>}
|
||||
sideOffset={4}
|
||||
horizontalOffset={8}
|
||||
>
|
||||
<span className="flex relative justify-center items-center w-4 h-4">
|
||||
<button
|
||||
type="button"
|
||||
className="flex justify-center items-center p-0 border-none cursor-pointer"
|
||||
onClick={() => meta.handleIconClick(profile.id)}
|
||||
aria-label="Select profile"
|
||||
>
|
||||
<span className="w-4 h-4 group">
|
||||
{IconComponent && (
|
||||
<IconComponent className="w-4 h-4 group-hover:hidden" />
|
||||
)}
|
||||
<span className="peer border-input dark:bg-input/30 dark:data-[state=checked]:bg-primary size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none w-4 h-4 hidden group-hover:block pointer-events-none items-center justify-center duration-200" />
|
||||
</span>
|
||||
</button>
|
||||
</span>
|
||||
</NonHoverableTooltip>
|
||||
);
|
||||
},
|
||||
enableSorting: false,
|
||||
@@ -1172,11 +1444,6 @@ export function ProfilesDataTable({
|
||||
const isEditing = meta.profileToRename?.id === profile.id;
|
||||
|
||||
if (isEditing) {
|
||||
const isSaveDisabled =
|
||||
meta.isRenamingSaving ||
|
||||
meta.newProfileName.trim().length === 0 ||
|
||||
meta.newProfileName.trim() === profile.name;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={renameContainerRef}
|
||||
@@ -1190,7 +1457,9 @@ export function ProfilesDataTable({
|
||||
if (meta.renameError) meta.setRenameError(null);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
if (e.key === "Enter" && !(e.metaKey || e.ctrlKey)) {
|
||||
void meta.handleRename();
|
||||
} else if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
||||
void meta.handleRename();
|
||||
} else if (e.key === "Escape") {
|
||||
meta.setProfileToRename(null);
|
||||
@@ -1198,20 +1467,20 @@ export function ProfilesDataTable({
|
||||
meta.setRenameError(null);
|
||||
}
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (
|
||||
meta.newProfileName.trim().length > 0 &&
|
||||
meta.newProfileName.trim() !== profile.name
|
||||
) {
|
||||
void meta.handleRename();
|
||||
} else {
|
||||
meta.setProfileToRename(null);
|
||||
meta.setNewProfileName("");
|
||||
meta.setRenameError(null);
|
||||
}
|
||||
}}
|
||||
className="w-30 h-6 px-2 py-1 text-sm font-medium leading-none border-0 shadow-none focus-visible:ring-0"
|
||||
/>
|
||||
<div className="flex absolute right-0 top-full z-50 gap-1 translate-y-[30%] opacity-100 bg-black rounded-md">
|
||||
<LoadingButton
|
||||
isLoading={meta.isRenamingSaving}
|
||||
size="sm"
|
||||
variant="default"
|
||||
disabled={isSaveDisabled}
|
||||
className="cursor-pointer [[disabled]]:bg-primary/80"
|
||||
onClick={() => void meta.handleRename()}
|
||||
>
|
||||
Save
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1295,51 +1564,28 @@ export function ProfilesDataTable({
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "browser",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
className="justify-start p-0 h-auto font-semibold text-left cursor-pointer"
|
||||
>
|
||||
Browser
|
||||
{column.getIsSorted() === "asc" ? (
|
||||
<LuChevronUp className="ml-2 w-4 h-4" />
|
||||
) : column.getIsSorted() === "desc" ? (
|
||||
<LuChevronDown className="ml-2 w-4 h-4" />
|
||||
) : null}
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const browser: string = row.getValue("browser");
|
||||
const name = getBrowserDisplayName(browser);
|
||||
if (name.length < 14) {
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<span>{name}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
id: "note",
|
||||
header: "Note",
|
||||
cell: ({ row, table }) => {
|
||||
const meta = table.options.meta as TableMeta;
|
||||
const profile = row.original;
|
||||
const isRunning =
|
||||
meta.isClient && meta.runningProfiles.has(profile.id);
|
||||
const isLaunching = meta.launchingProfiles.has(profile.id);
|
||||
const isStopping = meta.stoppingProfiles.has(profile.id);
|
||||
const isBrowserUpdating = meta.isUpdating(profile.browser);
|
||||
const isDisabled =
|
||||
isRunning || isLaunching || isStopping || isBrowserUpdating;
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span>{trimName(name, 14)}</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{name}</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
enableSorting: true,
|
||||
sortingFn: (rowA, rowB, columnId) => {
|
||||
const browserA: string = rowA.getValue(columnId);
|
||||
const browserB: string = rowB.getValue(columnId);
|
||||
return getBrowserDisplayName(browserA).localeCompare(
|
||||
getBrowserDisplayName(browserB),
|
||||
<NoteCell
|
||||
profile={profile}
|
||||
isDisabled={isDisabled}
|
||||
noteOverrides={meta.noteOverrides || {}}
|
||||
openNoteEditorFor={meta.openNoteEditorFor || null}
|
||||
setOpenNoteEditorFor={meta.setOpenNoteEditorFor}
|
||||
setNoteOverrides={meta.setNoteOverrides}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -37,14 +37,19 @@ function TooltipTrigger({
|
||||
function TooltipContent({
|
||||
className,
|
||||
sideOffset = 0,
|
||||
alignOffset,
|
||||
arrowOffset = 0,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Content> & {
|
||||
arrowOffset?: number;
|
||||
}) {
|
||||
return (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
data-slot="tooltip-content"
|
||||
sideOffset={sideOffset}
|
||||
alignOffset={alignOffset}
|
||||
className={cn(
|
||||
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-[50000] w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
|
||||
className,
|
||||
@@ -52,7 +57,14 @@ function TooltipContent({
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-[50000] size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
||||
<TooltipPrimitive.Arrow
|
||||
className="fill-primary z-[50000]"
|
||||
style={
|
||||
arrowOffset !== 0
|
||||
? { transform: `translateX(${-arrowOffset}px)` }
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</TooltipPrimitive.Content>
|
||||
</TooltipPrimitive.Portal>
|
||||
);
|
||||
|
||||
+2
-1
@@ -7,7 +7,7 @@ export interface ProxySettings {
|
||||
}
|
||||
|
||||
export interface TableSortingSettings {
|
||||
column: string; // "name", "browser", "status"
|
||||
column: string; // "name", "note", "status"
|
||||
direction: string; // "asc" or "desc"
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ export interface BrowserProfile {
|
||||
camoufox_config?: CamoufoxConfig; // Camoufox configuration
|
||||
group_id?: string; // Reference to profile group
|
||||
tags?: string[];
|
||||
note?: string; // User note
|
||||
}
|
||||
|
||||
export interface ProxyCheckResult {
|
||||
|
||||
Reference in New Issue
Block a user