This page covers the React/TypeScript components that make up the Profiles page: how profiles are listed, imported, reordered, activated, deleted in batch, and edited. It specifically covers the view-layer behavior of ProfilePage, ProfileItem, and ProfileMore.
For the backend data structures and the enhancement pipeline that profiles are fed into, see Profile Backend Architecture. For the specialized YAML editors that open from the context menu (rules, proxies, groups), see Profile Editors. For the auto-update timer that drives periodic profile refreshes, see Timer and Auto-Update System.
Component tree of the Profiles page
Sources: src/pages/profiles.tsx1-86 src/components/profile/profile-item.tsx1-50 src/components/profile/profile-more.tsx1-30
ProfilePage is the top-level component exported from src/pages/profiles.tsx99-1085 It owns all page-level state and orchestrates the import bar, the drag-and-drop list, batch mode, and profile activation.
| State variable | Type | Purpose |
|---|---|---|
url | string | URL input for subscription import |
loading | boolean | Import button loading state |
activatings | string[] | UIDs of profiles currently activating |
batchMode | boolean | Whether batch-select mode is on |
selectedProfiles | Set<string> | UIDs selected in batch mode |
switchingProfileRef | ref<string|null> | UID currently being switched (prevents duplicates) |
abortControllerRef | ref<AbortController|null> | Allows cancelling an in-flight activation |
requestSequenceRef | ref<number> | Monotonically-increasing counter for sequencing requests |
pendingRequestRef | ref<Promise> | Reference to the pending patchProfiles call |
Sources: src/pages/profiles.tsx103-124
Profile data is loaded via the useProfiles hook (SWR-backed):
const { profiles, activateSelected, patchProfiles, mutateProfiles } = useProfiles();
Runtime chain logs (for the Script card) are fetched separately:
const { data: chainLogs } = useSWR("getRuntimeLogs", getRuntimeLogs);
The page also listens for a Tauri "profile-changed" event emitted by the backend, which triggers a debounced mutateProfiles() call to keep the UI in sync with backend changes.
Sources: src/pages/profiles.tsx173-181 src/pages/profiles.tsx253-256 src/pages/profiles.tsx738-788
Profile import sequence
Key behaviors:
https?:// or an error notice fires immediately src/pages/profiles.tsx277-280self_proxy: true (routes through Clash itself) src/pages/profiles.tsx298-306performRobustRefresh attempts up to 5 retries with exponential back-off before clearing the SWR cache as a last resort src/pages/profiles.tsx319-368readText() from the Tauri clipboard plugin to pre-fill the URL field src/pages/profiles.tsx644-647TauriEvent.DRAG_DROP listener src/pages/profiles.tsx182-219Sources: src/pages/profiles.tsx274-317 src/pages/profiles.tsx319-368 src/pages/profiles.tsx182-219
This is the most complex part of ProfilePage. Activation uses three refs to prevent duplicate or stale switches.
Profile activation state machine
Each call to activateProfile increments requestSequenceRef.current and captures the current value as currentSequence. After every await, the code checks two conditions before proceeding:
isRequestOutdated: the captured sequence no longer equals requestSequenceRef.current, meaning a newer request has superseded this one.isOperationAborted: abortControllerRef.current.signal.aborted is true, meaning the previous activation was explicitly interrupted.Sources: src/pages/profiles.tsx70-97 src/pages/profiles.tsx408-541
When activateProfile is called while switchingProfileRef.current is set to a different UID:
handleProfileInterrupt fires.abortControllerRef.current.abort() is called.activatings.switchInterrupted.Sources: src/pages/profiles.tsx127-152
| Step | Code |
|---|---|
Increment sequence, set switchingProfileRef | src/pages/profiles.tsx415-432 |
| Abort previous controller, create new one | src/pages/profiles.tsx434-435 |
Call patchProfiles({ current: profile }) | src/pages/profiles.tsx456-463 |
| Check staleness/abort after await | src/pages/profiles.tsx470-475 |
mutateLogs(), closeAllConnections() | src/pages/profiles.tsx478-479 |
Schedule executeBackgroundTasks in 50 ms | src/pages/profiles.tsx493-501 |
cleanupSwitchState in finally (if still owner) | src/pages/profiles.tsx517-530 |
executeBackgroundTasks calls activateSelected(profiles) to refresh the active proxy group selection, but only if the sequence and abort signal are still valid src/pages/profiles.tsx380-406
The profile grid uses @dnd-kit/core and @dnd-kit/sortable.
ProfileItem wraps its content in a Box with setNodeRef, transform, and transition from useSortable src/components/profile/profile-item.tsx79-87DragIndicatorRounded icon wired to {...attributes} {...listeners} src/components/profile/profile-item.tsx651-667onDragEnd calls reorderProfile(active.id, over.id) then mutateProfiles() src/pages/profiles.tsx370-378local and remote type profiles appear in the sortable section. The Merge and Script items below the divider are fixed-position ProfileMore cards src/pages/profiles.tsx262-268 src/pages/profiles.tsx1041-1065Sources: src/pages/profiles.tsx165-170 src/pages/profiles.tsx370-378 src/pages/profiles.tsx984-1068
Batch mode is toggled by the header CheckBoxOutlineBlankRounded icon button. When active, the header displays a different control set.
Normal vs batch mode header
| Normal Mode | Batch Mode |
|---|---|
| Toggle batch mode icon | Select-all / deselect-all toggle |
Update all subscriptions (onUpdateAll) | Delete selected (deleteSelectedProfiles) |
View runtime config (ConfigViewer) | Done button (exits batch mode) |
Re-activate profiles (onEnhance) | Selected count display |
Emergency refresh (if error || isStale) | — |
Sources: src/pages/profiles.tsx806-909
getSelectionState() returns "none", "partial", or "all". The header toggle icon cycles through CheckBoxOutlineBlankRounded, IndeterminateCheckBoxRounded, and CheckBoxRounded accordingly src/pages/profiles.tsx684-692 src/pages/profiles.tsx880-887
In batch mode, clicking a ProfileItem's delete menu entry calls toggleProfileSelection instead of showing the confirm dialog src/components/profile/profile-item.tsx460-468
deleteSelectedProfiles iterates selectedProfiles and calls deleteProfile(uid) for each, then re-runs onEnhance(false) if the active profile was deleted src/pages/profiles.tsx694-729
Sources: src/pages/profiles.tsx649-729
ProfileItem (src/components/profile/profile-item.tsx64-916) renders a single profile card inside a ProfileBox. It is both a DnD sortable node and the host for all per-profile dialogs.
┌──────────────────────────────────────────────┐
│ [drag] [checkbox?] Profile Name [refresh] │ ← row 1: name
│ Description / from domain last updated │ ← row 2: meta
│ Used / Total Expire │ ← row 3: traffic (remote only)
│ ████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │ ← LinearProgress (remote only)
└──────────────────────────────────────────────┘
h6), drag handle (DragIndicatorRounded), optional batch checkbox, optional refresh icon (remote only).description if set, otherwise the hostname extracted from url by parseUrl(). Right-aligned: last-updated time via dayjs.fromNow(), clickable to toggle to next-update countdown.parseTraffic(upload + download) / parseTraffic(total) and expiry date from parseExpire().extra.total > 0, showing data usage percentage src/components/profile/profile-item.tsx786-791While activating, a blurred overlay with a CircularProgress spinner covers the card src/components/profile/profile-item.tsx605-629
Sources: src/components/profile/profile-item.tsx226-791
Clicking the "last updated" timestamp calls toggleUpdateTimeDisplay, which in turn calls fetchNextUpdateTime. This invokes the getNextUpdateTime(uid) command and formats the result as "Next up Xh Ym" or "Next up Xm" src/components/profile/profile-item.tsx102-166
The display refreshes automatically when:
showNextUpdate flag is true and the update_interval or updated fields change."verge://timer-updated" DOM event fires with a matching UID src/components/profile/profile-item.tsx192-221"profile-update-started" / "profile-update-completed" event fires src/components/profile/profile-item.tsx545-576Sources: src/components/profile/profile-item.tsx95-221
Right-clicking a card opens a Menu anchored at the cursor position. Menu items differ by profile type:
| Menu Item | Remote (urlModeMenu) | Local (fileModeMenu) |
|---|---|---|
| Home | ✅ (if itemData.home set) | — |
| Select | ✅ | ✅ |
| Edit Info | ✅ | ✅ |
| Edit File | ✅ | ✅ |
| Edit Rules | ✅ (if option.rules set) | ✅ (if option.rules set) |
| Edit Proxies | ✅ (if option.proxies set) | ✅ (if option.proxies set) |
| Edit Groups | ✅ (if option.groups set) | ✅ (if option.groups set) |
| Extend Config | ✅ (if option.merge set) | ✅ (if option.merge set) |
| Extend Script | ✅ (if option.script set) | ✅ (if option.script set) |
| Open File | ✅ | ✅ |
| Update (direct) | ✅ | — |
| Update via Proxy | ✅ | — |
| Delete | ✅ | ✅ |
Sources: src/components/profile/profile-item.tsx371-535
Each dialog type is controlled by a local boolean state. Selecting a context menu action sets the flag to true:
| Flag | Dialog Component | Language |
|---|---|---|
fileOpen | EditorViewer | yaml |
rulesOpen | RulesEditorViewer | — |
proxiesOpen | ProxiesEditorViewer | — |
groupsOpen | GroupsEditorViewer | — |
mergeOpen | EditorViewer (merge file) | yaml |
scriptOpen | EditorViewer (script file) | javascript |
confirmOpen | ConfirmViewer (delete confirm) | — |
Sources: src/components/profile/profile-item.tsx272-279 src/components/profile/profile-item.tsx831-913
onUpdate(type) is called with one of three numeric modes:
| Type | Behavior |
|---|---|
0 | Direct, no proxy (with_proxy: false, self_proxy: false) |
1 | Default (backend decides based on profile's stored option) |
2 | Uses proxy: self_proxy: true if configured, otherwise with_proxy: true |
After update, mutate("getProfiles") refreshes the SWR cache src/components/profile/profile-item.tsx337-369
Sources: src/components/profile/profile-item.tsx337-369
ProfileMore (src/components/profile/profile-more.tsx31-203) renders the two fixed cards at the bottom of the profile list: Global Merge (id="Merge") and Global Script (id="Script").
These cards are not sortable and not activatable. Their only interactions are:
EditorViewer for the corresponding file (Merge → YAML, Script → JavaScript).LogViewer showing chainLogs["Script"] entries. If any entry has level === "exception", the icon renders as color="error" with a red Badge dot src/components/profile/profile-more.tsx55 src/components/profile/profile-more.tsx117-141chainLogs is fetched from getRuntimeLogs in ProfilePage and passed as logInfo to the Script card src/pages/profiles.tsx253-256 src/pages/profiles.tsx1053-1063
Sources: src/components/profile/profile-more.tsx31-203
EditorViewer (src/components/profile/editor-viewer.tsx65-556) is the shared Monaco-based editor dialog used by ProfileItem, ProfileMore, and several other settings dialogs. For the purposes of the Profiles page it provides:
configureMonacoYaml).addExtraLib).hasLoadedOnce guard that prevents saving an empty buffer if the initial load failed.ConfigViewer (src/components/setting/mods/config-viewer.tsx9-41) is a thin wrapper around EditorViewer in readOnly mode. It is opened from the header "View Runtime Config" button and displays the output of getRuntimeYaml().
For fuller coverage of EditorViewer, see Monaco Editor Integration.
Sources: src/components/profile/editor-viewer.tsx50-63 src/components/setting/mods/config-viewer.tsx9-41
ProfilePage header toolbar action map
Sources: src/pages/profiles.tsx806-909
Data flow through ProfilePage
Sources: src/pages/profiles.tsx173-181 src/pages/profiles.tsx253-256 src/pages/profiles.tsx542-594
Refresh this wiki