@mseep/anything-analyzer 3.6.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/.codeartsdoer/.codebaseignore +0 -0
  2. package/.codeartsdoer/AGENTS.md +12 -0
  3. package/.github/workflows/build.yml +146 -0
  4. package/README.en.md +264 -0
  5. package/README.md +276 -0
  6. package/RELEASE_NOTES.md +16 -0
  7. package/USAGE.md +490 -0
  8. package/color-preview-r3.html +414 -0
  9. package/color-preview.html +414 -0
  10. package/dev-app-update.yml +3 -0
  11. package/electron-builder.yml +36 -0
  12. package/electron.vite.config.ts +40 -0
  13. package/package.json +53 -0
  14. package/report-2026-04-13-copilot-claude-sonnet-4.6.md +955 -0
  15. package/resources/doloffer-logo.png +0 -0
  16. package/resources/entitlements.mac.plist +12 -0
  17. package/resources/icon.ico +0 -0
  18. package/resources/icon.png +0 -0
  19. package/src/main/ai/ai-analyzer.ts +517 -0
  20. package/src/main/ai/crypto-script-extractor.ts +206 -0
  21. package/src/main/ai/data-assembler.ts +205 -0
  22. package/src/main/ai/llm-router.ts +1120 -0
  23. package/src/main/ai/prompt-builder.ts +349 -0
  24. package/src/main/ai/scene-detector.ts +302 -0
  25. package/src/main/capture/capture-engine.ts +130 -0
  26. package/src/main/capture/interaction-recorder.ts +171 -0
  27. package/src/main/capture/js-injector.ts +57 -0
  28. package/src/main/capture/replay-engine.ts +256 -0
  29. package/src/main/capture/storage-collector.ts +76 -0
  30. package/src/main/cdp/cdp-manager.ts +233 -0
  31. package/src/main/db/database.ts +41 -0
  32. package/src/main/db/migrations.ts +235 -0
  33. package/src/main/db/repositories.ts +574 -0
  34. package/src/main/fingerprint/http-spoofing.ts +48 -0
  35. package/src/main/fingerprint/presets.ts +173 -0
  36. package/src/main/fingerprint/profile-generator.ts +115 -0
  37. package/src/main/fingerprint/profile-store.ts +52 -0
  38. package/src/main/index.ts +260 -0
  39. package/src/main/ipc.ts +856 -0
  40. package/src/main/logger.ts +42 -0
  41. package/src/main/mcp/mcp-config.ts +66 -0
  42. package/src/main/mcp/mcp-manager.ts +155 -0
  43. package/src/main/mcp/mcp-server.ts +1038 -0
  44. package/src/main/prompt-templates.ts +170 -0
  45. package/src/main/proxy/ca-manager.ts +204 -0
  46. package/src/main/proxy/cert-download-page.ts +171 -0
  47. package/src/main/proxy/cert-installer.ts +242 -0
  48. package/src/main/proxy/mitm-proxy-config.ts +37 -0
  49. package/src/main/proxy/mitm-proxy-server.ts +1085 -0
  50. package/src/main/proxy/system-proxy.ts +248 -0
  51. package/src/main/session/session-manager.ts +724 -0
  52. package/src/main/tab-manager.ts +582 -0
  53. package/src/main/updater.ts +111 -0
  54. package/src/main/window.ts +235 -0
  55. package/src/preload/hook-script.ts +270 -0
  56. package/src/preload/index.ts +211 -0
  57. package/src/preload/interaction-hook.ts +286 -0
  58. package/src/preload/stealth-script.ts +302 -0
  59. package/src/preload/target-preload.ts +15 -0
  60. package/src/renderer/App.tsx +656 -0
  61. package/src/renderer/components/AiLogDetail.tsx +173 -0
  62. package/src/renderer/components/AiLogList.tsx +101 -0
  63. package/src/renderer/components/AiLogView.module.css +364 -0
  64. package/src/renderer/components/AiLogView.tsx +86 -0
  65. package/src/renderer/components/AnalyzeBar.module.css +79 -0
  66. package/src/renderer/components/AnalyzeBar.tsx +104 -0
  67. package/src/renderer/components/BrowserPanel.module.css +67 -0
  68. package/src/renderer/components/BrowserPanel.tsx +90 -0
  69. package/src/renderer/components/ControlBar.module.css +47 -0
  70. package/src/renderer/components/ControlBar.tsx +205 -0
  71. package/src/renderer/components/HookLog.tsx +132 -0
  72. package/src/renderer/components/InteractionLog.tsx +183 -0
  73. package/src/renderer/components/MCPServerModal.tsx +427 -0
  74. package/src/renderer/components/PromptTemplateModal.tsx +254 -0
  75. package/src/renderer/components/ReportView.module.css +413 -0
  76. package/src/renderer/components/ReportView.tsx +429 -0
  77. package/src/renderer/components/RequestDetail.module.css +191 -0
  78. package/src/renderer/components/RequestDetail.tsx +202 -0
  79. package/src/renderer/components/RequestLog.module.css +69 -0
  80. package/src/renderer/components/RequestLog.tsx +208 -0
  81. package/src/renderer/components/SessionList.module.css +245 -0
  82. package/src/renderer/components/SessionList.tsx +247 -0
  83. package/src/renderer/components/SettingsModal.tsx +100 -0
  84. package/src/renderer/components/StatusBar.module.css +44 -0
  85. package/src/renderer/components/StatusBar.tsx +102 -0
  86. package/src/renderer/components/StorageView.module.css +41 -0
  87. package/src/renderer/components/StorageView.tsx +178 -0
  88. package/src/renderer/components/TabBar.module.css +88 -0
  89. package/src/renderer/components/TabBar.tsx +70 -0
  90. package/src/renderer/components/Titlebar.module.css +254 -0
  91. package/src/renderer/components/Titlebar.tsx +169 -0
  92. package/src/renderer/components/settings/FingerprintSection.tsx +198 -0
  93. package/src/renderer/components/settings/GeneralSection.tsx +164 -0
  94. package/src/renderer/components/settings/LLMSection.tsx +148 -0
  95. package/src/renderer/components/settings/MCPServerSection.tsx +136 -0
  96. package/src/renderer/components/settings/MitmProxySection.tsx +320 -0
  97. package/src/renderer/components/settings/ProxySection.tsx +110 -0
  98. package/src/renderer/css-modules.d.ts +4 -0
  99. package/src/renderer/hooks/useCapture.ts +383 -0
  100. package/src/renderer/hooks/useConfirm.tsx +91 -0
  101. package/src/renderer/hooks/useSession.ts +136 -0
  102. package/src/renderer/hooks/useTabs.ts +103 -0
  103. package/src/renderer/i18n/en.ts +167 -0
  104. package/src/renderer/i18n/index.ts +47 -0
  105. package/src/renderer/i18n/zh.ts +170 -0
  106. package/src/renderer/index.html +12 -0
  107. package/src/renderer/main.tsx +15 -0
  108. package/src/renderer/styles/global.css +144 -0
  109. package/src/renderer/styles/themes/ayu-dark.css +59 -0
  110. package/src/renderer/styles/themes/catppuccin.css +59 -0
  111. package/src/renderer/styles/themes/discord.css +59 -0
  112. package/src/renderer/styles/themes/dracula.css +59 -0
  113. package/src/renderer/styles/themes/github-dark.css +59 -0
  114. package/src/renderer/styles/themes/gruvbox.css +59 -0
  115. package/src/renderer/styles/themes/index.css +11 -0
  116. package/src/renderer/styles/themes/light.css +59 -0
  117. package/src/renderer/styles/themes/nord.css +59 -0
  118. package/src/renderer/styles/themes/one-dark.css +59 -0
  119. package/src/renderer/styles/themes/tokyo-night.css +59 -0
  120. package/src/renderer/styles/tokens.css +137 -0
  121. package/src/renderer/theme.ts +31 -0
  122. package/src/renderer/ui/Badge.module.css +38 -0
  123. package/src/renderer/ui/Badge.tsx +36 -0
  124. package/src/renderer/ui/Button.module.css +142 -0
  125. package/src/renderer/ui/Button.tsx +46 -0
  126. package/src/renderer/ui/Collapse.module.css +49 -0
  127. package/src/renderer/ui/Collapse.tsx +57 -0
  128. package/src/renderer/ui/CopyableBlock.module.css +56 -0
  129. package/src/renderer/ui/CopyableBlock.tsx +42 -0
  130. package/src/renderer/ui/Empty.module.css +19 -0
  131. package/src/renderer/ui/Empty.tsx +34 -0
  132. package/src/renderer/ui/Icons.tsx +346 -0
  133. package/src/renderer/ui/Input.module.css +103 -0
  134. package/src/renderer/ui/Input.tsx +94 -0
  135. package/src/renderer/ui/InputNumber.module.css +68 -0
  136. package/src/renderer/ui/InputNumber.tsx +104 -0
  137. package/src/renderer/ui/Modal.module.css +83 -0
  138. package/src/renderer/ui/Modal.tsx +67 -0
  139. package/src/renderer/ui/Popconfirm.module.css +73 -0
  140. package/src/renderer/ui/Popconfirm.tsx +74 -0
  141. package/src/renderer/ui/Progress.module.css +35 -0
  142. package/src/renderer/ui/Progress.tsx +30 -0
  143. package/src/renderer/ui/Select.module.css +91 -0
  144. package/src/renderer/ui/Select.tsx +100 -0
  145. package/src/renderer/ui/Spinner.module.css +44 -0
  146. package/src/renderer/ui/Spinner.tsx +27 -0
  147. package/src/renderer/ui/Switch.module.css +39 -0
  148. package/src/renderer/ui/Switch.tsx +43 -0
  149. package/src/renderer/ui/Tabs.module.css +76 -0
  150. package/src/renderer/ui/Tabs.tsx +53 -0
  151. package/src/renderer/ui/Tag.module.css +66 -0
  152. package/src/renderer/ui/Tag.tsx +47 -0
  153. package/src/renderer/ui/Timeline.module.css +42 -0
  154. package/src/renderer/ui/Timeline.tsx +29 -0
  155. package/src/renderer/ui/Toast.module.css +99 -0
  156. package/src/renderer/ui/Toast.tsx +90 -0
  157. package/src/renderer/ui/Tooltip.module.css +26 -0
  158. package/src/renderer/ui/Tooltip.tsx +23 -0
  159. package/src/renderer/ui/VirtualTable.module.css +230 -0
  160. package/src/renderer/ui/VirtualTable.tsx +416 -0
  161. package/src/renderer/ui/index.ts +55 -0
  162. package/src/shared/types.ts +695 -0
  163. package/tests/main/ai/crypto-script-extractor.test.ts +281 -0
  164. package/tests/main/ai/llm-router.test.ts +1537 -0
  165. package/tests/main/ai/prompt-builder.test.ts +178 -0
  166. package/tests/main/ai/scene-detector.test.ts +212 -0
  167. package/tests/main/db/migrations.test.ts +134 -0
  168. package/tests/main/release-workflow.test.ts +59 -0
  169. package/tsconfig.json +7 -0
  170. package/tsconfig.node.json +23 -0
  171. package/tsconfig.web.json +24 -0
  172. package/vitest.config.ts +13 -0
