Skip to content

Commit a7c444d

Browse files
committed
feat: add Flutter platform directory icons
1 parent 219af9b commit a7c444d

24 files changed

Lines changed: 399 additions & 13 deletions

docs/settings-ui-dsl2-redesign.md

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
# FlutterX Settings UI DSL 2.0 Redesign Draft
2+
3+
## Current Issues
4+
5+
- The basic page mixes global language, Flutter update checking, Dio socket, request list display, assets scan, and copy-key mapping in one long vertical flow.
6+
- The language row uses six radio buttons in one line, which becomes crowded in narrow settings windows and creates a very wide first row.
7+
- The Flutter version section contains enable state, mirror selection, current version, changelog, ignore list, and documentation, but the hierarchy is not clear.
8+
- The Dio section combines server control and display options. The port field and restart button do not visually read as a single server-control row.
9+
- Several dependent options stay visually active even when their parent feature is disabled.
10+
- Advanced options such as copy-key mapping are always visible, increasing scan cost for regular users.
11+
12+
## Proposed Information Architecture
13+
14+
```text
15+
Basic
16+
├─ Language
17+
│ └─ Language: [System v] Restart IDEA to take effect
18+
19+
├─ Flutter SDK Updates
20+
│ ├─ [x] Check for new Flutter versions
21+
│ ├─ Flutter release source: [Use default v] Visit
22+
│ ├─ Pub server: [Use default v] Visit
23+
│ ├─ Current version: 3.44.0 [Changelog]
24+
│ ├─ Ignored versions: [list]
25+
│ └─ Documentation
26+
27+
├─ Dio Socket
28+
│ ├─ [x] Enable FlutterX socket (?)
29+
│ ├─ Listening port: [9999] [Restart]
30+
│ └─ [x] New API notification
31+
32+
├─ Dio Request List
33+
│ ├─ [x] Display domain name [ ] Display query parameters
34+
│ ├─ [x] Show request method [x] Display status code
35+
│ ├─ [x] Display request time [x] Display date
36+
│ ├─ [x] Bold URL [x] Display data size
37+
│ └─ Documentation
38+
39+
├─ Assets Scan
40+
│ ├─ [x] Enable assets scan
41+
│ ├─ Trigger text: [assets]
42+
│ ├─ Trigger length:[3]
43+
│ ├─ Scan folder: [assets]
44+
│ └─ Documentation
45+
46+
└─ Advanced
47+
└─ Copy All Keys
48+
├─ URL: [url]
49+
├─ Method: [method]
50+
├─ Headers: [headers]
51+
├─ Query params: [query]
52+
├─ Body: [body]
53+
├─ Response status code:[responseStatusCode]
54+
├─ Response: [response]
55+
├─ Request time: [requestTime]
56+
└─ Timestamp: [timestamp]
57+
```
58+
59+
## Layout Rules
60+
61+
- Use `comboBox` for language selection instead of six radio buttons. This keeps the first group compact and localizes better.
62+
- Keep server control in a dedicated `Dio Socket` group and request display switches in a separate `Dio Request List` group.
63+
- Wrap dependent rows with `indent { ... }.enabledIf(master.selected)` so disabled features look disabled.
64+
- Move copy-key mapping into `collapsibleGroup("Advanced")`, collapsed by default if supported by the current platform version.
65+
- Prefer `row("Label:")` for editable fields so the DSL creates correct label relationships and accessibility metadata.
66+
- Use `columns(COLUMNS_SHORT)` for the port and numeric fields, and `resizableColumn()` only on fields that should expand.
67+
68+
## Kotlin UI DSL 2.0 Draft
69+
70+
```kotlin
71+
fun redesignedBasicSettingsPanel(
72+
project: Project,
73+
model: AppStateModel,
74+
dioSetting: DoxListeningSetting,
75+
parentDisposable: Disposable,
76+
onChange: (state: AppStateModel) -> Unit
77+
): DialogPanel {
78+
val languageList = listOf("System", "中文", "繁體", "English", "한국어", "日本語")
79+
80+
return panel {
81+
group(PluginBundle.get("setting.language")) {
82+
row("${PluginBundle.get("setting.language")}:") {
83+
comboBox(languageList)
84+
.bindItem(
85+
{ model.lang },
86+
{ model.lang = it ?: "System" }
87+
)
88+
.columns(COLUMNS_MEDIUM)
89+
}.rowComment(PluginBundle.get("setting.reset.tip"))
90+
}
91+
92+
group(PluginBundle.get("check_flutter_version_title")) {
93+
lateinit var checkFlutterVersion: Cell<JBCheckBox>
94+
95+
row {
96+
checkFlutterVersion = checkBox(PluginBundle.get("open"))
97+
.bindSelected(dioSetting::checkFlutterVersion)
98+
}.rowComment(PluginBundle.get("check_flutter_version_comment"))
99+
100+
indent {
101+
row("Flutter release source:") {
102+
PubDevMirrorImageSetting.flutterCheckComboBox(this, dioSetting)
103+
browserLink("Visit", dioSetting.checkFlutterVersionUrl)
104+
}
105+
106+
row("Pub server:") {
107+
PubDevMirrorImageSetting.pubServerComboBox(this, dioSetting)
108+
browserLink("Visit", dioSetting.pubServerUrl)
109+
}
110+
111+
row("Current Flutter version:") {
112+
cell(FlutterVersionCheckPanel(project = project))
113+
}
114+
115+
row(PluginBundle.get("ignore_this_version_to_check")) {
116+
cell(FlutterVersionIgnoreList())
117+
.align(AlignX.FILL)
118+
.resizableColumn()
119+
}.resizableRow()
120+
}.enabledIf(checkFlutterVersion.selected)
121+
122+
documentCommentRow(Links.CHECK_FLUTTER_VERSION_DOC_LINK)
123+
}
124+
125+
group("Dio Socket") {
126+
lateinit var enableSocket: Cell<JBCheckBox>
127+
128+
row {
129+
enableSocket = checkBox("Enable FlutterX socket")
130+
.bindSelected(dioSetting::enableFlutterXDioSocket)
131+
.gap(RightGap.SMALL)
132+
contextHelp(
133+
PluginBundle.get("enable_flutterx_socket_setting_contexthelp") + " (Dio, SP, Hive, Log)",
134+
"Tips"
135+
)
136+
}
137+
138+
indent {
139+
row("Listening port:") {
140+
intTextField()
141+
.bindIntText(
142+
{ model.serverPort.toInt() },
143+
{ model.serverPort = it.toString() }
144+
)
145+
.columns(COLUMNS_SHORT)
146+
.gap(RightGap.SMALL)
147+
.onChanged { component ->
148+
PluginStateService.changeState { it.copy(serverPort = component.text) }
149+
}
150+
151+
button(PluginBundle.get("reset")) {
152+
DioApiService.getInstance().reset(project)
153+
}
154+
}
155+
156+
row {
157+
checkBox(PluginBundle.get("setting.new.tips"))
158+
.bindSelected(model::apiInToolwindowTop)
159+
}
160+
}.enabledIf(enableSocket.selected)
161+
}
162+
163+
group("Dio Request List") {
164+
twoColumnsRow({
165+
checkBox(PluginBundle.get("display_domain_name"))
166+
.bindSelected(dioSetting::showHost)
167+
}, {
168+
checkBox(PluginBundle.get("display.query.parameters"))
169+
.bindSelected(dioSetting::showQueryParams)
170+
})
171+
172+
twoColumnsRow({
173+
checkBox(PluginBundle.get("show.request.method"))
174+
.bindSelected(dioSetting::showMethod)
175+
}, {
176+
checkBox(PluginBundle.get("display.status.code"))
177+
.bindSelected(dioSetting::showStatusCode)
178+
})
179+
180+
twoColumnsRow({
181+
checkBox(PluginBundle.get("display.time"))
182+
.bindSelected(dioSetting::showTimestamp)
183+
}, {
184+
checkBox(PluginBundle.get("time"))
185+
.bindSelected(dioSetting::showDate)
186+
})
187+
188+
twoColumnsRow({
189+
checkBox(PluginBundle.get("bold.link"))
190+
.bindSelected(dioSetting::urlBold)
191+
}, {
192+
checkBox(PluginBundle.get("dio.setting.show.data.size"))
193+
.bindSelected(dioSetting::showDataSize)
194+
})
195+
196+
row {
197+
comment(Links.generateDocCommit(Links.DIO))
198+
}
199+
}
200+
201+
group(PluginBundle.get("ass.setting.title")) {
202+
lateinit var enableAssetsScan: Cell<JBCheckBox>
203+
204+
row {
205+
enableAssetsScan = checkBox("Enable assets scan")
206+
.bindSelected(model::assetsScanEnable)
207+
}
208+
209+
indent {
210+
row(PluginBundle.get("ass.1")) {
211+
textField()
212+
.bindText(model::assetCompilationTriggerString)
213+
.columns(COLUMNS_MEDIUM)
214+
}
215+
216+
row(PluginBundle.get("ass.3")) {
217+
intTextField()
218+
.bindIntText(model::assetCompilationTriggerLen)
219+
.columns(COLUMNS_SHORT)
220+
}
221+
222+
row(PluginBundle.get("ass.5")) {
223+
textField()
224+
.bindText(model::assetScanFolderName)
225+
.columns(COLUMNS_MEDIUM)
226+
}
227+
}.enabledIf(enableAssetsScan.selected)
228+
229+
row {
230+
comment(Links.generateDocCommit(Links.ACCESS_ICON))
231+
}
232+
}
233+
234+
collapsibleGroup("Advanced") {
235+
group("Copy All Keys") {
236+
row("URL:") { textField().bindText(dioSetting.copyKeys::url).columns(COLUMNS_MEDIUM) }
237+
row("Method:") { textField().bindText(dioSetting.copyKeys::method).columns(COLUMNS_MEDIUM) }
238+
row("Headers:") { textField().bindText(dioSetting.copyKeys::headers).columns(COLUMNS_MEDIUM) }
239+
row("Query params:") { textField().bindText(dioSetting.copyKeys::queryParams).columns(COLUMNS_MEDIUM) }
240+
row("Body:") { textField().bindText(dioSetting.copyKeys::body).columns(COLUMNS_MEDIUM) }
241+
row("Response status code:") { textField().bindText(dioSetting.copyKeys::responseStatusCode).columns(COLUMNS_MEDIUM) }
242+
row("Response:") { textField().bindText(dioSetting.copyKeys::response).columns(COLUMNS_MEDIUM) }
243+
row("Request time:") { textField().bindText(dioSetting.copyKeys::requestTime).columns(COLUMNS_MEDIUM) }
244+
row("Timestamp:") { textField().bindText(dioSetting.copyKeys::timestamp).columns(COLUMNS_MEDIUM) }
245+
row { comment(Links.generateDocCommit(Links.DIO_IMAGE)) }
246+
}
247+
}
248+
}
249+
}
250+
```
251+
252+
## Optional Extraction
253+
254+
The current `PubDevMirrorImageSetting.createPanel(...)` and `createFlutterCheckUrlPanel(...)` create complete rows. For the redesigned layout, split them into smaller cell-level helpers:
255+
256+
```kotlin
257+
fun pubServerComboBox(row: Row, setting: DoxListeningSetting): Cell<ComboBox<DartPubMirrorImage>>
258+
fun flutterCheckComboBox(row: Row, setting: DoxListeningSetting): Cell<ComboBox<FlutterCheckUrlMirrorImage>>
259+
```
260+
261+
This lets the settings page own labels and links while the mirror helper only owns the combo-box model and binding.

