flutter_automate 1.1.0 copy "flutter_automate: ^1.1.0" to clipboard
flutter_automate: ^1.1.0 copied to clipboard

PlatformAndroid

A multi-language Android automation framework. Control UI, gestures, apps via Accessibility Service. Supports JavaScript scripting via WASM runtime. No Auto.js dependency.

example/lib/main.dart

import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_automate/flutter_automate.dart';

void main() {
  runApp(const MyApp());
}

// ==================== 悬浮球入口点 ====================
@pragma('vm:entry-point')
void floatWindowMain() {
  runApp(const AssistiveBubble());
}

// ==================== 展开面板入口点 ====================
@pragma('vm:entry-point')
void floatPanelMain() {
  WidgetsFlutterBinding.ensureInitialized();
  print('[floatPanelMain] Starting TaskPanel');
  runApp(const TaskPanel());
}

// ==================== 主应用 ====================
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Automate',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: const HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin {
  final automate = FlutterAutomate.instance;
  final _scriptController = TextEditingController();
  final _floatwing = FloatwingPlugin();
  
  late TabController _tabController;
  
  // 权限状态
  bool _accessibilityEnabled = false;
  bool _floatPermission = false;
  bool _capturePermission = false;
  bool _storagePermission = false;
  bool _manageStoragePermission = false;
  bool _batteryOptimization = false;
  bool _notificationListener = false;
  
  String _log = '';
  
  ScriptExecution? _currentTask;
  bool _isRunning = false;
  String _taskStatus = '空闲';
  
  Window? _bubbleWindow;
  
  // 日志订阅
  StreamSubscription<LogEntry>? _logSubscription;
  
  // 截图预览
  Uint8List? _screenshotData;

  final String _defaultScript = '''// Auto.js API 兼容性测试
console.log("=== Flutter Automate API Test ===");

// ==================== 基础 API ====================
toast("开始 API 测试...");
sleep(500);

// Device API
var w = device.getWidth();
var h = device.getHeight();
console.log("屏幕尺寸: " + w + " x " + h);

var battery = device.getBattery();
console.log("电量: " + battery + "%");

// App API
var pkg = currentPackage();
console.log("当前包名: " + pkg);

// ==================== 选择器 API ====================
console.log("\\n--- 选择器测试 ---");

// 基础选择器
var btn = text("运行").findOne();
if (btn) {
  console.log("找到 [运行] 按钮:");
  console.log("  text: " + btn.text());
  console.log("  className: " + btn.className());
  console.log("  bounds: " + btn.left() + "," + btn.top() + "," + btn.right() + "," + btn.bottom());
}

// 扩展选择器
var editor = className("EditText").findOne();
if (editor) {
  console.log("\\n找到输入框:");
  console.log("  editable: " + editor.editable());
  console.log("  childCount: " + editor.childCount());
}

// ==================== 完成 ====================
console.log("\\n=== 测试完成 ===");
toast("API 测试完成!");

"success"''';

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: 3, vsync: this);
    _scriptController.text = _defaultScript;
    _init();
  }
  
  Future<void> _init() async {
    await _checkAllPermissions();
    await _subscribeLog();
    Future.delayed(const Duration(milliseconds: 500), () {
      _initFloatwing();
    });
  }
  
  Future<void> _subscribeLog() async {
    await automate.logs.subscribe();
    _logSubscription = automate.logs.stream.listen((entry) {
      _addLog('[JS] ${entry.message}');
    });
  }
  
  @override
  void dispose() {
    _tabController.dispose();
    _logSubscription?.cancel();
    automate.logs.unsubscribe();
    _scriptController.dispose();
    super.dispose();
  }

  Future<void> _checkAllPermissions() async {
    final accEnabled = await automate.isAccessibilityEnabled();
    final floatPerm = await _floatwing.checkPermission();
    final capturePerm = await automate.permissions.hasMediaProjection();
    final storagePerm = await automate.permissions.hasStorage();
    final manageStoragePerm = await automate.permissions.hasManageStorage();
    final batteryPerm = await automate.permissions.hasBatteryOptimizationExemption();
    final notifPerm = await automate.permissions.hasNotificationListener();
    
    setState(() {
      _accessibilityEnabled = accEnabled;
      _floatPermission = floatPerm;
      _capturePermission = capturePerm;
      _storagePermission = storagePerm;
      _manageStoragePermission = manageStoragePerm;
      _batteryOptimization = batteryPerm;
      _notificationListener = notifPerm;
    });
  }
  
  Future<void> _initFloatwing() async {
    try {
      await _floatwing.initialize();
      _addLog('Floatwing 初始化完成');
    } catch (e) {
      _addLog('Floatwing 初始化失败: $e');
    }
  }

  Future<void> _requestAccessibility() async {
    await automate.requestAccessibilityPermission(wait: true, timeout: 30000);
    await _checkAllPermissions();
  }
  
  Future<void> _requestFloatPermission() async {
    await _floatwing.openPermissionSetting();
    await Future.delayed(const Duration(seconds: 2));
    await _checkAllPermissions();
  }
  
  Future<void> _requestCapturePermission() async {
    final result = await automate.permissions.requestMediaProjection();
    _addLog('截屏权限: ${result ? "已授权" : "未授权"}');
    await _checkAllPermissions();
  }
  
  Future<void> _requestStoragePermission() async {
    final result = await automate.permissions.requestStorage();
    _addLog('存储权限: ${result ? "已授权" : "未授权"}');
    await _checkAllPermissions();
  }
  
  Future<void> _requestManageStoragePermission() async {
    final result = await automate.permissions.requestManageStorage();
    _addLog('所有文件访问: ${result ? "已授权" : "未授权"}');
    await _checkAllPermissions();
  }
  
  Future<void> _requestBatteryOptimization() async {
    final result = await automate.permissions.requestBatteryOptimizationExemption();
    _addLog('电池优化白名单: ${result ? "已加入" : "未加入"}');
    await _checkAllPermissions();
  }
  
  Future<void> _requestNotificationListener() async {
    final result = await automate.permissions.requestNotificationListener();
    _addLog('通知监听: ${result ? "已授权" : "未授权"}');
    await _checkAllPermissions();
  }

  void _addLog(String message) {
    setState(() {
      _log = '${DateTime.now().toString().substring(11, 19)} $message\n$_log';
      if (_log.length > 5000) {
        _log = _log.substring(0, 5000);
      }
    });
  }

  Future<void> _runScript() async {
    final script = _scriptController.text.trim();
    if (script.isEmpty) {
      _addLog('错误: 脚本为空');
      return;
    }
    
    setState(() {
      _isRunning = true;
      _taskStatus = '运行中...';
    });
    _addLog('开始执行脚本...');
    
    _bubbleWindow?.share({'status': '运行中', 'taskId': 1, 'name': 'main.js'});
    
    try {
      final execution = await automate.execute(script, language: 'js');
      if (execution != null) {
        setState(() {
          _currentTask = execution;
          _taskStatus = '任务ID: ${execution.id}';
        });
        _addLog('脚本已启动: id=${execution.id}');
      } else {
        setState(() => _taskStatus = '启动失败');
        _addLog('脚本启动失败');
      }
    } catch (e) {
      setState(() => _taskStatus = '错误: $e');
      _addLog('执行错误: $e');
    } finally {
      Future.delayed(const Duration(seconds: 1), () {
        setState(() {
          _isRunning = false;
          if (_taskStatus == '运行中...') _taskStatus = '完成';
        });
        _bubbleWindow?.share({'status': '完成', 'taskId': 0});
      });
    }
  }

  Future<void> _stopScript() async {
    if (_currentTask != null) {
      _addLog('停止任务: ${_currentTask!.id}');
      await automate.stopAll();
      setState(() {
        _currentTask = null;
        _taskStatus = '已停止';
        _isRunning = false;
      });
      _bubbleWindow?.share({'status': '已停止', 'taskId': 0});
    }
  }
  
  Future<void> _takeScreenshot() async {
    if (!_capturePermission) {
      _addLog('需要先授权截屏权限');
      return;
    }
    
    _addLog('正在截屏...');
    try {
      final data = await automate.capture.capture();
      if (data != null) {
        setState(() => _screenshotData = data);
        _addLog('截屏成功: ${data.length} bytes');
      } else {
        _addLog('截屏失败');
      }
    } catch (e) {
      _addLog('截屏错误: $e');
    }
  }
  
  Future<void> _showBubble() async {
    if (!_floatPermission) {
      _addLog('无悬浮窗权限');
      return;
    }
    
    _addLog('创建悬浮球...');
    
    try {
      final existing = _floatwing.windows['automate_bubble'];
      if (existing != null) {
        await existing.show();
        setState(() => _bubbleWindow = existing);
        _addLog('悬浮球已显示');
        return;
      }
      
      final config = WindowConfig(
        id: 'automate_bubble',
        width: 168,
        height: 168,
        x: 0,
        y: 400,
        draggable: true,
        clickable: true,
        entry: 'floatWindowMain',
      );
      
      final window = await _floatwing.createWindow('automate_bubble', config, start: true);
      setState(() => _bubbleWindow = window);
      _addLog('悬浮球已创建');
    } catch (e) {
      _addLog('悬浮球创建失败: $e');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter Automate'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        bottom: TabBar(
          controller: _tabController,
          tabs: const [
            Tab(icon: Icon(Icons.lock_open), text: '权限'),
            Tab(icon: Icon(Icons.code), text: '脚本'),
            Tab(icon: Icon(Icons.screenshot), text: '截屏'),
          ],
        ),
        actions: [
          IconButton(
            icon: Icon(_floatPermission 
                ? (_bubbleWindow != null ? Icons.visibility : Icons.visibility_off)
                : Icons.visibility_off),
            onPressed: _floatPermission ? _showBubble : null,
            tooltip: _floatPermission ? '悬浮窗' : '需要悬浮窗权限',
          ),
        ],
      ),
      body: TabBarView(
        controller: _tabController,
        children: [
          _buildPermissionsTab(),
          _buildScriptTab(),
          _buildCaptureTab(),
        ],
      ),
    );
  }
  
  Widget _buildPermissionsTab() {
    return SingleChildScrollView(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          Card(
            child: Padding(
              padding: const EdgeInsets.all(12),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  const Text('权限状态', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
                  const SizedBox(height: 12),
                  _PermissionRow(
                    icon: Icons.accessibility,
                    title: '无障碍服务',
                    subtitle: '控制 UI、执行手势',
                    enabled: _accessibilityEnabled,
                    onRequest: _requestAccessibility,
                  ),
                  const Divider(),
                  _PermissionRow(
                    icon: Icons.picture_in_picture,
                    title: '悬浮窗权限',
                    subtitle: '显示悬浮控制面板',
                    enabled: _floatPermission,
                    onRequest: _requestFloatPermission,
                  ),
                  const Divider(),
                  _PermissionRow(
                    icon: Icons.screenshot,
                    title: '截屏权限',
                    subtitle: 'MediaProjection 屏幕截图',
                    enabled: _capturePermission,
                    onRequest: _requestCapturePermission,
                  ),
                  const Divider(),
                  _PermissionRow(
                    icon: Icons.folder,
                    title: '存储权限',
                    subtitle: '读写外部存储',
                    enabled: _storagePermission,
                    onRequest: _requestStoragePermission,
                  ),
                  const Divider(),
                  _PermissionRow(
                    icon: Icons.folder_special,
                    title: '所有文件访问',
                    subtitle: 'Android 11+ MANAGE_EXTERNAL_STORAGE',
                    enabled: _manageStoragePermission,
                    onRequest: _requestManageStoragePermission,
                  ),
                  const Divider(),
                  _PermissionRow(
                    icon: Icons.battery_saver,
                    title: '电池优化白名单',
                    subtitle: '后台保活',
                    enabled: _batteryOptimization,
                    onRequest: _requestBatteryOptimization,
                  ),
                  const Divider(),
                  _PermissionRow(
                    icon: Icons.notifications,
                    title: '通知监听',
                    subtitle: '读取系统通知',
                    enabled: _notificationListener,
                    onRequest: _requestNotificationListener,
                  ),
                ],
              ),
            ),
          ),
          const SizedBox(height: 16),
          ElevatedButton.icon(
            onPressed: _checkAllPermissions,
            icon: const Icon(Icons.refresh),
            label: const Text('刷新权限状态'),
          ),
          const SizedBox(height: 16),
          // 日志
          Card(
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Row(
                    children: [
                      const Text('日志', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
                      const Spacer(),
                      TextButton(onPressed: () => setState(() => _log = ''), child: const Text('清空')),
                    ],
                  ),
                  const SizedBox(height: 8),
                  Container(
                    height: 150,
                    width: double.infinity,
                    padding: const EdgeInsets.all(8),
                    decoration: BoxDecoration(
                      color: Colors.grey.shade100,
                      borderRadius: BorderRadius.circular(8),
                    ),
                    child: SingleChildScrollView(
                      child: Text(_log, style: const TextStyle(fontFamily: 'monospace', fontSize: 11)),
                    ),
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
  
  Widget _buildScriptTab() {
    return SingleChildScrollView(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          // 任务状态
          Card(
            color: _isRunning ? Colors.blue.shade50 : null,
            child: Padding(
              padding: const EdgeInsets.all(12),
              child: Row(
                children: [
                  if (_isRunning)
                    const SizedBox(
                      width: 20, height: 20,
                      child: CircularProgressIndicator(strokeWidth: 2),
                    )
                  else
                    Icon(
                      _taskStatus.contains('错误') ? Icons.error : 
                      _taskStatus == '完成' ? Icons.check_circle : Icons.circle_outlined,
                      color: _taskStatus.contains('错误') ? Colors.red : Colors.green,
                      size: 20,
                    ),
                  const SizedBox(width: 12),
                  Expanded(
                    child: Text(_taskStatus, style: const TextStyle(fontWeight: FontWeight.bold)),
                  ),
                  if (_isRunning)
                    IconButton(onPressed: _stopScript, icon: const Icon(Icons.stop, color: Colors.red)),
                ],
              ),
            ),
          ),
          
          const SizedBox(height: 16),
          
          // 脚本编辑器
          Card(
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  const Text('脚本编辑器', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
                  const SizedBox(height: 12),
                  TextField(
                    controller: _scriptController,
                    maxLines: 15,
                    style: const TextStyle(fontFamily: 'monospace', fontSize: 13),
                    decoration: InputDecoration(
                      border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
                      contentPadding: const EdgeInsets.all(12),
                    ),
                  ),
                  const SizedBox(height: 12),
                  Row(
                    children: [
                      Expanded(
                        child: ElevatedButton.icon(
                          onPressed: _isRunning ? null : _runScript,
                          icon: const Icon(Icons.play_arrow),
                          label: const Text('运行'),
                          style: ElevatedButton.styleFrom(
                            backgroundColor: Colors.green,
                            foregroundColor: Colors.white,
                          ),
                        ),
                      ),
                      const SizedBox(width: 12),
                      Expanded(
                        child: OutlinedButton.icon(
                          onPressed: () => _scriptController.clear(),
                          icon: const Icon(Icons.clear),
                          label: const Text('清空'),
                        ),
                      ),
                    ],
                  ),
                ],
              ),
            ),
          ),
          
          const SizedBox(height: 16),
          
          // 日志
          Card(
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Row(
                    children: [
                      const Text('日志', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
                      const Spacer(),
                      TextButton(onPressed: () => setState(() => _log = ''), child: const Text('清空')),
                    ],
                  ),
                  const SizedBox(height: 8),
                  Container(
                    height: 150,
                    width: double.infinity,
                    padding: const EdgeInsets.all(8),
                    decoration: BoxDecoration(
                      color: Colors.grey.shade100,
                      borderRadius: BorderRadius.circular(8),
                    ),
                    child: SingleChildScrollView(
                      child: Text(_log, style: const TextStyle(fontFamily: 'monospace', fontSize: 11)),
                    ),
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
  
  Widget _buildCaptureTab() {
    return SingleChildScrollView(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          Card(
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  const Text('屏幕截图', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
                  const SizedBox(height: 12),
                  if (!_capturePermission)
                    Container(
                      padding: const EdgeInsets.all(16),
                      decoration: BoxDecoration(
                        color: Colors.orange.shade50,
                        borderRadius: BorderRadius.circular(8),
                      ),
                      child: Row(
                        children: [
                          const Icon(Icons.warning, color: Colors.orange),
                          const SizedBox(width: 12),
                          const Expanded(child: Text('需要先授权截屏权限')),
                          ElevatedButton(
                            onPressed: _requestCapturePermission,
                            child: const Text('授权'),
                          ),
                        ],
                      ),
                    )
                  else
                    ElevatedButton.icon(
                      onPressed: _takeScreenshot,
                      icon: const Icon(Icons.camera),
                      label: const Text('截取屏幕'),
                    ),
                  const SizedBox(height: 16),
                  if (_screenshotData != null)
                    Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text('截图预览 (${_screenshotData!.length} bytes)', 
                          style: const TextStyle(fontWeight: FontWeight.bold)),
                        const SizedBox(height: 8),
                        ClipRRect(
                          borderRadius: BorderRadius.circular(8),
                          child: Image.memory(
                            _screenshotData!,
                            fit: BoxFit.contain,
                          ),
                        ),
                        const SizedBox(height: 8),
                        Row(
                          children: [
                            ElevatedButton.icon(
                              onPressed: () async {
                                final success = await automate.capture.captureToFile(
                                  '/sdcard/Download/screenshot_${DateTime.now().millisecondsSinceEpoch}.png',
                                );
                                _addLog('保存截图: ${success ? "成功" : "失败"}');
                              },
                              icon: const Icon(Icons.save),
                              label: const Text('保存'),
                            ),
                            const SizedBox(width: 12),
                            OutlinedButton.icon(
                              onPressed: () => setState(() => _screenshotData = null),
                              icon: const Icon(Icons.clear),
                              label: const Text('清除'),
                            ),
                          ],
                        ),
                      ],
                    ),
                ],
              ),
            ),
          ),
          const SizedBox(height: 16),
          // 日志
          Card(
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Row(
                    children: [
                      const Text('日志', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
                      const Spacer(),
                      TextButton(onPressed: () => setState(() => _log = ''), child: const Text('清空')),
                    ],
                  ),
                  const SizedBox(height: 8),
                  Container(
                    height: 150,
                    width: double.infinity,
                    padding: const EdgeInsets.all(8),
                    decoration: BoxDecoration(
                      color: Colors.grey.shade100,
                      borderRadius: BorderRadius.circular(8),
                    ),
                    child: SingleChildScrollView(
                      child: Text(_log, style: const TextStyle(fontFamily: 'monospace', fontSize: 11)),
                    ),
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}

// ==================== 悬浮球 (仿 AssistiveTouch) ====================
class AssistiveBubble extends StatefulWidget {
  const AssistiveBubble({super.key});

  @override
  State<AssistiveBubble> createState() => _AssistiveBubbleState();
}

class _AssistiveBubbleState extends State<AssistiveBubble> with TickerProviderStateMixin {
  Window? _window;
  Window? _panelWindow;
  int _taskCount = 0;
  bool _isIdle = true;
  Timer? _idleTimer;
  
  late final AnimationController _scaleController = AnimationController(
    duration: const Duration(milliseconds: 200),
    vsync: this,
  )..forward();

  @override
  void initState() {
    super.initState();
    SchedulerBinding.instance.addPostFrameCallback((_) => _initWindow());
  }
  
  @override
  void dispose() {
    _idleTimer?.cancel();
    _scaleController.dispose();
    super.dispose();
  }

  Future<void> _initWindow() async {
    _window = Window.of(context);
    
    _window?.on(EventType.WindowDragStart, (w, d) => _onActivity());
    _window?.on(EventType.WindowDragging, (w, d) => _onActivity());
    _window?.on(EventType.WindowDragEnd, (w, d) => _scheduleIdle());
    
    _window?.onData((source, name, data) async {
      if (data is Map) {
        final status = data['status'] as String?;
        setState(() {
          _taskCount = status == '运行中' ? 1 : 0;
        });
      }
      return null;
    });
    
    _scheduleIdle();
  }
  
  void _onActivity() {
    setState(() => _isIdle = false);
    _idleTimer?.cancel();
  }
  
  void _scheduleIdle() {
    _idleTimer?.cancel();
    _idleTimer = Timer(const Duration(seconds: 3), () {
      if (mounted) setState(() => _isIdle = true);
    });
  }

  void _onTap() async {
    _onActivity();
    _scheduleIdle();
    
    final floatwing = FloatwingPlugin();
    var panel = floatwing.windows['automate_panel'];
    
    if (panel != null) {
      final x = _window?.config?.x ?? 0;
      final y = _window?.config?.y ?? 0;
      panel.share([x, y]);
      await panel.start();
    } else {
      final config = WindowConfig(
        id: 'automate_panel',
        width: WindowSize.MatchParent,
        height: WindowSize.MatchParent,
        clickable: true,
        focusable: true,
        entry: 'floatPanelMain',
      );
      panel = await floatwing.createWindow('automate_panel', config, start: false);
      panel?.on(EventType.WindowCreated, (w, d) {
        final x = _window?.config?.x ?? 0;
        final y = _window?.config?.y ?? 0;
        panel?.share([x, y]);
        panel?.start();
      });
      await panel?.create();
    }
    
    _panelWindow = panel;
  }

  @override
  Widget build(BuildContext context) {
    final hasTask = _taskCount > 0;
    
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: GestureDetector(
        onTap: _onTap,
        child: ScaleTransition(
          scale: _scaleController,
          child: AnimatedOpacity(
            opacity: _isIdle ? 0.3 : 1.0,
            duration: const Duration(milliseconds: 300),
            child: Container(
              height: 56,
              width: 56,
              alignment: Alignment.center,
              decoration: BoxDecoration(
                color: Colors.grey[900],
                borderRadius: BorderRadius.circular(28),
              ),
              child: Container(
                height: 40,
                width: 40,
                alignment: Alignment.center,
                decoration: BoxDecoration(
                  color: hasTask ? Colors.blue.withOpacity(0.6) : Colors.grey[400]!.withOpacity(0.6),
                  borderRadius: BorderRadius.circular(20),
                ),
                child: Container(
                  height: 32,
                  width: 32,
                  alignment: Alignment.center,
                  decoration: BoxDecoration(
                    color: hasTask ? Colors.blue.withOpacity(0.8) : Colors.grey[300]!.withOpacity(0.6),
                    borderRadius: BorderRadius.circular(16),
                  ),
                  child: Container(
                    height: 24,
                    width: 24,
                    alignment: Alignment.center,
                    decoration: BoxDecoration(
                      color: hasTask ? Colors.blue : Colors.white,
                      borderRadius: BorderRadius.circular(12),
                    ),
                    child: hasTask 
                      ? const SizedBox(
                          width: 16, height: 16,
                          child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
                        )
                      : const SizedBox.shrink(),
                  ),
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

// ==================== 任务面板 ====================
class TaskPanel extends StatefulWidget {
  const TaskPanel({super.key});

  @override
  State<TaskPanel> createState() => _TaskPanelState();
}

class _TaskPanelState extends State<TaskPanel> with SingleTickerProviderStateMixin {
  Window? _window;
  late TabController _tabController;
  bool _show = false;
  double _bubbleX = 0;
  double _bubbleY = 0;
  
  List<Map<String, dynamic>> _tasks = [];
  List<String> _logs = ['欢迎使用 Flutter Automate'];
  
  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: 2, vsync: this);
    SchedulerBinding.instance.addPostFrameCallback((_) => _initWindow());
  }
  
  @override
  void dispose() {
    _tabController.dispose();
    super.dispose();
  }

  Future<void> _initWindow() async {
    _window = Window.of(context);
    _window?.on(EventType.WindowStarted, (w, d) {
      setState(() => _show = true);
    });
    _window?.onData((source, name, data) async {
      if (data is List && data.length >= 2) {
        setState(() {
          _bubbleX = (data[0] as num).toDouble();
          _bubbleY = (data[1] as num).toDouble();
        });
      }
      return null;
    });
    setState(() => _show = true);
  }

  void _close() {
    setState(() => _show = false);
    _window?.close(force: true);
  }

  @override
  Widget build(BuildContext context) {
    final screenWidth = MediaQuery.of(context).size.width;
    final screenHeight = MediaQuery.of(context).size.height;
    
    if (screenWidth == 0 || screenHeight == 0) {
      return const MaterialApp(
        debugShowCheckedModeBanner: false,
        home: Scaffold(
          backgroundColor: Colors.transparent,
          body: Center(child: CircularProgressIndicator()),
        ),
      );
    }
    
    final panelWidth = screenWidth * 0.85;
    final panelHeight = 400.0;
    final panelLeft = (screenWidth - panelWidth) / 2;
    final panelTop = (screenHeight - panelHeight) / 2;

    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData.dark(),
      home: Container(
        color: Colors.black54,
        child: Stack(
          children: [
            Positioned(
              left: panelLeft,
              top: panelTop,
              width: panelWidth,
              height: panelHeight,
              child: AnimatedScale(
                scale: _show ? 1.0 : 0.0,
                duration: const Duration(milliseconds: 200),
                curve: Curves.easeOutCubic,
                child: Material(
                  color: Colors.grey[900],
                  borderRadius: BorderRadius.circular(16),
                  clipBehavior: Clip.antiAlias,
                  elevation: 8,
                  shadowColor: Colors.black54,
                  child: Column(
                    children: [
                      Container(
                        padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
                        decoration: BoxDecoration(color: Colors.grey[850]),
                        child: Row(
                          children: [
                            const Icon(Icons.play_circle, color: Colors.blue, size: 24),
                            const SizedBox(width: 8),
                            const Expanded(
                              child: Text('Flutter Automate', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
                            ),
                            if (_tasks.isNotEmpty)
                              TextButton(
                                onPressed: () {
                                  FlutterAutomate.instance.stopAll();
                                  setState(() => _tasks.clear());
                                },
                                child: const Text('全部停止', style: TextStyle(color: Colors.red)),
                              ),
                            IconButton(
                              onPressed: _close,
                              icon: const Icon(Icons.close, size: 20),
                            ),
                          ],
                        ),
                      ),
                      TabBar(
                        controller: _tabController,
                        labelColor: Colors.blue,
                        unselectedLabelColor: Colors.grey,
                        indicatorColor: Colors.blue,
                        tabs: [
                          Tab(text: '任务 (${_tasks.length})'),
                          Tab(text: '日志 (${_logs.length})'),
                        ],
                      ),
                      Expanded(
                        child: TabBarView(
                          controller: _tabController,
                          children: [
                            _buildTasksTab(),
                            _buildLogsTab(),
                          ],
                        ),
                      ),
                    ],
                  ),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
  
  Widget _buildTasksTab() {
    if (_tasks.isEmpty) {
      return const Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Icon(Icons.check_circle_outline, color: Colors.grey, size: 48),
            SizedBox(height: 12),
            Text('没有运行中的任务', style: TextStyle(color: Colors.grey)),
          ],
        ),
      );
    }
    
    return ListView.builder(
      padding: const EdgeInsets.all(12),
      itemCount: _tasks.length,
      itemBuilder: (context, i) {
        final task = _tasks[i];
        return Card(
          child: ListTile(
            leading: const SizedBox(
              width: 20, height: 20,
              child: CircularProgressIndicator(strokeWidth: 2),
            ),
            title: Text(task['name'] ?? '任务'),
            subtitle: const Text('运行中'),
            trailing: IconButton(
              icon: const Icon(Icons.stop, color: Colors.red),
              onPressed: () => FlutterAutomate.instance.stopAll(),
            ),
          ),
        );
      },
    );
  }
  
  Widget _buildLogsTab() {
    return ListView.builder(
      padding: const EdgeInsets.all(12),
      itemCount: _logs.length,
      itemBuilder: (context, i) {
        return Padding(
          padding: const EdgeInsets.only(bottom: 4),
          child: Text(
            _logs[i],
            style: TextStyle(color: Colors.grey[400], fontSize: 12, fontFamily: 'monospace'),
          ),
        );
      },
    );
  }
}

// ==================== 辅助组件 ====================
class _PermissionRow extends StatelessWidget {
  final IconData icon;
  final String title;
  final String subtitle;
  final bool enabled;
  final VoidCallback onRequest;

  const _PermissionRow({
    required this.icon,
    required this.title,
    required this.subtitle,
    required this.enabled,
    required this.onRequest,
  });

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 8),
      child: Row(
        children: [
          Icon(icon, size: 24, color: enabled ? Colors.green : Colors.grey),
          const SizedBox(width: 12),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(title, style: const TextStyle(fontWeight: FontWeight.w500)),
                Text(subtitle, style: TextStyle(fontSize: 12, color: Colors.grey.shade600)),
              ],
            ),
          ),
          if (enabled)
            const Icon(Icons.check_circle, color: Colors.green, size: 24)
          else
            ElevatedButton(onPressed: onRequest, child: const Text('开启')),
        ],
      ),
    );
  }
}
1
likes
160
points
134
downloads

Publisher

unverified uploader

Weekly Downloads

A multi-language Android automation framework. Control UI, gestures, apps via Accessibility Service. Supports JavaScript scripting via WASM runtime. No Auto.js dependency.

Repository (GitHub)
View/report issues

Topics

#automation #android #accessibility #ui-testing #scripting

Documentation

API reference

License

MIT (license)

Dependencies

flutter, flutter_floatwing, flutter_notification_listener

More

Packages that depend on flutter_automate

Packages that implement flutter_automate