Skip to content

Commit 61e9073

Browse files
committed
feature(wireless-discovery): Rewrite Pairing device with pairing code using mDNS like Android Studio (#23)
1 parent b18c1f9 commit 61e9073

24 files changed

Lines changed: 1151 additions & 302 deletions

AGENTS.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ viewer. Entry point: [bin/simutil.dart](bin/simutil.dart). Main app component:
99

1010
## Stack (only the non-obvious bits)
1111

12-
- Dart `^3.11.0`. UI framework is [`nocterm`](https://nocterm.dev/) — a Flutter-like
12+
- Dart `^3.11.0`. UI framework is `[nocterm](https://nocterm.dev/)` — a Flutter-like
1313
component model for terminals (`StatefulComponent`, `BuildContext`, `Focusable`,
1414
`setState`). Treat widgets as Flutter widgets.
1515
- CLI uses `args` `CommandRunner` — see [lib/cli/simutil_command_runner.dart](lib/cli/simutil_command_runner.dart).
@@ -55,10 +55,12 @@ per [build.yaml](build.yaml) — do not hand-edit. CI definition lives in
5555

5656
## When changing code
5757

58-
- Bump [CHANGELOG.md](CHANGELOG.md) under `[Unreleased]` using Keep-a-Changelog
58+
- Bump mainly user-visible changes [CHANGELOG.md](CHANGELOG.md) under `[Unreleased]` using Keep-a-Changelog
5959
sections (`Added` / `Changed` / `Fixed`).
6060
- Follow [.github/PULL_REQUEST_TEMPLATE.md](.github/PULL_REQUEST_TEMPLATE.md) — fill
6161
the description and tick the Type-of-Change checkboxes.
62+
- For UI/dialog code, always split large widget trees into smaller focused components
63+
(prefer reusable `StatelessComponent`/`StatefulComponent` units over monolithic build methods).
6264
- Before finishing: `dart analyze --fatal-infos` must pass.
6365
- More: [docs/ai/contributing.md](docs/ai/contributing.md),
6466
[docs/ai/running_tests.md](docs/ai/running_tests.md),

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.5.0] - 2026-05-02
11+
1012
### Changed
1113

1214
- Add Windows PowerShell installer command (`install.ps1`) to README installation section.
1315

16+
- Refactor Wi-Fi pairing flow to match Android Studio: discover pairing-code endpoints first, require code entry after selecting a discovered device, then resolve and connect to the post-pair ADB connect endpoint.
17+
18+
### Fixed
19+
20+
- Fix text color in dialogs and panels to avoid wrong overlay effect.
21+
1422
## [0.4.1] - 2026-04-11
1523

1624
### Fixed

docs/ai/contributing.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ code change itself.
55

66
## Per-PR checklist
77

8-
1. **Update the changelog.** Add an entry under `[Unreleased]` in
8+
1. **Update the changelog.** Add mainly user-visible changes under `[Unreleased]` in
99
[CHANGELOG.md](../../CHANGELOG.md) using the existing
1010
[Keep-a-Changelog](https://keepachangelog.com/en/1.1.0/) sections
1111
(`Added` / `Changed` / `Fixed` / `Removed`). One bullet per user-visible change.

lib/components/error_dialog.dart

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,9 @@ class ErrorDialog extends StatelessComponent {
3131
return false;
3232
},
3333
child: Container(
34+
width: 100,
3435
margin: EdgeInsets.all(16),
35-
decoration: BoxDecoration(
36-
border: BoxBorder.all(
37-
style: BoxBorderStyle.rounded,
38-
color: st.error,
39-
),
40-
title: BorderTitle(text: title),
41-
color: st.background,
42-
),
36+
decoration: st.errorDialogPanel(title),
4337
child: Padding(
4438
padding: EdgeInsets.all(1),
4539
child: Column(

lib/components/input_dialog.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import 'package:simutil/components/show_overlay_dialog.dart';
55
import 'package:simutil/components/simutil_theme.dart';
66

77
class InputDialog extends StatefulComponent {
8-
98
const InputDialog({
109
super.key,
1110
required this.title,
@@ -64,6 +63,7 @@ class _InputDialogState extends State<InputDialog> {
6463
focused: true,
6564
onKeyEvent: _handleKeyEvent,
6665
child: Container(
66+
constraints: BoxConstraints(minWidth: 50, maxWidth: 120),
6767
margin: EdgeInsets.all(16),
6868
decoration: st.dialogPanel(component.title),
6969
child: Padding(

lib/components/loading_state.dart

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import 'dart:async';
2+
3+
import 'package:nocterm/nocterm.dart';
4+
5+
class LoadingState extends StatefulComponent {
6+
const LoadingState({
7+
this.spinnerFrames = defaultSpinnerFrames,
8+
this.message,
9+
this.duration = defaultDuration,
10+
this.style = const TextStyle(fontWeight: FontWeight.dim),
11+
});
12+
13+
final List<String> spinnerFrames;
14+
final String? message;
15+
final Duration duration;
16+
final TextStyle style;
17+
18+
static const defaultDuration = Duration(milliseconds: 150);
19+
20+
static const defaultSpinnerFrames = [
21+
'⠋',
22+
'⠙',
23+
'⠹',
24+
'⠸',
25+
'⠼',
26+
'⠴',
27+
'⠦',
28+
'⠧',
29+
'⠇',
30+
'⠏',
31+
];
32+
33+
@override
34+
State<LoadingState> createState() => _LoadingState();
35+
}
36+
37+
class _LoadingState extends State<LoadingState> {
38+
Timer? _spinnerTimer;
39+
int _spinnerIndex = 0;
40+
41+
@override
42+
void initState() {
43+
super.initState();
44+
_spinnerTimer = Timer.periodic(component.duration, (_) {
45+
setState(() {
46+
_spinnerIndex = (_spinnerIndex + 1) % component.spinnerFrames.length;
47+
});
48+
});
49+
}
50+
51+
@override
52+
void dispose() {
53+
_spinnerTimer?.cancel();
54+
super.dispose();
55+
}
56+
57+
@override
58+
Component build(BuildContext context) {
59+
final message = component.message;
60+
final spinner = component.spinnerFrames[_spinnerIndex];
61+
final text = message != null ? '$spinner $message' : spinner;
62+
return Text(text, style: component.style);
63+
}
64+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import 'package:nocterm/nocterm.dart';
2+
import 'package:simutil/components/simutil_theme.dart';
3+
4+
class PinCodeFields extends StatelessComponent {
5+
const PinCodeFields({
6+
required this.label,
7+
required this.groupFocused,
8+
this.crossAxisAlignment = CrossAxisAlignment.center,
9+
this.spacing = 1.0,
10+
this.cellSpacing = 1.0,
11+
required this.pinControllers,
12+
required this.focusedPinIndex,
13+
required this.onPinChanged,
14+
required this.onPinKeyEvent,
15+
required this.onSubmitted,
16+
});
17+
18+
final String label;
19+
final bool groupFocused;
20+
final CrossAxisAlignment crossAxisAlignment;
21+
final double spacing;
22+
final double cellSpacing;
23+
final List<TextEditingController> pinControllers;
24+
final int focusedPinIndex;
25+
final void Function(int index, String value) onPinChanged;
26+
final bool Function(int index, KeyboardEvent event) onPinKeyEvent;
27+
final VoidCallback onSubmitted;
28+
29+
@override
30+
Component build(BuildContext context) {
31+
final st = context.simutilTheme;
32+
return Column(
33+
crossAxisAlignment: crossAxisAlignment,
34+
mainAxisSize: MainAxisSize.min,
35+
children: [
36+
Text(' $label', style: groupFocused ? st.label : st.body),
37+
SizedBox(height: spacing),
38+
Row(
39+
mainAxisSize: MainAxisSize.min,
40+
children: [
41+
Text(' ', style: st.body),
42+
...List.generate(pinControllers.length, (index) {
43+
return Row(
44+
children: [
45+
_PinCell(
46+
controller: pinControllers[index],
47+
focused: groupFocused && focusedPinIndex == index,
48+
onChanged: (value) => onPinChanged(index, value),
49+
onSubmitted: onSubmitted,
50+
onKeyEvent: (event) => onPinKeyEvent(index, event),
51+
),
52+
if (index < pinControllers.length - 1)
53+
SizedBox(width: cellSpacing),
54+
],
55+
);
56+
}),
57+
],
58+
),
59+
],
60+
);
61+
}
62+
}
63+
64+
class _PinCell extends StatelessComponent {
65+
const _PinCell({
66+
required this.controller,
67+
required this.focused,
68+
required this.onChanged,
69+
required this.onSubmitted,
70+
required this.onKeyEvent,
71+
});
72+
73+
final TextEditingController controller;
74+
final bool focused;
75+
final void Function(String value) onChanged;
76+
final VoidCallback onSubmitted;
77+
final bool Function(KeyboardEvent event) onKeyEvent;
78+
79+
@override
80+
Component build(BuildContext context) {
81+
final st = context.simutilTheme;
82+
return Container(
83+
width: 5,
84+
height: 3,
85+
child: TextField(
86+
controller: controller,
87+
focused: focused,
88+
placeholder: '',
89+
placeholderStyle: st.dimmed,
90+
style: TextStyle(fontWeight: FontWeight.bold, color: st.onSurface),
91+
showCursor: false,
92+
textAlign: TextAlign.center,
93+
onChanged: onChanged,
94+
onSubmitted: (_) => onSubmitted(),
95+
onKeyEvent: onKeyEvent,
96+
decoration: InputDecoration(
97+
border: BoxBorder.all(
98+
style: BoxBorderStyle.rounded,
99+
color: st.outline,
100+
),
101+
focusedBorder: BoxBorder.all(
102+
style: BoxBorderStyle.rounded,
103+
color: st.primary,
104+
),
105+
contentPadding: EdgeInsets.zero,
106+
),
107+
),
108+
);
109+
}
110+
}

lib/components/simutil_theme.dart

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,14 @@ class SimutilTheme {
3333

3434
TextStyle get body => const TextStyle();
3535

36-
TextStyle get dimmed => const TextStyle(fontWeight: FontWeight.dim);
36+
TextStyle get dimmed =>
37+
const TextStyle(fontWeight: FontWeight.dim, color: Color.defaultColor);
3738

38-
TextStyle get bold => const TextStyle(fontWeight: FontWeight.bold);
39+
TextStyle get bold =>
40+
const TextStyle(fontWeight: FontWeight.bold, color: Color.defaultColor);
3941

40-
TextStyle get selected => const TextStyle(reverse: true);
42+
TextStyle get selected =>
43+
const TextStyle(reverse: true, color: Color.defaultColor);
4144

4245
TextStyle get label => TextStyle(color: primary);
4346

@@ -75,6 +78,12 @@ class SimutilTheme {
7578
color: Color.defaultColor
7679
);
7780

81+
BoxDecoration errorDialogPanel(String title) => BoxDecoration(
82+
border: BoxBorder.all(style: BoxBorderStyle.rounded, color: error),
83+
title: BorderTitle(text: title),
84+
color: Color.defaultColor
85+
);
86+
7887
static TuiThemeData resolveTheme(String name) {
7988
return switch (name) {
8089
'light' => TuiThemeData.light,

lib/models/adb_connect_result.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
class AdbConnectResult {
2+
const AdbConnectResult({required this.success, required this.message});
3+
4+
final bool success;
5+
final String message;
6+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
class WifiPairingDevice {
2+
const WifiPairingDevice({
3+
required this.name,
4+
required this.host,
5+
required this.port,
6+
});
7+
8+
final String name;
9+
final String host;
10+
final int port;
11+
12+
String get hostPort => '$host:$port';
13+
}

0 commit comments

Comments
 (0)