Skip to content

Commit 3a80669

Browse files
authored
feature: support physical android devices (#5)
1 parent f679cec commit 3a80669

17 files changed

Lines changed: 317 additions & 131 deletions

.github/workflows/deploy-homebrew.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ name: Deploy to Homebrew
33
on:
44
release:
55
types: [released]
6-
workflow_call:
76
workflow_dispatch:
87

98
jobs:

.github/workflows/deploy-pub-dev.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
name: Deploy to pub.dev
22

33
on:
4-
workflow_call:
4+
push:
5+
tags:
6+
- 'v*'
57
workflow_dispatch:
68

79
jobs:

.github/workflows/deploy-winget.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
name: Deploy to WinGet
22

33
on:
4-
release:
5-
types: [released]
4+
# release:
5+
# types: [released]
66
workflow_call:
77
workflow_dispatch:
88

.github/workflows/release.yaml

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -103,12 +103,6 @@ jobs:
103103
artifacts/*.zip
104104
artifacts/checksums.txt
105105
106-
# deploy-pub-dev:
107-
# name: Deploy to pub.dev
108-
# needs: release
109-
# uses: ./.github/workflows/deploy-pub-dev.yaml
110-
# secrets: inherit
111-
112106
# deploy-homebrew:
113107
# name: Deploy to Homebrew
114108
# needs: release

CHANGELOG.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,15 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8-
<!-- ## [Unreleased] -->
8+
## [0.1.0] - 2026-03-20
9+
10+
### Added
11+
12+
- Support Android physical devices
13+
14+
### Changed
15+
16+
- Reopen ADB tools menu
917

1018
## [0.0.4] - 2026-03-17
1119

lib/components/device_detail_panel.dart

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,11 @@ class DeviceDetailPanel extends StatelessComponent {
2525
return Column(
2626
crossAxisAlignment: CrossAxisAlignment.start,
2727
children: [
28-
_row(st, 'Name', device.name),
29-
_row(st, 'ID', device.id),
30-
_row(st, 'Platform', device.platform),
31-
_row(st, 'Type', device.os.name),
32-
_row(st, 'State', device.state.label),
28+
_row(st, label: 'Name', value: device.name),
29+
_row(st, label: 'ID', value: device.id),
30+
_row(st, label: 'Platform', value: device.platform),
31+
_row(st, label: 'Type', value: device.os.label),
32+
_row(st, label: 'State', value: device.state.label),
3333
],
3434
);
3535
}
@@ -40,7 +40,11 @@ class DeviceDetailPanel extends StatelessComponent {
4040
);
4141
}
4242

43-
Component _row(SimutilTheme st, String label, String value) {
43+
Component _row(
44+
SimutilTheme st, {
45+
required String label,
46+
required String value,
47+
}) {
4448
return Row(
4549
children: [
4650
SizedBox(width: 12, child: Text(label, style: st.label)),

lib/components/device_list_component.dart

Lines changed: 68 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ import 'package:nocterm/nocterm.dart';
22
import 'package:simutil/components/simutil_icons.dart';
33
import 'package:simutil/components/simutil_theme.dart';
44
import 'package:simutil/models/device.dart';
5+
import 'package:simutil/models/device_type.dart';
56

67
class DeviceListComponent extends StatefulComponent {
78
const DeviceListComponent({
89
super.key,
910
required this.devices,
1011
this.focused = false,
1112
this.selectedIndex = 0,
13+
this.scrollBufferItems = 2,
1214
this.onSelectionChanged,
1315
this.onDeviceLaunch,
1416
this.onDeviceShowOptions,
@@ -20,6 +22,7 @@ class DeviceListComponent extends StatefulComponent {
2022
final List<Device> devices;
2123
final bool focused;
2224
final int selectedIndex;
25+
final int scrollBufferItems;
2326
final ValueChanged<int>? onSelectionChanged;
2427
final ValueChanged<Device>? onDeviceLaunch;
2528
final ValueChanged<Device>? onDeviceShowOptions;
@@ -32,6 +35,14 @@ class DeviceListComponent extends StatefulComponent {
3235
}
3336

3437
class _DeviceListComponentState extends State<DeviceListComponent> {
38+
late final ScrollController _scrollController = ScrollController();
39+
40+
@override
41+
void dispose() {
42+
_scrollController.dispose();
43+
super.dispose();
44+
}
45+
3546
@override
3647
Component build(BuildContext context) {
3748
final st = SimutilTheme.of(context);
@@ -60,6 +71,7 @@ class _DeviceListComponentState extends State<DeviceListComponent> {
6071
focused: component.focused,
6172
onKeyEvent: _handleKeyEvent,
6273
child: ListView.builder(
74+
controller: _scrollController,
6375
itemCount: component.devices.length,
6476
itemBuilder: (context, index) {
6577
final device = component.devices[index];
@@ -76,44 +88,64 @@ class _DeviceListComponentState extends State<DeviceListComponent> {
7688

7789
bool _handleKeyEvent(KeyboardEvent event) {
7890
if (component.devices.isEmpty) return false;
79-
80-
if (event.logicalKey == LogicalKey.arrowUp) {
81-
final newIndex = (component.selectedIndex - 1).clamp(
82-
0,
83-
component.devices.length - 1,
84-
);
85-
component.onSelectionChanged?.call(newIndex);
86-
return true;
91+
switch (event.logicalKey) {
92+
case LogicalKey.arrowUp:
93+
_handleArrowUp();
94+
return true;
95+
case LogicalKey.arrowDown:
96+
_handleArrowDown();
97+
return true;
98+
case LogicalKey.enter:
99+
_handleEnter();
100+
return true;
101+
case LogicalKey.space:
102+
_handleSpace();
103+
return true;
104+
default:
105+
return false;
87106
}
107+
}
88108

89-
if (event.logicalKey == LogicalKey.arrowDown) {
90-
final newIndex = (component.selectedIndex + 1).clamp(
91-
0,
92-
component.devices.length - 1,
93-
);
94-
component.onSelectionChanged?.call(newIndex);
95-
return true;
96-
}
109+
void _handleArrowUp() {
110+
final newIndex = (component.selectedIndex - 1).clamp(
111+
0,
112+
component.devices.length - 1,
113+
);
114+
component.onSelectionChanged?.call(newIndex);
115+
final scrollTarget = (newIndex - component.scrollBufferItems).clamp(
116+
0,
117+
component.devices.length - 1,
118+
);
119+
_scrollController.ensureIndexVisible(index: scrollTarget);
120+
}
97121

98-
if (event.logicalKey == LogicalKey.enter) {
99-
if (component.selectedIndex < component.devices.length) {
100-
component.onDeviceLaunch?.call(
101-
component.devices[component.selectedIndex],
102-
);
103-
}
104-
return true;
105-
}
122+
void _handleArrowDown() {
123+
final newIndex = (component.selectedIndex + 1).clamp(
124+
0,
125+
component.devices.length - 1,
126+
);
127+
component.onSelectionChanged?.call(newIndex);
128+
final scrollTarget = (newIndex + component.scrollBufferItems).clamp(
129+
0,
130+
component.devices.length - 1,
131+
);
132+
_scrollController.ensureIndexVisible(index: scrollTarget);
133+
}
106134

107-
if (event.logicalKey == LogicalKey.space) {
108-
if (component.selectedIndex < component.devices.length) {
109-
component.onDeviceShowOptions?.call(
110-
component.devices[component.selectedIndex],
111-
);
112-
}
113-
return true;
135+
void _handleEnter() {
136+
if (component.selectedIndex < component.devices.length) {
137+
component.onDeviceLaunch?.call(
138+
component.devices[component.selectedIndex],
139+
);
114140
}
141+
}
115142

116-
return false;
143+
void _handleSpace() {
144+
if (component.selectedIndex < component.devices.length) {
145+
component.onDeviceShowOptions?.call(
146+
component.devices[component.selectedIndex],
147+
);
148+
}
117149
}
118150
}
119151

@@ -135,7 +167,10 @@ class _DeviceRow extends StatelessComponent {
135167
child: Text(device.name, style: isSelected ? st.selected : st.body),
136168
),
137169
Text('${device.platform} ', style: st.muted),
138-
Text('${device.state.label} ', style: stateStyle),
170+
if (device.type == DeviceType.simulator)
171+
Text('${device.state.label} ', style: stateStyle)
172+
else
173+
Text('Physical', style: st.muted),
139174
],
140175
);
141176
}

lib/models/device.dart

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import 'package:simutil/models/device_state.dart';
22
import 'package:simutil/models/device_type.dart';
3-
import 'package:simutil/models/os.dart';
3+
import 'package:simutil/models/device_os.dart';
44

55
class Device {
66
const Device({
@@ -12,11 +12,44 @@ class Device {
1212
required this.type,
1313
});
1414

15+
factory Device.android({
16+
required String id,
17+
required String name,
18+
required DeviceState state,
19+
required DeviceType type,
20+
}) {
21+
return Device(
22+
id: id,
23+
name: name,
24+
platform: 'Android',
25+
os: DeviceOs.android,
26+
state: state,
27+
type: type,
28+
);
29+
}
30+
31+
factory Device.ios({
32+
required String id,
33+
required String name,
34+
String? platform,
35+
required DeviceState state,
36+
required DeviceType type,
37+
}) {
38+
return Device(
39+
id: id,
40+
name: name,
41+
platform: platform ?? 'iOS',
42+
os: DeviceOs.ios,
43+
state: state,
44+
type: type,
45+
);
46+
}
47+
1548
factory Device.fromJson(Map<String, dynamic> json) {
1649
return Device(
1750
id: json['id'] as String,
1851
name: json['name'] as String,
19-
os: Os.values.byName(json['type'] as String),
52+
os: DeviceOs.values.byName(json['type'] as String),
2053
platform: json['platform'] as String? ?? '',
2154
state: DeviceState.fromString(json['state'] as String? ?? 'Shutdown'),
2255
type: DeviceType.values.byName(json['type'] as String),
@@ -27,7 +60,7 @@ class Device {
2760

2861
final String name;
2962

30-
final Os os;
63+
final DeviceOs os;
3164

3265
final String platform;
3366

@@ -40,7 +73,7 @@ class Device {
4073
Device copyWith({
4174
String? id,
4275
String? name,
43-
Os? os,
76+
DeviceOs? os,
4477
String? platform,
4578
DeviceState? state,
4679
DeviceType? type,
@@ -59,9 +92,10 @@ class Device {
5992
return {
6093
'id': id,
6194
'name': name,
62-
'type': os.name,
95+
'os': os.name,
6396
'platform': platform,
6497
'state': state.label,
98+
'type': type.name,
6599
};
66100
}
67101

lib/models/device_os.dart

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
enum DeviceOs {
2+
android,
3+
ios;
4+
5+
String get label {
6+
switch (this) {
7+
case DeviceOs.android:
8+
return 'Android';
9+
case DeviceOs.ios:
10+
return 'iOS';
11+
}
12+
}
13+
}

lib/models/device_type.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
enum DeviceType {
22
physical,
33

4-
simulator,
4+
simulator;
5+
6+
bool get isPhysical => this == DeviceType.physical;
7+
8+
bool get isSimulator => this == DeviceType.simulator;
59
}

0 commit comments

Comments
 (0)