Files
袁智鸿 6314188835 prepare v1.1.4 release with native bridge and stability cleanups
- align Android/iOS native bridge implementations and audio handling paths
- improve Bluetooth provisioning/verification flow and related error handling
- refactor WebSocket, music, and device utility logic for more stable behavior
- clean up noisy debug logs and normalize comments across Flutter and native code
- update AR view, dance/agent/device pages, and platform integration details
2026-04-28 10:57:01 +08:00

683 lines
23 KiB
Dart

/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:pull_down_button/pull_down_button.dart';
import 'package:stack_chan/app_state.dart';
import 'package:stack_chan/view/home/conversation_page.dart';
import 'package:stack_chan/view/home/mcp_page.dart';
import 'package:stack_chan/view/popup/user_info_page.dart';
import '../popup/xiaozhi_welcome_page.dart';
class Settings extends StatefulWidget {
const Settings({super.key});
@override
State<StatefulWidget> createState() => _SettingsState();
}
class _SettingsState extends State<Settings> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (AppState.shared.isLogin.value) {
AppState.shared.getUserInfo();
AppState.shared.getDeviceInfo();
}
});
}
@override
void dispose() {
if (mounted) {
FocusScope.of(context).unfocus();
}
super.dispose();
}
void _navigateToPage(Widget page) {
Navigator.of(
context,
rootNavigator: true,
).push(CupertinoPageRoute(builder: (context) => page));
}
bool _checkDeviceBinding() {
if (AppState.shared.deviceMac.isEmpty) {
AppState.shared.showBindingDevice(context);
return false;
}
return true;
}
//selectdevicemoreoption
Future<void> showUnbindingPopup() async {
showCupertinoModalPopup(
context: context,
builder: (context) {
return CupertinoActionSheet(
title: Text(AppState.shared.deviceInfo.value?.name ?? "StackChan"),
cancelButton: CupertinoActionSheetAction(
onPressed: () {
Navigator.pop(context);
},
child: Text("Cancel"),
),
actions: [
CupertinoActionSheetAction(
onPressed: () {
Navigator.pop(context);
showCupertinoDialog(
context: context,
builder: (context) {
return CupertinoAlertDialog(
title: const Text('Unbind Confirmation'),
content: const Text(
'Are you sure you want to unbind this device?',
),
actions: [
CupertinoDialogAction(
child: const Text('Cancel'),
onPressed: () {
Navigator.pop(context);
},
),
CupertinoDialogAction(
isDestructiveAction: true,
onPressed: () {
Navigator.pop(context);
AppState.shared.unbindDevice(
AppState.shared.deviceMac,
);
},
child: const Text('Unbind'),
),
],
);
},
);
},
isDestructiveAction: true,
child: Text("Unbind"),
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
backgroundColor: CupertinoColors.systemGroupedBackground.resolveFrom(
context,
),
child: CustomScrollView(
slivers: [
CupertinoSliverNavigationBar(
largeTitle: Text("Settings"),
trailing: CupertinoButton(
padding: .zero,
child: Icon(
CupertinoIcons.person_alt_circle,
color: CupertinoTheme.of(context).primaryColor,
size: 44,
),
onPressed: () {
if (AppState.shared.isLogin.value) {
showCupertinoSheet(
context: context,
showDragHandle: true,
builder: (context) {
return UserInfoPage();
},
);
} else {
AppState.shared.showLoginPopup(context);
}
},
),
),
SliverList.list(
children: [
Padding(
padding: .all(20),
child: Row(
children: [
Image.asset("assets/image1.png", width: 100, height: 100),
Spacer(),
Obx(() {
if (AppState.shared.devices.isEmpty) {
return CupertinoButton(
padding: .all(15),
minimumSize: .zero,
pressedOpacity: 1,
borderRadius: .circular(50),
color: CupertinoColors.secondarySystemFill
.resolveFrom(context),
onPressed: () {
AppState.shared.showBindingDevice(context);
},
child: Row(
mainAxisSize: .min,
spacing: 5,
children: [
Icon(CupertinoIcons.add_circled, size: 18),
ConstrainedBox(
constraints: BoxConstraints(maxWidth: 180),
child: Text(
"Add a new StackChan",
maxLines: 2,
overflow: .visible,
style: TextStyle(
fontSize: 17,
fontWeight: .bold,
),
),
),
],
),
);
} else {
return PullDownButton(
itemBuilder: (context) {
final list = AppState.shared.devices
.map(
(value) => PullDownMenuItem.selectable(
iconWidget: Image.asset(
"assets/image1.png",
),
selected:
value.mac == AppState.shared.deviceMac,
onTap: () {
if (value.mac !=
AppState
.shared
.deviceInfo
.value
?.mac) {
AppState.shared.switchDevice(value);
}
},
title: value.getDisplayName(),
),
)
.toList();
list.add(
PullDownMenuItem(
icon: CupertinoIcons.add_circled,
onTap: () =>
AppState.shared.showBindingDevice(context),
title: "Add a new StackChan",
),
);
return list;
},
buttonBuilder: (context, showMenu) => CupertinoButton(
padding: .all(15),
minimumSize: .zero,
onLongPress: () {
showUnbindingPopup();
},
pressedOpacity: 1,
borderRadius: .circular(50),
color: CupertinoColors.secondarySystemFill
.resolveFrom(context),
onPressed: showMenu,
child: Row(
mainAxisSize: .min,
spacing: 5,
children: [
if (AppState.shared.devices.length > 1)
Icon(CupertinoIcons.chevron_down, size: 25),
ConstrainedBox(
constraints: BoxConstraints(maxWidth: 150),
child: Text(
AppState.shared.deviceInfo.value
?.getDisplayName() ??
"",
style: TextStyle(
fontSize: 18,
fontWeight: .bold,
),
),
),
],
),
),
);
}
}),
],
),
),
//====================== userinfo ======================
// CupertinoListSection.insetGrouped(
// children: [_buildUserProfileTile()],
// ),
// Obx(
// () => CupertinoListSection.insetGrouped(
// header: const Text("Devices"),
// children: _devices(),
// ),
// ),
//====================== set ======================
CupertinoListSection.insetGrouped(
children: [_buildChangeNameTile()],
),
//====================== AIset ======================
CupertinoListSection.insetGrouped(
children: [
_buildAgentConfigTile(),
_buildMcpListTile(),
_buildConversationListTile(),
],
),
//====================== Systemset ======================
CupertinoListSection.insetGrouped(
children: [_unbindResetTitle(), _buildBindDeviceTile()],
),
],
),
],
),
);
}
Widget _buildUserProfileTile() {
return CupertinoListTile(
onTap: () {
if (AppState.shared.isLogin.value) {
showCupertinoSheet(
context: context,
showDragHandle: true,
builder: (context) {
return UserInfoPage();
},
);
} else {
AppState.shared.showLoginPopup(context);
}
},
padding: .symmetric(vertical: 10, horizontal: 20),
title: Obx(() {
final user = AppState.shared.userInfo.value;
final title = AppState.shared.isLogin.value
? (user?.displayName?.isNotEmpty == true
? user!.displayName!
: "Name")
: "Please Login";
return Text(title, style: TextStyle(fontSize: 18, fontWeight: .bold));
}),
leading: ClipOval(
child: Container(
color: CupertinoTheme.of(context).primaryColor,
alignment: Alignment.center,
child: const Icon(
CupertinoIcons.person_fill,
color: CupertinoColors.white,
size: 30,
),
),
),
leadingSize: 50,
trailing: SvgPicture.asset(
"assets/chevron.right.svg",
width: 15,
height: 15,
colorFilter: ColorFilter.mode(
CupertinoColors.secondaryLabel.resolveFrom(context),
BlendMode.srcIn,
),
),
);
}
List<Widget> _devices() {
if (AppState.shared.devices.isNotEmpty) {
return AppState.shared.devices
.map(
(value) => Slidable(
key: Key(value.mac),
endActionPane: ActionPane(
extentRatio: 0.25,
motion: const ScrollMotion(),
children: [
SlidableAction(
onPressed: (_) {
showCupertinoDialog(
context: context,
builder: (context) {
return CupertinoAlertDialog(
title: const Text('Unbind Confirmation'),
content: const Text(
'Are you sure you want to unbind this device?',
),
actions: [
CupertinoDialogAction(
child: const Text('Cancel'),
onPressed: () {
Navigator.pop(context);
},
),
CupertinoDialogAction(
isDestructiveAction: true,
onPressed: () {
Navigator.pop(context);
AppState.shared.unbindDevice(value.mac);
},
child: const Text('Unbind'),
),
],
);
},
);
},
backgroundColor: CupertinoColors.systemOrange.resolveFrom(
context,
),
foregroundColor: CupertinoColors.white,
icon: CupertinoIcons.link,
label: 'Unbind',
),
],
),
child: CupertinoListTile(
leading: Image.asset(
"assets/image1.png",
width: 28,
height: 28,
),
title: Text(value.getDisplayName()),
trailing: value.mac == AppState.shared.deviceMac
? SvgPicture.asset(
"assets/checkmark.svg",
width: 15,
height: 15,
colorFilter: ColorFilter.mode(
CupertinoColors.secondaryLabel.resolveFrom(context),
BlendMode.srcIn,
),
)
: SizedBox.shrink(),
onTap: () {
if (AppState.shared.deviceMac != value.mac) {
AppState.shared.switchDevice(value);
}
},
),
),
)
.toList();
} else {
return [
CupertinoListTile(
title: Center(child: Text("No device bound")),
onTap: () {
AppState.shared.showBindingDevice(context);
},
),
];
}
}
Widget _buildChangeNameTile() {
return CupertinoListTile(
title: const Text("Change Device Name"),
onTap: () => _showChangeNameDialog(),
leading: _buildSectionIcon(
iconPath: "assets/character.svg",
color: CupertinoColors.activeGreen,
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
spacing: 15,
children: [
// Obx(
// () => Text(
// AppState.shared.deviceInfo.value?.name ?? "",
// style: TextStyle(
// color: CupertinoColors.secondaryLabel.resolveFrom(context),
// ),
// ),
// ),
_buildChevronIcon(),
],
),
);
}
Widget _configureWiFi() {
return CupertinoListTile(
title: const Text("Configure Wi-Fi"),
onTap: () {},
leading: _buildSectionIcon(
iconPath: "assets/wifi.svg",
color: CupertinoColors.activeBlue,
),
trailing: _buildChevronIcon(),
);
}
Widget _buildAgentConfigTile() {
return CupertinoListTile(
title: const Text("AI Agent Config"),
onTap: () {
if (_checkDeviceBinding()) {
showCupertinoSheet(
context: context,
builder: (context) {
return XiaoZhiWelcomePage(isWelCome: false);
},
);
// showCupertinoSheet(
// useNestedNavigation: true,
// context: context,
// builder: (context) => const AgentConfiguration(),
// );
}
},
leading: _buildSectionIcon(
iconPath: "assets/rectangle.badge.sparkles.fill.svg",
color: CupertinoColors.activeGreen.resolveFrom(context),
),
trailing: _buildChevronIcon(),
);
}
Widget _buildMcpListTile() {
return CupertinoListTile(
title: const Text("MCP"),
onTap: () {
if (_checkDeviceBinding()) {
showCupertinoSheet(
context: context,
builder: (context) {
return const McpPage();
},
);
}
},
leading: _buildSectionIcon(
iconPath: "assets/network.badge.shield.half.filled.svg",
color: CupertinoColors.activeBlue,
),
trailing: _buildChevronIcon(),
);
}
Widget _buildConversationListTile() {
return CupertinoListTile(
title: const Text("Chat History"),
onTap: () {
if (_checkDeviceBinding()) _navigateToPage(const ConversationPage());
},
leading: _buildSectionIcon(
iconPath: "assets/bubble.left.and.bubble.right.fill.svg",
color: CupertinoColors.activeOrange,
),
trailing: _buildChevronIcon(),
);
}
Widget _unbindResetTitle() {
return PullDownButton(
itemBuilder: (context) => AppState.shared.devices
.map(
(device) => PullDownMenuItem.selectable(
selected: AppState.shared.deviceMac == device.mac,
iconWidget: SvgPicture.asset(
"assets/personalhotspot.slash.svg",
colorFilter: const ColorFilter.mode(
CupertinoColors.destructiveRed,
BlendMode.srcIn,
),
),
onTap: () {
showCupertinoDialog(
context: context,
builder: (context) {
return CupertinoAlertDialog(
title: const Text('Unbind Confirmation'),
content: const Text(
'Are you sure you want to unbind this device?',
),
actions: [
CupertinoDialogAction(
child: const Text('Cancel'),
onPressed: () {
Navigator.pop(context);
},
),
CupertinoDialogAction(
isDestructiveAction: true,
onPressed: () {
Navigator.pop(context);
AppState.shared.unbindDevice(device.mac);
},
child: const Text('Unbind'),
),
],
);
},
);
},
title: device.name ?? device.mac,
),
)
.toList(),
buttonBuilder: (context, showMenu) => CupertinoListTile(
title: const Text("Unbind & Reset"),
onTap: () {
if (_checkDeviceBinding()) showMenu();
},
leading: _buildSectionIcon(
iconPath: "assets/personalhotspot.slash.svg",
color: CupertinoColors.destructiveRed,
),
trailing: _buildChevronIcon(),
),
);
}
Widget _buildBindDeviceTile() {
return CupertinoListTile(
title: const Text("Add a new StackChan"),
onTap: () => AppState.shared.showBindingDevice(context),
leading: _buildSectionIcon(
iconPath: "assets/plus.app.svg",
color: CupertinoColors.systemBlue,
),
trailing: _buildChevronIcon(),
);
}
Widget _buildSectionIcon({required String iconPath, required Color color}) {
return Container(
alignment: Alignment.center,
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(8),
),
width: 28,
height: 28,
padding: const EdgeInsets.all(6),
child: SvgPicture.asset(
iconPath,
colorFilter: const ColorFilter.mode(
CupertinoColors.white,
BlendMode.srcIn,
),
),
);
}
Widget _buildChevronIcon() {
return SvgPicture.asset(
"assets/chevron.right.svg",
width: 15,
height: 15,
colorFilter: ColorFilter.mode(
CupertinoColors.secondaryLabel.resolveFrom(context),
BlendMode.srcIn,
),
);
}
void _showChangeNameDialog() {
if (!_checkDeviceBinding()) return;
String newName = AppState.shared.deviceInfo.value?.name ?? "My StackChan";
showCupertinoDialog(
context: context,
builder: (context) => CupertinoAlertDialog(
title: const Text("Please enter device name"),
content: Padding(
padding: const EdgeInsets.only(top: 10),
child: CupertinoTextField(
controller: TextEditingController(text: newName),
maxLength: 15,
maxLines: 1,
autofocus: true,
inputFormatters: [
//addinputlimit,
LengthLimitingTextInputFormatter(15),
],
onChanged: (value) => newName = value,
),
),
actions: [
CupertinoDialogAction(
onPressed: () => Navigator.pop(context),
child: const Text("Cancel"),
),
CupertinoDialogAction(
isDefaultAction: true,
onPressed: () {
if (newName.trim().isNotEmpty) {
AppState.shared.deviceInfo.value?.name = newName.trim();
AppState.shared.updateDeviceInfo();
}
Navigator.pop(context);
},
child: const Text("Confirm"),
),
],
),
);
}
}