src/main/kotlin/shop/itbug/flutterx/config/PluginConfig.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ class PluginSetting : BaseState() {
3434
//启用 freezed 工具
3535
var enableFreezedIntentionActions by property(false)
3636

37+
//在 Project View 中显示 Flutter 平台目录图标
38+
var showFlutterPlatformDirectoryIcons by property(true)
39+
3740

3841
}
3942

src/main/kotlin/shop/itbug/flutterx/icons/MyIcons.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,15 @@ object MyIcons {
2222

2323
val moreHorizontal = IconLoader.getIcon("/icons/moreHorizontal.svg", MyIcons::class.java)
2424

25+
val platformWeb = IconLoader.getIcon("/icons/platforms/web.png", MyIcons::class.java)
26+
val platformWindows = IconLoader.getIcon("/icons/platforms/windows.png", MyIcons::class.java)
27+
val platformLinux = IconLoader.getIcon("/icons/platforms/linux.png", MyIcons::class.java)
28+
val platformMacos = IconLoader.getIcon("/icons/platforms/macos.png", MyIcons::class.java)
29+
val platformAndroid = IconLoader.getIcon("/icons/platforms/android.png", MyIcons::class.java)
30+
val platformIos = IconLoader.getIcon("/icons/platforms/ios.png", MyIcons::class.java)
31+
2532
/// compose
2633
val download = IntelliJIconKey("/icons/new/download.svg", "/icons/new/download.svg", MyIcons::class.java)
2734
val kofi = IntelliJIconKey("/icons/kofi.svg", "/icons/kofi.svg", MyIcons::class.java)
2835
}
2936

