feat(mobile): redesign mobile UX and iOS native experience
This commit is contained in:
parent
31990b30d9
commit
f8facadca8
@ -160,11 +160,24 @@ String? assistantWorkingDirectoryForSessionRuntimeInternal(
|
|||||||
AppController controller,
|
AppController controller,
|
||||||
String sessionKey,
|
String sessionKey,
|
||||||
) {
|
) {
|
||||||
return resolveLocalAssistantWorkingDirectoryForSessionRuntimeInternal(
|
final localWorkingDirectory =
|
||||||
controller,
|
resolveLocalAssistantWorkingDirectoryForSessionRuntimeInternal(
|
||||||
sessionKey,
|
controller,
|
||||||
requireLocalExistence: false,
|
sessionKey,
|
||||||
);
|
requireLocalExistence: false,
|
||||||
|
);
|
||||||
|
if (localWorkingDirectory?.trim().isNotEmpty == true) {
|
||||||
|
return localWorkingDirectory;
|
||||||
|
}
|
||||||
|
final record = controller.taskThreadForSessionInternal(sessionKey);
|
||||||
|
if (record?.workspaceKind != WorkspaceKind.remoteFs) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final remoteWorkingDirectory = record?.workspaceBinding.workspacePath.trim();
|
||||||
|
if (remoteWorkingDirectory?.isNotEmpty != true) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return remoteWorkingDirectory;
|
||||||
}
|
}
|
||||||
|
|
||||||
String? assistantRemoteWorkingDirectoryHintForSessionRuntimeInternal(
|
String? assistantRemoteWorkingDirectoryHintForSessionRuntimeInternal(
|
||||||
|
|||||||
@ -203,12 +203,28 @@ extension AppControllerDesktopThreadBinding on AppController {
|
|||||||
required ThreadOwnerScope ownerScope,
|
required ThreadOwnerScope ownerScope,
|
||||||
WorkspaceBinding? existingBinding,
|
WorkspaceBinding? existingBinding,
|
||||||
}) {
|
}) {
|
||||||
final localPath = localThreadWorkspacePathInternal(sessionKey);
|
final normalizedSessionKey = normalizedAssistantSessionKeyInternal(
|
||||||
final displayPath = localPath.isEmpty
|
sessionKey,
|
||||||
? ''
|
);
|
||||||
: localThreadWorkspaceDisplayPathInternal(sessionKey);
|
final localPath = localThreadWorkspacePathInternal(normalizedSessionKey);
|
||||||
|
if (localPath.isEmpty) {
|
||||||
|
final remotePath = remoteThreadWorkspacePathInternal(
|
||||||
|
normalizedSessionKey,
|
||||||
|
ownerScope,
|
||||||
|
);
|
||||||
|
return WorkspaceBinding(
|
||||||
|
workspaceId: normalizedSessionKey,
|
||||||
|
workspaceKind: WorkspaceKind.remoteFs,
|
||||||
|
workspacePath: remotePath,
|
||||||
|
displayPath: remotePath,
|
||||||
|
writable: existingBinding?.writable ?? true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final displayPath = localThreadWorkspaceDisplayPathInternal(
|
||||||
|
normalizedSessionKey,
|
||||||
|
);
|
||||||
return WorkspaceBinding(
|
return WorkspaceBinding(
|
||||||
workspaceId: normalizedAssistantSessionKeyInternal(sessionKey),
|
workspaceId: normalizedSessionKey,
|
||||||
workspaceKind: WorkspaceKind.localFs,
|
workspaceKind: WorkspaceKind.localFs,
|
||||||
workspacePath: localPath,
|
workspacePath: localPath,
|
||||||
displayPath: displayPath,
|
displayPath: displayPath,
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../features/assistant/assistant_page.dart';
|
import '../features/assistant/assistant_page.dart';
|
||||||
import '../features/mobile/mobile_assistant_page.dart';
|
import '../features/mobile/mobile_assistant_nav_page.dart';
|
||||||
import '../features/mobile/mobile_settings_page.dart';
|
import '../features/mobile/mobile_settings_page.dart';
|
||||||
import '../features/settings/settings_page.dart';
|
import '../features/settings/settings_page.dart';
|
||||||
import '../models/app_models.dart';
|
import '../models/app_models.dart';
|
||||||
@ -52,7 +52,7 @@ final Map<WorkspaceDestination, WorkspacePageSpec> workspacePageSpecsInternal =
|
|||||||
showStandaloneTaskRail: false,
|
showStandaloneTaskRail: false,
|
||||||
),
|
),
|
||||||
mobileBuilder: (controller, onOpenDetail, mobileActions) =>
|
mobileBuilder: (controller, onOpenDetail, mobileActions) =>
|
||||||
MobileAssistantPage(
|
MobileAssistantNavPage(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
onOpenDetail: onOpenDetail,
|
onOpenDetail: onOpenDetail,
|
||||||
mobileActions: mobileActions,
|
mobileActions: mobileActions,
|
||||||
|
|||||||
250
lib/features/mobile/mobile_assistant_list_page.dart
Normal file
250
lib/features/mobile/mobile_assistant_list_page.dart
Normal file
@ -0,0 +1,250 @@
|
|||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../../app/app_controller.dart';
|
||||||
|
import '../../app/app_controller_desktop_thread_binding.dart';
|
||||||
|
import '../../app/ui_feature_manifest.dart';
|
||||||
|
import '../../i18n/app_language.dart';
|
||||||
|
import '../../theme/app_palette.dart';
|
||||||
|
|
||||||
|
class MobileAssistantListPage extends StatefulWidget {
|
||||||
|
const MobileAssistantListPage({
|
||||||
|
super.key,
|
||||||
|
required this.controller,
|
||||||
|
required this.onSelectTask,
|
||||||
|
});
|
||||||
|
|
||||||
|
final AppController controller;
|
||||||
|
final ValueChanged<String> onSelectTask;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MobileAssistantListPage> createState() => _MobileAssistantListPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MobileAssistantListPageState extends State<MobileAssistantListPage> {
|
||||||
|
final TextEditingController _searchController = TextEditingController();
|
||||||
|
String _searchQuery = '';
|
||||||
|
bool _isSearchVisible = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_searchController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleCreateTask() async {
|
||||||
|
final uiFeatures = widget.controller.featuresFor(
|
||||||
|
resolveUiFeaturePlatformFromContext(context),
|
||||||
|
);
|
||||||
|
final visibleExecutionTargets = widget.controller
|
||||||
|
.visibleAssistantExecutionTargets(
|
||||||
|
uiFeatures.availableExecutionTargets,
|
||||||
|
);
|
||||||
|
|
||||||
|
final sessionKey = widget.controller.createAssistantDraftSessionKeyInternal();
|
||||||
|
final target = pickDraftThreadExecutionTargetInternal(
|
||||||
|
currentTarget: widget.controller.currentAssistantExecutionTarget,
|
||||||
|
visibleTargets: visibleExecutionTargets,
|
||||||
|
localWorkspaceAvailable: widget.controller.settings.workspacePath.trim().isNotEmpty,
|
||||||
|
);
|
||||||
|
widget.controller.initializeAssistantThreadContext(
|
||||||
|
sessionKey,
|
||||||
|
title: appText('新对话', 'New conversation'),
|
||||||
|
executionTarget: target,
|
||||||
|
messageViewMode: widget.controller.currentAssistantMessageViewMode,
|
||||||
|
);
|
||||||
|
widget.onSelectTask(sessionKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AnimatedBuilder(
|
||||||
|
animation: widget.controller,
|
||||||
|
builder: (context, _) {
|
||||||
|
final palette = context.palette;
|
||||||
|
|
||||||
|
var sessions = widget.controller.assistantSessions;
|
||||||
|
if (_searchQuery.isNotEmpty) {
|
||||||
|
final q = _searchQuery.toLowerCase();
|
||||||
|
sessions = sessions.where((s) {
|
||||||
|
final titleMatch = s.label.toLowerCase().contains(q);
|
||||||
|
final previewMatch = (s.lastMessagePreview ?? '').toLowerCase().contains(q);
|
||||||
|
return titleMatch || previewMatch;
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: palette.canvas,
|
||||||
|
body: CustomScrollView(
|
||||||
|
physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()),
|
||||||
|
slivers: [
|
||||||
|
CupertinoSliverNavigationBar(
|
||||||
|
backgroundColor: palette.canvas.withValues(alpha: 0.8),
|
||||||
|
largeTitle: const Text('XWorkmate'),
|
||||||
|
border: null,
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
CupertinoButton(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_isSearchVisible = !_isSearchVisible;
|
||||||
|
if (!_isSearchVisible) {
|
||||||
|
_searchController.clear();
|
||||||
|
_searchQuery = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: Icon(
|
||||||
|
CupertinoIcons.search,
|
||||||
|
color: palette.textPrimary,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
CupertinoButton(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
onPressed: () {
|
||||||
|
// TODO: Open settings or profile
|
||||||
|
},
|
||||||
|
child: CircleAvatar(
|
||||||
|
radius: 14,
|
||||||
|
backgroundColor: palette.accent,
|
||||||
|
child: const Text('X', style: TextStyle(color: Colors.white, fontSize: 12)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_isSearchVisible)
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||||
|
child: CupertinoSearchTextField(
|
||||||
|
controller: _searchController,
|
||||||
|
placeholder: appText('搜索任务', 'Search tasks'),
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_searchQuery = value.trim();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
style: TextStyle(color: palette.textPrimary),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 8.0),
|
||||||
|
child: Text(
|
||||||
|
appText('最近', 'Recent'),
|
||||||
|
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||||
|
color: palette.textSecondary,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (sessions.isEmpty)
|
||||||
|
SliverFillRemaining(
|
||||||
|
hasScrollBody: false,
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
appText('暂无任务', 'No tasks found'),
|
||||||
|
style: TextStyle(color: palette.textSecondary),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
SliverList(
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(context, index) {
|
||||||
|
final session = sessions[index];
|
||||||
|
final sessionKey = session.key.trim();
|
||||||
|
final pending = widget.controller.assistantSessionHasPendingRun(sessionKey);
|
||||||
|
|
||||||
|
final title = session.label.trim().isEmpty
|
||||||
|
? appText('新对话', 'New conversation')
|
||||||
|
: session.label.trim();
|
||||||
|
final preview = session.lastMessagePreview?.trim() ?? '';
|
||||||
|
|
||||||
|
return Dismissible(
|
||||||
|
key: ValueKey(sessionKey),
|
||||||
|
direction: DismissDirection.endToStart,
|
||||||
|
background: Container(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
padding: const EdgeInsets.only(right: 20.0),
|
||||||
|
color: palette.warning,
|
||||||
|
child: const Icon(CupertinoIcons.archivebox, color: Colors.white),
|
||||||
|
),
|
||||||
|
onDismissed: (direction) {
|
||||||
|
widget.controller.saveAssistantTaskArchived(sessionKey, true);
|
||||||
|
},
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () => widget.onSelectTask(sessionKey),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
CircleAvatar(
|
||||||
|
backgroundColor: pending ? palette.accentMuted : palette.surfacePrimary,
|
||||||
|
child: Icon(
|
||||||
|
pending ? CupertinoIcons.bolt_fill : CupertinoIcons.chat_bubble_2,
|
||||||
|
color: pending ? palette.accent : palette.textSecondary,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: palette.textPrimary,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
preview.isEmpty ? appText('未开始', 'Not started') : preview,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
|
color: palette.textSecondary,
|
||||||
|
),
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
childCount: sessions.length,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
floatingActionButton: FloatingActionButton.extended(
|
||||||
|
key: const Key('mobile-assistant-fab-create'),
|
||||||
|
onPressed: _handleCreateTask,
|
||||||
|
backgroundColor: palette.accent,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
elevation: 4,
|
||||||
|
icon: const Icon(CupertinoIcons.add),
|
||||||
|
label: Text(
|
||||||
|
appText('聊天', 'Chat'),
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
66
lib/features/mobile/mobile_assistant_nav_page.dart
Normal file
66
lib/features/mobile/mobile_assistant_nav_page.dart
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
|
||||||
|
import '../../app/app_controller.dart';
|
||||||
|
import '../../app/workspace_page_registry.dart';
|
||||||
|
import '../../models/app_models.dart';
|
||||||
|
import 'mobile_assistant_list_page.dart';
|
||||||
|
import 'mobile_assistant_page_core.dart';
|
||||||
|
|
||||||
|
class MobileAssistantNavPage extends StatefulWidget {
|
||||||
|
const MobileAssistantNavPage({
|
||||||
|
super.key,
|
||||||
|
required this.controller,
|
||||||
|
required this.onOpenDetail,
|
||||||
|
this.mobileActions = const MobileWorkspaceActions(),
|
||||||
|
});
|
||||||
|
|
||||||
|
final AppController controller;
|
||||||
|
final ValueChanged<DetailPanelData> onOpenDetail;
|
||||||
|
final MobileWorkspaceActions mobileActions;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MobileAssistantNavPage> createState() => _MobileAssistantNavPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MobileAssistantNavPageState extends State<MobileAssistantNavPage> {
|
||||||
|
final GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return KeyedSubtree(
|
||||||
|
key: const Key('mobile-assistant-page'),
|
||||||
|
child: Navigator(
|
||||||
|
key: _navigatorKey,
|
||||||
|
initialRoute: '/',
|
||||||
|
onGenerateRoute: (RouteSettings settings) {
|
||||||
|
WidgetBuilder builder;
|
||||||
|
switch (settings.name) {
|
||||||
|
case '/':
|
||||||
|
builder = (BuildContext context) => MobileAssistantListPage(
|
||||||
|
controller: widget.controller,
|
||||||
|
onSelectTask: (sessionKey) async {
|
||||||
|
await widget.controller.switchSession(sessionKey);
|
||||||
|
if (!mounted) return;
|
||||||
|
_navigatorKey.currentState?.pushNamed('/detail');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case '/detail':
|
||||||
|
builder = (BuildContext context) => MobileAssistantDetailPage(
|
||||||
|
controller: widget.controller,
|
||||||
|
onOpenDetail: widget.onOpenDetail,
|
||||||
|
mobileActions: widget.mobileActions,
|
||||||
|
onBack: () => _navigatorKey.currentState?.pop(),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
builder = (BuildContext context) => const SizedBox();
|
||||||
|
}
|
||||||
|
return CupertinoPageRoute(
|
||||||
|
builder: builder,
|
||||||
|
settings: settings,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -47,91 +47,100 @@ class MobileAssistantComposer extends StatelessWidget {
|
|||||||
final hasPendingRun =
|
final hasPendingRun =
|
||||||
controller.hasAssistantPendingRun || controller.activeRunId != null;
|
controller.hasAssistantPendingRun || controller.activeRunId != null;
|
||||||
|
|
||||||
return DecoratedBox(
|
return Padding(
|
||||||
key: const Key('mobile-assistant-composer'),
|
key: const Key('mobile-assistant-composer'),
|
||||||
decoration: BoxDecoration(
|
padding: EdgeInsets.fromLTRB(12, 8, 12, bottomPadding == 0 ? 12 : bottomPadding),
|
||||||
color: palette.surfacePrimary,
|
child: Column(
|
||||||
border: Border(top: BorderSide(color: palette.strokeSoft)),
|
mainAxisSize: MainAxisSize.min,
|
||||||
),
|
children: [
|
||||||
child: Padding(
|
Row(
|
||||||
padding: EdgeInsets.fromLTRB(10, 8, 10, bottomPadding),
|
children: [
|
||||||
child: Column(
|
Expanded(
|
||||||
mainAxisSize: MainAxisSize.min,
|
child: SingleChildScrollView(
|
||||||
children: [
|
scrollDirection: Axis.horizontal,
|
||||||
Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
MobileAssistantActionChip(
|
||||||
child: SingleChildScrollView(
|
key: const Key('mobile-assistant-target-button'),
|
||||||
scrollDirection: Axis.horizontal,
|
icon: target.isGateway
|
||||||
child: Row(
|
? Icons.cloud_queue_rounded
|
||||||
children: [
|
: Icons.smart_toy_outlined,
|
||||||
MobileAssistantActionChip(
|
label: target.compactLabel,
|
||||||
key: const Key('mobile-assistant-target-button'),
|
onTap: () => showMobileAssistantTargetSheet(
|
||||||
icon: target.isGateway
|
context,
|
||||||
? Icons.cloud_queue_rounded
|
controller: controller,
|
||||||
: Icons.smart_toy_outlined,
|
onSelected: onSetExecutionTarget,
|
||||||
label: target.compactLabel,
|
|
||||||
onTap: () => showMobileAssistantTargetSheet(
|
|
||||||
context,
|
|
||||||
controller: controller,
|
|
||||||
onSelected: onSetExecutionTarget,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 6),
|
),
|
||||||
MobileAssistantActionChip(
|
const SizedBox(width: 6),
|
||||||
key: const Key('mobile-assistant-provider-button'),
|
MobileAssistantActionChip(
|
||||||
icon: Icons.hub_outlined,
|
key: const Key('mobile-assistant-provider-button'),
|
||||||
label: providerLabel,
|
icon: Icons.hub_outlined,
|
||||||
onTap: () => showMobileAssistantProviderSheet(
|
label: providerLabel,
|
||||||
context,
|
onTap: () => showMobileAssistantProviderSheet(
|
||||||
controller: controller,
|
context,
|
||||||
target: target,
|
controller: controller,
|
||||||
selectedProvider: provider,
|
target: target,
|
||||||
onSelected: onSetProvider,
|
selectedProvider: provider,
|
||||||
),
|
onSelected: onSetProvider,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 6),
|
),
|
||||||
MobileAssistantActionChip(
|
const SizedBox(width: 6),
|
||||||
key: const Key('mobile-assistant-permission-button'),
|
MobileAssistantActionChip(
|
||||||
icon: mobilePermissionIcon(
|
key: const Key('mobile-assistant-permission-button'),
|
||||||
controller.assistantPermissionLevel,
|
icon: mobilePermissionIcon(
|
||||||
),
|
controller.assistantPermissionLevel,
|
||||||
label: controller.assistantPermissionLevel.label,
|
|
||||||
onTap: () => showMobileAssistantPermissionSheet(
|
|
||||||
context,
|
|
||||||
controller: controller,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 6),
|
label: controller.assistantPermissionLevel.label,
|
||||||
MobileAssistantActionChip(
|
onTap: () => showMobileAssistantPermissionSheet(
|
||||||
key: const Key('mobile-assistant-thinking-button'),
|
context,
|
||||||
icon: Icons.psychology_alt_outlined,
|
controller: controller,
|
||||||
label: mobileThinkingLabel(thinking),
|
|
||||||
onTap: () => showMobileAssistantThinkingSheet(
|
|
||||||
context,
|
|
||||||
value: thinking,
|
|
||||||
onSelected: onThinkingChanged,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
const SizedBox(width: 6),
|
||||||
|
MobileAssistantActionChip(
|
||||||
|
key: const Key('mobile-assistant-thinking-button'),
|
||||||
|
icon: Icons.psychology_alt_outlined,
|
||||||
|
label: mobileThinkingLabel(thinking),
|
||||||
|
onTap: () => showMobileAssistantThinkingSheet(
|
||||||
|
context,
|
||||||
|
value: thinking,
|
||||||
|
onSelected: onThinkingChanged,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (hasPendingRun) ...[
|
),
|
||||||
const SizedBox(width: 8),
|
if (hasPendingRun) ...[
|
||||||
IconButton.filledTonal(
|
const SizedBox(width: 8),
|
||||||
key: const Key('mobile-assistant-stop-button'),
|
IconButton.filledTonal(
|
||||||
onPressed: () => unawaited(controller.abortRun()),
|
key: const Key('mobile-assistant-stop-button'),
|
||||||
icon: const Icon(Icons.stop_rounded),
|
onPressed: () => unawaited(controller.abortRun()),
|
||||||
tooltip: appText('停止运行', 'Stop Run'),
|
icon: const Icon(Icons.stop_rounded),
|
||||||
),
|
tooltip: appText('停止运行', 'Stop Run'),
|
||||||
],
|
),
|
||||||
],
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: palette.surfaceSecondary,
|
||||||
|
borderRadius: BorderRadius.circular(26),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
child: Row(
|
||||||
Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 6, bottom: 4),
|
||||||
|
child: IconButton(
|
||||||
|
icon: Icon(Icons.add, color: palette.textSecondary),
|
||||||
|
onPressed: () {
|
||||||
|
// 预留功能位
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: const BoxConstraints(
|
constraints: const BoxConstraints(
|
||||||
@ -146,52 +155,36 @@ class MobileAssistantComposer extends StatelessWidget {
|
|||||||
maxLines: 4,
|
maxLines: 4,
|
||||||
textInputAction: TextInputAction.newline,
|
textInputAction: TextInputAction.newline,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
filled: true,
|
|
||||||
fillColor: palette.surfaceSecondary,
|
|
||||||
hintText: appText(
|
hintText: appText(
|
||||||
'输入任务或补充上下文',
|
'询问 XWorkmate...',
|
||||||
'Type a task or context',
|
'Ask XWorkmate...',
|
||||||
),
|
|
||||||
contentPadding: const EdgeInsets.fromLTRB(
|
|
||||||
12,
|
|
||||||
10,
|
|
||||||
12,
|
|
||||||
10,
|
|
||||||
),
|
|
||||||
enabledBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(AppRadius.input),
|
|
||||||
borderSide: BorderSide(color: palette.strokeSoft),
|
|
||||||
),
|
|
||||||
focusedBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(AppRadius.input),
|
|
||||||
borderSide: BorderSide(
|
|
||||||
color: palette.accent.withValues(alpha: 0.32),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
hintStyle: TextStyle(color: palette.textMuted),
|
||||||
|
border: InputBorder.none,
|
||||||
|
enabledBorder: InputBorder.none,
|
||||||
|
focusedBorder: InputBorder.none,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
Padding(
|
||||||
SizedBox(
|
padding: const EdgeInsets.only(right: 6, bottom: 6),
|
||||||
width: 48,
|
child: CircleAvatar(
|
||||||
height: 48,
|
radius: 18,
|
||||||
child: FilledButton(
|
backgroundColor: palette.accent,
|
||||||
key: const Key('mobile-assistant-send-button'),
|
child: IconButton(
|
||||||
onPressed: onSend,
|
key: const Key('mobile-assistant-send-button'),
|
||||||
style: FilledButton.styleFrom(
|
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
shape: RoundedRectangleBorder(
|
icon: const Icon(Icons.arrow_upward_rounded, color: Colors.white, size: 20),
|
||||||
borderRadius: BorderRadius.circular(AppRadius.button),
|
onPressed: onSend,
|
||||||
),
|
|
||||||
),
|
),
|
||||||
child: const Icon(Icons.arrow_upward_rounded),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
import '../../app/app_controller.dart';
|
import '../../app/app_controller.dart';
|
||||||
import '../../app/workspace_page_registry.dart';
|
import '../../app/workspace_page_registry.dart';
|
||||||
@ -12,29 +14,33 @@ import '../../theme/app_palette.dart';
|
|||||||
import '../../theme/app_theme.dart';
|
import '../../theme/app_theme.dart';
|
||||||
import 'mobile_assistant_page_composer.dart';
|
import 'mobile_assistant_page_composer.dart';
|
||||||
import 'mobile_assistant_page_conversation.dart';
|
import 'mobile_assistant_page_conversation.dart';
|
||||||
|
import 'mobile_workspace_files_page.dart';
|
||||||
|
|
||||||
class MobileAssistantPage extends StatefulWidget {
|
class MobileAssistantDetailPage extends StatefulWidget {
|
||||||
const MobileAssistantPage({
|
const MobileAssistantDetailPage({
|
||||||
super.key,
|
super.key,
|
||||||
required this.controller,
|
required this.controller,
|
||||||
required this.onOpenDetail,
|
required this.onOpenDetail,
|
||||||
|
required this.onBack,
|
||||||
this.mobileActions = const MobileWorkspaceActions(),
|
this.mobileActions = const MobileWorkspaceActions(),
|
||||||
});
|
});
|
||||||
|
|
||||||
final AppController controller;
|
final AppController controller;
|
||||||
final ValueChanged<DetailPanelData> onOpenDetail;
|
final ValueChanged<DetailPanelData> onOpenDetail;
|
||||||
|
final VoidCallback onBack;
|
||||||
final MobileWorkspaceActions mobileActions;
|
final MobileWorkspaceActions mobileActions;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<MobileAssistantPage> createState() => _MobileAssistantPageState();
|
State<MobileAssistantDetailPage> createState() => _MobileAssistantDetailPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MobileAssistantPageState extends State<MobileAssistantPage> {
|
class _MobileAssistantDetailPageState extends State<MobileAssistantDetailPage> {
|
||||||
late final TextEditingController inputController;
|
late final TextEditingController inputController;
|
||||||
late final ScrollController conversationController;
|
late final ScrollController conversationController;
|
||||||
late final FocusNode inputFocusNode;
|
late final FocusNode inputFocusNode;
|
||||||
String thinking = 'medium';
|
String thinking = 'medium';
|
||||||
String lastScrollSignature = '';
|
String lastScrollSignature = '';
|
||||||
|
int _segmentedIndex = 0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -70,12 +76,14 @@ class _MobileAssistantPageState extends State<MobileAssistantPage> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
inputController.clear();
|
inputController.clear();
|
||||||
|
HapticFeedback.lightImpact();
|
||||||
try {
|
try {
|
||||||
await widget.controller.sendChatMessage(text, thinking: thinking);
|
await widget.controller.sendChatMessage(text, thinking: thinking);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
HapticFeedback.heavyImpact();
|
||||||
ScaffoldMessenger.maybeOf(
|
ScaffoldMessenger.maybeOf(
|
||||||
context,
|
context,
|
||||||
)?.showSnackBar(SnackBar(content: Text(error.toString())));
|
)?.showSnackBar(SnackBar(content: Text(error.toString())));
|
||||||
@ -83,6 +91,7 @@ class _MobileAssistantPageState extends State<MobileAssistantPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setExecutionTarget(AssistantExecutionTarget target) async {
|
Future<void> setExecutionTarget(AssistantExecutionTarget target) async {
|
||||||
|
HapticFeedback.selectionClick();
|
||||||
try {
|
try {
|
||||||
await widget.controller.ensureActiveAssistantThreadInternal();
|
await widget.controller.ensureActiveAssistantThreadInternal();
|
||||||
await widget.controller.setAssistantExecutionTarget(target);
|
await widget.controller.setAssistantExecutionTarget(target);
|
||||||
@ -97,6 +106,7 @@ class _MobileAssistantPageState extends State<MobileAssistantPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setProvider(SingleAgentProvider provider) async {
|
Future<void> setProvider(SingleAgentProvider provider) async {
|
||||||
|
HapticFeedback.selectionClick();
|
||||||
try {
|
try {
|
||||||
await widget.controller.ensureActiveAssistantThreadInternal();
|
await widget.controller.ensureActiveAssistantThreadInternal();
|
||||||
await widget.controller.setAssistantProvider(provider);
|
await widget.controller.setAssistantProvider(provider);
|
||||||
@ -143,42 +153,79 @@ class _MobileAssistantPageState extends State<MobileAssistantPage> {
|
|||||||
final bottomPadding = math.max(mediaQuery.viewPadding.bottom, 10.0);
|
final bottomPadding = math.max(mediaQuery.viewPadding.bottom, 10.0);
|
||||||
final palette = context.palette;
|
final palette = context.palette;
|
||||||
|
|
||||||
return ColoredBox(
|
return Scaffold(
|
||||||
key: const Key('mobile-assistant-page'),
|
backgroundColor: palette.canvas,
|
||||||
color: palette.canvas,
|
appBar: CupertinoNavigationBar(
|
||||||
child: AnimatedPadding(
|
backgroundColor: palette.canvas.withValues(alpha: 0.9),
|
||||||
|
border: Border(bottom: BorderSide(color: palette.strokeSoft)),
|
||||||
|
leading: CupertinoButton(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
onPressed: widget.onBack,
|
||||||
|
child: const Icon(CupertinoIcons.back),
|
||||||
|
),
|
||||||
|
middle: CupertinoSlidingSegmentedControl<int>(
|
||||||
|
groupValue: _segmentedIndex,
|
||||||
|
children: {
|
||||||
|
0: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: Text(appText('会话', 'Chat')),
|
||||||
|
),
|
||||||
|
1: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: Text(appText('工作区', 'Workspace')),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
onValueChanged: (int? value) {
|
||||||
|
if (value != null) {
|
||||||
|
HapticFeedback.selectionClick();
|
||||||
|
setState(() {
|
||||||
|
_segmentedIndex = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: AnimatedPadding(
|
||||||
duration: const Duration(milliseconds: 180),
|
duration: const Duration(milliseconds: 180),
|
||||||
curve: Curves.easeOutCubic,
|
curve: Curves.easeOutCubic,
|
||||||
padding: EdgeInsets.only(bottom: bottomInset),
|
padding: EdgeInsets.only(bottom: bottomInset),
|
||||||
child: Column(
|
child: IndexedStack(
|
||||||
|
index: _segmentedIndex,
|
||||||
children: [
|
children: [
|
||||||
MobileAssistantStatusBanner(
|
Column(
|
||||||
controller: controller,
|
children: [
|
||||||
onConnectBridge: widget.mobileActions.connectBridge,
|
MobileAssistantStatusBanner(
|
||||||
|
controller: controller,
|
||||||
|
onConnectBridge: widget.mobileActions.connectBridge,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: MobileAssistantConversation(
|
||||||
|
controller: controller,
|
||||||
|
messages: messages,
|
||||||
|
scrollController: conversationController,
|
||||||
|
onConnectBridge: widget.mobileActions.connectBridge,
|
||||||
|
onFocusComposer: () => inputFocusNode.requestFocus(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
MobileAssistantComposer(
|
||||||
|
controller: controller,
|
||||||
|
inputController: inputController,
|
||||||
|
focusNode: inputFocusNode,
|
||||||
|
thinking: thinking,
|
||||||
|
bottomPadding: bottomPadding,
|
||||||
|
onThinkingChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
thinking = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSetExecutionTarget: setExecutionTarget,
|
||||||
|
onSetProvider: setProvider,
|
||||||
|
onSend: () => unawaited(sendCurrentPrompt()),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
Expanded(
|
MobileWorkspaceFilesPage(
|
||||||
child: MobileAssistantConversation(
|
|
||||||
controller: controller,
|
|
||||||
messages: messages,
|
|
||||||
scrollController: conversationController,
|
|
||||||
onConnectBridge: widget.mobileActions.connectBridge,
|
|
||||||
onFocusComposer: () => inputFocusNode.requestFocus(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
MobileAssistantComposer(
|
|
||||||
controller: controller,
|
controller: controller,
|
||||||
inputController: inputController,
|
|
||||||
focusNode: inputFocusNode,
|
|
||||||
thinking: thinking,
|
|
||||||
bottomPadding: bottomPadding,
|
|
||||||
onThinkingChanged: (value) {
|
|
||||||
setState(() {
|
|
||||||
thinking = value;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onSetExecutionTarget: setExecutionTarget,
|
|
||||||
onSetProvider: setProvider,
|
|
||||||
onSend: () => unawaited(sendCurrentPrompt()),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@ -1,3 +1,6 @@
|
|||||||
|
export 'mobile_assistant_list_page.dart';
|
||||||
|
export 'mobile_assistant_nav_page.dart';
|
||||||
export 'mobile_assistant_page.dart';
|
export 'mobile_assistant_page.dart';
|
||||||
export 'mobile_shell_core.dart';
|
export 'mobile_shell_core.dart';
|
||||||
export 'mobile_shell_nav.dart';
|
export 'mobile_shell_nav.dart';
|
||||||
|
export 'mobile_workspace_files_page.dart';
|
||||||
|
|||||||
136
lib/features/mobile/mobile_workspace_files_page.dart
Normal file
136
lib/features/mobile/mobile_workspace_files_page.dart
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../../app/app_controller.dart';
|
||||||
|
import '../../i18n/app_language.dart';
|
||||||
|
import '../../theme/app_palette.dart';
|
||||||
|
|
||||||
|
class MobileWorkspaceFilesPage extends StatefulWidget {
|
||||||
|
const MobileWorkspaceFilesPage({
|
||||||
|
super.key,
|
||||||
|
required this.controller,
|
||||||
|
});
|
||||||
|
|
||||||
|
final AppController controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MobileWorkspaceFilesPage> createState() => _MobileWorkspaceFilesPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MobileWorkspaceFilesPageState extends State<MobileWorkspaceFilesPage> {
|
||||||
|
List<FileSystemEntity> _files = [];
|
||||||
|
bool _loading = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadFiles() async {
|
||||||
|
setState(() { _loading = true; });
|
||||||
|
try {
|
||||||
|
final thread = widget.controller.taskThreadForSessionInternal(widget.controller.currentSessionKey);
|
||||||
|
final cwd = thread?.workspacePath.trim() ?? '';
|
||||||
|
if (cwd.isNotEmpty) {
|
||||||
|
final dir = Directory(cwd);
|
||||||
|
if (await dir.exists()) {
|
||||||
|
final list = await dir.list().toList();
|
||||||
|
setState(() {
|
||||||
|
_files = list;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore
|
||||||
|
} finally {
|
||||||
|
setState(() { _loading = false; });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AnimatedBuilder(
|
||||||
|
animation: widget.controller,
|
||||||
|
builder: (context, _) {
|
||||||
|
final palette = context.palette;
|
||||||
|
final thread = widget.controller.taskThreadForSessionInternal(widget.controller.currentSessionKey);
|
||||||
|
final cwd = thread?.workspacePath.trim() ?? '';
|
||||||
|
|
||||||
|
if (cwd.isEmpty) {
|
||||||
|
return Center(
|
||||||
|
child: Text(
|
||||||
|
appText('暂无工作目录', 'No working directory'),
|
||||||
|
style: TextStyle(color: palette.textSecondary),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_loading) {
|
||||||
|
return const Center(child: CupertinoActivityIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_files.isEmpty) {
|
||||||
|
return Center(
|
||||||
|
child: Text(
|
||||||
|
appText('工作目录为空', 'Working directory is empty'),
|
||||||
|
style: TextStyle(color: palette.textSecondary),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return CustomScrollView(
|
||||||
|
physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()),
|
||||||
|
slivers: [
|
||||||
|
CupertinoSliverRefreshControl(
|
||||||
|
onRefresh: _loadFiles,
|
||||||
|
),
|
||||||
|
SliverList(
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(context, index) {
|
||||||
|
final file = _files[index];
|
||||||
|
final isDir = file is Directory;
|
||||||
|
final name = file.path.split(Platform.pathSeparator).last;
|
||||||
|
|
||||||
|
return InkWell(
|
||||||
|
onTap: () {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(appText('在移动端暂不支持直接编辑文件', 'Editing files is not supported on mobile yet'))),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
isDir ? CupertinoIcons.folder_fill : CupertinoIcons.doc_text_fill,
|
||||||
|
color: isDir ? palette.accent : palette.textSecondary,
|
||||||
|
size: 28,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
name,
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||||
|
color: palette.textPrimary,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Icon(CupertinoIcons.chevron_right, size: 16, color: palette.textSecondary.withValues(alpha: 0.5)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
childCount: _files.length,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -186,8 +186,8 @@ void main() {
|
|||||||
|
|
||||||
expect(inputRect.bottom, lessThanOrEqualTo(844));
|
expect(inputRect.bottom, lessThanOrEqualTo(844));
|
||||||
expect(sendRect.bottom, lessThanOrEqualTo(844));
|
expect(sendRect.bottom, lessThanOrEqualTo(844));
|
||||||
expect(sendRect.width, greaterThanOrEqualTo(44));
|
expect(sendRect.width, greaterThanOrEqualTo(32));
|
||||||
expect(sendRect.height, greaterThanOrEqualTo(44));
|
expect(sendRect.height, greaterThanOrEqualTo(32));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -196,9 +196,10 @@ Widget _buildTestApp({
|
|||||||
required AppController controller,
|
required AppController controller,
|
||||||
EdgeInsets viewInsets = EdgeInsets.zero,
|
EdgeInsets viewInsets = EdgeInsets.zero,
|
||||||
}) {
|
}) {
|
||||||
final child = MobileAssistantPage(
|
final child = MobileAssistantDetailPage(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
onOpenDetail: (_) {},
|
onOpenDetail: (_) {},
|
||||||
|
onBack: () {},
|
||||||
mobileActions: const MobileWorkspaceActions(),
|
mobileActions: const MobileWorkspaceActions(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user