import 'dart:async'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/instance_manager.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'models/model.dart'; final globalKey = GlobalKey(); final navigationBarKey = GlobalKey(); var isAndroid = false; var isIOS = false; var isWeb = false; var isWebDesktop = false; var isDesktop = false; var version = ""; int androidVersion = 0; typedef F = String Function(String); typedef FMethod = String Function(String, dynamic); class Translator { static late F call; } class MyTheme { MyTheme._(); static const Color grayBg = Color(0xFFEEEEEE); static const Color white = Color(0xFFFFFFFF); static const Color accent = Color(0xFF0071FF); static const Color accent50 = Color(0x770071FF); static const Color accent80 = Color(0xAA0071FF); static const Color canvasColor = Color(0xFF212121); static const Color border = Color(0xFFCCCCCC); static const Color idColor = Color(0xFF00B6F0); static const Color darkGray = Color(0xFFB9BABC); static const Color dark = Colors.black87; static ThemeData lightTheme = ThemeData( brightness: Brightness.light, primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, tabBarTheme: TabBarTheme(labelColor: Colors.black87), ); static ThemeData darkTheme = ThemeData( brightness: Brightness.dark, primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, tabBarTheme: TabBarTheme(labelColor: Colors.white70)); } bool isDarkTheme() { final isDark = "Y" == Get.find().getString("darkTheme"); debugPrint("current is dark theme: $isDark"); return isDark; } final ButtonStyle flatButtonStyle = TextButton.styleFrom( minimumSize: Size(0, 36), padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 10.0), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(2.0)), ), ); void showToast(String text, {Duration? duration}) { SmartDialog.showToast(text, displayTime: duration); } void showLoading(String text, {bool clickMaskDismiss = false}) { SmartDialog.dismiss(); SmartDialog.showLoading( clickMaskDismiss: false, builder: (context) { return Container( color: MyTheme.white, constraints: BoxConstraints(maxWidth: 240), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox(height: 30), Center(child: CircularProgressIndicator()), SizedBox(height: 20), Center( child: Text(Translator.call(text), style: TextStyle(fontSize: 15))), SizedBox(height: 20), Center( child: TextButton( style: flatButtonStyle, onPressed: () { SmartDialog.dismiss(); backToHome(); }, child: Text(Translator.call('Cancel'), style: TextStyle(color: MyTheme.accent)))) ])); }); } backToHome() { Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/")); } typedef DialogBuilder = CustomAlertDialog Function( StateSetter setState, void Function([dynamic]) close); class DialogManager { static int _tag = 0; static dismissByTag(String tag, [result]) { SmartDialog.dismiss(tag: tag, result: result); } static Future show(DialogBuilder builder, {bool clickMaskDismiss = false, bool backDismiss = false, String? tag, bool useAnimation = true}) async { final t; if (tag != null) { t = tag; } else { _tag += 1; t = _tag.toString(); } SmartDialog.dismiss(status: SmartStatus.allToast); SmartDialog.dismiss(status: SmartStatus.loading); final close = ([res]) { SmartDialog.dismiss(tag: t, result: res); }; final res = await SmartDialog.show( tag: t, clickMaskDismiss: clickMaskDismiss, backDismiss: backDismiss, useAnimation: useAnimation, builder: (_) => StatefulBuilder( builder: (_, setState) => builder(setState, close))); return res; } } class CustomAlertDialog extends StatelessWidget { CustomAlertDialog({required this.title, required this.content, required this.actions, this.contentPadding}); final Widget title; final Widget content; final List actions; final double? contentPadding; @override Widget build(BuildContext context) { return AlertDialog( scrollable: true, title: title, contentPadding: EdgeInsets.symmetric(horizontal: contentPadding ?? 25, vertical: 10), content: content, actions: actions, ); } } void msgBox(String type, String title, String text, {bool? hasCancel}) { var wrap = (String text, void Function() onPressed) => ButtonTheme( padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, //limits the touch area to the button area minWidth: 0, //wraps child's width height: 0, child: TextButton( style: flatButtonStyle, onPressed: onPressed, child: Text(Translator.call(text), style: TextStyle(color: MyTheme.accent)))); SmartDialog.dismiss(); final buttons = [ wrap(Translator.call('OK'), () { SmartDialog.dismiss(); backToHome(); }) ]; if (hasCancel == null) { hasCancel = type != 'error'; } if (hasCancel) { buttons.insert( 0, wrap(Translator.call('Cancel'), () { SmartDialog.dismiss(); })); } DialogManager.show((setState, close) => CustomAlertDialog( title: Text(translate(title), style: TextStyle(fontSize: 21)), content: Text(Translator.call(text), style: TextStyle(fontSize: 15)), actions: buttons)); } Color str2color(String str, [alpha = 0xFF]) { var hash = 160 << 16 + 114 << 8 + 91; for (var i = 0; i < str.length; i += 1) { hash = str.codeUnitAt(i) + ((hash << 5) - hash); } hash = hash % 16777216; return Color((hash & 0xFF7FFF) | (alpha << 24)); } const K = 1024; const M = K * K; const G = M * K; String readableFileSize(double size) { if (size < K) { return size.toStringAsFixed(2) + " B"; } else if (size < M) { return (size / K).toStringAsFixed(2) + " KB"; } else if (size < G) { return (size / M).toStringAsFixed(2) + " MB"; } else { return (size / G).toStringAsFixed(2) + " GB"; } } /// Flutter can't not catch PointerMoveEvent when size is 1 /// This will happen in Android AccessibilityService Input /// android can't init dispatching size yet ,see: https://stackoverflow.com/questions/59960451/android-accessibility-dispatchgesture-is-it-possible-to-specify-pressure-for-a /// use this temporary solution until flutter or android fixes the bug class AccessibilityListener extends StatelessWidget { final Widget? child; static final offset = 100; AccessibilityListener({this.child}); @override Widget build(BuildContext context) { return Listener( onPointerDown: (evt) { if (evt.size == 1) { GestureBinding.instance.handlePointerEvent(PointerAddedEvent( pointer: evt.pointer + offset, position: evt.position)); GestureBinding.instance.handlePointerEvent(PointerDownEvent( pointer: evt.pointer + offset, size: 0.1, position: evt.position)); } }, onPointerUp: (evt) { if (evt.size == 1) { GestureBinding.instance.handlePointerEvent(PointerUpEvent( pointer: evt.pointer + offset, size: 0.1, position: evt.position)); GestureBinding.instance.handlePointerEvent(PointerRemovedEvent( pointer: evt.pointer + offset, position: evt.position)); } }, onPointerMove: (evt) { if (evt.size == 1) { GestureBinding.instance.handlePointerEvent(PointerMoveEvent( pointer: evt.pointer + offset, size: 0.1, delta: evt.delta, position: evt.position)); } }, child: child); } } class PermissionManager { static Completer? _completer; static Timer? _timer; static var _current = ""; static final permissions = [ "audio", "file", "ignore_battery_optimizations", "application_details_settings" ]; static bool isWaitingFile() { if (_completer != null) { return !_completer!.isCompleted && _current == "file"; } return false; } static Future check(String type) { if (!permissions.contains(type)) return Future.error("Wrong permission!$type"); return gFFI.invokeMethod("check_permission", type); } static Future request(String type) { if (!permissions.contains(type)) return Future.error("Wrong permission!$type"); gFFI.invokeMethod("request_permission", type); if (type == "ignore_battery_optimizations") { return Future.value(false); } _current = type; _completer = Completer(); gFFI.invokeMethod("request_permission", type); // timeout _timer?.cancel(); _timer = Timer(Duration(seconds: 60), () { if (_completer == null) return; if (!_completer!.isCompleted) { _completer!.complete(false); } _completer = null; _current = ""; }); return _completer!.future; } static complete(String type, bool res) { if (type != _current) { res = false; } _timer?.cancel(); _completer?.complete(res); _current = ""; } } RadioListTile getRadio( String name, T toValue, T curValue, void Function(T?) onChange) { return RadioListTile( controlAffinity: ListTileControlAffinity.trailing, title: Text(translate(name)), value: toValue, groupValue: curValue, onChanged: onChange, dense: true, ); } /// find ffi, tag is Remote ID /// for session specific usage FFI ffi(String? tag) { return Get.find(tag: tag); } /// Global FFI object late FFI _globalFFI; FFI get gFFI => _globalFFI; Future initGlobalFFI() async { _globalFFI = FFI(); // after `put`, can also be globally found by Get.find(); Get.put(_globalFFI, permanent: true); await _globalFFI.ffiModel.init(); // trigger connection status updater await _globalFFI.bind.mainCheckConnectStatus(); // global shared preference await Get.putAsync(() => SharedPreferences.getInstance()); }