30-

src/main/kotlin/shop/itbug/flutterx/inlay/dartfile/DartTypeClickActionHandle.kt

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,27 @@ package shop.itbug.flutterx.inlay.dartfile
33
import com.intellij.codeInsight.hints.declarative.InlayActionHandler
44
import com.intellij.codeInsight.hints.declarative.InlayActionPayload
55
import com.intellij.codeInsight.hints.declarative.PsiPointerInlayActionPayload
6+
import com.intellij.openapi.application.ModalityState
7+
import com.intellij.openapi.application.ReadAction
68
import com.intellij.openapi.editor.event.EditorMouseEvent
9+
import com.intellij.openapi.fileEditor.OpenFileDescriptor
10+
import com.intellij.openapi.project.Project
11+
import com.intellij.openapi.vfs.VirtualFile
712
import com.intellij.psi.PsiElement
13+
import com.intellij.psi.SmartPsiElementPointer
814
import shop.itbug.flutterx.document.getDartElementType
915
import shop.itbug.flutterx.util.MyDartPsiElementUtil
16+
import com.intellij.util.concurrency.AppExecutorUtil
1017

1118
/**
1219
* 处理inlay代码点击 (dart type)
1320
*/
1421
class DartTypeClickActionHandle : InlayActionHandler {
1522

16-
1723
override fun handleClick(e: EditorMouseEvent, payload: InlayActionPayload) {
1824
when (payload) {
1925
is PsiPointerInlayActionPayload -> {
20-
payload.pointer.element?.let {
21-
findUseAge(it)
22-
}
26+
findUseAge(payload.pointer)
2327
}
2428

2529
else -> {}
@@ -37,12 +41,33 @@ class DartTypeClickActionHandle : InlayActionHandler {
3741
// }
3842
// }
3943

40-
//查找类型的定义
41-
private fun findUseAge(element: PsiElement) {
42-
val typeText = element.getDartElementType()
43-
if (typeText != null) {
44-
val findType = MyDartPsiElementUtil.searchClassByText(element.project, typeText)
45-
findType?.navigate(true)
46-
}
44+
// 查找类型的定义
45+
private fun findUseAge(pointer: SmartPsiElementPointer<out PsiElement>) {
46+
val project = pointer.project
47+
ReadAction
48+
.nonBlocking<DartTypeNavigationTarget?> {
49+
if (project.isDisposed) return@nonBlocking null
50+
val element = pointer.element ?: return@nonBlocking null
51+
if (!element.isValid) return@nonBlocking null
52+
val typeText = element.getDartElementType() ?: return@nonBlocking null
53+
val findType = MyDartPsiElementUtil.searchClassByText(project, typeText) ?: return@nonBlocking null
54+
val virtualFile = findType.containingFile?.virtualFile ?: return@nonBlocking null
55+
DartTypeNavigationTarget(project, virtualFile, findType.textOffset)
56+
}
57+
.inSmartMode(project)
58+
.withDocumentsCommitted(project)
59+
.coalesceBy(this, pointer)
60+
.finishOnUiThread(ModalityState.defaultModalityState()) { target ->
61+
if (target != null && !target.project.isDisposed && target.file.isValid) {
62+
OpenFileDescriptor(target.project, target.file, target.offset).navigate(true)
63+
}
64+
}
65+
.submit(AppExecutorUtil.getAppExecutorService())
4766
}
48-
}
67+
68+
private data class DartTypeNavigationTarget(
69+
val project: Project,
70+
val file: VirtualFile,
71+
val offset: Int
72+
)
73+
}

0 commit comments

Comments
 (0)