2022-05-30 08:25:06 +03:00
import ' dart:async ' ;
2022-08-03 17:03:31 +03:00
import ' dart:io ' ;
2022-05-30 08:25:06 +03:00
2022-08-09 14:32:19 +03:00
import ' package:desktop_multi_window/desktop_multi_window.dart ' ;
2022-04-14 10:37:47 +03:00
import ' package:flutter/gestures.dart ' ;
2020-11-15 15:04:05 +03:00
import ' package:flutter/material.dart ' ;
2022-06-13 16:07:26 +03:00
import ' package:get/instance_manager.dart ' ;
2022-07-29 11:47:24 +03:00
import ' package:shared_preferences/shared_preferences.dart ' ;
2022-08-09 04:01:06 +03:00
import ' package:window_manager/window_manager.dart ' ;
2022-08-12 13:42:02 +03:00
import ' package:back_button_interceptor/back_button_interceptor.dart ' ;
2022-04-19 08:07:45 +03:00
import ' models/model.dart ' ;
2022-08-03 17:03:31 +03:00
import ' models/platform_model.dart ' ;
2021-08-02 15:54:56 +03:00
2022-03-07 17:54:34 +03:00
final globalKey = GlobalKey < NavigatorState > ( ) ;
2022-04-12 17:38:39 +03:00
final navigationBarKey = GlobalKey ( ) ;
2022-03-07 17:54:34 +03:00
2022-08-03 17:03:31 +03:00
var isAndroid = Platform . isAndroid ;
var isIOS = Platform . isIOS ;
2022-03-17 16:03:52 +03:00
var isWeb = false ;
2022-05-23 11:02:37 +03:00
var isWebDesktop = false ;
2022-08-03 17:03:31 +03:00
var isDesktop = Platform . isWindows | | Platform . isMacOS | | Platform . isLinux ;
2022-03-17 16:03:52 +03:00
var version = " " ;
2022-03-24 12:58:33 +03:00
int androidVersion = 0 ;
2022-03-17 16:03:52 +03:00
2021-08-02 15:54:56 +03:00
typedef F = String Function ( String ) ;
2022-02-09 21:07:53 +03:00
typedef FMethod = String Function ( String , dynamic ) ;
2021-08-02 15:54:56 +03:00
2020-11-15 20:13:26 +03:00
class MyTheme {
2020-11-20 11:37:48 +03:00
MyTheme . _ ( ) ;
2022-02-02 12:25:56 +03:00
2020-11-15 20:13:26 +03:00
static const Color grayBg = Color ( 0xFFEEEEEE ) ;
static const Color white = Color ( 0xFFFFFFFF ) ;
2020-11-16 17:00:09 +03:00
static const Color accent = Color ( 0xFF0071FF ) ;
2020-11-19 12:22:42 +03:00
static const Color accent50 = Color ( 0x770071FF ) ;
2020-11-26 21:14:27 +03:00
static const Color accent80 = Color ( 0xAA0071FF ) ;
2020-11-19 13:41:37 +03:00
static const Color canvasColor = Color ( 0xFF212121 ) ;
2020-11-20 08:06:52 +03:00
static const Color border = Color ( 0xFFCCCCCC ) ;
2022-01-31 11:22:05 +03:00
static const Color idColor = Color ( 0xFF00B6F0 ) ;
static const Color darkGray = Color ( 0xFFB9BABC ) ;
2022-05-30 08:25:06 +03:00
static const Color dark = Colors . black87 ;
2022-07-29 11:47:24 +03:00
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 < SharedPreferences > ( ) . getString ( " darkTheme " ) ;
debugPrint ( " current is dark theme: $ isDark " ) ;
return isDark ;
2020-11-15 20:13:26 +03:00
}
2021-08-02 15:54:56 +03:00
final ButtonStyle flatButtonStyle = TextButton . styleFrom (
2022-05-18 10:47:07 +03:00
minimumSize: Size ( 0 , 36 ) ,
padding: EdgeInsets . symmetric ( horizontal: 16.0 , vertical: 10.0 ) ,
2021-08-02 15:54:56 +03:00
shape: const RoundedRectangleBorder (
borderRadius: BorderRadius . all ( Radius . circular ( 2.0 ) ) ,
) ,
) ;
2022-08-12 13:42:02 +03:00
backToHomePage ( ) {
if ( isAndroid | | isIOS ) {
Navigator . popUntil ( globalKey . currentContext ! , ModalRoute . withName ( " / " ) ) ;
} else {
// TODO desktop
}
2022-03-13 18:07:52 +03:00
}
2022-08-09 14:32:19 +03:00
void window_on_top ( int ? id ) {
if ( id = = null ) {
// main window
windowManager . restore ( ) ;
windowManager . show ( ) ;
windowManager . focus ( ) ;
} else {
2022-08-09 16:12:55 +03:00
WindowController . fromWindowId ( id )
. . focus ( )
. . show ( ) ;
2022-08-09 14:32:19 +03:00
}
2022-08-09 04:01:06 +03:00
}
2022-03-12 19:32:44 +03:00
typedef DialogBuilder = CustomAlertDialog Function (
2022-04-19 08:07:45 +03:00
StateSetter setState , void Function ( [ dynamic ] ) close ) ;
2022-03-12 19:32:44 +03:00
2022-08-12 13:42:02 +03:00
class Dialog < T > {
OverlayEntry ? entry ;
Completer < T ? > completer = Completer < T > ( ) ;
Dialog ( ) ;
void complete ( T ? res ) {
try {
if ( ! completer . isCompleted ) {
completer . complete ( res ) ;
}
entry ? . remove ( ) ;
} catch ( e ) {
debugPrint ( " Dialog complete catch error: $ e " ) ;
}
}
}
class OverlayDialogManager {
OverlayState ? _overlayState ;
Map < String , Dialog > _dialogs = Map ( ) ;
int _tagCount = 0 ;
2022-03-12 19:32:44 +03:00
2022-08-12 13:42:02 +03:00
/// By default OverlayDialogManager use global overlay
OverlayDialogManager ( ) {
_overlayState = globalKey . currentState ? . overlay ;
2022-02-28 13:29:25 +03:00
}
2022-03-12 19:32:44 +03:00
2022-08-12 13:42:02 +03:00
void setOverlayState ( OverlayState ? overlayState ) {
_overlayState = overlayState ;
}
void dismissAll ( ) {
_dialogs . forEach ( ( key , value ) {
value . complete ( null ) ;
BackButtonInterceptor . removeByName ( key ) ;
} ) ;
_dialogs . clear ( ) ;
}
void dismissByTag ( String tag ) {
_dialogs [ tag ] ? . complete ( null ) ;
_dialogs . remove ( tag ) ;
BackButtonInterceptor . removeByName ( tag ) ;
}
// TODO clickMaskDismiss
Future < T ? > show < T > ( DialogBuilder builder ,
2022-04-20 18:43:19 +03:00
{ bool clickMaskDismiss = false ,
2022-08-03 17:03:31 +03:00
bool backDismiss = false ,
String ? tag ,
2022-08-12 13:42:02 +03:00
bool useAnimation = true ,
bool forceGlobal = false } ) {
final overlayState =
forceGlobal ? globalKey . currentState ? . overlay : _overlayState ;
if ( overlayState = = null ) {
return Future . error (
" [OverlayDialogManager] Failed to show dialog, _overlayState is null, call [setOverlayState] first " ) ;
}
final _tag ;
2022-04-19 08:07:45 +03:00
if ( tag ! = null ) {
2022-08-12 13:42:02 +03:00
_tag = tag ;
2022-04-19 08:07:45 +03:00
} else {
2022-08-12 13:42:02 +03:00
_tag = _tagCount . toString ( ) ;
_tagCount + + ;
2022-04-19 08:07:45 +03:00
}
2022-08-12 13:42:02 +03:00
final dialog = Dialog < T > ( ) ;
_dialogs [ _tag ] = dialog ;
2022-04-19 08:07:45 +03:00
final close = ( [ res ] ) {
2022-08-12 13:42:02 +03:00
_dialogs . remove ( _tag ) ;
dialog . complete ( res ) ;
BackButtonInterceptor . removeByName ( _tag ) ;
2022-04-19 08:07:45 +03:00
} ;
2022-08-12 13:42:02 +03:00
dialog . entry = OverlayEntry ( builder: ( _ ) {
return Container (
color: Colors . transparent ,
child: StatefulBuilder (
builder: ( _ , setState ) = > builder ( setState , close ) ) ) ;
} ) ;
overlayState . insert ( dialog . entry ! ) ;
BackButtonInterceptor . add ( ( stopDefaultButtonEvent , routeInfo ) {
if ( backDismiss ) {
close ( ) ;
}
return true ;
} , name: _tag ) ;
return dialog . completer . future ;
}
void showLoading ( String text ,
{ bool clickMaskDismiss = false , bool cancelToClose = false } ) {
show ( ( setState , close ) = > CustomAlertDialog (
content: 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 ( translate ( text ) ,
style: TextStyle ( fontSize: 15 ) ) ) ,
SizedBox ( height: 20 ) ,
Center (
child: TextButton (
style: flatButtonStyle ,
onPressed: ( ) {
dismissAll ( ) ;
if ( cancelToClose ) backToHomePage ( ) ;
} ,
child: Text ( translate ( ' Cancel ' ) ,
style: TextStyle ( color: MyTheme . accent ) ) ) )
] ) ) ) ) ;
}
void showToast ( String text ) {
// TODO
2022-03-12 19:32:44 +03:00
}
2022-02-28 11:11:21 +03:00
}
2022-02-02 12:25:56 +03:00
2022-03-12 19:32:44 +03:00
class CustomAlertDialog extends StatelessWidget {
2022-08-03 17:03:31 +03:00
CustomAlertDialog (
2022-08-12 13:42:02 +03:00
{ this . title , required this . content , this . actions , this . contentPadding } ) ;
2020-11-18 07:49:43 +03:00
2022-08-12 13:42:02 +03:00
final Widget ? title ;
2022-03-12 19:32:44 +03:00
final Widget content ;
2022-08-12 13:42:02 +03:00
final List < Widget > ? actions ;
2022-03-12 19:32:44 +03:00
final double ? contentPadding ;
@ override
Widget build ( BuildContext context ) {
2022-04-21 05:02:47 +03:00
return AlertDialog (
scrollable: true ,
title: title ,
contentPadding:
2022-08-03 17:03:31 +03:00
EdgeInsets . symmetric ( horizontal: contentPadding ? ? 25 , vertical: 10 ) ,
2022-04-21 05:02:47 +03:00
content: content ,
actions: actions ,
) ;
2022-03-12 19:32:44 +03:00
}
2020-11-16 17:00:09 +03:00
}
2020-11-16 17:12:32 +03:00
2022-08-12 13:42:02 +03:00
void msgBox (
String type , String title , String text , OverlayDialogManager dialogManager ,
{ bool ? hasCancel } ) {
2020-11-28 20:36:10 +03:00
var wrap = ( String text , void Function ( ) onPressed ) = > ButtonTheme (
padding: EdgeInsets . symmetric ( horizontal: 20 , vertical: 10 ) ,
2022-02-02 12:25:56 +03:00
materialTapTargetSize: MaterialTapTargetSize . shrinkWrap ,
//limits the touch area to the button area
minWidth: 0 ,
//wraps child's width
2020-11-28 20:36:10 +03:00
height: 0 ,
2021-08-02 15:54:56 +03:00
child: TextButton (
style: flatButtonStyle ,
2020-11-28 20:36:10 +03:00
onPressed: onPressed ,
2022-08-12 13:42:02 +03:00
child:
Text ( translate ( text ) , style: TextStyle ( color: MyTheme . accent ) ) ) ) ;
2020-11-28 20:36:10 +03:00
2022-08-12 13:42:02 +03:00
dialogManager . dismissAll ( ) ;
2022-08-08 17:00:01 +03:00
List < Widget > buttons = [ ] ;
if ( type ! = " connecting " & & type ! = " success " & & type . indexOf ( " nook " ) < 0 ) {
buttons . insert (
0 ,
2022-08-12 13:42:02 +03:00
wrap ( translate ( ' OK ' ) , ( ) {
dialogManager . dismissAll ( ) ;
backToHomePage ( ) ;
2022-08-08 17:00:01 +03:00
} ) ) ;
}
2020-11-24 06:25:56 +03:00
if ( hasCancel = = null ) {
2022-08-08 17:00:01 +03:00
// hasCancel = type != 'error';
hasCancel = type . indexOf ( " error " ) < 0 & &
type . indexOf ( " nocancel " ) < 0 & &
type ! = " restarting " ;
2020-11-24 06:25:56 +03:00
}
2020-11-28 20:36:10 +03:00
if ( hasCancel ) {
buttons . insert (
2022-04-19 08:07:45 +03:00
0 ,
2022-08-12 13:42:02 +03:00
wrap ( translate ( ' Cancel ' ) , ( ) {
dialogManager . dismissAll ( ) ;
2020-11-28 20:36:10 +03:00
} ) ) ;
}
2022-08-08 17:00:01 +03:00
// TODO: test this button
if ( type . indexOf ( " hasclose " ) > = 0 ) {
buttons . insert (
0 ,
2022-08-12 13:42:02 +03:00
wrap ( translate ( ' Close ' ) , ( ) {
dialogManager . dismissAll ( ) ;
2022-08-08 17:00:01 +03:00
} ) ) ;
}
2022-08-12 13:42:02 +03:00
dialogManager . show ( ( setState , close ) = > CustomAlertDialog (
2022-04-19 08:07:45 +03:00
title: Text ( translate ( title ) , style: TextStyle ( fontSize: 21 ) ) ,
2022-08-12 13:42:02 +03:00
content: Text ( translate ( text ) , style: TextStyle ( fontSize: 15 ) ) ,
2022-04-19 08:07:45 +03:00
actions: buttons ) ) ;
2020-11-21 09:40:28 +03:00
}
2020-11-25 13:33:09 +03:00
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 ) ;
}
2021-08-14 09:14:01 +03:00
hash = hash % 16777216 ;
return Color ( ( hash & 0xFF7FFF ) | ( alpha < < 24 ) ) ;
2020-11-25 13:33:09 +03:00
}
2022-02-02 12:25:56 +03:00
2022-03-17 16:03:52 +03:00
const K = 1024 ;
const M = K * K ;
const G = M * K ;
String readableFileSize ( double size ) {
if ( size < K ) {
2022-07-11 05:30:45 +03:00
return size . toStringAsFixed ( 2 ) + " B " ;
2022-03-17 16:03:52 +03:00
} 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 " ;
}
}
2022-04-14 10:37:47 +03:00
/// 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 ) {
2022-05-26 13:25:16 +03:00
if ( evt . size = = 1 ) {
GestureBinding . instance . handlePointerEvent ( PointerAddedEvent (
2022-04-14 10:37:47 +03:00
pointer: evt . pointer + offset , position: evt . position ) ) ;
2022-05-26 13:25:16 +03:00
GestureBinding . instance . handlePointerEvent ( PointerDownEvent (
2022-04-14 10:37:47 +03:00
pointer: evt . pointer + offset ,
size: 0.1 ,
position: evt . position ) ) ;
}
} ,
onPointerUp: ( evt ) {
2022-06-19 19:15:37 +03:00
if ( evt . size = = 1 ) {
2022-05-26 13:25:16 +03:00
GestureBinding . instance . handlePointerEvent ( PointerUpEvent (
2022-04-14 10:37:47 +03:00
pointer: evt . pointer + offset ,
size: 0.1 ,
position: evt . position ) ) ;
2022-05-26 13:25:16 +03:00
GestureBinding . instance . handlePointerEvent ( PointerRemovedEvent (
2022-04-15 12:45:48 +03:00
pointer: evt . pointer + offset , position: evt . position ) ) ;
2022-04-14 10:37:47 +03:00
}
} ,
onPointerMove: ( evt ) {
2022-06-19 19:15:37 +03:00
if ( evt . size = = 1 ) {
2022-05-26 13:25:16 +03:00
GestureBinding . instance . handlePointerEvent ( PointerMoveEvent (
2022-04-14 10:37:47 +03:00
pointer: evt . pointer + offset ,
size: 0.1 ,
delta: evt . delta ,
position: evt . position ) ) ;
}
} ,
child: child ) ;
}
}
2022-04-20 17:37:47 +03:00
class PermissionManager {
static Completer < bool > ? _completer ;
static Timer ? _timer ;
static var _current = " " ;
2022-07-16 17:31:44 +03:00
static final permissions = [
" audio " ,
" file " ,
" ignore_battery_optimizations " ,
" application_details_settings "
] ;
2022-04-20 17:37:47 +03:00
static bool isWaitingFile ( ) {
if ( _completer ! = null ) {
return ! _completer ! . isCompleted & & _current = = " file " ;
}
return false ;
}
static Future < bool > check ( String type ) {
if ( ! permissions . contains ( type ) )
return Future . error ( " Wrong permission! $ type " ) ;
2022-06-13 16:07:26 +03:00
return gFFI . invokeMethod ( " check_permission " , type ) ;
2022-04-20 17:37:47 +03:00
}
static Future < bool > request ( String type ) {
if ( ! permissions . contains ( type ) )
return Future . error ( " Wrong permission! $ type " ) ;
2022-08-01 09:33:08 +03:00
gFFI . invokeMethod ( " request_permission " , type ) ;
2022-07-14 12:44:37 +03:00
if ( type = = " ignore_battery_optimizations " ) {
return Future . value ( false ) ;
}
2022-04-20 17:37:47 +03:00
_current = type ;
_completer = Completer < bool > ( ) ;
2022-06-13 16:07:26 +03:00
gFFI . invokeMethod ( " request_permission " , type ) ;
2022-04-20 17:37:47 +03:00
// 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 = " " ;
}
}
2022-06-13 16:07:26 +03:00
2022-07-29 13:34:25 +03:00
RadioListTile < T > getRadio < T > (
String name , T toValue , T curValue , void Function ( T ? ) onChange ) {
return RadioListTile < T > (
controlAffinity: ListTileControlAffinity . trailing ,
title: Text ( translate ( name ) ) ,
value: toValue ,
groupValue: curValue ,
onChanged: onChange ,
dense: true ,
) ;
}
2022-08-01 05:44:05 +03:00
2022-08-05 15:29:43 +03:00
CheckboxListTile getToggle (
String id , void Function ( void Function ( ) ) setState , option , name ) {
final opt = bind . getSessionToggleOptionSync ( id: id , arg: option ) ;
return CheckboxListTile (
value: opt ,
onChanged: ( v ) {
setState ( ( ) {
bind . sessionToggleOption ( id: id , value: option ) ;
} ) ;
if ( option = = " show-quality-monitor " ) {
gFFI . qualityMonitorModel . checkShowQualityMonitor ( id ) ;
}
} ,
dense: true ,
title: Text ( translate ( name ) ) ) ;
}
2022-06-13 16:07:26 +03:00
/// find ffi, tag is Remote ID
/// for session specific usage
FFI ffi ( String ? tag ) {
return Get . find < FFI > ( tag: tag ) ;
}
/// Global FFI object
late FFI _globalFFI ;
FFI get gFFI = > _globalFFI ;
Future < void > initGlobalFFI ( ) async {
2022-08-04 12:24:02 +03:00
debugPrint ( " _globalFFI init " ) ;
2022-06-13 16:07:26 +03:00
_globalFFI = FFI ( ) ;
2022-08-04 12:24:02 +03:00
debugPrint ( " _globalFFI init end " ) ;
2022-06-13 16:07:26 +03:00
// after `put`, can also be globally found by Get.find<FFI>();
Get . put ( _globalFFI , permanent: true ) ;
2022-07-25 11:23:45 +03:00
// trigger connection status updater
2022-08-03 17:03:31 +03:00
await bind . mainCheckConnectStatus ( ) ;
2022-07-29 11:47:24 +03:00
// global shared preference
await Get . putAsync ( ( ) = > SharedPreferences . getInstance ( ) ) ;
2022-08-03 17:03:31 +03:00
}
2022-08-08 12:53:51 +03:00
String translate ( String name ) {
if ( name . startsWith ( ' Failed to ' ) & & name . contains ( ' : ' ) ) {
return name . split ( ' : ' ) . map ( ( x ) = > translate ( x ) ) . join ( ' : ' ) ;
}
return platformFFI . translate ( name , localeName ) ;
}