473 lines
16 KiB
Dart
473 lines
16 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import '../../app/app_controller.dart';
|
|
import '../../i18n/app_language.dart';
|
|
import '../../models/app_models.dart';
|
|
import '../../theme/app_palette.dart';
|
|
import '../../widgets/section_header.dart';
|
|
import '../../widgets/surface_card.dart';
|
|
import '../../widgets/top_bar.dart';
|
|
|
|
class ClawHubPage extends StatefulWidget {
|
|
const ClawHubPage({
|
|
super.key,
|
|
required this.controller,
|
|
required this.onOpenDetail,
|
|
});
|
|
|
|
final AppController controller;
|
|
final ValueChanged<DetailPanelData> onOpenDetail;
|
|
|
|
@override
|
|
State<ClawHubPage> createState() => _ClawHubPageState();
|
|
}
|
|
|
|
class _ClawHubPageState extends State<ClawHubPage> {
|
|
final _searchController = TextEditingController();
|
|
final _commandController = TextEditingController();
|
|
final _scrollController = ScrollController();
|
|
final List<ClawHubLogEntry> _logs = [];
|
|
bool _isExecuting = false;
|
|
|
|
@override
|
|
void dispose() {
|
|
_searchController.dispose();
|
|
_commandController.dispose();
|
|
_scrollController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _addLog(String message, {ClawHubLogType type = ClawHubLogType.info}) {
|
|
setState(() {
|
|
_logs.add(ClawHubLogEntry(
|
|
timestamp: DateTime.now(),
|
|
message: message,
|
|
type: type,
|
|
));
|
|
});
|
|
// Auto-scroll to bottom
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (_scrollController.hasClients) {
|
|
_scrollController.animateTo(
|
|
_scrollController.position.maxScrollExtent,
|
|
duration: const Duration(milliseconds: 200),
|
|
curve: Curves.easeOut,
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
void _executeCommand(String input) {
|
|
if (input.trim().isEmpty) return;
|
|
|
|
_addLog('\$ clawhub \$input', type: ClawHubLogType.command);
|
|
_commandController.clear();
|
|
|
|
final parts = input.trim().split(RegExp(r'\s+'));
|
|
final command = parts.isNotEmpty ? parts[0] : '';
|
|
final args = parts.length > 1 ? parts.sublist(1) : <String>[];
|
|
|
|
switch (command) {
|
|
case 'search':
|
|
_handleSearch(args);
|
|
break;
|
|
case 'install':
|
|
_handleInstall(args);
|
|
break;
|
|
case 'update':
|
|
_handleUpdate(args);
|
|
break;
|
|
case 'help':
|
|
case '--help':
|
|
case '-h':
|
|
_showHelp();
|
|
break;
|
|
default:
|
|
_addLog(
|
|
'Unknown command: \$command. Type "clawhub help" for available commands.',
|
|
type: ClawHubLogType.error,
|
|
);
|
|
}
|
|
}
|
|
|
|
void _handleSearch(List<String> args) {
|
|
final query = args.join(' ');
|
|
if (query.isEmpty) {
|
|
_addLog('Usage: clawhub search "<query>"', type: ClawHubLogType.warning);
|
|
return;
|
|
}
|
|
|
|
setState(() => _isExecuting = true);
|
|
_addLog('Searching for "\$query"...');
|
|
|
|
// Simulate search results
|
|
Future.delayed(const Duration(milliseconds: 800), () {
|
|
setState(() => _isExecuting = false);
|
|
_addLog('');
|
|
_addLog('Found 3 packages:', type: ClawHubLogType.success);
|
|
_addLog(' ├─ skill-analyzer v1.2.0 Code analysis skill');
|
|
_addLog(' ├─ feishu-connector v2.1.3 Feishu integration');
|
|
_addLog(' └─ azure-deploy v3.0.1 Azure deployment helper');
|
|
_addLog('');
|
|
_addLog('Use "clawhub install <slug>" to install a package.');
|
|
});
|
|
}
|
|
|
|
void _handleInstall(List<String> args) {
|
|
if (args.isEmpty) {
|
|
_addLog('Usage: clawhub install <slug>', type: ClawHubLogType.warning);
|
|
return;
|
|
}
|
|
|
|
setState(() => _isExecuting = true);
|
|
_addLog('Installing ${args[0]}...');
|
|
|
|
Future.delayed(const Duration(milliseconds: 1200), () {
|
|
setState(() => _isExecuting = false);
|
|
_addLog(
|
|
'✓ Successfully installed ${args[0]}',
|
|
type: ClawHubLogType.success,
|
|
);
|
|
_addLog(' Location: ~/.clawhub/skills/${args[0]}');
|
|
_addLog(' Run "clawhub update" to check for updates.');
|
|
});
|
|
}
|
|
|
|
void _handleUpdate(List<String> args) {
|
|
final isAll = args.contains('--all') || args.contains('-a');
|
|
final slug = isAll ? null : (args.isNotEmpty ? args[0] : null);
|
|
|
|
setState(() => _isExecuting = true);
|
|
|
|
if (isAll) {
|
|
_addLog('Checking for updates...');
|
|
Future.delayed(const Duration(milliseconds: 1000), () {
|
|
setState(() => _isExecuting = false);
|
|
_addLog('✓ All packages are up to date', type: ClawHubLogType.success);
|
|
});
|
|
} else if (slug != null) {
|
|
_addLog('Updating \$slug...');
|
|
Future.delayed(const Duration(milliseconds: 800), () {
|
|
setState(() => _isExecuting = false);
|
|
_addLog('✓ \$slug updated to latest version', type: ClawHubLogType.success);
|
|
});
|
|
} else {
|
|
_addLog('Usage: clawhub update <slug> or clawhub update --all',
|
|
type: ClawHubLogType.warning);
|
|
setState(() => _isExecuting = false);
|
|
}
|
|
}
|
|
|
|
void _showHelp() {
|
|
_addLog('');
|
|
_addLog('ClawHub Package Manager', type: ClawHubLogType.success);
|
|
_addLog('Usage: clawhub <command> [options]');
|
|
_addLog('');
|
|
_addLog('Commands:');
|
|
_addLog(' search "<query>" Search for packages');
|
|
_addLog(' install <slug> Install a package');
|
|
_addLog(' update <slug> Update a specific package');
|
|
_addLog(' update --all Update all packages');
|
|
_addLog(' help Show this help message');
|
|
_addLog('');
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final palette = context.palette;
|
|
|
|
return AnimatedBuilder(
|
|
animation: widget.controller,
|
|
builder: (context, _) {
|
|
return SingleChildScrollView(
|
|
padding: const EdgeInsets.fromLTRB(32, 32, 32, 8),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
TopBar(
|
|
breadcrumbs: [
|
|
AppBreadcrumbItem(
|
|
label: appText('主页', 'Home'),
|
|
icon: Icons.home_rounded,
|
|
onTap: widget.controller.navigateHome,
|
|
),
|
|
const AppBreadcrumbItem(label: 'ClawHub'),
|
|
],
|
|
title: 'ClawHub',
|
|
subtitle: appText(
|
|
'NPM 风格的包管理中心,支持搜索、安装和更新 Skills。',
|
|
'NPM-style package manager for skills.',
|
|
),
|
|
),
|
|
const SizedBox(height: 24),
|
|
SectionHeader(
|
|
title: appText('终端', 'Terminal'),
|
|
subtitle: appText('执行终端命令', 'Execute terminal commands'),
|
|
),
|
|
const SizedBox(height: 12),
|
|
SurfaceCard(
|
|
child: Container(
|
|
height: 400,
|
|
decoration: BoxDecoration(
|
|
color: palette.surfaceSecondary.withValues(alpha: 0.5),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
// Terminal header
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 12,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: palette.surfaceSecondary,
|
|
borderRadius: const BorderRadius.vertical(
|
|
top: Radius.circular(12),
|
|
),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Icon(
|
|
Icons.terminal_rounded,
|
|
size: 16,
|
|
color: palette.textSecondary,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'clawhub',
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w600,
|
|
color: palette.textSecondary,
|
|
),
|
|
),
|
|
const Spacer(),
|
|
if (_isExecuting)
|
|
SizedBox(
|
|
width: 14,
|
|
height: 14,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 2,
|
|
color: palette.accent,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
// Terminal output
|
|
Expanded(
|
|
child: Container(
|
|
padding: const EdgeInsets.all(16),
|
|
child: ListView.builder(
|
|
controller: _scrollController,
|
|
itemCount: _logs.length,
|
|
itemBuilder: (context, index) {
|
|
final log = _logs[index];
|
|
return _LogLine(entry: log, palette: palette);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
// Command input
|
|
Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: palette.surfaceSecondary,
|
|
border: Border(
|
|
top: BorderSide(color: palette.strokeSoft),
|
|
),
|
|
borderRadius: const BorderRadius.vertical(
|
|
bottom: Radius.circular(12),
|
|
),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Text(
|
|
'\$',
|
|
style: TextStyle(
|
|
fontFamily: 'monospace',
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w600,
|
|
color: palette.accent,
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: TextField(
|
|
controller: _commandController,
|
|
style: TextStyle(
|
|
fontFamily: 'monospace',
|
|
fontSize: 14,
|
|
color: palette.textPrimary,
|
|
),
|
|
decoration: InputDecoration(
|
|
hintText: appText(
|
|
'输入命令 (search, install, update)',
|
|
'Type command (search, install, update)',
|
|
),
|
|
hintStyle: TextStyle(
|
|
fontFamily: 'monospace',
|
|
fontSize: 14,
|
|
color: palette.textMuted,
|
|
),
|
|
border: InputBorder.none,
|
|
isDense: true,
|
|
contentPadding: EdgeInsets.zero,
|
|
),
|
|
onSubmitted: _executeCommand,
|
|
),
|
|
),
|
|
IconButton(
|
|
icon: Icon(
|
|
Icons.send_rounded,
|
|
size: 18,
|
|
color: palette.accent,
|
|
),
|
|
onPressed: () =>
|
|
_executeCommand(_commandController.text),
|
|
visualDensity: VisualDensity.compact,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 24),
|
|
SectionHeader(
|
|
title: appText('快速操作', 'Quick Actions'),
|
|
subtitle: appText('常用操作快捷入口', 'Quick access to common actions'),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Wrap(
|
|
spacing: 12,
|
|
runSpacing: 12,
|
|
children: [
|
|
_QuickActionButton(
|
|
icon: Icons.search_rounded,
|
|
label: appText('搜索技能', 'Search Skills'),
|
|
onTap: () => _executeCommand('search analytics'),
|
|
),
|
|
_QuickActionButton(
|
|
icon: Icons.download_rounded,
|
|
label: appText('安装技能', 'Install Skill'),
|
|
onTap: () => _executeCommand('install example-skill'),
|
|
),
|
|
_QuickActionButton(
|
|
icon: Icons.update_rounded,
|
|
label: appText('更新全部', 'Update All'),
|
|
onTap: () => _executeCommand('update --all'),
|
|
),
|
|
_QuickActionButton(
|
|
icon: Icons.help_outline_rounded,
|
|
label: appText('查看帮助', 'View Help'),
|
|
onTap: () => _executeCommand('help'),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
enum ClawHubLogType { info, command, success, warning, error }
|
|
|
|
class ClawHubLogEntry {
|
|
final DateTime timestamp;
|
|
final String message;
|
|
final ClawHubLogType type;
|
|
|
|
ClawHubLogEntry({
|
|
required this.timestamp,
|
|
required this.message,
|
|
required this.type,
|
|
});
|
|
}
|
|
|
|
class _LogLine extends StatelessWidget {
|
|
const _LogLine({required this.entry, required this.palette});
|
|
|
|
final ClawHubLogEntry entry;
|
|
final AppPalette palette;
|
|
|
|
Color get _color {
|
|
switch (entry.type) {
|
|
case ClawHubLogType.command:
|
|
return palette.accent;
|
|
case ClawHubLogType.success:
|
|
return Colors.green;
|
|
case ClawHubLogType.warning:
|
|
return Colors.orange;
|
|
case ClawHubLogType.error:
|
|
return Colors.red;
|
|
case ClawHubLogType.info:
|
|
return palette.textPrimary;
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: 4),
|
|
child: Text(
|
|
entry.message,
|
|
style: TextStyle(
|
|
fontFamily: 'monospace',
|
|
fontSize: 13,
|
|
color: _color,
|
|
height: 1.4,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _QuickActionButton extends StatelessWidget {
|
|
const _QuickActionButton({
|
|
required this.icon,
|
|
required this.label,
|
|
required this.onTap,
|
|
});
|
|
|
|
final IconData icon;
|
|
final String label;
|
|
final VoidCallback onTap;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final palette = context.palette;
|
|
|
|
return Material(
|
|
color: palette.surfaceSecondary,
|
|
borderRadius: BorderRadius.circular(8),
|
|
child: InkWell(
|
|
onTap: onTap,
|
|
borderRadius: BorderRadius.circular(8),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(icon, size: 18, color: palette.accent),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
label,
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w500,
|
|
color: palette.textPrimary,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|