appbridge 1.0.1 copy "appbridge: ^1.0.1" to clipboard
appbridge: ^1.0.1 copied to clipboard

This is a rich flutter javascript interactive plugin.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart'; // Import for SystemNavigator.pop()
import 'dart:ui' if (dart.library.io) 'dart:ui';
import 'dart:isolate' if (dart.library.io) 'dart:isolate';
import 'package:appbridge/appbridge.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:flutter_downloader/flutter_downloader.dart';
import 'downloader_stub.dart' if (dart.library.io) 'downloader_io.dart';
import 'package:appbridge/src/models/bridge_response.dart'; // Import BridgeResponse
import 'package:quick_actions/quick_actions.dart'; // Import quick_actions

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  if (!kIsWeb) {
    await FlutterDownloader.initialize(debug: true);
    FlutterDownloader.registerCallback(downloadCallback);
  }
  runApp(const MyApp());
}

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

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  InAppWebViewController? _webViewController;
  final ReceivePort _port = ReceivePort();
  Appbridge? appbridgePlugin;
  String _appBarTitle = 'JS插件 Demo';
  bool _isAppBarVisible = true;
  DateTime? _lastPopTime; // Add this field to track last back press time

  final QuickActions _quickActions = const QuickActions(); // Initialize QuickActions
  static const MethodChannel _platformChannel = MethodChannel('com.example.appbridge_example/platform'); // New MethodChannel

  @override
  void initState() {
    super.initState();
    _bindBackgroundIsolate();
    appbridgePlugin = Appbridge(); // Initialize Appbridge here
    debugPrint('[_MyAppState] Appbridge singleton instance obtained in initState.');

    _platformChannel.setMethodCallHandler((MethodCall call) async {
      if (call.method == 'loadShortcutUrl') {
        final String? url = call.arguments['url'];
        if (url != null) {
          final String urlToLoad = url.isEmpty
              ? 'file:///android_asset/flutter_assets/packages/appbridge/assets/demo.html' // Default initial URL
              : url;
          debugPrint('Received shortcut URL from native: $urlToLoad. Loading in webview.');
          await _webViewController!.loadUrl(urlRequest: URLRequest(url: WebUri(urlToLoad)));
          return true;
        }
      }
      return false;
    });
    // Setup QuickActions listener for when app starts from a shortcut
    _quickActions.initialize((String shortcutType) {
      // ScaffoldMessenger.of(context).showSnackBar( // Removed due to context issue in initState
      //   SnackBar(content: Text('App launched via shortcut: $shortcutType')),
      // );
      debugPrint('App launched via shortcut: $shortcutType');
      // You can implement custom logic here based on shortcutType
    });
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    // The appbridgePlugin.initialize call is now in onWebViewCreated
  }

  void _bindBackgroundIsolate() {
    debugPrint('[_MyAppState] _bindBackgroundIsolate called.');
    bool isSuccess = IsolateNameServer.registerPortWithName(
      _port.sendPort,
      'downloader_send_port',
    );
    if (!isSuccess) {
      debugPrint(
        '[_MyAppState] Failed to register downloader_send_port. It might already be registered.',
      );
      _unbindBackgroundIsolate(); // Try to unbind and re-register
      isSuccess = IsolateNameServer.registerPortWithName(
        _port.sendPort,
        'downloader_send_port',
      );
      if (!isSuccess) {
        debugPrint(
          '[_MyAppState] Still failed to register downloader_send_port after unbinding. Downloads might not update.',
        );
      }
    }

    _port.listen((dynamic data) {
      debugPrint(
        '[_MyAppState] Received download data from background isolate: $data',
      );
      final id = data['id'] as String;
      final status = DownloadTaskStatus.fromInt(data['status'] as int);
      final progress = data['progress'] as int;
      final speed = data['speed'] as String;

      if (_webViewController != null && appbridgePlugin != null) {
        debugPrint(
          '[_MyAppState] appbridgePlugin and _webViewController are available. Emitting event for id: $id, status: $status, progress: $progress, speed: $speed',
        );
        if (status == DownloadTaskStatus.running) {
          appbridgePlugin!.emitEvent('download.progress', {
            'id': id,
            'status': 'downloading',
            'progress': progress,
            'speed': speed,
          });
        } else if (status == DownloadTaskStatus.complete) {
          FlutterDownloader.loadTasksWithRawQuery(
            query: 'SELECT * FROM task WHERE task_id="$id"',
          ).then((tasks) {
            if (tasks != null && tasks.isNotEmpty) {
              final task = tasks.first;
              appbridgePlugin!.emitEvent('download.completed', {
                'id': id,
                'status': 'completed',
                'progress': progress,
                'path': '${task.savedDir}/${task.filename}',
              });
            }
          });
        } else if (status == DownloadTaskStatus.failed) {
          appbridgePlugin!.emitEvent('download.failed', {
            'id': id,
            'status': 'failed',
            'progress': progress,
          });
        } else if (status == DownloadTaskStatus.canceled) {
          appbridgePlugin!.emitEvent('download.canceled', {
            'id': id,
            'status': 'canceled',
          });
        }
      } else {
        debugPrint(
          '[_MyAppState] WARNING: appbridgePlugin or _webViewController is NULL. Cannot emit download event for id: $id',
        );
      }
    });
  }

  Future<BridgeResponse> _handleOnAddShortcut(BuildContext context, String title, String url) async {
    debugPrint('[_handleOnAddShortcut] title: $title, url: $url');
    if (!mounted) return BridgeResponse.error(-1, 'Widget not mounted');
    try {
      final response = await _platformChannel.invokeMethod('addShortcuts', {'title': title, 'url': url});
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text(response?.toString() ?? 'Shortcut added successfully!')), // Display native response message
        );
      }
      return BridgeResponse.success(true); // Assuming native method returns true on success
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Failed to add shortcut: ${e.toString()}')),
        );
      }
      return BridgeResponse.error(-1, 'Failed to add shortcut: ${e.toString()}');
    }
  }

  Future<BridgeResponse> _handleOnAppIcon(BuildContext context, String styleId) async {
    if (!mounted) return BridgeResponse.error(-1, 'Widget not mounted');
    try {
      await _platformChannel.invokeMethod('setAppIcon', {'styleId': styleId});
      if (mounted) {
        // Use a Builder to get a context that is a descendant of ScaffoldMessenger
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('App icon change requested for style: $styleId')),
        );
      }
      return BridgeResponse.success(true);
    } catch (e) {
      if (mounted) {
        // Use a Builder to get a context that is a descendant of ScaffoldMessenger
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Failed to change app icon: ${e.toString()}')),
        );
      }
      return BridgeResponse.error(-1, 'Failed to change app icon: ${e.toString()}');
    }
  }

  void _unbindBackgroundIsolate() {
    debugPrint('[_MyAppState] _unbindBackgroundIsolate called.');
    IsolateNameServer.removePortNameMapping('downloader_send_port');
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: _isAppBarVisible ? AppBar(title: Text(_appBarTitle)) : null,
        body: Builder(
          builder: (BuildContext builderContext) {
            return PopScope(
              canPop: false, // We handle popping manually
              onPopInvokedWithResult: (didPop, result) async {
                if (didPop) return; // If system already popped, do nothing

                // If WebView cannot go back, handle double-tap to exit
                final now = DateTime.now();
                if (_lastPopTime == null ||
                    now.difference(_lastPopTime!) >
                        const Duration(seconds: 2)) {
                  _lastPopTime = now;
                  if (mounted) {
                    ScaffoldMessenger.of(builderContext).showSnackBar(
                      const SnackBar(
                        content: Text('再按一次退出程序'),
                        duration: Duration(seconds: 2),
                      ),
                    );
                  }
                  // Do not pop yet
                } else {
                  // If double-tapped within 2 seconds, exit the app
                  SystemNavigator.pop(); // Use SystemNavigator.pop() to exit the app
                }

                if (_webViewController != null) {
                  // If WebView can go back, navigate within WebView
                  if (await _webViewController!.canGoBack()) {
                    _webViewController!.goBack();
                    return; // Prevent app from exiting
                  }
                }
              },
              child: InAppWebView(
                initialFile: 'packages/appbridge/assets/demo.html',
                initialSettings: InAppWebViewSettings(
                  // Common settings here
                ),
                onWebViewCreated: (controller) async {
                  _webViewController = controller;
                  debugPrint(
                    '[_MyAppState] _webViewController set in onWebViewCreated: $_webViewController',
                  );
                  if (appbridgePlugin != null && _webViewController != null) {
                    await appbridgePlugin!.initialize(
                      _webViewController!,
                      builderContext, // Pass the current context
                      onAddShortcut: (title, url) => _handleOnAddShortcut(builderContext, title, url),
                      onAppIcon: (styleId) => _handleOnAppIcon(builderContext, styleId),
                      onNavClose: () {
                        debugPrint(
                          '>>> NavCloseCallback triggered at ${DateTime.now()} <<<',
                        );
                        if (!mounted) return;
                        Navigator.of(builderContext).pop();
                        debugPrint(
                          '>>> Navigator.pop() called in NavCloseCallback <<<',
                        );
                      },
                      onNavSetTitle: (title) {
                        setState(() {
                          _appBarTitle = title;
                        });
                      },
                      onNavReplace: (url, title) {
                        debugPrint('onNavReplace---url=$url+“;title=”+$title');
                      },
                      onNavSetBars: (visible) {
                        setState(() {
                          _isAppBarVisible = visible;
                        });
                      },
                      onLoadUrl: (url, title) async {
                        if (_webViewController != null) {
                          if (appbridgePlugin != null) {
                            appbridgePlugin?.nav?.open(
                              url: url,
                              title: title,
                            ); // Pass the title
                            appbridgePlugin?.ui?.toast(message: '加载URL: $url');
                          }
                        } else {
                          debugPrint(
                            'Error: _webViewController is null when trying to load URL via loadUrl',
                          );
                        }
                      },
                    );
                    // This call is now being moved to onLoadStop
                  } else {
                    debugPrint(
                      '!!! appbridgePlugin or _webViewController is NULL in main onWebViewCreated. Appbridge not initialized. !!!',
                    );
                  }
                },
                onLoadStop: (controller, url) async {
                                  debugPrint('Page finished loading: $url');
                                  if (appbridgePlugin != null) {
                                    await appbridgePlugin!.injectJavaScript();
                                    _webViewController?.evaluateJavascript(
                                      source: 'flutterIsReady();',
                                    );
                                  }
                                },                onConsoleMessage: (controller, consoleMessage) {
                  debugPrint('Console Message: ${consoleMessage.message}');
                },
              ),
            );
          },
        ),
      ),
    );
  }
}