@@ -0,0 +1,254 @@
1
+ .titlebar {
2
+ display: flex;
3
+ align-items: stretch;
4
+ height: var(--titlebar-height);
5
+ background: var(--color-bar);
6
+ border-bottom: 1px solid var(--color-border);
7
+ -webkit-app-region: drag;
8
+ user-select: none;
9
+ flex-shrink: 0;
10
+ }
11
+
12
+ /* Logo area — aligned with sidebar width */
13
+ .logoArea {
14
+ width: var(--sidebar-width);
15
+ display: flex;
16
+ align-items: center;
17
+ padding: 0 16px;
18
+ gap: var(--space-md);
19
+ border-right: 1px solid var(--color-border);
20
+ flex-shrink: 0;
21
+ -webkit-app-region: drag;
22
+ }
23
+
24
+ .logoIcon {
25
+ width: 20px;
26
+ height: 20px;
27
+ border-radius: 5px;
28
+ background: linear-gradient(135deg, var(--text-disabled), var(--text-muted));
29
+ display: flex;
30
+ align-items: center;
31
+ justify-content: center;
32
+ font-size: var(--font-size-3xs);
33
+ color: var(--text-secondary);
34
+ font-weight: 700;
35
+ flex-shrink: 0;
36
+ }
37
+
38
+ .logoText {
39
+ font-size: var(--font-size-sm);
40
+ color: var(--text-primary);
41
+ font-weight: 600;
42
+ letter-spacing: 1px;
43
+ white-space: nowrap;
44
+ }
45
+
46
+ /* Navigation tabs */
47
+ .tabs {
48
+ display: flex;
49
+ align-items: stretch;
50
+ -webkit-app-region: no-drag;
51
+ }
52
+
53
+ .tab {
54
+ padding: 0 14px;
55
+ margin: 6px 2px;
56
+ display: flex;
57
+ align-items: center;
58
+ gap: 7px;
59
+ font-family: var(--font-sans);
60
+ font-size: var(--font-size-xs);
61
+ color: var(--text-muted);
62
+ background: none;
63
+ border: none;
64
+ border-radius: var(--radius-button);
65
+ cursor: pointer;
66
+ transition: color var(--transition-normal), background var(--transition-normal);
67
+ white-space: nowrap;
68
+ -webkit-app-region: no-drag;
69
+ }
70
+
71
+ .tab:hover {
72
+ color: var(--text-secondary);
73
+ background: var(--color-hover);
74
+ }
75
+
76
+ .tabActive {
77
+ color: var(--text-primary);
78
+ background: var(--color-active);
79
+ }
80
+
81
+ .tabIcon {
82
+ display: flex;
83
+ align-items: center;
84
+ font-size: var(--font-size-sm);
85
+ opacity: 0.7;
86
+ }
87
+
88
+ .tabBadge {
89
+ font-size: var(--font-size-3xs);
90
+ padding: 1px 5px;
91
+ border-radius: var(--radius-lg);
92
+ background: var(--color-success-bg);
93
+ color: var(--color-success);
94
+ margin-left: 4px;
95
+ }
96
+
97
+ /* Spacer = draggable area */
98
+ .spacer {
99
+ flex: 1;
100
+ -webkit-app-region: drag;
101
+ }
102
+
103
+ /* Right controls area */
104
+ .rightControls {
105
+ display: flex;
106
+ align-items: center;
107
+ gap: var(--space-md);
108
+ padding: 0 var(--space-sm);
109
+ -webkit-app-region: no-drag;
110
+ }
111
+
112
+ /* Language switch */
113
+ .langSwitch {
114
+ font-family: var(--font-sans);
115
+ font-size: var(--font-size-3xs);
116
+ color: var(--text-muted);
117
+ padding: 2px 8px;
118
+ border: 1px solid var(--color-border-subtle);
119
+ border-radius: var(--radius-badge);
120
+ background: none;
121
+ cursor: pointer;
122
+ -webkit-app-region: no-drag;
123
+ transition: border-color var(--transition-fast);
124
+ }
125
+
126
+ .langSwitch:hover {
127
+ border-color: var(--color-border-hover);
128
+ }
129
+
130
+ .langActive {
131
+ color: var(--text-secondary);
132
+ }
133
+
134
+ /* Action buttons (theme toggle) */
135
+ .actionBtn {
136
+ display: flex;
137
+ align-items: center;
138
+ justify-content: center;
139
+ width: 32px;
140
+ height: 28px;
141
+ background: none;
142
+ border: none;
143
+ border-radius: var(--radius-button);
144
+ color: var(--text-muted);
145
+ cursor: pointer;
146
+ font-family: var(--font-sans);
147
+ font-size: var(--font-size-xs);
148
+ font-weight: 500;
149
+ transition: background var(--transition-fast), color var(--transition-fast);
150
+ -webkit-app-region: no-drag;
151
+ }
152
+
153
+ .actionBtn:hover {
154
+ background: var(--color-hover);
155
+ color: var(--text-secondary);
156
+ }
157
+
158
+ .separator {
159
+ width: 1px;
160
+ height: 16px;
161
+ background: var(--color-border-subtle);
162
+ margin: 0 var(--space-xs);
163
+ }
164
+
165
+ /* Window control buttons */
166
+ .windowControls {
167
+ display: flex;
168
+ align-items: stretch;
169
+ -webkit-app-region: no-drag;
170
+ }
171
+
172
+ .windowBtn {
173
+ display: flex;
174
+ align-items: center;
175
+ justify-content: center;
176
+ width: 44px;
177
+ height: var(--titlebar-height);
178
+ background: none;
179
+ border: none;
180
+ color: var(--text-muted);
181
+ cursor: pointer;
182
+ transition: background var(--transition-fast);
183
+ }
184
+
185
+ .windowBtn:hover {
186
+ background: var(--color-hover);
187
+ }
188
+
189
+ .windowBtnClose:hover {
190
+ background: #e81123;
191
+ color: var(--text-primary);
192
+ }
193
+
194
+ /* Theme popover */
195
+ .themePopover {
196
+ position: absolute;
197
+ top: calc(100% + 6px);
198
+ right: 0;
199
+ width: 180px;
200
+ max-height: 400px;
201
+ overflow-y: auto;
202
+ background: var(--color-elevated);
203
+ border: 1px solid var(--color-border-hover);
204
+ border-radius: var(--radius-lg);
205
+ box-shadow: var(--shadow-lg);
206
+ padding: 4px;
207
+ z-index: var(--z-dropdown);
208
+ display: flex;
209
+ flex-direction: column;
210
+ gap: 1px;
211
+ }
212
+
213
+ .themeItem {
214
+ display: flex;
215
+ align-items: center;
216
+ gap: 8px;
217
+ padding: 6px 10px;
218
+ border: none;
219
+ border-radius: var(--radius-badge);
220
+ background: none;
221
+ color: var(--text-secondary);
222
+ font-family: var(--font-sans);
223
+ font-size: var(--font-size-2xs);
224
+ cursor: pointer;
225
+ transition: background var(--transition-fast);
226
+ text-align: left;
227
+ width: 100%;
228
+ }
229
+
230
+ .themeItem:hover {
231
+ background: var(--color-hover);
232
+ color: var(--text-primary);
233
+ }
234
+
235
+ .themeItemActive {
236
+ color: var(--text-primary);
237
+ background: var(--color-selected);
238
+ }
239
+
240
+ .themeDot {
241
+ width: 8px;
242
+ height: 8px;
243
+ border-radius: 50%;
244
+ flex-shrink: 0;
245
+ }
246
+
247
+ .themeName {
248
+ flex: 1;
249
+ }
250
+
251
+ .themeCheck {
252
+ font-size: var(--font-size-3xs);
253
+ color: var(--color-accent);
254
+ }
@@ -0,0 +1,169 @@
1
+ import React, { useState, useEffect, useCallback, useRef } from 'react'
2
+ import { IconMinimize, IconMaximize, IconClose, IconGlobe, IconCode, IconRobot } from '../ui/Icons'
3
+ import { useLocale } from '../i18n'
4
+ import type { LocaleKey } from '../i18n'
5
+ import { THEMES } from '../theme'
6
+ import styles from './Titlebar.module.css'
7
+
8
+ export type AppView = 'browser' | 'inspector' | 'report'
9
+
10
+ interface TitlebarProps {
11
+ theme: string
12
+ onThemeChange: (themeId: string) => void
13
+ locale: 'en' | 'zh'
14
+ onLocaleToggle: () => void
15
+ activeView: AppView
16
+ onViewChange: (view: AppView) => void
17
+ requestCount?: number
18
+ }
19
+
20
+ const navTabs: { key: AppView; labelKey: LocaleKey; icon: React.ReactNode }[] = [
21
+ { key: 'browser', labelKey: 'nav.browser', icon: <IconGlobe size={13} /> },
22
+ { key: 'inspector', labelKey: 'nav.inspector', icon: <IconCode size={13} /> },
23
+ { key: 'report', labelKey: 'nav.report', icon: <IconRobot size={13} /> },
24
+ ]
25
+
26
+ const Titlebar: React.FC<TitlebarProps> = ({
27
+ theme,
28
+ onThemeChange,
29
+ locale,
30
+ onLocaleToggle,
31
+ activeView,
32
+ onViewChange,
33
+ requestCount = 0,
34
+ }) => {
35
+ const { t } = useLocale()
36
+ const [isMaximized, setIsMaximized] = useState(false)
37
+ const [themeOpen, setThemeOpen] = useState(false)
38
+ const themeRef = useRef<HTMLDivElement>(null)
39
+
40
+ useEffect(() => {
41
+ window.electronAPI.isWindowMaximized().then(setIsMaximized)
42
+ }, [])
43
+
44
+ // Close theme popover on outside click
45
+ useEffect(() => {
46
+ if (!themeOpen) return
47
+ const handleClick = (e: MouseEvent) => {
48
+ if (themeRef.current && !themeRef.current.contains(e.target as Node)) {
49
+ setThemeOpen(false)
50
+ }
51
+ }
52
+ document.addEventListener('mousedown', handleClick)
53
+ return () => document.removeEventListener('mousedown', handleClick)
54
+ }, [themeOpen])
55
+
56
+ const handleMinimize = useCallback(() => {
57
+ window.electronAPI.minimizeWindow()
58
+ }, [])
59
+
60
+ const handleMaximize = useCallback(async () => {
61
+ await window.electronAPI.maximizeWindow()
62
+ const maximized = await window.electronAPI.isWindowMaximized()
63
+ setIsMaximized(maximized)
64
+ }, [])
65
+
66
+ const handleClose = useCallback(() => {
67
+ window.electronAPI.closeWindow()
68
+ }, [])
69
+
70
+ const currentTheme = THEMES.find(t => t.id === theme)
71
+
72
+ return (
73
+ <div className={styles.titlebar}>
74
+ {/* Logo area — matches sidebar width */}
75
+ <div className={styles.logoArea}>
76
+ <div className={styles.logoIcon}>A</div>
77
+ <span className={styles.logoText}>Anything Analyzer</span>
78
+ </div>
79
+
80
+ {/* Navigation Tabs */}
81
+ <div className={styles.tabs}>
82
+ {navTabs.map((tab) => (
83
+ <button
84
+ key={tab.key}
85
+ className={`${styles.tab} ${activeView === tab.key ? styles.tabActive : ''}`}
86
+ onClick={() => onViewChange(tab.key)}
87
+ >
88
+ <span className={styles.tabIcon}>{tab.icon}</span>
89
+ {t(tab.labelKey)}
90
+ {tab.key === 'inspector' && requestCount > 0 && (
91
+ <span className={styles.tabBadge}>{requestCount}</span>
92
+ )}
93
+ </button>
94
+ ))}
95
+ </div>
96
+
97
+ {/* Draggable spacer */}
98
+ <div className={styles.spacer} />
99
+
100
+ {/* Right controls */}
101
+ <div className={styles.rightControls}>
102
+ {/* Language toggle */}
103
+ <button className={styles.langSwitch} onClick={onLocaleToggle}>
104
+ <span className={locale === 'zh' ? styles.langActive : ''}>中</span>
105
+ {' / '}
106
+ <span className={locale === 'en' ? styles.langActive : ''}>En</span>
107
+ </button>
108
+
109
+ {/* Theme selector */}
110
+ <div ref={themeRef} style={{ position: 'relative' }}>
111
+ <button
112
+ className={styles.actionBtn}
113
+ onClick={() => setThemeOpen(prev => !prev)}
114
+ title={locale === 'zh' ? '切换主题' : 'Switch theme'}
115
+ >
116
+ <span
117
+ style={{
118
+ width: 10,
119
+ height: 10,
120
+ borderRadius: '50%',
121
+ background: currentTheme?.accent || '#60a5fa',
122
+ display: 'inline-block',
123
+ border: '1px solid var(--color-border-hover)',
124
+ }}
125
+ />
126
+ </button>
127
+
128
+ {themeOpen && (
129
+ <div className={styles.themePopover}>
130
+ {THEMES.map((t) => (
131
+ <button
132
+ key={t.id}
133
+ className={`${styles.themeItem} ${theme === t.id ? styles.themeItemActive : ''}`}
134
+ onClick={() => { onThemeChange(t.id); setThemeOpen(false) }}
135
+ >
136
+ <span
137
+ className={styles.themeDot}
138
+ style={{ background: t.accent }}
139
+ />
140
+ <span className={styles.themeName}>
141
+ {locale === 'zh' ? t.name : t.nameEn}
142
+ </span>
143
+ {theme === t.id && <span className={styles.themeCheck}>✓</span>}
144
+ </button>
145
+ ))}
146
+ </div>
147
+ )}
148
+ </div>
149
+
150
+ <div className={styles.separator} />
151
+ </div>
152
+
153
+ {/* Window controls */}
154
+ <div className={styles.windowControls}>
155
+ <button className={styles.windowBtn} onClick={handleMinimize}>
156
+ <IconMinimize size={12} />
157
+ </button>
158
+ <button className={styles.windowBtn} onClick={handleMaximize}>
159
+ <IconMaximize size={12} />
160
+ </button>
161
+ <button className={`${styles.windowBtn} ${styles.windowBtnClose}`} onClick={handleClose}>
162
+ <IconClose size={12} />
163
+ </button>
164
+ </div>
165
+ </div>
166
+ )
167
+ }
168
+
169
+ export default Titlebar
@@ -0,0 +1,198 @@
1
+ import { useEffect, useState } from 'react'
2
+ import { Button, Input, InputNumber, Select, useToast } from '../../ui'
3
+ import { useLocale } from '../../i18n'
4
+ import type { FingerprintProfile } from '@shared/types'
5
+
6
+ interface Props {
7
+ currentSessionId?: string | null
8
+ }
9
+
10
+ export default function FingerprintSection({ currentSessionId }: Props) {
11
+ const toast = useToast()
12
+ const { t } = useLocale()
13
+ const [profile, setProfile] = useState<FingerprintProfile | null>(null)
14
+ const [expanded, setExpanded] = useState(false)
15
+
16
+ useEffect(() => {
17
+ if (!currentSessionId) { setProfile(null); return }
18
+ window.electronAPI.getFingerprintProfile(currentSessionId).then(p => {
19
+ setProfile(p)
20
+ })
21
+ }, [currentSessionId])
22
+
23
+ if (!currentSessionId) {
24
+ return (
25
+ <div style={{ color: 'var(--text-muted)', padding: 20, textAlign: 'center' }}>
26
+ {t('fingerprint.noSession')}
27
+ </div>
28
+ )
29
+ }
30
+
31
+ if (!profile) return null
32
+
33
+ const update = (partial: Partial<FingerprintProfile>) => {
34
+ setProfile(prev => prev ? { ...prev, ...partial } : prev)
35
+ }
36
+
37
+ const handleSave = async () => {
38
+ if (!profile) return
39
+ await window.electronAPI.updateFingerprintProfile(profile)
40
+ toast.success(t('fingerprint.saved'))
41
+ }
42
+
43
+ const handleRegenerate = async () => {
44
+ if (!currentSessionId) return
45
+ const newProfile = await window.electronAPI.regenerateFingerprintProfile(currentSessionId)
46
+ setProfile(newProfile)
47
+ toast.success(t('fingerprint.regenerated'))
48
+ }
49
+
50
+ const rowStyle: React.CSSProperties = {
51
+ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8,
52
+ }
53
+ const labelStyle: React.CSSProperties = {
54
+ fontSize: 'var(--font-size-base)', minWidth: 100, flexShrink: 0,
55
+ }
56
+
57
+ return (
58
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 10, width: '100%' }}>
59
+ {/* Summary */}
60
+ <div style={{
61
+ padding: '8px 12px',
62
+ background: 'var(--color-surface)',
63
+ borderRadius: 6,
64
+ fontSize: 'var(--font-size-sm)',
65
+ fontFamily: 'var(--font-mono)',
66
+ lineHeight: 1.6,
67
+ color: 'var(--text-secondary)',
68
+ }}>
69
+ <div>{profile.platform} · Chrome/{profile.userAgent.match(/Chrome\/(\S+)/)?.[1]}</div>
70
+ <div>{profile.screenWidth}x{profile.screenHeight} @{profile.devicePixelRatio}x · {profile.hardwareConcurrency} cores · {profile.deviceMemory}GB</div>
71
+ <div>{profile.timezone} · {profile.languages.join(', ')}</div>
72
+ </div>
73
+
74
+ {/* Actions */}
75
+ <div style={{ display: 'flex', gap: 8 }}>
76
+ <Button variant="primary" onClick={handleRegenerate}>{t('fingerprint.regenerate')}</Button>
77
+ <Button onClick={handleSave}>{t('fingerprint.save')}</Button>
78
+ </div>
79
+
80
+ {/* Expandable details */}
81
+ <div
82
+ onClick={() => setExpanded(!expanded)}
83
+ style={{
84
+ cursor: 'pointer',
85
+ fontSize: 'var(--font-size-sm)',
86
+ color: 'var(--text-muted)',
87
+ userSelect: 'none',
88
+ }}
89
+ >
90
+ {expanded ? '▼' : '▶'} {t('fingerprint.detail')}
91
+ </div>
92
+
93
+ {expanded && (
94
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
95
+ <div style={rowStyle}>
96
+ <span style={labelStyle}>{t('fingerprint.ua')}</span>
97
+ <Input
98
+ value={profile.userAgent}
99
+ onChange={e => update({ userAgent: e.target.value })}
100
+ style={{ flex: 1, fontFamily: 'var(--font-mono)', fontSize: 'var(--font-size-2xs)' }}
101
+ />
102
+ </div>
103
+ <div style={rowStyle}>
104
+ <span style={labelStyle}>{t('fingerprint.platform')}</span>
105
+ <Select
106
+ value={profile.platform}
107
+ onChange={v => update({ platform: v })}
108
+ style={{ width: 180 }}
109
+ options={[
110
+ { label: 'Windows', value: 'Win32' },
111
+ { label: 'macOS', value: 'MacIntel' },
112
+ { label: 'Linux', value: 'Linux x86_64' },
113
+ ]}
114
+ />
115
+ </div>
116
+ <div style={rowStyle}>
117
+ <span style={labelStyle}>{t('fingerprint.screen')}</span>
118
+ <InputNumber value={profile.screenWidth} onChange={v => v !== null && update({ screenWidth: v })} style={{ width: 80 }} />
119
+ <span>x</span>
120
+ <InputNumber value={profile.screenHeight} onChange={v => v !== null && update({ screenHeight: v })} style={{ width: 80 }} />
121
+ </div>
122
+ <div style={rowStyle}>
123
+ <span style={labelStyle}>{t('fingerprint.dpr')}</span>
124
+ <InputNumber value={profile.devicePixelRatio} min={1} max={3} step={0.25} onChange={v => v !== null && update({ devicePixelRatio: v })} style={{ width: 80 }} />
125
+ </div>
126
+ <div style={rowStyle}>
127
+ <span style={labelStyle}>{t('fingerprint.cpu')}</span>
128
+ <InputNumber value={profile.hardwareConcurrency} min={1} max={32} onChange={v => v !== null && update({ hardwareConcurrency: v })} style={{ width: 80 }} />
129
+ </div>
130
+ <div style={rowStyle}>
131
+ <span style={labelStyle}>{t('fingerprint.memory')}</span>
132
+ <InputNumber value={profile.deviceMemory} min={2} max={64} onChange={v => v !== null && update({ deviceMemory: v })} style={{ width: 80 }} />
133
+ </div>
134
+ <div style={rowStyle}>
135
+ <span style={labelStyle}>{t('fingerprint.webgl')}</span>
136
+ <Input
137
+ value={profile.webglRenderer}
138
+ onChange={e => update({ webglRenderer: e.target.value })}
139
+ style={{ flex: 1, fontFamily: 'var(--font-mono)', fontSize: 'var(--font-size-2xs)' }}
140
+ />
141
+ </div>
142
+ <div style={rowStyle}>
143
+ <span style={labelStyle}>{t('fingerprint.timezone')}</span>
144
+ <Input value={profile.timezone} onChange={e => update({ timezone: e.target.value })} style={{ width: 200 }} />
145
+ </div>
146
+ <div style={rowStyle}>
147
+ <span style={labelStyle}>{t('fingerprint.languages')}</span>
148
+ <Input
149
+ value={profile.languages.join(', ')}
150
+ onChange={e => update({ languages: e.target.value.split(',').map(s => s.trim()).filter(Boolean) })}
151
+ style={{ flex: 1 }}
152
+ />
153
+ </div>
154
+ <div style={rowStyle}>
155
+ <span style={labelStyle}>{t('fingerprint.webrtc')}</span>
156
+ <Select
157
+ value={profile.webrtcPolicy}
158
+ onChange={v => update({ webrtcPolicy: v as 'block' | 'real' | 'fake' })}
159
+ style={{ width: 120 }}
160
+ options={[
161
+ { label: 'Block', value: 'block' },
162
+ { label: 'Real', value: 'real' },
163
+ { label: 'Fake', value: 'fake' },
164
+ ]}
165
+ />
166
+ </div>
167
+ </div>
168
+ )}
169
+
170
+ {/* Test links */}
171
+ <div style={{ marginTop: 4 }}>
172
+ <span style={{ fontSize: 'var(--font-size-sm)', color: 'var(--text-muted)' }}>
173
+ {t('fingerprint.testLinks')}:
174
+ </span>
175
+ <div style={{ display: 'flex', gap: 12, marginTop: 4 }}>
176
+ {[
177
+ { label: 'BrowserLeaks', url: 'https://browserleaks.com/javascript' },
178
+ { label: 'CreepJS', url: 'https://abrahamjuliot.github.io/creepjs/' },
179
+ { label: 'Bot Detect', url: 'https://bot.sannysoft.com/' },
180
+ ].map(link => (
181
+ <span
182
+ key={link.url}
183
+ onClick={() => window.electronAPI.openExternal(link.url)}
184
+ style={{
185
+ fontSize: 'var(--font-size-sm)',
186
+ color: 'var(--color-accent)',
187
+ cursor: 'pointer',
188
+ textDecoration: 'underline',
189
+ }}
190
+ >
191
+ {link.label}
192
+ </span>
193
+ ))}
194
+ </div>
195
+ </div>
196
+ </div>
197
+ )
198
+ }