common.dart 110 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630
  1. import 'dart:async';
  2. import 'dart:convert';
  3. import 'dart:math';
  4. import 'package:back_button_interceptor/back_button_interceptor.dart';
  5. import 'package:desktop_multi_window/desktop_multi_window.dart';
  6. import 'package:flutter/foundation.dart';
  7. import 'package:flutter/gestures.dart';
  8. import 'package:flutter/material.dart';
  9. import 'package:flutter/services.dart';
  10. import 'package:flutter_hbb/common/formatter/id_formatter.dart';
  11. import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart';
  12. import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
  13. import 'package:flutter_hbb/main.dart';
  14. import 'package:flutter_hbb/models/peer_model.dart';
  15. import 'package:flutter_hbb/models/state_model.dart';
  16. import 'package:flutter_hbb/utils/multi_window_manager.dart';
  17. import 'package:flutter_hbb/utils/platform_channel.dart';
  18. import 'package:flutter_svg/flutter_svg.dart';
  19. import 'package:get/get.dart';
  20. import 'package:provider/provider.dart';
  21. import 'package:uni_links/uni_links.dart';
  22. import 'package:url_launcher/url_launcher.dart';
  23. import 'package:uuid/uuid.dart';
  24. import 'package:window_manager/window_manager.dart';
  25. import 'package:window_size/window_size.dart' as window_size;
  26. import '../consts.dart';
  27. import 'common/widgets/overlay.dart';
  28. import 'mobile/pages/file_manager_page.dart';
  29. import 'mobile/pages/remote_page.dart';
  30. import 'desktop/pages/remote_page.dart' as desktop_remote;
  31. import 'desktop/pages/file_manager_page.dart' as desktop_file_manager;
  32. import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
  33. import 'models/model.dart';
  34. import 'models/platform_model.dart';
  35. import 'package:flutter_hbb/native/win32.dart'
  36. if (dart.library.html) 'package:flutter_hbb/web/win32.dart';
  37. import 'package:flutter_hbb/native/common.dart'
  38. if (dart.library.html) 'package:flutter_hbb/web/common.dart';
  39. final globalKey = GlobalKey<NavigatorState>();
  40. final navigationBarKey = GlobalKey();
  41. final isAndroid = isAndroid_;
  42. final isIOS = isIOS_;
  43. final isWindows = isWindows_;
  44. final isMacOS = isMacOS_;
  45. final isLinux = isLinux_;
  46. final isDesktop = isDesktop_;
  47. final isWeb = isWeb_;
  48. final isWebDesktop = isWebDesktop_;
  49. final isWebOnWindows = isWebOnWindows_;
  50. final isWebOnLinux = isWebOnLinux_;
  51. final isWebOnMacOs = isWebOnMacOS_;
  52. var isMobile = isAndroid || isIOS;
  53. var version = '';
  54. int androidVersion = 0;
  55. // Only used on Linux.
  56. // `windowManager.setResizable(false)` will reset the window size to the default size on Linux.
  57. // https://stackoverflow.com/questions/8193613/gtk-window-resize-disable-without-going-back-to-default
  58. // So we need to use this flag to enable/disable resizable.
  59. bool _linuxWindowResizable = true;
  60. // Only used on Windows(window manager).
  61. bool _ignoreDevicePixelRatio = true;
  62. /// only available for Windows target
  63. int windowsBuildNumber = 0;
  64. DesktopType? desktopType;
  65. bool get isMainDesktopWindow =>
  66. desktopType == DesktopType.main || desktopType == DesktopType.cm;
  67. String get screenInfo => screenInfo_;
  68. /// Check if the app is running with single view mode.
  69. bool isSingleViewApp() {
  70. return desktopType == DesktopType.cm;
  71. }
  72. /// * debug or test only, DO NOT enable in release build
  73. bool isTest = false;
  74. typedef F = String Function(String);
  75. typedef FMethod = String Function(String, dynamic);
  76. typedef StreamEventHandler = Future<void> Function(Map<String, dynamic>);
  77. typedef SessionID = UuidValue;
  78. final iconHardDrive = MemoryImage(Uint8List.fromList(base64Decode(
  79. 'iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAMAAACahl6sAAAAmVBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjHWqVAAAAMnRSTlMAv0BmzLJNXlhiUu2fxXDgu7WuSUUe29LJvpqUjX53VTstD7ilNujCqTEk5IYH+vEoFjKvAagAAAPpSURBVHja7d0JbhpBEIXhB3jYzb5vBgzYgO04df/DJXGUKMwU9ECmZ6pQfSfw028LCXW3YYwxxhhjjDHGGGOM0eZ9VV1MckdKWLM1bRQ/35GW/WxHHu1me6ShuyHvNl34VhlTKsYVeDWj1EzgUZ1S1DrAk/UDparZgxd9Sl0BHnxSBhpI3jfKQG2FpLUpE69I2ILikv1nsvygjBwPSNKYMlNHggqUoSKS80AZCnwHqQ1zCRvW+CRegwRFeFAMKKrtM8gTPJlzSfwFgT9dJom3IDN4VGaSeAryAK8m0SSeghTg1ZYiql6CjBDhO8mzlyAVhKhIwgXxrh5NojGIhyRckEdwpCdhgpSQgiWTRGMQNonGIGySp0SDvMDBX5KWxiB8Eo1BgE00SYJBykhNnkmSWJAcLpGaJNMgfJKyxiDAK4WNEwryhMtkJsk8CJtEYxA+icYgQIfCcgkEqcJNXhIRQdgkGoPwSTQG+e8khdu/7JOVREwQIKCwF41B2CQljUH4JLcH6SI+OUlEBQHa0SQag/BJNAbhkjxqDMIn0RgEeI4muSlID9eSkERgEKAVTaIxCJ9EYxA2ydVB8hCASVLRGAQYR5NoDMIn0RgEyFHYSGMQPonGII4kziCNvBgNJonEk4u3GAk8Sprk6eYaqbMDY0oKvUm5jfC/viGiSypV7+M3i2iDsAGpNEDYjlTa3W8RdR/r544g50ilnA0RxoZIE2NIXqQbhkAkGyKNDZHGhkhjQ6SxIdLYEGlsiDQ2JGTVeD0264U9zipPh7XOooffpA6pfNCXjxl4/c3pUzlChwzor53zwYYVfpI5pOV6LWFF/2jiJ5FDSs5jdY/0rwUAkUMeXWdBqnSqD0DikBqdqCHsjTvELm9In0IOri/0pwAEDtlSyNaRjAIAAoesKWTtuusxByBwCJp0oomwBXcYUuCQgE50ENajE4OvZAKHLB1/68Br5NqiyCGYOY8YRd77kTkEb64n7lZN+mOIX4QOwb5FX0ZVx3uOxwW+SB0CbBubemWP8/rlaaeRX+M3uUOuZENsiA25zIbYkPsZElBIHwL13U/PTjJ/cyOOEoVM3I+hziDQlELm7pPxw3eI8/7gPh1fpLA6xGnEeDDgO0UcIAzzM35HxLPIq5SXe9BLzOsj9eUaQqyXzxS1QFSfWM2cCANiHcAISJ0AnCKpUwTuIkkA3EeSInAXSQKcs1V18e24wlllUmQp9v9zXKeHi+akRAMOPVKhAqdPBZeUmnnEsO6QcJ0+4qmOSbBxFfGVRiTUqITrdKcCbyYO3/K4wX4+aQ+FfNjXhu3JfAVjjDHGGGOMMcYYY4xIPwCgfqT6TbhCLAAAAABJRU5ErkJggg==')));
  80. enum DesktopType {
  81. main,
  82. remote,
  83. fileTransfer,
  84. cm,
  85. portForward,
  86. }
  87. class IconFont {
  88. static const _family1 = 'Tabbar';
  89. static const _family2 = 'PeerSearchbar';
  90. IconFont._();
  91. static const IconData max = IconData(0xe606, fontFamily: _family1);
  92. static const IconData restore = IconData(0xe607, fontFamily: _family1);
  93. static const IconData close = IconData(0xe668, fontFamily: _family1);
  94. static const IconData min = IconData(0xe609, fontFamily: _family1);
  95. static const IconData add = IconData(0xe664, fontFamily: _family1);
  96. static const IconData menu = IconData(0xe628, fontFamily: _family1);
  97. static const IconData search = IconData(0xe6a4, fontFamily: _family2);
  98. static const IconData roundClose = IconData(0xe6ed, fontFamily: _family2);
  99. static const IconData addressBook =
  100. IconData(0xe602, fontFamily: "AddressBook");
  101. }
  102. class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {
  103. const ColorThemeExtension({
  104. required this.border,
  105. required this.border2,
  106. required this.border3,
  107. required this.highlight,
  108. required this.drag_indicator,
  109. required this.shadow,
  110. required this.errorBannerBg,
  111. required this.me,
  112. required this.toastBg,
  113. required this.toastText,
  114. required this.divider,
  115. });
  116. final Color? border;
  117. final Color? border2;
  118. final Color? border3;
  119. final Color? highlight;
  120. final Color? drag_indicator;
  121. final Color? shadow;
  122. final Color? errorBannerBg;
  123. final Color? me;
  124. final Color? toastBg;
  125. final Color? toastText;
  126. final Color? divider;
  127. static final light = ColorThemeExtension(
  128. border: Color(0xFFCCCCCC),
  129. border2: Color(0xFFBBBBBB),
  130. border3: Colors.black26,
  131. highlight: Color(0xFFE5E5E5),
  132. drag_indicator: Colors.grey[800],
  133. shadow: Colors.black,
  134. errorBannerBg: Color(0xFFFDEEEB),
  135. me: Colors.green,
  136. toastBg: Colors.black.withOpacity(0.6),
  137. toastText: Colors.white,
  138. divider: Colors.black38,
  139. );
  140. static final dark = ColorThemeExtension(
  141. border: Color(0xFF555555),
  142. border2: Color(0xFFE5E5E5),
  143. border3: Colors.white24,
  144. highlight: Color(0xFF3F3F3F),
  145. drag_indicator: Colors.grey,
  146. shadow: Colors.grey,
  147. errorBannerBg: Color(0xFF470F2D),
  148. me: Colors.greenAccent,
  149. toastBg: Colors.white.withOpacity(0.6),
  150. toastText: Colors.black,
  151. divider: Colors.white38,
  152. );
  153. @override
  154. ThemeExtension<ColorThemeExtension> copyWith({
  155. Color? border,
  156. Color? border2,
  157. Color? border3,
  158. Color? highlight,
  159. Color? drag_indicator,
  160. Color? shadow,
  161. Color? errorBannerBg,
  162. Color? me,
  163. Color? toastBg,
  164. Color? toastText,
  165. Color? divider,
  166. }) {
  167. return ColorThemeExtension(
  168. border: border ?? this.border,
  169. border2: border2 ?? this.border2,
  170. border3: border3 ?? this.border3,
  171. highlight: highlight ?? this.highlight,
  172. drag_indicator: drag_indicator ?? this.drag_indicator,
  173. shadow: shadow ?? this.shadow,
  174. errorBannerBg: errorBannerBg ?? this.errorBannerBg,
  175. me: me ?? this.me,
  176. toastBg: toastBg ?? this.toastBg,
  177. toastText: toastText ?? this.toastText,
  178. divider: divider ?? this.divider,
  179. );
  180. }
  181. @override
  182. ThemeExtension<ColorThemeExtension> lerp(
  183. ThemeExtension<ColorThemeExtension>? other, double t) {
  184. if (other is! ColorThemeExtension) {
  185. return this;
  186. }
  187. return ColorThemeExtension(
  188. border: Color.lerp(border, other.border, t),
  189. border2: Color.lerp(border2, other.border2, t),
  190. border3: Color.lerp(border3, other.border3, t),
  191. highlight: Color.lerp(highlight, other.highlight, t),
  192. drag_indicator: Color.lerp(drag_indicator, other.drag_indicator, t),
  193. shadow: Color.lerp(shadow, other.shadow, t),
  194. errorBannerBg: Color.lerp(shadow, other.errorBannerBg, t),
  195. me: Color.lerp(shadow, other.me, t),
  196. toastBg: Color.lerp(shadow, other.toastBg, t),
  197. toastText: Color.lerp(shadow, other.toastText, t),
  198. divider: Color.lerp(shadow, other.divider, t),
  199. );
  200. }
  201. }
  202. class MyTheme {
  203. MyTheme._();
  204. static const Color grayBg = Color(0xFFEFEFF2);
  205. static const Color accent = Color(0xFF0071FF);
  206. static const Color accent50 = Color(0x770071FF);
  207. static const Color accent80 = Color(0xAA0071FF);
  208. static const Color canvasColor = Color(0xFF212121);
  209. static const Color border = Color(0xFFCCCCCC);
  210. static const Color idColor = Color(0xFF00B6F0);
  211. static const Color darkGray = Color.fromARGB(255, 148, 148, 148);
  212. static const Color cmIdColor = Color(0xFF21790B);
  213. static const Color dark = Colors.black87;
  214. static const Color button = Color(0xFF2C8CFF);
  215. static const Color hoverBorder = Color(0xFF999999);
  216. // ListTile
  217. static const ListTileThemeData listTileTheme = ListTileThemeData(
  218. shape: RoundedRectangleBorder(
  219. borderRadius: BorderRadius.all(
  220. Radius.circular(5),
  221. ),
  222. ),
  223. );
  224. static SwitchThemeData switchTheme() {
  225. return SwitchThemeData(
  226. splashRadius: (isDesktop || isWebDesktop) ? 0 : kRadialReactionRadius);
  227. }
  228. static RadioThemeData radioTheme() {
  229. return RadioThemeData(
  230. splashRadius: (isDesktop || isWebDesktop) ? 0 : kRadialReactionRadius);
  231. }
  232. // Checkbox
  233. static const CheckboxThemeData checkboxTheme = CheckboxThemeData(
  234. splashRadius: 0,
  235. shape: RoundedRectangleBorder(
  236. borderRadius: BorderRadius.all(
  237. Radius.circular(5),
  238. ),
  239. ),
  240. );
  241. // TextButton
  242. // Value is used to calculate "dialog.actionsPadding"
  243. static const double mobileTextButtonPaddingLR = 20;
  244. // TextButton on mobile needs a fixed padding, otherwise small buttons
  245. // like "OK" has a larger left/right padding.
  246. static TextButtonThemeData mobileTextButtonTheme = TextButtonThemeData(
  247. style: TextButton.styleFrom(
  248. padding: EdgeInsets.symmetric(horizontal: mobileTextButtonPaddingLR),
  249. shape: RoundedRectangleBorder(
  250. borderRadius: BorderRadius.circular(8.0),
  251. ),
  252. ),
  253. );
  254. //tooltip
  255. static TooltipThemeData tooltipTheme() {
  256. return TooltipThemeData(
  257. waitDuration: Duration(seconds: 1, milliseconds: 500),
  258. );
  259. }
  260. // Dialogs
  261. static const double dialogPadding = 24;
  262. // padding bottom depends on content (some dialogs has no content)
  263. static EdgeInsets dialogTitlePadding({bool content = true}) {
  264. final double p = dialogPadding;
  265. return EdgeInsets.fromLTRB(p, p, p, content ? 0 : p);
  266. }
  267. // padding bottom depends on actions (mobile has dialogs without actions)
  268. static EdgeInsets dialogContentPadding({bool actions = true}) {
  269. final double p = dialogPadding;
  270. return (isDesktop || isWebDesktop)
  271. ? EdgeInsets.fromLTRB(p, p, p, actions ? (p - 4) : p)
  272. : EdgeInsets.fromLTRB(p, p, p, actions ? (p / 2) : p);
  273. }
  274. static EdgeInsets dialogActionsPadding() {
  275. final double p = dialogPadding;
  276. return (isDesktop || isWebDesktop)
  277. ? EdgeInsets.fromLTRB(p, 0, p, (p - 4))
  278. : EdgeInsets.fromLTRB(p, 0, (p - mobileTextButtonPaddingLR), (p / 2));
  279. }
  280. static EdgeInsets dialogButtonPadding = (isDesktop || isWebDesktop)
  281. ? EdgeInsets.only(left: dialogPadding)
  282. : EdgeInsets.only(left: dialogPadding / 3);
  283. static ScrollbarThemeData scrollbarTheme = ScrollbarThemeData(
  284. thickness: MaterialStateProperty.all(6),
  285. thumbColor: MaterialStateProperty.resolveWith<Color?>((states) {
  286. if (states.contains(MaterialState.dragged)) {
  287. return Colors.grey[900];
  288. } else if (states.contains(MaterialState.hovered)) {
  289. return Colors.grey[700];
  290. } else {
  291. return Colors.grey[500];
  292. }
  293. }),
  294. crossAxisMargin: 4,
  295. );
  296. static ScrollbarThemeData scrollbarThemeDark = scrollbarTheme.copyWith(
  297. thumbColor: MaterialStateProperty.resolveWith<Color?>((states) {
  298. if (states.contains(MaterialState.dragged)) {
  299. return Colors.grey[100];
  300. } else if (states.contains(MaterialState.hovered)) {
  301. return Colors.grey[300];
  302. } else {
  303. return Colors.grey[500];
  304. }
  305. }),
  306. );
  307. static ThemeData lightTheme = ThemeData(
  308. // https://stackoverflow.com/questions/77537315/after-upgrading-to-flutter-3-16-the-app-bar-background-color-button-size-and
  309. useMaterial3: false,
  310. brightness: Brightness.light,
  311. hoverColor: Color.fromARGB(255, 224, 224, 224),
  312. scaffoldBackgroundColor: Colors.white,
  313. dialogBackgroundColor: Colors.white,
  314. appBarTheme: AppBarTheme(
  315. shadowColor: Colors.transparent,
  316. ),
  317. dialogTheme: DialogTheme(
  318. elevation: 15,
  319. shape: RoundedRectangleBorder(
  320. borderRadius: BorderRadius.circular(18.0),
  321. side: BorderSide(
  322. width: 1,
  323. color: grayBg,
  324. ),
  325. ),
  326. ),
  327. scrollbarTheme: scrollbarTheme,
  328. inputDecorationTheme: isDesktop
  329. ? InputDecorationTheme(
  330. fillColor: grayBg,
  331. filled: true,
  332. isDense: true,
  333. border: OutlineInputBorder(
  334. borderRadius: BorderRadius.circular(8),
  335. ),
  336. )
  337. : null,
  338. textTheme: const TextTheme(
  339. titleLarge: TextStyle(fontSize: 19, color: Colors.black87),
  340. titleSmall: TextStyle(fontSize: 14, color: Colors.black87),
  341. bodySmall: TextStyle(fontSize: 12, color: Colors.black87, height: 1.25),
  342. bodyMedium:
  343. TextStyle(fontSize: 14, color: Colors.black87, height: 1.25),
  344. labelLarge: TextStyle(fontSize: 16.0, color: MyTheme.accent80)),
  345. cardColor: grayBg,
  346. hintColor: Color(0xFFAAAAAA),
  347. visualDensity: VisualDensity.adaptivePlatformDensity,
  348. tabBarTheme: const TabBarTheme(
  349. labelColor: Colors.black87,
  350. ),
  351. tooltipTheme: tooltipTheme(),
  352. splashColor: (isDesktop || isWebDesktop) ? Colors.transparent : null,
  353. highlightColor: (isDesktop || isWebDesktop) ? Colors.transparent : null,
  354. splashFactory: (isDesktop || isWebDesktop) ? NoSplash.splashFactory : null,
  355. textButtonTheme: (isDesktop || isWebDesktop)
  356. ? TextButtonThemeData(
  357. style: TextButton.styleFrom(
  358. splashFactory: NoSplash.splashFactory,
  359. shape: RoundedRectangleBorder(
  360. borderRadius: BorderRadius.circular(18.0),
  361. ),
  362. ),
  363. )
  364. : mobileTextButtonTheme,
  365. elevatedButtonTheme: ElevatedButtonThemeData(
  366. style: ElevatedButton.styleFrom(
  367. backgroundColor: MyTheme.accent,
  368. shape: RoundedRectangleBorder(
  369. borderRadius: BorderRadius.circular(8.0),
  370. ),
  371. ),
  372. ),
  373. outlinedButtonTheme: OutlinedButtonThemeData(
  374. style: OutlinedButton.styleFrom(
  375. backgroundColor: grayBg,
  376. foregroundColor: Colors.black87,
  377. shape: RoundedRectangleBorder(
  378. borderRadius: BorderRadius.circular(8.0),
  379. ),
  380. ),
  381. ),
  382. switchTheme: switchTheme(),
  383. radioTheme: radioTheme(),
  384. checkboxTheme: checkboxTheme,
  385. listTileTheme: listTileTheme,
  386. menuBarTheme: MenuBarThemeData(
  387. style:
  388. MenuStyle(backgroundColor: MaterialStatePropertyAll(Colors.white))),
  389. colorScheme: ColorScheme.light(
  390. primary: Colors.blue, secondary: accent, background: grayBg),
  391. popupMenuTheme: PopupMenuThemeData(
  392. color: Colors.white,
  393. shape: RoundedRectangleBorder(
  394. side: BorderSide(
  395. color: (isDesktop || isWebDesktop)
  396. ? Color(0xFFECECEC)
  397. : Colors.transparent),
  398. borderRadius: BorderRadius.all(Radius.circular(8.0)),
  399. )),
  400. ).copyWith(
  401. extensions: <ThemeExtension<dynamic>>[
  402. ColorThemeExtension.light,
  403. TabbarTheme.light,
  404. ],
  405. );
  406. static ThemeData darkTheme = ThemeData(
  407. useMaterial3: false,
  408. brightness: Brightness.dark,
  409. hoverColor: Color.fromARGB(255, 45, 46, 53),
  410. scaffoldBackgroundColor: Color(0xFF18191E),
  411. dialogBackgroundColor: Color(0xFF18191E),
  412. appBarTheme: AppBarTheme(
  413. shadowColor: Colors.transparent,
  414. ),
  415. dialogTheme: DialogTheme(
  416. elevation: 15,
  417. shape: RoundedRectangleBorder(
  418. borderRadius: BorderRadius.circular(18.0),
  419. side: BorderSide(
  420. width: 1,
  421. color: Color(0xFF24252B),
  422. ),
  423. ),
  424. ),
  425. scrollbarTheme: scrollbarThemeDark,
  426. inputDecorationTheme: (isDesktop || isWebDesktop)
  427. ? InputDecorationTheme(
  428. fillColor: Color(0xFF24252B),
  429. filled: true,
  430. isDense: true,
  431. border: OutlineInputBorder(
  432. borderRadius: BorderRadius.circular(8),
  433. ),
  434. )
  435. : null,
  436. textTheme: const TextTheme(
  437. titleLarge: TextStyle(fontSize: 19),
  438. titleSmall: TextStyle(fontSize: 14),
  439. bodySmall: TextStyle(fontSize: 12, height: 1.25),
  440. bodyMedium: TextStyle(fontSize: 14, height: 1.25),
  441. labelLarge: TextStyle(
  442. fontSize: 16.0,
  443. fontWeight: FontWeight.bold,
  444. color: accent80,
  445. ),
  446. ),
  447. cardColor: Color(0xFF24252B),
  448. visualDensity: VisualDensity.adaptivePlatformDensity,
  449. tabBarTheme: const TabBarTheme(
  450. labelColor: Colors.white70,
  451. ),
  452. tooltipTheme: tooltipTheme(),
  453. splashColor: (isDesktop || isWebDesktop) ? Colors.transparent : null,
  454. highlightColor: (isDesktop || isWebDesktop) ? Colors.transparent : null,
  455. splashFactory: (isDesktop || isWebDesktop) ? NoSplash.splashFactory : null,
  456. textButtonTheme: (isDesktop || isWebDesktop)
  457. ? TextButtonThemeData(
  458. style: TextButton.styleFrom(
  459. splashFactory: NoSplash.splashFactory,
  460. disabledForegroundColor: Colors.white70,
  461. foregroundColor: Colors.white70,
  462. shape: RoundedRectangleBorder(
  463. borderRadius: BorderRadius.circular(18.0),
  464. ),
  465. ),
  466. )
  467. : mobileTextButtonTheme,
  468. elevatedButtonTheme: ElevatedButtonThemeData(
  469. style: ElevatedButton.styleFrom(
  470. backgroundColor: MyTheme.accent,
  471. foregroundColor: Colors.white,
  472. disabledForegroundColor: Colors.white70,
  473. disabledBackgroundColor: Colors.white10,
  474. shape: RoundedRectangleBorder(
  475. borderRadius: BorderRadius.circular(8.0),
  476. ),
  477. ),
  478. ),
  479. outlinedButtonTheme: OutlinedButtonThemeData(
  480. style: OutlinedButton.styleFrom(
  481. backgroundColor: Color(0xFF24252B),
  482. side: BorderSide(color: Colors.white12, width: 0.5),
  483. disabledForegroundColor: Colors.white70,
  484. foregroundColor: Colors.white70,
  485. shape: RoundedRectangleBorder(
  486. borderRadius: BorderRadius.circular(8.0),
  487. ),
  488. ),
  489. ),
  490. switchTheme: switchTheme(),
  491. radioTheme: radioTheme(),
  492. checkboxTheme: checkboxTheme,
  493. listTileTheme: listTileTheme,
  494. menuBarTheme: MenuBarThemeData(
  495. style: MenuStyle(
  496. backgroundColor: MaterialStatePropertyAll(Color(0xFF121212)))),
  497. colorScheme: ColorScheme.dark(
  498. primary: Colors.blue,
  499. secondary: accent,
  500. background: Color(0xFF24252B),
  501. ),
  502. popupMenuTheme: PopupMenuThemeData(
  503. shape: RoundedRectangleBorder(
  504. side: BorderSide(color: Colors.white24),
  505. borderRadius: BorderRadius.all(Radius.circular(8.0)),
  506. )),
  507. ).copyWith(
  508. extensions: <ThemeExtension<dynamic>>[
  509. ColorThemeExtension.dark,
  510. TabbarTheme.dark,
  511. ],
  512. );
  513. static ThemeMode getThemeModePreference() {
  514. return themeModeFromString(bind.mainGetLocalOption(key: kCommConfKeyTheme));
  515. }
  516. static Future<void> changeDarkMode(ThemeMode mode) async {
  517. Get.changeThemeMode(mode);
  518. if (desktopType == DesktopType.main || isAndroid || isIOS || isWeb) {
  519. if (mode == ThemeMode.system) {
  520. await bind.mainSetLocalOption(
  521. key: kCommConfKeyTheme, value: defaultOptionTheme);
  522. } else {
  523. await bind.mainSetLocalOption(
  524. key: kCommConfKeyTheme, value: mode.toShortString());
  525. }
  526. if (!isWeb) await bind.mainChangeTheme(dark: mode.toShortString());
  527. // Synchronize the window theme of the system.
  528. updateSystemWindowTheme();
  529. }
  530. }
  531. static ThemeMode currentThemeMode() {
  532. final preference = getThemeModePreference();
  533. if (preference == ThemeMode.system) {
  534. if (WidgetsBinding.instance.platformDispatcher.platformBrightness ==
  535. Brightness.light) {
  536. return ThemeMode.light;
  537. } else {
  538. return ThemeMode.dark;
  539. }
  540. } else {
  541. return preference;
  542. }
  543. }
  544. static ColorThemeExtension color(BuildContext context) {
  545. return Theme.of(context).extension<ColorThemeExtension>()!;
  546. }
  547. static TabbarTheme tabbar(BuildContext context) {
  548. return Theme.of(context).extension<TabbarTheme>()!;
  549. }
  550. static ThemeMode themeModeFromString(String v) {
  551. switch (v) {
  552. case "light":
  553. return ThemeMode.light;
  554. case "dark":
  555. return ThemeMode.dark;
  556. default:
  557. return ThemeMode.system;
  558. }
  559. }
  560. }
  561. extension ParseToString on ThemeMode {
  562. String toShortString() {
  563. return toString().split('.').last;
  564. }
  565. }
  566. final ButtonStyle flatButtonStyle = TextButton.styleFrom(
  567. minimumSize: Size(0, 36),
  568. padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 10.0),
  569. shape: const RoundedRectangleBorder(
  570. borderRadius: BorderRadius.all(Radius.circular(2.0)),
  571. ),
  572. );
  573. List<Locale> supportedLocales = const [
  574. Locale('en', 'US'),
  575. Locale('zh', 'CN'),
  576. Locale('zh', 'TW'),
  577. Locale('zh', 'SG'),
  578. Locale('fr'),
  579. Locale('de'),
  580. Locale('it'),
  581. Locale('ja'),
  582. Locale('cs'),
  583. Locale('pl'),
  584. Locale('ko'),
  585. Locale('hu'),
  586. Locale('pt'),
  587. Locale('ru'),
  588. Locale('sk'),
  589. Locale('id'),
  590. Locale('da'),
  591. Locale('eo'),
  592. Locale('tr'),
  593. Locale('kz'),
  594. Locale('es'),
  595. Locale('nl'),
  596. Locale('nb'),
  597. Locale('et'),
  598. Locale('eu'),
  599. Locale('bg'),
  600. Locale('be'),
  601. Locale('vn'),
  602. Locale('uk'),
  603. Locale('fa'),
  604. Locale('ca'),
  605. Locale('el'),
  606. Locale('sv'),
  607. Locale('sq'),
  608. Locale('sr'),
  609. Locale('th'),
  610. Locale('sl'),
  611. Locale('ro'),
  612. Locale('lt'),
  613. Locale('lv'),
  614. Locale('ar'),
  615. Locale('he'),
  616. Locale('hr'),
  617. ];
  618. String formatDurationToTime(Duration duration) {
  619. var totalTime = duration.inSeconds;
  620. final secs = totalTime % 60;
  621. totalTime = (totalTime - secs) ~/ 60;
  622. final mins = totalTime % 60;
  623. totalTime = (totalTime - mins) ~/ 60;
  624. return "${totalTime.toString().padLeft(2, "0")}:${mins.toString().padLeft(2, "0")}:${secs.toString().padLeft(2, "0")}";
  625. }
  626. closeConnection({String? id}) {
  627. if (isAndroid || isIOS) {
  628. () async {
  629. await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
  630. overlays: SystemUiOverlay.values);
  631. gFFI.chatModel.hideChatOverlay();
  632. Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/"));
  633. stateGlobal.isInMainPage = true;
  634. }();
  635. } else {
  636. if (isWeb) {
  637. Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/"));
  638. stateGlobal.isInMainPage = true;
  639. } else {
  640. final controller = Get.find<DesktopTabController>();
  641. controller.closeBy(id);
  642. }
  643. }
  644. }
  645. Future<void> windowOnTop(int? id) async {
  646. if (!isDesktop) {
  647. return;
  648. }
  649. print("Bring window '$id' on top");
  650. if (id == null) {
  651. // main window
  652. if (stateGlobal.isMinimized) {
  653. await windowManager.restore();
  654. }
  655. await windowManager.show();
  656. await windowManager.focus();
  657. await rustDeskWinManager.registerActiveWindow(kWindowMainId);
  658. } else {
  659. WindowController.fromWindowId(id)
  660. ..focus()
  661. ..show();
  662. rustDeskWinManager.call(WindowType.Main, kWindowEventShow, {"id": id});
  663. }
  664. }
  665. typedef DialogBuilder = CustomAlertDialog Function(
  666. StateSetter setState, void Function([dynamic]) close, BuildContext context);
  667. class Dialog<T> {
  668. OverlayEntry? entry;
  669. Completer<T?> completer = Completer<T?>();
  670. Dialog();
  671. void complete(T? res) {
  672. try {
  673. if (!completer.isCompleted) {
  674. completer.complete(res);
  675. }
  676. } catch (e) {
  677. debugPrint("Dialog complete catch error: $e");
  678. } finally {
  679. entry?.remove();
  680. }
  681. }
  682. }
  683. class OverlayKeyState {
  684. final _overlayKey = GlobalKey<OverlayState>();
  685. /// use global overlay by default
  686. OverlayState? get state =>
  687. _overlayKey.currentState ?? globalKey.currentState?.overlay;
  688. GlobalKey<OverlayState>? get key => _overlayKey;
  689. }
  690. class OverlayDialogManager {
  691. final Map<String, Dialog> _dialogs = {};
  692. var _overlayKeyState = OverlayKeyState();
  693. int _tagCount = 0;
  694. OverlayEntry? _mobileActionsOverlayEntry;
  695. RxBool mobileActionsOverlayVisible = true.obs;
  696. setMobileActionsOverlayVisible(bool v, {store = true}) {
  697. if (store) {
  698. bind.setLocalFlutterOption(k: kOptionShowMobileAction, v: v ? 'Y' : 'N');
  699. }
  700. // No need to read the value from local storage after setting it.
  701. // It better to toggle the value directly.
  702. mobileActionsOverlayVisible.value = v;
  703. }
  704. loadMobileActionsOverlayVisible() {
  705. mobileActionsOverlayVisible.value =
  706. bind.getLocalFlutterOption(k: kOptionShowMobileAction) != 'N';
  707. }
  708. void setOverlayState(OverlayKeyState overlayKeyState) {
  709. _overlayKeyState = overlayKeyState;
  710. }
  711. void dismissAll() {
  712. _dialogs.forEach((key, value) {
  713. value.complete(null);
  714. BackButtonInterceptor.removeByName(key);
  715. });
  716. _dialogs.clear();
  717. }
  718. void dismissByTag(String tag) {
  719. _dialogs[tag]?.complete(null);
  720. _dialogs.remove(tag);
  721. BackButtonInterceptor.removeByName(tag);
  722. }
  723. Future<T?> show<T>(DialogBuilder builder,
  724. {bool clickMaskDismiss = false,
  725. bool backDismiss = false,
  726. String? tag,
  727. bool useAnimation = true,
  728. bool forceGlobal = false}) {
  729. final overlayState =
  730. forceGlobal ? globalKey.currentState?.overlay : _overlayKeyState.state;
  731. if (overlayState == null) {
  732. return Future.error(
  733. "[OverlayDialogManager] Failed to show dialog, _overlayState is null, call [setOverlayState] first");
  734. }
  735. final String dialogTag;
  736. if (tag != null) {
  737. dialogTag = tag;
  738. } else {
  739. dialogTag = _tagCount.toString();
  740. _tagCount++;
  741. }
  742. final dialog = Dialog<T>();
  743. _dialogs[dialogTag] = dialog;
  744. close([res]) {
  745. _dialogs.remove(dialogTag);
  746. dialog.complete(res);
  747. BackButtonInterceptor.removeByName(dialogTag);
  748. }
  749. dialog.entry = OverlayEntry(builder: (context) {
  750. bool innerClicked = false;
  751. return Listener(
  752. onPointerUp: (_) {
  753. if (!innerClicked && clickMaskDismiss) {
  754. close();
  755. }
  756. innerClicked = false;
  757. },
  758. child: Container(
  759. color: Theme.of(context).brightness == Brightness.light
  760. ? Colors.black12
  761. : Colors.black45,
  762. child: StatefulBuilder(builder: (context, setState) {
  763. return Listener(
  764. onPointerUp: (_) => innerClicked = true,
  765. child: builder(setState, close, overlayState.context),
  766. );
  767. })));
  768. });
  769. overlayState.insert(dialog.entry!);
  770. BackButtonInterceptor.add((stopDefaultButtonEvent, routeInfo) {
  771. if (backDismiss) {
  772. close();
  773. }
  774. return true;
  775. }, name: dialogTag);
  776. return dialog.completer.future;
  777. }
  778. String showLoading(String text,
  779. {bool clickMaskDismiss = false,
  780. bool showCancel = true,
  781. VoidCallback? onCancel,
  782. String? tag}) {
  783. if (tag == null) {
  784. tag = _tagCount.toString();
  785. _tagCount++;
  786. }
  787. show((setState, close, context) {
  788. cancel() {
  789. dismissAll();
  790. if (onCancel != null) {
  791. onCancel();
  792. }
  793. }
  794. return CustomAlertDialog(
  795. content: Container(
  796. constraints: const BoxConstraints(maxWidth: 240),
  797. child: Column(
  798. mainAxisSize: MainAxisSize.min,
  799. crossAxisAlignment: CrossAxisAlignment.start,
  800. children: [
  801. const SizedBox(height: 30),
  802. const Center(child: CircularProgressIndicator()),
  803. const SizedBox(height: 20),
  804. Center(
  805. child: Text(translate(text),
  806. style: const TextStyle(fontSize: 15))),
  807. const SizedBox(height: 20),
  808. Offstage(
  809. offstage: !showCancel,
  810. child: Center(
  811. child: (isDesktop || isWebDesktop)
  812. ? dialogButton('Cancel', onPressed: cancel)
  813. : TextButton(
  814. style: flatButtonStyle,
  815. onPressed: cancel,
  816. child: Text(translate('Cancel'),
  817. style: const TextStyle(
  818. color: MyTheme.accent)))))
  819. ])),
  820. onCancel: showCancel ? cancel : null,
  821. );
  822. }, tag: tag);
  823. return tag;
  824. }
  825. void resetMobileActionsOverlay({FFI? ffi}) {
  826. if (_mobileActionsOverlayEntry == null) return;
  827. hideMobileActionsOverlay();
  828. showMobileActionsOverlay(ffi: ffi);
  829. }
  830. void showMobileActionsOverlay({FFI? ffi}) {
  831. if (_mobileActionsOverlayEntry != null) return;
  832. final overlayState = _overlayKeyState.state;
  833. if (overlayState == null) return;
  834. final overlay = makeMobileActionsOverlayEntry(
  835. () => hideMobileActionsOverlay(),
  836. ffi: ffi,
  837. );
  838. overlayState.insert(overlay);
  839. _mobileActionsOverlayEntry = overlay;
  840. setMobileActionsOverlayVisible(true);
  841. }
  842. void hideMobileActionsOverlay({store = true}) {
  843. if (_mobileActionsOverlayEntry != null) {
  844. _mobileActionsOverlayEntry!.remove();
  845. _mobileActionsOverlayEntry = null;
  846. setMobileActionsOverlayVisible(false, store: store);
  847. return;
  848. }
  849. }
  850. void toggleMobileActionsOverlay({FFI? ffi}) {
  851. if (_mobileActionsOverlayEntry == null) {
  852. showMobileActionsOverlay(ffi: ffi);
  853. } else {
  854. hideMobileActionsOverlay();
  855. }
  856. }
  857. bool existing(String tag) {
  858. return _dialogs.keys.contains(tag);
  859. }
  860. }
  861. makeMobileActionsOverlayEntry(VoidCallback? onHide, {FFI? ffi}) {
  862. makeMobileActions(BuildContext context, double s) {
  863. final scale = s < 0.85 ? 0.85 : s;
  864. final session = ffi ?? gFFI;
  865. const double overlayW = 200;
  866. const double overlayH = 45;
  867. computeOverlayPosition() {
  868. final screenW = MediaQuery.of(context).size.width;
  869. final screenH = MediaQuery.of(context).size.height;
  870. final left = (screenW - overlayW * scale) / 2;
  871. final top = screenH - (overlayH + 80) * scale;
  872. return Offset(left, top);
  873. }
  874. if (draggablePositions.mobileActions.isInvalid()) {
  875. draggablePositions.mobileActions.update(computeOverlayPosition());
  876. } else {
  877. draggablePositions.mobileActions.tryAdjust(overlayW, overlayH, scale);
  878. }
  879. return DraggableMobileActions(
  880. scale: scale,
  881. position: draggablePositions.mobileActions,
  882. width: overlayW,
  883. height: overlayH,
  884. onBackPressed: session.inputModel.onMobileBack,
  885. onHomePressed: session.inputModel.onMobileHome,
  886. onRecentPressed: session.inputModel.onMobileApps,
  887. onHidePressed: onHide,
  888. );
  889. }
  890. return OverlayEntry(builder: (context) {
  891. if (isDesktop) {
  892. final c = Provider.of<CanvasModel>(context);
  893. return makeMobileActions(context, c.scale * 2.0);
  894. } else {
  895. return makeMobileActions(globalKey.currentContext!, 1.0);
  896. }
  897. });
  898. }
  899. void showToast(String text, {Duration timeout = const Duration(seconds: 3)}) {
  900. final overlayState = globalKey.currentState?.overlay;
  901. if (overlayState == null) return;
  902. final entry = OverlayEntry(builder: (context) {
  903. return IgnorePointer(
  904. child: Align(
  905. alignment: const Alignment(0.0, 0.8),
  906. child: Container(
  907. decoration: BoxDecoration(
  908. color: MyTheme.color(context).toastBg,
  909. borderRadius: const BorderRadius.all(
  910. Radius.circular(20),
  911. ),
  912. ),
  913. padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 5),
  914. child: Text(
  915. text,
  916. textAlign: TextAlign.center,
  917. style: TextStyle(
  918. decoration: TextDecoration.none,
  919. fontWeight: FontWeight.w300,
  920. fontSize: 18,
  921. color: MyTheme.color(context).toastText),
  922. ),
  923. )));
  924. });
  925. overlayState.insert(entry);
  926. Future.delayed(timeout, () {
  927. entry.remove();
  928. });
  929. }
  930. // TODO
  931. // - Remove argument "contentPadding", no need for it, all should look the same.
  932. // - Remove "required" for argument "content". See simple confirm dialog "delete peer", only title and actions are used. No need to "content: SizedBox.shrink()".
  933. // - Make dead code alive, transform arguments "onSubmit" and "onCancel" into correspondenting buttons "ConfirmOkButton", "CancelButton".
  934. class CustomAlertDialog extends StatelessWidget {
  935. const CustomAlertDialog(
  936. {Key? key,
  937. this.title,
  938. this.titlePadding,
  939. required this.content,
  940. this.actions,
  941. this.contentPadding,
  942. this.contentBoxConstraints = const BoxConstraints(maxWidth: 500),
  943. this.onSubmit,
  944. this.onCancel})
  945. : super(key: key);
  946. final Widget? title;
  947. final EdgeInsetsGeometry? titlePadding;
  948. final Widget content;
  949. final List<Widget>? actions;
  950. final double? contentPadding;
  951. final BoxConstraints contentBoxConstraints;
  952. final Function()? onSubmit;
  953. final Function()? onCancel;
  954. @override
  955. Widget build(BuildContext context) {
  956. // request focus
  957. FocusScopeNode scopeNode = FocusScopeNode();
  958. Future.delayed(Duration.zero, () {
  959. if (!scopeNode.hasFocus) scopeNode.requestFocus();
  960. });
  961. bool tabTapped = false;
  962. if (isAndroid) gFFI.invokeMethod("enable_soft_keyboard", true);
  963. return FocusScope(
  964. node: scopeNode,
  965. autofocus: true,
  966. onKey: (node, key) {
  967. if (key.logicalKey == LogicalKeyboardKey.escape) {
  968. if (key is RawKeyDownEvent) {
  969. onCancel?.call();
  970. }
  971. return KeyEventResult.handled; // avoid TextField exception on escape
  972. } else if (!tabTapped &&
  973. onSubmit != null &&
  974. (key.logicalKey == LogicalKeyboardKey.enter ||
  975. key.logicalKey == LogicalKeyboardKey.numpadEnter)) {
  976. if (key is RawKeyDownEvent) onSubmit?.call();
  977. return KeyEventResult.handled;
  978. } else if (key.logicalKey == LogicalKeyboardKey.tab) {
  979. if (key is RawKeyDownEvent) {
  980. scopeNode.nextFocus();
  981. tabTapped = true;
  982. }
  983. return KeyEventResult.handled;
  984. }
  985. return KeyEventResult.ignored;
  986. },
  987. child: AlertDialog(
  988. scrollable: true,
  989. title: title,
  990. content: ConstrainedBox(
  991. constraints: contentBoxConstraints,
  992. child: content,
  993. ),
  994. actions: actions,
  995. titlePadding: titlePadding ?? MyTheme.dialogTitlePadding(),
  996. contentPadding:
  997. MyTheme.dialogContentPadding(actions: actions is List),
  998. actionsPadding: MyTheme.dialogActionsPadding(),
  999. buttonPadding: MyTheme.dialogButtonPadding),
  1000. );
  1001. }
  1002. }
  1003. Widget createDialogContent(String text) {
  1004. final RegExp linkRegExp = RegExp(r'(https?://[^\s]+)');
  1005. final List<TextSpan> spans = [];
  1006. int start = 0;
  1007. bool hasLink = false;
  1008. linkRegExp.allMatches(text).forEach((match) {
  1009. hasLink = true;
  1010. if (match.start > start) {
  1011. spans.add(TextSpan(text: text.substring(start, match.start)));
  1012. }
  1013. spans.add(TextSpan(
  1014. text: match.group(0) ?? '',
  1015. style: TextStyle(
  1016. color: Colors.blue,
  1017. decoration: TextDecoration.underline,
  1018. ),
  1019. recognizer: TapGestureRecognizer()
  1020. ..onTap = () {
  1021. String linkText = match.group(0) ?? '';
  1022. linkText = linkText.replaceAll(RegExp(r'[.,;!?]+$'), '');
  1023. launchUrl(Uri.parse(linkText));
  1024. },
  1025. ));
  1026. start = match.end;
  1027. });
  1028. if (start < text.length) {
  1029. spans.add(TextSpan(text: text.substring(start)));
  1030. }
  1031. if (!hasLink) {
  1032. return SelectableText(text, style: const TextStyle(fontSize: 15));
  1033. }
  1034. return SelectableText.rich(
  1035. TextSpan(
  1036. style: TextStyle(color: Colors.black, fontSize: 15),
  1037. children: spans,
  1038. ),
  1039. );
  1040. }
  1041. void msgBox(SessionID sessionId, String type, String title, String text,
  1042. String link, OverlayDialogManager dialogManager,
  1043. {bool? hasCancel, ReconnectHandle? reconnect, int? reconnectTimeout}) {
  1044. dialogManager.dismissAll();
  1045. List<Widget> buttons = [];
  1046. bool hasOk = false;
  1047. submit() {
  1048. dialogManager.dismissAll();
  1049. // https://github.com/rustdesk/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263
  1050. if (!type.contains("custom") && desktopType != DesktopType.portForward) {
  1051. closeConnection();
  1052. }
  1053. }
  1054. cancel() {
  1055. dialogManager.dismissAll();
  1056. }
  1057. jumplink() {
  1058. if (link.startsWith('http')) {
  1059. launchUrl(Uri.parse(link));
  1060. }
  1061. }
  1062. if (type != "connecting" && type != "success" && !type.contains("nook")) {
  1063. hasOk = true;
  1064. buttons.insert(0, dialogButton('OK', onPressed: submit));
  1065. }
  1066. hasCancel ??= !type.contains("error") &&
  1067. !type.contains("nocancel") &&
  1068. type != "restarting";
  1069. if (hasCancel) {
  1070. buttons.insert(
  1071. 0, dialogButton('Cancel', onPressed: cancel, isOutline: true));
  1072. }
  1073. if (type.contains("hasclose")) {
  1074. buttons.insert(
  1075. 0,
  1076. dialogButton('Close', onPressed: () {
  1077. dialogManager.dismissAll();
  1078. }));
  1079. }
  1080. if (reconnect != null &&
  1081. title == "Connection Error" &&
  1082. reconnectTimeout != null) {
  1083. // `enabled` is used to disable the dialog button once the button is clicked.
  1084. final enabled = true.obs;
  1085. final button = Obx(() => _ReconnectCountDownButton(
  1086. second: reconnectTimeout,
  1087. onPressed: enabled.isTrue
  1088. ? () {
  1089. // Disable the button
  1090. enabled.value = false;
  1091. reconnect(dialogManager, sessionId, false);
  1092. }
  1093. : null,
  1094. ));
  1095. buttons.insert(0, button);
  1096. }
  1097. if (link.isNotEmpty) {
  1098. buttons.insert(0, dialogButton('JumpLink', onPressed: jumplink));
  1099. }
  1100. dialogManager.show(
  1101. (setState, close, context) => CustomAlertDialog(
  1102. title: null,
  1103. content: SelectionArea(child: msgboxContent(type, title, text)),
  1104. actions: buttons,
  1105. onSubmit: hasOk ? submit : null,
  1106. onCancel: hasCancel == true ? cancel : null,
  1107. ),
  1108. tag: '$sessionId-$type-$title-$text-$link',
  1109. );
  1110. }
  1111. Color? _msgboxColor(String type) {
  1112. if (type == "input-password" || type == "custom-os-password") {
  1113. return Color(0xFFAD448E);
  1114. }
  1115. if (type.contains("success")) {
  1116. return Color(0xFF32bea6);
  1117. }
  1118. if (type.contains("error") || type == "re-input-password") {
  1119. return Color(0xFFE04F5F);
  1120. }
  1121. return Color(0xFF2C8CFF);
  1122. }
  1123. Widget msgboxIcon(String type) {
  1124. IconData? iconData;
  1125. if (type.contains("error") || type == "re-input-password") {
  1126. iconData = Icons.cancel;
  1127. }
  1128. if (type.contains("success")) {
  1129. iconData = Icons.check_circle;
  1130. }
  1131. if (type == "wait-uac" || type == "wait-remote-accept-nook") {
  1132. iconData = Icons.hourglass_top;
  1133. }
  1134. if (type == 'on-uac' || type == 'on-foreground-elevated') {
  1135. iconData = Icons.admin_panel_settings;
  1136. }
  1137. if (type.contains('info')) {
  1138. iconData = Icons.info;
  1139. }
  1140. if (iconData != null) {
  1141. return Icon(iconData, size: 50, color: _msgboxColor(type))
  1142. .marginOnly(right: 16);
  1143. }
  1144. return Offstage();
  1145. }
  1146. // title should be null
  1147. Widget msgboxContent(String type, String title, String text) {
  1148. String translateText(String text) {
  1149. if (text.indexOf('Failed') == 0 && text.indexOf(': ') > 0) {
  1150. List<String> words = text.split(': ');
  1151. for (var i = 0; i < words.length; ++i) {
  1152. words[i] = translate(words[i]);
  1153. }
  1154. text = words.join(': ');
  1155. } else {
  1156. List<String> words = text.split(' ');
  1157. if (words.length > 1 && words[0].endsWith('_tip')) {
  1158. words[0] = translate(words[0]);
  1159. final rest = text.substring(words[0].length + 1);
  1160. text = '${words[0]} ${translate(rest)}';
  1161. } else {
  1162. text = translate(text);
  1163. }
  1164. }
  1165. return text;
  1166. }
  1167. return Row(
  1168. children: [
  1169. msgboxIcon(type),
  1170. Expanded(
  1171. child: Column(
  1172. crossAxisAlignment: CrossAxisAlignment.start,
  1173. children: [
  1174. Text(
  1175. translate(title),
  1176. style: TextStyle(fontSize: 21),
  1177. ).marginOnly(bottom: 10),
  1178. createDialogContent(translateText(text)),
  1179. ],
  1180. ),
  1181. ),
  1182. ],
  1183. ).marginOnly(bottom: 12);
  1184. }
  1185. void msgBoxCommon(OverlayDialogManager dialogManager, String title,
  1186. Widget content, List<Widget> buttons,
  1187. {bool hasCancel = true}) {
  1188. dialogManager.show((setState, close, context) => CustomAlertDialog(
  1189. title: Text(
  1190. translate(title),
  1191. style: TextStyle(fontSize: 21),
  1192. ),
  1193. content: content,
  1194. actions: buttons,
  1195. onCancel: hasCancel ? close : null,
  1196. ));
  1197. }
  1198. Color str2color(String str, [alpha = 0xFF]) {
  1199. var hash = 160 << 16 + 114 << 8 + 91;
  1200. for (var i = 0; i < str.length; i += 1) {
  1201. hash = str.codeUnitAt(i) + ((hash << 5) - hash);
  1202. }
  1203. hash = hash % 16777216;
  1204. return Color((hash & 0xFF7FFF) | (alpha << 24));
  1205. }
  1206. Color str2color2(String str, {List<int> existing = const []}) {
  1207. Map<String, Color> colorMap = {
  1208. "red": Colors.red,
  1209. "green": Colors.green,
  1210. "blue": Colors.blue,
  1211. "orange": Colors.orange,
  1212. "purple": Colors.purple,
  1213. "grey": Colors.grey,
  1214. "cyan": Colors.cyan,
  1215. "lime": Colors.lime,
  1216. "teal": Colors.teal,
  1217. "pink": Colors.pink[200]!,
  1218. "indigo": Colors.indigo,
  1219. "brown": Colors.brown,
  1220. };
  1221. final color = colorMap[str.toLowerCase()];
  1222. if (color != null) {
  1223. return color.withAlpha(0xFF);
  1224. }
  1225. if (str.toLowerCase() == 'yellow') {
  1226. return Colors.yellow.withAlpha(0xFF);
  1227. }
  1228. var hash = 0;
  1229. for (var i = 0; i < str.length; i++) {
  1230. hash += str.codeUnitAt(i);
  1231. }
  1232. List<Color> colorList = colorMap.values.toList();
  1233. hash = hash % colorList.length;
  1234. var result = colorList[hash].withAlpha(0xFF);
  1235. if (existing.contains(result.value)) {
  1236. Color? notUsed =
  1237. colorList.firstWhereOrNull((e) => !existing.contains(e.value));
  1238. if (notUsed != null) {
  1239. result = notUsed;
  1240. }
  1241. }
  1242. return result;
  1243. }
  1244. const K = 1024;
  1245. const M = K * K;
  1246. const G = M * K;
  1247. String readableFileSize(double size) {
  1248. if (size < K) {
  1249. return "${size.toStringAsFixed(2)} B";
  1250. } else if (size < M) {
  1251. return "${(size / K).toStringAsFixed(2)} KB";
  1252. } else if (size < G) {
  1253. return "${(size / M).toStringAsFixed(2)} MB";
  1254. } else {
  1255. return "${(size / G).toStringAsFixed(2)} GB";
  1256. }
  1257. }
  1258. /// Flutter can't not catch PointerMoveEvent when size is 1
  1259. /// This will happen in Android AccessibilityService Input
  1260. /// android can't init dispatching size yet ,see: https://stackoverflow.com/questions/59960451/android-accessibility-dispatchgesture-is-it-possible-to-specify-pressure-for-a
  1261. /// use this temporary solution until flutter or android fixes the bug
  1262. class AccessibilityListener extends StatelessWidget {
  1263. final Widget? child;
  1264. static final offset = 100;
  1265. AccessibilityListener({this.child});
  1266. @override
  1267. Widget build(BuildContext context) {
  1268. return Listener(
  1269. onPointerDown: (evt) {
  1270. if (evt.size == 1) {
  1271. GestureBinding.instance.handlePointerEvent(PointerAddedEvent(
  1272. pointer: evt.pointer + offset, position: evt.position));
  1273. GestureBinding.instance.handlePointerEvent(PointerDownEvent(
  1274. pointer: evt.pointer + offset,
  1275. size: 0.1,
  1276. position: evt.position));
  1277. }
  1278. },
  1279. onPointerUp: (evt) {
  1280. if (evt.size == 1) {
  1281. GestureBinding.instance.handlePointerEvent(PointerUpEvent(
  1282. pointer: evt.pointer + offset,
  1283. size: 0.1,
  1284. position: evt.position));
  1285. GestureBinding.instance.handlePointerEvent(PointerRemovedEvent(
  1286. pointer: evt.pointer + offset, position: evt.position));
  1287. }
  1288. },
  1289. onPointerMove: (evt) {
  1290. if (evt.size == 1) {
  1291. GestureBinding.instance.handlePointerEvent(PointerMoveEvent(
  1292. pointer: evt.pointer + offset,
  1293. size: 0.1,
  1294. delta: evt.delta,
  1295. position: evt.position));
  1296. }
  1297. },
  1298. child: child);
  1299. }
  1300. }
  1301. class AndroidPermissionManager {
  1302. static Completer<bool>? _completer;
  1303. static Timer? _timer;
  1304. static var _current = "";
  1305. static bool isWaitingFile() {
  1306. if (_completer != null) {
  1307. return !_completer!.isCompleted && _current == kManageExternalStorage;
  1308. }
  1309. return false;
  1310. }
  1311. static Future<bool> check(String type) {
  1312. if (isDesktop || isWeb) {
  1313. return Future.value(true);
  1314. }
  1315. return gFFI.invokeMethod("check_permission", type);
  1316. }
  1317. // startActivity goto Android Setting's page to request permission manually by user
  1318. static void startAction(String action) {
  1319. gFFI.invokeMethod(AndroidChannel.kStartAction, action);
  1320. }
  1321. /// We use XXPermissions to request permissions,
  1322. /// for supported types, see https://github.com/getActivity/XXPermissions/blob/e46caea32a64ad7819df62d448fb1c825481cd28/library/src/main/java/com/hjq/permissions/Permission.java
  1323. static Future<bool> request(String type) {
  1324. if (isDesktop || isWeb) {
  1325. return Future.value(true);
  1326. }
  1327. gFFI.invokeMethod("request_permission", type);
  1328. // clear last task
  1329. if (_completer?.isCompleted == false) {
  1330. _completer?.complete(false);
  1331. }
  1332. _timer?.cancel();
  1333. _current = type;
  1334. _completer = Completer<bool>();
  1335. _timer = Timer(Duration(seconds: 120), () {
  1336. if (_completer == null) return;
  1337. if (!_completer!.isCompleted) {
  1338. _completer!.complete(false);
  1339. }
  1340. _completer = null;
  1341. _current = "";
  1342. });
  1343. return _completer!.future;
  1344. }
  1345. static complete(String type, bool res) {
  1346. if (type != _current) {
  1347. res = false;
  1348. }
  1349. _timer?.cancel();
  1350. _completer?.complete(res);
  1351. _current = "";
  1352. }
  1353. }
  1354. RadioListTile<T> getRadio<T>(
  1355. Widget title, T toValue, T curValue, ValueChanged<T?>? onChange,
  1356. {bool? dense}) {
  1357. return RadioListTile<T>(
  1358. visualDensity: VisualDensity.compact,
  1359. controlAffinity: ListTileControlAffinity.trailing,
  1360. title: title,
  1361. value: toValue,
  1362. groupValue: curValue,
  1363. onChanged: onChange,
  1364. dense: dense,
  1365. );
  1366. }
  1367. /// find ffi, tag is Remote ID
  1368. /// for session specific usage
  1369. FFI ffi(String? tag) {
  1370. return Get.find<FFI>(tag: tag);
  1371. }
  1372. /// Global FFI object
  1373. late FFI _globalFFI;
  1374. FFI get gFFI => _globalFFI;
  1375. Future<void> initGlobalFFI() async {
  1376. debugPrint("_globalFFI init");
  1377. _globalFFI = FFI(null);
  1378. debugPrint("_globalFFI init end");
  1379. // after `put`, can also be globally found by Get.find<FFI>();
  1380. Get.put<FFI>(_globalFFI, permanent: true);
  1381. }
  1382. String translate(String name) {
  1383. if (name.startsWith('Failed to') && name.contains(': ')) {
  1384. return name.split(': ').map((x) => translate(x)).join(': ');
  1385. }
  1386. return platformFFI.translate(name, localeName);
  1387. }
  1388. // This function must be kept the same as the one in rust and sciter code.
  1389. // rust: libs/hbb_common/src/config.rs -> option2bool()
  1390. // sciter: Does not have the function, but it should be kept the same.
  1391. bool option2bool(String option, String value) {
  1392. bool res;
  1393. if (option.startsWith("enable-")) {
  1394. res = value != "N";
  1395. } else if (option.startsWith("allow-") ||
  1396. option == kOptionStopService ||
  1397. option == kOptionDirectServer ||
  1398. option == kOptionForceAlwaysRelay) {
  1399. res = value == "Y";
  1400. } else {
  1401. assert(false);
  1402. res = value != "N";
  1403. }
  1404. return res;
  1405. }
  1406. String bool2option(String option, bool b) {
  1407. String res;
  1408. if (option.startsWith('enable-')) {
  1409. res = b ? defaultOptionYes : 'N';
  1410. } else if (option.startsWith('allow-') ||
  1411. option == kOptionStopService ||
  1412. option == kOptionDirectServer ||
  1413. option == kOptionForceAlwaysRelay) {
  1414. res = b ? 'Y' : defaultOptionNo;
  1415. } else {
  1416. assert(false);
  1417. res = b ? 'Y' : 'N';
  1418. }
  1419. return res;
  1420. }
  1421. mainSetBoolOption(String key, bool value) async {
  1422. String v = bool2option(key, value);
  1423. await bind.mainSetOption(key: key, value: v);
  1424. }
  1425. Future<bool> mainGetBoolOption(String key) async {
  1426. return option2bool(key, await bind.mainGetOption(key: key));
  1427. }
  1428. bool mainGetBoolOptionSync(String key) {
  1429. return option2bool(key, bind.mainGetOptionSync(key: key));
  1430. }
  1431. mainSetLocalBoolOption(String key, bool value) async {
  1432. String v = bool2option(key, value);
  1433. await bind.mainSetLocalOption(key: key, value: v);
  1434. }
  1435. bool mainGetLocalBoolOptionSync(String key) {
  1436. return option2bool(key, bind.mainGetLocalOption(key: key));
  1437. }
  1438. bool mainGetPeerBoolOptionSync(String id, String key) {
  1439. return option2bool(key, bind.mainGetPeerOptionSync(id: id, key: key));
  1440. }
  1441. // Don't use `option2bool()` and `bool2option()` to convert the session option.
  1442. // Use `sessionGetToggleOption()` and `sessionToggleOption()` instead.
  1443. // Because all session options use `Y` and `<Empty>` as values.
  1444. Future<bool> matchPeer(String searchText, Peer peer) async {
  1445. if (searchText.isEmpty) {
  1446. return true;
  1447. }
  1448. if (peer.id.toLowerCase().contains(searchText)) {
  1449. return true;
  1450. }
  1451. if (peer.hostname.toLowerCase().contains(searchText) ||
  1452. peer.username.toLowerCase().contains(searchText)) {
  1453. return true;
  1454. }
  1455. final alias = peer.alias;
  1456. if (alias.isEmpty) {
  1457. return false;
  1458. }
  1459. return alias.toLowerCase().contains(searchText);
  1460. }
  1461. /// Get the image for the current [platform].
  1462. Widget getPlatformImage(String platform, {double size = 50}) {
  1463. if (platform.isEmpty) {
  1464. return Container(width: size, height: size);
  1465. }
  1466. if (platform == kPeerPlatformMacOS) {
  1467. platform = 'mac';
  1468. } else if (platform != kPeerPlatformLinux &&
  1469. platform != kPeerPlatformAndroid) {
  1470. platform = 'win';
  1471. } else {
  1472. platform = platform.toLowerCase();
  1473. }
  1474. return SvgPicture.asset('assets/$platform.svg', height: size, width: size);
  1475. }
  1476. class LastWindowPosition {
  1477. double? width;
  1478. double? height;
  1479. double? offsetWidth;
  1480. double? offsetHeight;
  1481. bool? isMaximized;
  1482. bool? isFullscreen;
  1483. LastWindowPosition(this.width, this.height, this.offsetWidth,
  1484. this.offsetHeight, this.isMaximized, this.isFullscreen);
  1485. Map<String, dynamic> toJson() {
  1486. return <String, dynamic>{
  1487. "width": width,
  1488. "height": height,
  1489. "offsetWidth": offsetWidth,
  1490. "offsetHeight": offsetHeight,
  1491. "isMaximized": isMaximized,
  1492. "isFullscreen": isFullscreen,
  1493. };
  1494. }
  1495. @override
  1496. String toString() {
  1497. return jsonEncode(toJson());
  1498. }
  1499. static LastWindowPosition? loadFromString(String content) {
  1500. if (content.isEmpty) {
  1501. return null;
  1502. }
  1503. try {
  1504. final m = jsonDecode(content);
  1505. return LastWindowPosition(m["width"], m["height"], m["offsetWidth"],
  1506. m["offsetHeight"], m["isMaximized"], m["isFullscreen"]);
  1507. } catch (e) {
  1508. debugPrintStack(
  1509. label:
  1510. 'Failed to load LastWindowPosition "$content" ${e.toString()}');
  1511. return null;
  1512. }
  1513. }
  1514. }
  1515. String get windowFramePrefix =>
  1516. kWindowPrefix +
  1517. (bind.isIncomingOnly()
  1518. ? "incoming_"
  1519. : (bind.isOutgoingOnly() ? "outgoing_" : ""));
  1520. /// Save window position and size on exit
  1521. /// Note that windowId must be provided if it's subwindow
  1522. Future<void> saveWindowPosition(WindowType type, {int? windowId}) async {
  1523. if (type != WindowType.Main && windowId == null) {
  1524. debugPrint(
  1525. "Error: windowId cannot be null when saving positions for sub window");
  1526. }
  1527. late Offset position;
  1528. late Size sz;
  1529. late bool isMaximized;
  1530. bool isFullscreen = stateGlobal.fullscreen.isTrue;
  1531. setPreFrame() {
  1532. final pos = bind.getLocalFlutterOption(k: windowFramePrefix + type.name);
  1533. var lpos = LastWindowPosition.loadFromString(pos);
  1534. position = Offset(
  1535. lpos?.offsetWidth ?? position.dx, lpos?.offsetHeight ?? position.dy);
  1536. sz = Size(lpos?.width ?? sz.width, lpos?.height ?? sz.height);
  1537. }
  1538. switch (type) {
  1539. case WindowType.Main:
  1540. // Checking `bind.isIncomingOnly()` is a simple workaround for MacOS.
  1541. // `await windowManager.isMaximized()` will always return true
  1542. // if is not resizable. The reason is unknown.
  1543. //
  1544. // `setResizable(!bind.isIncomingOnly());` in main.dart
  1545. isMaximized =
  1546. bind.isIncomingOnly() ? false : await windowManager.isMaximized();
  1547. if (isFullscreen || isMaximized) {
  1548. setPreFrame();
  1549. } else {
  1550. position = await windowManager.getPosition(
  1551. ignoreDevicePixelRatio: _ignoreDevicePixelRatio);
  1552. sz = await windowManager.getSize(
  1553. ignoreDevicePixelRatio: _ignoreDevicePixelRatio);
  1554. }
  1555. break;
  1556. default:
  1557. final wc = WindowController.fromWindowId(windowId!);
  1558. isMaximized = await wc.isMaximized();
  1559. if (isFullscreen || isMaximized) {
  1560. setPreFrame();
  1561. } else {
  1562. final Rect frame;
  1563. try {
  1564. frame = await wc.getFrame();
  1565. } catch (e) {
  1566. debugPrint(
  1567. "Failed to get frame of window $windowId, it may be hidden");
  1568. return;
  1569. }
  1570. position = frame.topLeft;
  1571. sz = frame.size;
  1572. }
  1573. break;
  1574. }
  1575. if (isWindows) {
  1576. const kMinOffset = -10000;
  1577. const kMaxOffset = 10000;
  1578. if (position.dx < kMinOffset ||
  1579. position.dy < kMinOffset ||
  1580. position.dx > kMaxOffset ||
  1581. position.dy > kMaxOffset) {
  1582. debugPrint("Invalid position: $position, ignore saving position");
  1583. return;
  1584. }
  1585. }
  1586. final pos = LastWindowPosition(
  1587. sz.width, sz.height, position.dx, position.dy, isMaximized, isFullscreen);
  1588. debugPrint(
  1589. "Saving frame: $windowId: ${pos.width}/${pos.height}, offset:${pos.offsetWidth}/${pos.offsetHeight}, isMaximized:${pos.isMaximized}, isFullscreen:${pos.isFullscreen}");
  1590. await bind.setLocalFlutterOption(
  1591. k: windowFramePrefix + type.name, v: pos.toString());
  1592. if (type == WindowType.RemoteDesktop && windowId != null) {
  1593. await _saveSessionWindowPosition(
  1594. type, windowId, isMaximized, isFullscreen, pos);
  1595. }
  1596. }
  1597. Future _saveSessionWindowPosition(WindowType windowType, int windowId,
  1598. bool isMaximized, bool isFullscreen, LastWindowPosition pos) async {
  1599. final remoteList = await DesktopMultiWindow.invokeMethod(
  1600. windowId, kWindowEventGetRemoteList, null);
  1601. getPeerPos(String peerId) {
  1602. if (isMaximized || isFullscreen) {
  1603. final peerPos = bind.mainGetPeerFlutterOptionSync(
  1604. id: peerId, k: windowFramePrefix + windowType.name);
  1605. var lpos = LastWindowPosition.loadFromString(peerPos);
  1606. return LastWindowPosition(
  1607. lpos?.width ?? pos.offsetWidth,
  1608. lpos?.height ?? pos.offsetHeight,
  1609. lpos?.offsetWidth ?? pos.offsetWidth,
  1610. lpos?.offsetHeight ?? pos.offsetHeight,
  1611. isMaximized,
  1612. isFullscreen)
  1613. .toString();
  1614. } else {
  1615. return pos.toString();
  1616. }
  1617. }
  1618. if (remoteList != null) {
  1619. for (final peerId in remoteList.split(',')) {
  1620. bind.mainSetPeerFlutterOptionSync(
  1621. id: peerId,
  1622. k: windowFramePrefix + windowType.name,
  1623. v: getPeerPos(peerId));
  1624. }
  1625. }
  1626. }
  1627. Future<Size> _adjustRestoreMainWindowSize(double? width, double? height) async {
  1628. const double minWidth = 1;
  1629. const double minHeight = 1;
  1630. const double maxWidth = 6480;
  1631. const double maxHeight = 6480;
  1632. final defaultWidth =
  1633. ((isDesktop || isWebDesktop) ? 1280 : kMobileDefaultDisplayWidth)
  1634. .toDouble();
  1635. final defaultHeight =
  1636. ((isDesktop || isWebDesktop) ? 720 : kMobileDefaultDisplayHeight)
  1637. .toDouble();
  1638. double restoreWidth = width ?? defaultWidth;
  1639. double restoreHeight = height ?? defaultHeight;
  1640. if (restoreWidth < minWidth) {
  1641. restoreWidth = defaultWidth;
  1642. }
  1643. if (restoreHeight < minHeight) {
  1644. restoreHeight = defaultHeight;
  1645. }
  1646. if (restoreWidth > maxWidth) {
  1647. restoreWidth = defaultWidth;
  1648. }
  1649. if (restoreHeight > maxHeight) {
  1650. restoreHeight = defaultHeight;
  1651. }
  1652. return Size(restoreWidth, restoreHeight);
  1653. }
  1654. bool isPointInRect(Offset point, Rect rect) {
  1655. return point.dx >= rect.left &&
  1656. point.dx <= rect.right &&
  1657. point.dy >= rect.top &&
  1658. point.dy <= rect.bottom;
  1659. }
  1660. /// return null means center
  1661. Future<Offset?> _adjustRestoreMainWindowOffset(
  1662. double? left,
  1663. double? top,
  1664. double? width,
  1665. double? height,
  1666. ) async {
  1667. if (left == null || top == null || width == null || height == null) {
  1668. return null;
  1669. }
  1670. double? frameLeft;
  1671. double? frameTop;
  1672. double? frameRight;
  1673. double? frameBottom;
  1674. if (isDesktop || isWebDesktop) {
  1675. for (final screen in await window_size.getScreenList()) {
  1676. frameLeft = frameLeft == null
  1677. ? screen.visibleFrame.left
  1678. : min(screen.visibleFrame.left, frameLeft);
  1679. frameTop = frameTop == null
  1680. ? screen.visibleFrame.top
  1681. : min(screen.visibleFrame.top, frameTop);
  1682. frameRight = frameRight == null
  1683. ? screen.visibleFrame.right
  1684. : max(screen.visibleFrame.right, frameRight);
  1685. frameBottom = frameBottom == null
  1686. ? screen.visibleFrame.bottom
  1687. : max(screen.visibleFrame.bottom, frameBottom);
  1688. }
  1689. }
  1690. if (frameLeft == null) {
  1691. frameLeft = 0.0;
  1692. frameTop = 0.0;
  1693. frameRight = ((isDesktop || isWebDesktop)
  1694. ? kDesktopMaxDisplaySize
  1695. : kMobileMaxDisplaySize)
  1696. .toDouble();
  1697. frameBottom = ((isDesktop || isWebDesktop)
  1698. ? kDesktopMaxDisplaySize
  1699. : kMobileMaxDisplaySize)
  1700. .toDouble();
  1701. }
  1702. final minWidth = 10.0;
  1703. if ((left + minWidth) > frameRight! ||
  1704. (top + minWidth) > frameBottom! ||
  1705. (left + width - minWidth) < frameLeft ||
  1706. top < frameTop!) {
  1707. return null;
  1708. } else {
  1709. return Offset(left, top);
  1710. }
  1711. }
  1712. /// Restore window position and size on start
  1713. /// Note that windowId must be provided if it's subwindow
  1714. //
  1715. // display is used to set the offset of the window in individual display mode.
  1716. Future<bool> restoreWindowPosition(WindowType type,
  1717. {int? windowId, String? peerId, int? display}) async {
  1718. if (bind
  1719. .mainGetEnv(key: "DISABLE_RUSTDESK_RESTORE_WINDOW_POSITION")
  1720. .isNotEmpty) {
  1721. return false;
  1722. }
  1723. if (type != WindowType.Main && windowId == null) {
  1724. debugPrint(
  1725. "Error: windowId cannot be null when saving positions for sub window");
  1726. return false;
  1727. }
  1728. bool isRemotePeerPos = false;
  1729. String? pos;
  1730. // No need to check mainGetLocalBoolOptionSync(kOptionOpenNewConnInTabs)
  1731. // Though "open in tabs" is true and the new window restore peer position, it's ok.
  1732. if (type == WindowType.RemoteDesktop && windowId != null && peerId != null) {
  1733. final peerPos = bind.mainGetPeerFlutterOptionSync(
  1734. id: peerId, k: windowFramePrefix + type.name);
  1735. if (peerPos.isNotEmpty) {
  1736. pos = peerPos;
  1737. }
  1738. isRemotePeerPos = pos != null;
  1739. }
  1740. pos ??= bind.getLocalFlutterOption(k: windowFramePrefix + type.name);
  1741. var lpos = LastWindowPosition.loadFromString(pos);
  1742. if (lpos == null) {
  1743. debugPrint("no window position saved, ignoring position restoration");
  1744. return false;
  1745. }
  1746. if (type == WindowType.RemoteDesktop) {
  1747. if (!isRemotePeerPos && windowId != null) {
  1748. if (lpos.offsetWidth != null) {
  1749. lpos.offsetWidth = lpos.offsetWidth! + windowId * kNewWindowOffset;
  1750. }
  1751. if (lpos.offsetHeight != null) {
  1752. lpos.offsetHeight = lpos.offsetHeight! + windowId * kNewWindowOffset;
  1753. }
  1754. }
  1755. if (display != null) {
  1756. if (lpos.offsetWidth != null) {
  1757. lpos.offsetWidth = lpos.offsetWidth! + display * kNewWindowOffset;
  1758. }
  1759. if (lpos.offsetHeight != null) {
  1760. lpos.offsetHeight = lpos.offsetHeight! + display * kNewWindowOffset;
  1761. }
  1762. }
  1763. }
  1764. final size = await _adjustRestoreMainWindowSize(lpos.width, lpos.height);
  1765. final offsetLeftTop = await _adjustRestoreMainWindowOffset(
  1766. lpos.offsetWidth,
  1767. lpos.offsetHeight,
  1768. size.width,
  1769. size.height,
  1770. );
  1771. debugPrint(
  1772. "restore lpos: ${size.width}/${size.height}, offset:${offsetLeftTop?.dx}/${offsetLeftTop?.dy}, isMaximized: ${lpos.isMaximized}, isFullscreen: ${lpos.isFullscreen}");
  1773. switch (type) {
  1774. case WindowType.Main:
  1775. restorePos() async {
  1776. if (offsetLeftTop == null) {
  1777. await windowManager.center();
  1778. } else {
  1779. await windowManager.setPosition(offsetLeftTop,
  1780. ignoreDevicePixelRatio: _ignoreDevicePixelRatio);
  1781. }
  1782. }
  1783. if (lpos.isMaximized == true) {
  1784. await restorePos();
  1785. if (!(bind.isIncomingOnly() || bind.isOutgoingOnly())) {
  1786. await windowManager.maximize();
  1787. }
  1788. } else {
  1789. final storeSize = !bind.isIncomingOnly() || bind.isOutgoingOnly();
  1790. if (isWindows) {
  1791. if (storeSize) {
  1792. // We need to set the window size first to avoid the incorrect size in some special cases.
  1793. // E.g. There are two monitors, the left one is 100% DPI and the right one is 175% DPI.
  1794. // The window belongs to the left monitor, but if it is moved a little to the right, it will belong to the right monitor.
  1795. // After restoring, the size will be incorrect.
  1796. // See known issue in https://github.com/rustdesk/rustdesk/pull/9840
  1797. await windowManager.setSize(size,
  1798. ignoreDevicePixelRatio: _ignoreDevicePixelRatio);
  1799. }
  1800. await restorePos();
  1801. if (storeSize) {
  1802. await windowManager.setSize(size,
  1803. ignoreDevicePixelRatio: _ignoreDevicePixelRatio);
  1804. }
  1805. } else {
  1806. if (storeSize) {
  1807. await windowManager.setSize(size,
  1808. ignoreDevicePixelRatio: _ignoreDevicePixelRatio);
  1809. }
  1810. await restorePos();
  1811. }
  1812. }
  1813. return true;
  1814. default:
  1815. final wc = WindowController.fromWindowId(windowId!);
  1816. restoreFrame() async {
  1817. if (offsetLeftTop == null) {
  1818. await wc.center();
  1819. } else {
  1820. final frame = Rect.fromLTWH(
  1821. offsetLeftTop.dx, offsetLeftTop.dy, size.width, size.height);
  1822. await wc.setFrame(frame);
  1823. }
  1824. }
  1825. if (lpos.isFullscreen == true) {
  1826. if (!isMacOS) {
  1827. await restoreFrame();
  1828. }
  1829. // An duration is needed to avoid the window being restored after fullscreen.
  1830. Future.delayed(Duration(milliseconds: 300), () async {
  1831. if (kWindowId == windowId) {
  1832. stateGlobal.setFullscreen(true);
  1833. } else {
  1834. // If is not current window, we need to send a fullscreen message to `windowId`
  1835. DesktopMultiWindow.invokeMethod(
  1836. windowId, kWindowEventSetFullscreen, 'true');
  1837. }
  1838. });
  1839. } else if (lpos.isMaximized == true) {
  1840. await restoreFrame();
  1841. // An duration is needed to avoid the window being restored after maximized.
  1842. Future.delayed(Duration(milliseconds: 300), () async {
  1843. await wc.maximize();
  1844. });
  1845. } else {
  1846. await restoreFrame();
  1847. }
  1848. break;
  1849. }
  1850. return false;
  1851. }
  1852. var webInitialLink = "";
  1853. /// Initialize uni links for macos/windows
  1854. ///
  1855. /// [Availability]
  1856. /// initUniLinks should only be used on macos/windows.
  1857. /// we use dbus for linux currently.
  1858. Future<bool> initUniLinks() async {
  1859. if (isLinux) {
  1860. return false;
  1861. }
  1862. // check cold boot
  1863. try {
  1864. final initialLink = await getInitialLink();
  1865. print("initialLink: $initialLink");
  1866. if (initialLink == null || initialLink.isEmpty) {
  1867. return false;
  1868. }
  1869. if (isWeb) {
  1870. webInitialLink = initialLink;
  1871. return false;
  1872. } else {
  1873. return handleUriLink(uriString: initialLink);
  1874. }
  1875. } catch (err) {
  1876. debugPrintStack(label: "$err");
  1877. return false;
  1878. }
  1879. }
  1880. /// Listen for uni links.
  1881. ///
  1882. /// * handleByFlutter: Should uni links be handled by Flutter.
  1883. ///
  1884. /// Returns a [StreamSubscription] which can listen the uni links.
  1885. StreamSubscription? listenUniLinks({handleByFlutter = true}) {
  1886. if (isLinux || isWeb) {
  1887. return null;
  1888. }
  1889. final sub = uriLinkStream.listen((Uri? uri) {
  1890. debugPrint("A uri was received: $uri. handleByFlutter $handleByFlutter");
  1891. if (uri != null) {
  1892. if (handleByFlutter) {
  1893. handleUriLink(uri: uri);
  1894. } else {
  1895. bind.sendUrlScheme(url: uri.toString());
  1896. }
  1897. } else {
  1898. print("uni listen error: uri is empty.");
  1899. }
  1900. }, onError: (err) {
  1901. print("uni links error: $err");
  1902. });
  1903. return sub;
  1904. }
  1905. enum UriLinkType {
  1906. remoteDesktop,
  1907. fileTransfer,
  1908. portForward,
  1909. rdp,
  1910. }
  1911. // uri link handler
  1912. bool handleUriLink({List<String>? cmdArgs, Uri? uri, String? uriString}) {
  1913. List<String>? args;
  1914. if (cmdArgs != null && cmdArgs.isNotEmpty) {
  1915. args = cmdArgs;
  1916. // rustdesk <uri link>
  1917. if (args[0].startsWith(bind.mainUriPrefixSync())) {
  1918. final uri = Uri.tryParse(args[0]);
  1919. if (uri != null) {
  1920. args = urlLinkToCmdArgs(uri);
  1921. }
  1922. }
  1923. } else if (uri != null) {
  1924. args = urlLinkToCmdArgs(uri);
  1925. } else if (uriString != null) {
  1926. final uri = Uri.tryParse(uriString);
  1927. if (uri != null) {
  1928. args = urlLinkToCmdArgs(uri);
  1929. }
  1930. }
  1931. if (args == null) {
  1932. return false;
  1933. }
  1934. if (args.isEmpty) {
  1935. windowOnTop(null);
  1936. return true;
  1937. }
  1938. UriLinkType? type;
  1939. String? id;
  1940. String? password;
  1941. String? switchUuid;
  1942. bool? forceRelay;
  1943. for (int i = 0; i < args.length; i++) {
  1944. switch (args[i]) {
  1945. case '--connect':
  1946. case '--play':
  1947. type = UriLinkType.remoteDesktop;
  1948. id = args[i + 1];
  1949. i++;
  1950. break;
  1951. case '--file-transfer':
  1952. type = UriLinkType.fileTransfer;
  1953. id = args[i + 1];
  1954. i++;
  1955. break;
  1956. case '--port-forward':
  1957. type = UriLinkType.portForward;
  1958. id = args[i + 1];
  1959. i++;
  1960. break;
  1961. case '--rdp':
  1962. type = UriLinkType.rdp;
  1963. id = args[i + 1];
  1964. i++;
  1965. break;
  1966. case '--password':
  1967. password = args[i + 1];
  1968. i++;
  1969. break;
  1970. case '--switch_uuid':
  1971. switchUuid = args[i + 1];
  1972. i++;
  1973. break;
  1974. case '--relay':
  1975. forceRelay = true;
  1976. break;
  1977. default:
  1978. break;
  1979. }
  1980. }
  1981. if (type != null && id != null) {
  1982. switch (type) {
  1983. case UriLinkType.remoteDesktop:
  1984. Future.delayed(Duration.zero, () {
  1985. rustDeskWinManager.newRemoteDesktop(id!,
  1986. password: password,
  1987. switchUuid: switchUuid,
  1988. forceRelay: forceRelay);
  1989. });
  1990. break;
  1991. case UriLinkType.fileTransfer:
  1992. Future.delayed(Duration.zero, () {
  1993. rustDeskWinManager.newFileTransfer(id!,
  1994. password: password, forceRelay: forceRelay);
  1995. });
  1996. break;
  1997. case UriLinkType.portForward:
  1998. Future.delayed(Duration.zero, () {
  1999. rustDeskWinManager.newPortForward(id!, false,
  2000. password: password, forceRelay: forceRelay);
  2001. });
  2002. break;
  2003. case UriLinkType.rdp:
  2004. Future.delayed(Duration.zero, () {
  2005. rustDeskWinManager.newPortForward(id!, true,
  2006. password: password, forceRelay: forceRelay);
  2007. });
  2008. break;
  2009. }
  2010. return true;
  2011. }
  2012. return false;
  2013. }
  2014. List<String>? urlLinkToCmdArgs(Uri uri) {
  2015. String? command;
  2016. String? id;
  2017. final options = ["connect", "play", "file-transfer", "port-forward", "rdp"];
  2018. if (uri.authority.isEmpty &&
  2019. uri.path.split('').every((char) => char == '/')) {
  2020. return [];
  2021. } else if (uri.authority == "connection" && uri.path.startsWith("/new/")) {
  2022. // For compatibility
  2023. command = '--connect';
  2024. id = uri.path.substring("/new/".length);
  2025. } else if (uri.authority == "config") {
  2026. if (isAndroid || isIOS) {
  2027. final config = uri.path.substring("/".length);
  2028. // add a timer to make showToast work
  2029. Timer(Duration(seconds: 1), () {
  2030. importConfig(null, null, config);
  2031. });
  2032. }
  2033. return null;
  2034. } else if (uri.authority == "password") {
  2035. if (isAndroid || isIOS) {
  2036. final password = uri.path.substring("/".length);
  2037. if (password.isNotEmpty) {
  2038. Timer(Duration(seconds: 1), () async {
  2039. await bind.mainSetPermanentPassword(password: password);
  2040. showToast(translate('Successful'));
  2041. });
  2042. }
  2043. }
  2044. } else if (options.contains(uri.authority)) {
  2045. final optionIndex = options.indexOf(uri.authority);
  2046. command = '--${uri.authority}';
  2047. if (uri.path.length > 1) {
  2048. id = uri.path.substring(1);
  2049. }
  2050. if (isMobile && id != null) {
  2051. if (optionIndex == 0 || optionIndex == 1) {
  2052. connect(Get.context!, id);
  2053. } else if (optionIndex == 2) {
  2054. connect(Get.context!, id, isFileTransfer: true);
  2055. }
  2056. return null;
  2057. }
  2058. } else if (uri.authority.length > 2 &&
  2059. (uri.path.length <= 1 ||
  2060. (uri.path == '/r' || uri.path.startsWith('/r@')))) {
  2061. // rustdesk://<connect-id>
  2062. // rustdesk://<connect-id>/r
  2063. // rustdesk://<connect-id>/r@<server>
  2064. command = '--connect';
  2065. id = uri.authority;
  2066. if (uri.path.length > 1) {
  2067. id = id + uri.path;
  2068. }
  2069. }
  2070. var queryParameters =
  2071. uri.queryParameters.map((k, v) => MapEntry(k.toLowerCase(), v));
  2072. var key = queryParameters["key"];
  2073. if (id != null) {
  2074. if (key != null) {
  2075. id = "$id?key=$key";
  2076. }
  2077. }
  2078. if (isMobile) {
  2079. if (id != null) {
  2080. final forceRelay = queryParameters["relay"] != null;
  2081. connect(Get.context!, id, forceRelay: forceRelay);
  2082. return null;
  2083. }
  2084. }
  2085. List<String> args = List.empty(growable: true);
  2086. if (command != null && id != null) {
  2087. args.add(command);
  2088. args.add(id);
  2089. var param = queryParameters;
  2090. String? password = param["password"];
  2091. if (password != null) args.addAll(['--password', password]);
  2092. String? switch_uuid = param["switch_uuid"];
  2093. if (switch_uuid != null) args.addAll(['--switch_uuid', switch_uuid]);
  2094. if (param["relay"] != null) args.add("--relay");
  2095. return args;
  2096. }
  2097. return null;
  2098. }
  2099. connectMainDesktop(String id,
  2100. {required bool isFileTransfer,
  2101. required bool isTcpTunneling,
  2102. required bool isRDP,
  2103. bool? forceRelay,
  2104. String? password,
  2105. String? connToken,
  2106. bool? isSharedPassword}) async {
  2107. if (isFileTransfer) {
  2108. await rustDeskWinManager.newFileTransfer(id,
  2109. password: password,
  2110. isSharedPassword: isSharedPassword,
  2111. connToken: connToken,
  2112. forceRelay: forceRelay);
  2113. } else if (isTcpTunneling || isRDP) {
  2114. await rustDeskWinManager.newPortForward(id, isRDP,
  2115. password: password,
  2116. isSharedPassword: isSharedPassword,
  2117. connToken: connToken,
  2118. forceRelay: forceRelay);
  2119. } else {
  2120. await rustDeskWinManager.newRemoteDesktop(id,
  2121. password: password,
  2122. isSharedPassword: isSharedPassword,
  2123. forceRelay: forceRelay);
  2124. }
  2125. }
  2126. /// Connect to a peer with [id].
  2127. /// If [isFileTransfer], starts a session only for file transfer.
  2128. /// If [isTcpTunneling], starts a session only for tcp tunneling.
  2129. /// If [isRDP], starts a session only for rdp.
  2130. connect(BuildContext context, String id,
  2131. {bool isFileTransfer = false,
  2132. bool isTcpTunneling = false,
  2133. bool isRDP = false,
  2134. bool forceRelay = false,
  2135. String? password,
  2136. String? connToken,
  2137. bool? isSharedPassword}) async {
  2138. if (id == '') return;
  2139. if (!isDesktop || desktopType == DesktopType.main) {
  2140. try {
  2141. if (Get.isRegistered<IDTextEditingController>()) {
  2142. final idController = Get.find<IDTextEditingController>();
  2143. idController.text = formatID(id);
  2144. }
  2145. if (Get.isRegistered<TextEditingController>()) {
  2146. final fieldTextEditingController = Get.find<TextEditingController>();
  2147. fieldTextEditingController.text = formatID(id);
  2148. }
  2149. } catch (_) {}
  2150. }
  2151. id = id.replaceAll(' ', '');
  2152. final oldId = id;
  2153. id = await bind.mainHandleRelayId(id: id);
  2154. final forceRelay2 = id != oldId || forceRelay;
  2155. assert(!(isFileTransfer && isTcpTunneling && isRDP),
  2156. "more than one connect type");
  2157. if (isDesktop) {
  2158. if (desktopType == DesktopType.main) {
  2159. await connectMainDesktop(
  2160. id,
  2161. isFileTransfer: isFileTransfer,
  2162. isTcpTunneling: isTcpTunneling,
  2163. isRDP: isRDP,
  2164. password: password,
  2165. isSharedPassword: isSharedPassword,
  2166. forceRelay: forceRelay2,
  2167. );
  2168. } else {
  2169. await rustDeskWinManager.call(WindowType.Main, kWindowConnect, {
  2170. 'id': id,
  2171. 'isFileTransfer': isFileTransfer,
  2172. 'isTcpTunneling': isTcpTunneling,
  2173. 'isRDP': isRDP,
  2174. 'password': password,
  2175. 'isSharedPassword': isSharedPassword,
  2176. 'forceRelay': forceRelay,
  2177. 'connToken': connToken,
  2178. });
  2179. }
  2180. } else {
  2181. if (isFileTransfer) {
  2182. if (isAndroid) {
  2183. if (!await AndroidPermissionManager.check(kManageExternalStorage)) {
  2184. if (!await AndroidPermissionManager.request(kManageExternalStorage)) {
  2185. return;
  2186. }
  2187. }
  2188. }
  2189. if (isWeb) {
  2190. Navigator.push(
  2191. context,
  2192. MaterialPageRoute(
  2193. builder: (BuildContext context) =>
  2194. desktop_file_manager.FileManagerPage(
  2195. id: id,
  2196. password: password,
  2197. isSharedPassword: isSharedPassword),
  2198. ),
  2199. );
  2200. } else {
  2201. Navigator.push(
  2202. context,
  2203. MaterialPageRoute(
  2204. builder: (BuildContext context) => FileManagerPage(
  2205. id: id, password: password, isSharedPassword: isSharedPassword),
  2206. ),
  2207. );
  2208. }
  2209. } else {
  2210. if (isWeb) {
  2211. Navigator.push(
  2212. context,
  2213. MaterialPageRoute(
  2214. builder: (BuildContext context) => desktop_remote.RemotePage(
  2215. key: ValueKey(id),
  2216. id: id,
  2217. toolbarState: ToolbarState(),
  2218. password: password,
  2219. forceRelay: forceRelay,
  2220. isSharedPassword: isSharedPassword,
  2221. ),
  2222. ),
  2223. );
  2224. } else {
  2225. Navigator.push(
  2226. context,
  2227. MaterialPageRoute(
  2228. builder: (BuildContext context) => RemotePage(
  2229. id: id, password: password, isSharedPassword: isSharedPassword),
  2230. ),
  2231. );
  2232. }
  2233. }
  2234. stateGlobal.isInMainPage = false;
  2235. }
  2236. FocusScopeNode currentFocus = FocusScope.of(context);
  2237. if (!currentFocus.hasPrimaryFocus) {
  2238. currentFocus.unfocus();
  2239. }
  2240. }
  2241. Map<String, String> getHttpHeaders() {
  2242. return {
  2243. 'Authorization': 'Bearer ${bind.mainGetLocalOption(key: 'access_token')}'
  2244. };
  2245. }
  2246. // Simple wrapper of built-in types for reference use.
  2247. class SimpleWrapper<T> {
  2248. T value;
  2249. SimpleWrapper(this.value);
  2250. }
  2251. /// call this to reload current window.
  2252. ///
  2253. /// [Note]
  2254. /// Must have [RefreshWrapper] on the top of widget tree.
  2255. void reloadCurrentWindow() {
  2256. if (Get.context != null) {
  2257. // reload self window
  2258. RefreshWrapper.of(Get.context!)?.rebuild();
  2259. } else {
  2260. debugPrint(
  2261. "reload current window failed, global BuildContext does not exist");
  2262. }
  2263. }
  2264. /// call this to reload all windows, including main + all sub windows.
  2265. Future<void> reloadAllWindows() async {
  2266. reloadCurrentWindow();
  2267. try {
  2268. final ids = await DesktopMultiWindow.getAllSubWindowIds();
  2269. for (final id in ids) {
  2270. DesktopMultiWindow.invokeMethod(id, kWindowActionRebuild);
  2271. }
  2272. } on AssertionError {
  2273. // ignore
  2274. }
  2275. }
  2276. /// Indicate the flutter app is running in portable mode.
  2277. ///
  2278. /// [Note]
  2279. /// Portable build is only available on Windows.
  2280. bool isRunningInPortableMode() {
  2281. if (!isWindows) {
  2282. return false;
  2283. }
  2284. return bool.hasEnvironment(kEnvPortableExecutable);
  2285. }
  2286. /// Window status callback
  2287. Future<void> onActiveWindowChanged() async {
  2288. print(
  2289. "[MultiWindowHandler] active window changed: ${rustDeskWinManager.getActiveWindows()}");
  2290. if (rustDeskWinManager.getActiveWindows().isEmpty) {
  2291. // close all sub windows
  2292. try {
  2293. if (isLinux) {
  2294. await Future.wait([
  2295. saveWindowPosition(WindowType.Main),
  2296. rustDeskWinManager.closeAllSubWindows()
  2297. ]);
  2298. } else {
  2299. await rustDeskWinManager.closeAllSubWindows();
  2300. }
  2301. } catch (err) {
  2302. debugPrintStack(label: "$err");
  2303. } finally {
  2304. debugPrint("Start closing RustDesk...");
  2305. await windowManager.setPreventClose(false);
  2306. await windowManager.close();
  2307. if (isMacOS) {
  2308. // If we call without delay, `flutter/macos/Runner/MainFlutterWindow.swift` can handle the "terminate" event.
  2309. // But the app will not close.
  2310. //
  2311. // No idea why we need to delay here, `terminate()` itself is also an async function.
  2312. //
  2313. // A quick workaround, use `Timer.periodic` to avoid the app not closing.
  2314. // Because `await windowManager.close()` and `RdPlatformChannel.instance.terminate()`
  2315. // may not work since `Flutter 3.24.4`, see the following logs.
  2316. // A delay will allow the app to close.
  2317. //
  2318. //```
  2319. // embedder.cc (2725): 'FlutterPlatformMessageCreateResponseHandle' returned 'kInvalidArguments'. Engine handle was invalid.
  2320. // 2024-11-11 11:41:11.546 RustDesk[90272:2567686] Failed to create a FlutterPlatformMessageResponseHandle (2)
  2321. // embedder.cc (2672): 'FlutterEngineSendPlatformMessage' returned 'kInvalidArguments'. Invalid engine handle.
  2322. // 2024-11-11 11:41:11.565 RustDesk[90272:2567686] Failed to send message to Flutter engine on channel 'flutter/lifecycle' (2).
  2323. // ```
  2324. periodic_immediate(
  2325. Duration(milliseconds: 30), RdPlatformChannel.instance.terminate);
  2326. }
  2327. }
  2328. }
  2329. }
  2330. Timer periodic_immediate(Duration duration, Future<void> Function() callback) {
  2331. Future.delayed(Duration.zero, callback);
  2332. return Timer.periodic(duration, (timer) async {
  2333. await callback();
  2334. });
  2335. }
  2336. /// return a human readable windows version
  2337. WindowsTarget getWindowsTarget(int buildNumber) {
  2338. if (!isWindows) {
  2339. return WindowsTarget.naw;
  2340. }
  2341. if (buildNumber >= 22000) {
  2342. return WindowsTarget.w11;
  2343. } else if (buildNumber >= 10240) {
  2344. return WindowsTarget.w10;
  2345. } else if (buildNumber >= 9600) {
  2346. return WindowsTarget.w8_1;
  2347. } else if (buildNumber >= 9200) {
  2348. return WindowsTarget.w8;
  2349. } else if (buildNumber >= 7601) {
  2350. return WindowsTarget.w7;
  2351. } else if (buildNumber >= 6002) {
  2352. return WindowsTarget.vista;
  2353. } else {
  2354. // minimum support
  2355. return WindowsTarget.xp;
  2356. }
  2357. }
  2358. /// Get windows target build number.
  2359. ///
  2360. /// [Note]
  2361. /// Please use this function wrapped with `Platform.isWindows`.
  2362. int getWindowsTargetBuildNumber() {
  2363. return getWindowsTargetBuildNumber_();
  2364. }
  2365. /// Indicating we need to use compatible ui mode.
  2366. ///
  2367. /// [Conditions]
  2368. /// - Windows 7, window will overflow when we use frameless ui.
  2369. bool get kUseCompatibleUiMode =>
  2370. isWindows &&
  2371. const [WindowsTarget.w7].contains(windowsBuildNumber.windowsVersion);
  2372. class ServerConfig {
  2373. late String idServer;
  2374. late String relayServer;
  2375. late String apiServer;
  2376. late String key;
  2377. ServerConfig(
  2378. {String? idServer, String? relayServer, String? apiServer, String? key}) {
  2379. this.idServer = idServer?.trim() ?? '';
  2380. this.relayServer = relayServer?.trim() ?? '';
  2381. this.apiServer = apiServer?.trim() ?? '';
  2382. this.key = key?.trim() ?? '';
  2383. }
  2384. /// decode from shared string (from user shared or rustdesk-server generated)
  2385. /// also see [encode]
  2386. /// throw when decoding failure
  2387. ServerConfig.decode(String msg) {
  2388. var json = {};
  2389. try {
  2390. // back compatible
  2391. json = jsonDecode(msg);
  2392. } catch (err) {
  2393. final input = msg.split('').reversed.join('');
  2394. final bytes = base64Decode(base64.normalize(input));
  2395. json = jsonDecode(utf8.decode(bytes));
  2396. }
  2397. idServer = json['host'] ?? '';
  2398. relayServer = json['relay'] ?? '';
  2399. apiServer = json['api'] ?? '';
  2400. key = json['key'] ?? '';
  2401. }
  2402. /// encode to shared string
  2403. /// also see [ServerConfig.decode]
  2404. String encode() {
  2405. Map<String, String> config = {};
  2406. config['host'] = idServer.trim();
  2407. config['relay'] = relayServer.trim();
  2408. config['api'] = apiServer.trim();
  2409. config['key'] = key.trim();
  2410. return base64UrlEncode(Uint8List.fromList(jsonEncode(config).codeUnits))
  2411. .split('')
  2412. .reversed
  2413. .join();
  2414. }
  2415. /// from local options
  2416. ServerConfig.fromOptions(Map<String, dynamic> options)
  2417. : idServer = options['custom-rendezvous-server'] ?? "",
  2418. relayServer = options['relay-server'] ?? "",
  2419. apiServer = options['api-server'] ?? "",
  2420. key = options['key'] ?? "";
  2421. }
  2422. Widget dialogButton(String text,
  2423. {required VoidCallback? onPressed,
  2424. bool isOutline = false,
  2425. Widget? icon,
  2426. TextStyle? style,
  2427. ButtonStyle? buttonStyle}) {
  2428. if (isDesktop || isWebDesktop) {
  2429. if (isOutline) {
  2430. return icon == null
  2431. ? OutlinedButton(
  2432. onPressed: onPressed,
  2433. child: Text(translate(text), style: style),
  2434. )
  2435. : OutlinedButton.icon(
  2436. icon: icon,
  2437. onPressed: onPressed,
  2438. label: Text(translate(text), style: style),
  2439. );
  2440. } else {
  2441. return icon == null
  2442. ? ElevatedButton(
  2443. style: ElevatedButton.styleFrom(elevation: 0).merge(buttonStyle),
  2444. onPressed: onPressed,
  2445. child: Text(translate(text), style: style),
  2446. )
  2447. : ElevatedButton.icon(
  2448. icon: icon,
  2449. style: ElevatedButton.styleFrom(elevation: 0).merge(buttonStyle),
  2450. onPressed: onPressed,
  2451. label: Text(translate(text), style: style),
  2452. );
  2453. }
  2454. } else {
  2455. return TextButton(
  2456. onPressed: onPressed,
  2457. child: Text(
  2458. translate(text),
  2459. style: style,
  2460. ),
  2461. );
  2462. }
  2463. }
  2464. int versionCmp(String v1, String v2) {
  2465. return bind.versionToNumber(v: v1) - bind.versionToNumber(v: v2);
  2466. }
  2467. String getWindowName({WindowType? overrideType}) {
  2468. final name = bind.mainGetAppNameSync();
  2469. switch (overrideType ?? kWindowType) {
  2470. case WindowType.Main:
  2471. return name;
  2472. case WindowType.FileTransfer:
  2473. return "File Transfer - $name";
  2474. case WindowType.PortForward:
  2475. return "Port Forward - $name";
  2476. case WindowType.RemoteDesktop:
  2477. return "Remote Desktop - $name";
  2478. default:
  2479. break;
  2480. }
  2481. return name;
  2482. }
  2483. String getWindowNameWithId(String id, {WindowType? overrideType}) {
  2484. return "${DesktopTab.tablabelGetter(id).value} - ${getWindowName(overrideType: overrideType)}";
  2485. }
  2486. Future<void> updateSystemWindowTheme() async {
  2487. // Set system window theme for macOS.
  2488. final userPreference = MyTheme.getThemeModePreference();
  2489. if (userPreference != ThemeMode.system) {
  2490. if (isMacOS) {
  2491. await RdPlatformChannel.instance.changeSystemWindowTheme(
  2492. userPreference == ThemeMode.light
  2493. ? SystemWindowTheme.light
  2494. : SystemWindowTheme.dark);
  2495. }
  2496. }
  2497. }
  2498. /// macOS only
  2499. ///
  2500. /// Note: not found a general solution for rust based AVFoundation bingding.
  2501. /// [AVFoundation] crate has compile error.
  2502. const kMacOSPermChannel = MethodChannel("org.rustdesk.rustdesk/macos");
  2503. enum PermissionAuthorizeType {
  2504. undetermined,
  2505. authorized,
  2506. denied, // and restricted
  2507. }
  2508. Future<PermissionAuthorizeType> osxCanRecordAudio() async {
  2509. int res = await kMacOSPermChannel.invokeMethod("canRecordAudio");
  2510. print(res);
  2511. if (res > 0) {
  2512. return PermissionAuthorizeType.authorized;
  2513. } else if (res == 0) {
  2514. return PermissionAuthorizeType.undetermined;
  2515. } else {
  2516. return PermissionAuthorizeType.denied;
  2517. }
  2518. }
  2519. Future<bool> osxRequestAudio() async {
  2520. return await kMacOSPermChannel.invokeMethod("requestRecordAudio");
  2521. }
  2522. class DraggableNeverScrollableScrollPhysics extends ScrollPhysics {
  2523. /// Creates scroll physics that does not let the user scroll.
  2524. const DraggableNeverScrollableScrollPhysics({super.parent});
  2525. @override
  2526. DraggableNeverScrollableScrollPhysics applyTo(ScrollPhysics? ancestor) {
  2527. return DraggableNeverScrollableScrollPhysics(parent: buildParent(ancestor));
  2528. }
  2529. @override
  2530. bool shouldAcceptUserOffset(ScrollMetrics position) {
  2531. // TODO: find a better solution to check if the offset change is caused by the scrollbar.
  2532. // Workaround: when dragging with the scrollbar, it always triggers an [IdleScrollActivity].
  2533. if (position is ScrollPositionWithSingleContext) {
  2534. // ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
  2535. return position.activity is IdleScrollActivity;
  2536. }
  2537. return false;
  2538. }
  2539. @override
  2540. bool get allowImplicitScrolling => false;
  2541. }
  2542. Widget futureBuilder(
  2543. {required Future? future, required Widget Function(dynamic data) hasData}) {
  2544. return FutureBuilder(
  2545. future: future,
  2546. builder: (BuildContext context, AsyncSnapshot snapshot) {
  2547. if (snapshot.hasData) {
  2548. return hasData(snapshot.data!);
  2549. } else {
  2550. if (snapshot.hasError) {
  2551. debugPrint(snapshot.error.toString());
  2552. }
  2553. return Container();
  2554. }
  2555. });
  2556. }
  2557. void onCopyFingerprint(String value) {
  2558. if (value.isNotEmpty) {
  2559. Clipboard.setData(ClipboardData(text: value));
  2560. showToast('$value\n${translate("Copied")}');
  2561. } else {
  2562. showToast(translate("no fingerprints"));
  2563. }
  2564. }
  2565. Future<bool> callMainCheckSuperUserPermission() async {
  2566. bool checked = await bind.mainCheckSuperUserPermission();
  2567. if (isMacOS) {
  2568. await windowManager.show();
  2569. }
  2570. return checked;
  2571. }
  2572. Future<void> start_service(bool is_start) async {
  2573. bool checked = !bind.mainIsInstalled() ||
  2574. !isMacOS ||
  2575. await callMainCheckSuperUserPermission();
  2576. if (checked) {
  2577. mainSetBoolOption(kOptionStopService, !is_start);
  2578. }
  2579. }
  2580. Future<bool> canBeBlocked() async {
  2581. var access_mode = await bind.mainGetOption(key: kOptionAccessMode);
  2582. var option = option2bool(kOptionAllowRemoteConfigModification,
  2583. await bind.mainGetOption(key: kOptionAllowRemoteConfigModification));
  2584. return access_mode == 'view' || (access_mode.isEmpty && !option);
  2585. }
  2586. Future<void> shouldBeBlocked(RxBool block, WhetherUseRemoteBlock? use) async {
  2587. if (use != null && !await use()) {
  2588. block.value = false;
  2589. return;
  2590. }
  2591. var time0 = DateTime.now().millisecondsSinceEpoch;
  2592. await bind.mainCheckMouseTime();
  2593. Timer(const Duration(milliseconds: 120), () async {
  2594. var d = time0 - await bind.mainGetMouseTime();
  2595. if (d < 120) {
  2596. block.value = true;
  2597. } else {
  2598. block.value = false;
  2599. }
  2600. });
  2601. }
  2602. typedef WhetherUseRemoteBlock = Future<bool> Function();
  2603. Widget buildRemoteBlock(
  2604. {required Widget child,
  2605. required RxBool block,
  2606. required bool mask,
  2607. WhetherUseRemoteBlock? use}) {
  2608. return Obx(() => MouseRegion(
  2609. onEnter: (_) async {
  2610. await shouldBeBlocked(block, use);
  2611. },
  2612. onExit: (event) => block.value = false,
  2613. child: Stack(children: [
  2614. // scope block tab
  2615. FocusScope(child: child, canRequestFocus: !block.value),
  2616. // mask block click, cm not block click and still use check_click_time to avoid block local click
  2617. if (mask)
  2618. Offstage(
  2619. offstage: !block.value,
  2620. child: Container(
  2621. color: Colors.black.withOpacity(0.5),
  2622. )),
  2623. ]),
  2624. ));
  2625. }
  2626. Widget unreadMessageCountBuilder(RxInt? count,
  2627. {double? size, double? fontSize}) {
  2628. return Obx(() => Offstage(
  2629. offstage: !((count?.value ?? 0) > 0),
  2630. child: Container(
  2631. width: size ?? 16,
  2632. height: size ?? 16,
  2633. decoration: BoxDecoration(
  2634. color: Colors.red,
  2635. shape: BoxShape.circle,
  2636. ),
  2637. child: Center(
  2638. child: Text("${count?.value ?? 0}",
  2639. maxLines: 1,
  2640. style: TextStyle(color: Colors.white, fontSize: fontSize ?? 10)),
  2641. ),
  2642. )));
  2643. }
  2644. Widget unreadTopRightBuilder(RxInt? count, {Widget? icon}) {
  2645. return Stack(
  2646. children: [
  2647. icon ?? Icon(Icons.chat),
  2648. Positioned(
  2649. top: 0,
  2650. right: 0,
  2651. child: unreadMessageCountBuilder(count, size: 12, fontSize: 8))
  2652. ],
  2653. );
  2654. }
  2655. String toCapitalized(String s) {
  2656. if (s.isEmpty) {
  2657. return s;
  2658. }
  2659. return s.substring(0, 1).toUpperCase() + s.substring(1);
  2660. }
  2661. Widget buildErrorBanner(BuildContext context,
  2662. {required RxBool loading,
  2663. required RxString err,
  2664. required Function? retry,
  2665. required Function close}) {
  2666. return Obx(() => Offstage(
  2667. offstage: !(!loading.value && err.value.isNotEmpty),
  2668. child: Center(
  2669. child: Container(
  2670. color: MyTheme.color(context).errorBannerBg,
  2671. child: Row(
  2672. mainAxisAlignment: MainAxisAlignment.center,
  2673. crossAxisAlignment: CrossAxisAlignment.center,
  2674. children: [
  2675. FittedBox(
  2676. child: Icon(
  2677. Icons.info,
  2678. color: Color.fromARGB(255, 249, 81, 81),
  2679. ),
  2680. ).marginAll(4),
  2681. Flexible(
  2682. child: Align(
  2683. alignment: Alignment.centerLeft,
  2684. child: Tooltip(
  2685. message: translate(err.value),
  2686. child: SelectableText(
  2687. translate(err.value),
  2688. ),
  2689. )).marginSymmetric(vertical: 2),
  2690. ),
  2691. if (retry != null)
  2692. InkWell(
  2693. onTap: () {
  2694. retry.call();
  2695. },
  2696. child: Text(
  2697. translate("Retry"),
  2698. style: TextStyle(color: MyTheme.accent),
  2699. )).marginSymmetric(horizontal: 5),
  2700. FittedBox(
  2701. child: InkWell(
  2702. onTap: () {
  2703. close.call();
  2704. },
  2705. child: Icon(Icons.close).marginSymmetric(horizontal: 5),
  2706. ),
  2707. ).marginAll(4)
  2708. ],
  2709. ),
  2710. )).marginOnly(bottom: 14),
  2711. ));
  2712. }
  2713. String getDesktopTabLabel(String peerId, String alias) {
  2714. String label = alias.isEmpty ? peerId : alias;
  2715. try {
  2716. String peer = bind.mainGetPeerSync(id: peerId);
  2717. Map<String, dynamic> config = jsonDecode(peer);
  2718. if (config['info']['hostname'] is String) {
  2719. String hostname = config['info']['hostname'];
  2720. if (hostname.isNotEmpty &&
  2721. !label.toLowerCase().contains(hostname.toLowerCase())) {
  2722. label += "@$hostname";
  2723. }
  2724. }
  2725. } catch (e) {
  2726. debugPrint("Failed to get hostname:$e");
  2727. }
  2728. return label;
  2729. }
  2730. sessionRefreshVideo(SessionID sessionId, PeerInfo pi) async {
  2731. if (pi.currentDisplay == kAllDisplayValue) {
  2732. for (int i = 0; i < pi.displays.length; i++) {
  2733. await bind.sessionRefresh(sessionId: sessionId, display: i);
  2734. }
  2735. } else {
  2736. await bind.sessionRefresh(sessionId: sessionId, display: pi.currentDisplay);
  2737. }
  2738. }
  2739. Future<List<Rect>> getScreenListWayland() async {
  2740. final screenRectList = <Rect>[];
  2741. if (isMainDesktopWindow) {
  2742. for (var screen in await window_size.getScreenList()) {
  2743. final scale = kIgnoreDpi ? 1.0 : screen.scaleFactor;
  2744. double l = screen.frame.left;
  2745. double t = screen.frame.top;
  2746. double r = screen.frame.right;
  2747. double b = screen.frame.bottom;
  2748. final rect = Rect.fromLTRB(l / scale, t / scale, r / scale, b / scale);
  2749. screenRectList.add(rect);
  2750. }
  2751. } else {
  2752. final screenList = await rustDeskWinManager.call(
  2753. WindowType.Main, kWindowGetScreenList, '');
  2754. try {
  2755. for (var screen in jsonDecode(screenList.result) as List<dynamic>) {
  2756. final scale = kIgnoreDpi ? 1.0 : screen['scaleFactor'];
  2757. double l = screen['frame']['l'];
  2758. double t = screen['frame']['t'];
  2759. double r = screen['frame']['r'];
  2760. double b = screen['frame']['b'];
  2761. final rect = Rect.fromLTRB(l / scale, t / scale, r / scale, b / scale);
  2762. screenRectList.add(rect);
  2763. }
  2764. } catch (e) {
  2765. debugPrint('Failed to parse screenList: $e');
  2766. }
  2767. }
  2768. return screenRectList;
  2769. }
  2770. Future<List<Rect>> getScreenListNotWayland() async {
  2771. final screenRectList = <Rect>[];
  2772. final displays = bind.mainGetDisplays();
  2773. if (displays.isEmpty) {
  2774. return screenRectList;
  2775. }
  2776. try {
  2777. for (var display in jsonDecode(displays) as List<dynamic>) {
  2778. // to-do: scale factor ?
  2779. // final scale = kIgnoreDpi ? 1.0 : screen.scaleFactor;
  2780. double l = display['x'].toDouble();
  2781. double t = display['y'].toDouble();
  2782. double r = (display['x'] + display['w']).toDouble();
  2783. double b = (display['y'] + display['h']).toDouble();
  2784. screenRectList.add(Rect.fromLTRB(l, t, r, b));
  2785. }
  2786. } catch (e) {
  2787. debugPrint('Failed to parse displays: $e');
  2788. }
  2789. return screenRectList;
  2790. }
  2791. Future<List<Rect>> getScreenRectList() async {
  2792. return bind.mainCurrentIsWayland()
  2793. ? await getScreenListWayland()
  2794. : await getScreenListNotWayland();
  2795. }
  2796. openMonitorInTheSameTab(int i, FFI ffi, PeerInfo pi,
  2797. {bool updateCursorPos = true}) {
  2798. final displays = i == kAllDisplayValue
  2799. ? List.generate(pi.displays.length, (index) => index)
  2800. : [i];
  2801. // Try clear image model before switching from all displays
  2802. // 1. The remote side has multiple displays.
  2803. // 2. Do not use texture render.
  2804. // 3. Connect to Display 1.
  2805. // 4. Switch to multi-displays `kAllDisplayValue`
  2806. // 5. Switch to Display 2.
  2807. // Then the remote page will display last picture of Display 1 at the beginning.
  2808. if (pi.forceTextureRender && i != kAllDisplayValue) {
  2809. ffi.imageModel.clearImage();
  2810. }
  2811. bind.sessionSwitchDisplay(
  2812. isDesktop: isDesktop,
  2813. sessionId: ffi.sessionId,
  2814. value: Int32List.fromList(displays),
  2815. );
  2816. ffi.ffiModel.switchToNewDisplay(i, ffi.sessionId, ffi.id,
  2817. updateCursorPos: updateCursorPos);
  2818. }
  2819. // Open new tab or window to show this monitor.
  2820. // For now just open new window.
  2821. //
  2822. // screenRect is used to move the new window to the specified screen and set fullscreen.
  2823. openMonitorInNewTabOrWindow(int i, String peerId, PeerInfo pi,
  2824. {Rect? screenRect}) {
  2825. final args = {
  2826. 'window_id': stateGlobal.windowId,
  2827. 'peer_id': peerId,
  2828. 'display': i,
  2829. 'display_count': pi.displays.length,
  2830. };
  2831. if (screenRect != null) {
  2832. args['screen_rect'] = {
  2833. 'l': screenRect.left,
  2834. 't': screenRect.top,
  2835. 'r': screenRect.right,
  2836. 'b': screenRect.bottom,
  2837. };
  2838. }
  2839. DesktopMultiWindow.invokeMethod(
  2840. kMainWindowId, kWindowEventOpenMonitorSession, jsonEncode(args));
  2841. }
  2842. setNewConnectWindowFrame(int windowId, String peerId, int preSessionCount,
  2843. int? display, Rect? screenRect) async {
  2844. if (screenRect == null) {
  2845. // Do not restore window position to new connection if there's a pre-session.
  2846. // https://github.com/rustdesk/rustdesk/discussions/8825
  2847. if (preSessionCount == 0) {
  2848. await restoreWindowPosition(WindowType.RemoteDesktop,
  2849. windowId: windowId, display: display, peerId: peerId);
  2850. }
  2851. } else {
  2852. await tryMoveToScreenAndSetFullscreen(screenRect);
  2853. }
  2854. }
  2855. tryMoveToScreenAndSetFullscreen(Rect? screenRect) async {
  2856. if (screenRect == null) {
  2857. return;
  2858. }
  2859. final wc = WindowController.fromWindowId(stateGlobal.windowId);
  2860. final curFrame = await wc.getFrame();
  2861. final frame =
  2862. Rect.fromLTWH(screenRect.left + 30, screenRect.top + 30, 600, 400);
  2863. if (stateGlobal.fullscreen.isTrue &&
  2864. curFrame.left <= frame.left &&
  2865. curFrame.top <= frame.top &&
  2866. curFrame.width >= frame.width &&
  2867. curFrame.height >= frame.height) {
  2868. return;
  2869. }
  2870. await wc.setFrame(frame);
  2871. // An duration is needed to avoid the window being restored after fullscreen.
  2872. Future.delayed(Duration(milliseconds: 300), () async {
  2873. stateGlobal.setFullscreen(true);
  2874. });
  2875. }
  2876. parseParamScreenRect(Map<String, dynamic> params) {
  2877. Rect? screenRect;
  2878. if (params['screen_rect'] != null) {
  2879. double l = params['screen_rect']['l'];
  2880. double t = params['screen_rect']['t'];
  2881. double r = params['screen_rect']['r'];
  2882. double b = params['screen_rect']['b'];
  2883. screenRect = Rect.fromLTRB(l, t, r, b);
  2884. }
  2885. return screenRect;
  2886. }
  2887. get isInputSourceFlutter => stateGlobal.getInputSource() == "Input source 2";
  2888. class _ReconnectCountDownButton extends StatefulWidget {
  2889. _ReconnectCountDownButton({
  2890. Key? key,
  2891. required this.second,
  2892. required this.onPressed,
  2893. }) : super(key: key);
  2894. final VoidCallback? onPressed;
  2895. final int second;
  2896. @override
  2897. State<_ReconnectCountDownButton> createState() =>
  2898. _ReconnectCountDownButtonState();
  2899. }
  2900. class _ReconnectCountDownButtonState extends State<_ReconnectCountDownButton> {
  2901. late int _countdownSeconds = widget.second;
  2902. Timer? _timer;
  2903. @override
  2904. void initState() {
  2905. super.initState();
  2906. _startCountdownTimer();
  2907. }
  2908. @override
  2909. void dispose() {
  2910. _timer?.cancel();
  2911. super.dispose();
  2912. }
  2913. void _startCountdownTimer() {
  2914. _timer = Timer.periodic(Duration(seconds: 1), (timer) {
  2915. if (_countdownSeconds <= 0) {
  2916. timer.cancel();
  2917. } else {
  2918. setState(() {
  2919. _countdownSeconds--;
  2920. });
  2921. }
  2922. });
  2923. }
  2924. @override
  2925. Widget build(BuildContext context) {
  2926. return dialogButton(
  2927. '${translate('Reconnect')} (${_countdownSeconds}s)',
  2928. onPressed: widget.onPressed,
  2929. isOutline: true,
  2930. );
  2931. }
  2932. }
  2933. importConfig(List<TextEditingController>? controllers, List<RxString>? errMsgs,
  2934. String? text) {
  2935. text = text?.trim();
  2936. if (text != null && text.isNotEmpty) {
  2937. try {
  2938. final sc = ServerConfig.decode(text);
  2939. if (isWeb || isIOS) {
  2940. sc.relayServer = '';
  2941. }
  2942. if (sc.idServer.isNotEmpty) {
  2943. Future<bool> success = setServerConfig(controllers, errMsgs, sc);
  2944. success.then((value) {
  2945. if (value) {
  2946. showToast(translate('Import server configuration successfully'));
  2947. } else {
  2948. showToast(translate('Invalid server configuration'));
  2949. }
  2950. });
  2951. } else {
  2952. showToast(translate('Invalid server configuration'));
  2953. }
  2954. return sc;
  2955. } catch (e) {
  2956. showToast(translate('Invalid server configuration'));
  2957. }
  2958. } else {
  2959. showToast(translate('Clipboard is empty'));
  2960. }
  2961. }
  2962. Future<bool> setServerConfig(
  2963. List<TextEditingController>? controllers,
  2964. List<RxString>? errMsgs,
  2965. ServerConfig config,
  2966. ) async {
  2967. String removeEndSlash(String input) {
  2968. if (input.endsWith('/')) {
  2969. return input.substring(0, input.length - 1);
  2970. }
  2971. return input;
  2972. }
  2973. config.idServer = removeEndSlash(config.idServer.trim());
  2974. config.relayServer = removeEndSlash(config.relayServer.trim());
  2975. config.apiServer = removeEndSlash(config.apiServer.trim());
  2976. config.key = config.key.trim();
  2977. if (controllers != null) {
  2978. controllers[0].text = config.idServer;
  2979. controllers[1].text = config.relayServer;
  2980. controllers[2].text = config.apiServer;
  2981. controllers[3].text = config.key;
  2982. }
  2983. // id
  2984. if (config.idServer.isNotEmpty && errMsgs != null) {
  2985. errMsgs[0].value = translate(await bind.mainTestIfValidServer(
  2986. server: config.idServer, testWithProxy: true));
  2987. if (errMsgs[0].isNotEmpty) {
  2988. return false;
  2989. }
  2990. }
  2991. // relay
  2992. if (config.relayServer.isNotEmpty && errMsgs != null) {
  2993. errMsgs[1].value = translate(await bind.mainTestIfValidServer(
  2994. server: config.relayServer, testWithProxy: true));
  2995. if (errMsgs[1].isNotEmpty) {
  2996. return false;
  2997. }
  2998. }
  2999. // api
  3000. if (config.apiServer.isNotEmpty && errMsgs != null) {
  3001. if (!config.apiServer.startsWith('http://') &&
  3002. !config.apiServer.startsWith('https://')) {
  3003. errMsgs[2].value =
  3004. '${translate("API Server")}: ${translate("invalid_http")}';
  3005. return false;
  3006. }
  3007. }
  3008. final oldApiServer = await bind.mainGetApiServer();
  3009. // should set one by one
  3010. await bind.mainSetOption(
  3011. key: 'custom-rendezvous-server', value: config.idServer);
  3012. await bind.mainSetOption(key: 'relay-server', value: config.relayServer);
  3013. await bind.mainSetOption(key: 'api-server', value: config.apiServer);
  3014. await bind.mainSetOption(key: 'key', value: config.key);
  3015. final newApiServer = await bind.mainGetApiServer();
  3016. if (oldApiServer.isNotEmpty &&
  3017. oldApiServer != newApiServer &&
  3018. gFFI.userModel.isLogin) {
  3019. gFFI.userModel.logOut(apiServer: oldApiServer);
  3020. }
  3021. return true;
  3022. }
  3023. ColorFilter? svgColor(Color? color) {
  3024. if (color == null) {
  3025. return null;
  3026. } else {
  3027. return ColorFilter.mode(color, BlendMode.srcIn);
  3028. }
  3029. }
  3030. // ignore: must_be_immutable
  3031. class ComboBox extends StatelessWidget {
  3032. late final List<String> keys;
  3033. late final List<String> values;
  3034. late final String initialKey;
  3035. late final Function(String key) onChanged;
  3036. late final bool enabled;
  3037. late String current;
  3038. ComboBox({
  3039. Key? key,
  3040. required this.keys,
  3041. required this.values,
  3042. required this.initialKey,
  3043. required this.onChanged,
  3044. this.enabled = true,
  3045. }) : super(key: key);
  3046. @override
  3047. Widget build(BuildContext context) {
  3048. var index = keys.indexOf(initialKey);
  3049. if (index < 0) {
  3050. index = 0;
  3051. }
  3052. var ref = values[index].obs;
  3053. current = keys[index];
  3054. return Container(
  3055. decoration: BoxDecoration(
  3056. border: Border.all(
  3057. color: enabled
  3058. ? MyTheme.color(context).border2 ?? MyTheme.border
  3059. : MyTheme.border,
  3060. ),
  3061. borderRadius:
  3062. BorderRadius.circular(8), //border raiuds of dropdown button
  3063. ),
  3064. height: 42, // should be the height of a TextField
  3065. child: Obx(() => DropdownButton<String>(
  3066. isExpanded: true,
  3067. value: ref.value,
  3068. elevation: 16,
  3069. underline: Container(),
  3070. style: TextStyle(
  3071. color: enabled
  3072. ? Theme.of(context).textTheme.titleMedium?.color
  3073. : disabledTextColor(context, enabled)),
  3074. icon: const Icon(
  3075. Icons.expand_more_sharp,
  3076. size: 20,
  3077. ).marginOnly(right: 15),
  3078. onChanged: enabled
  3079. ? (String? newValue) {
  3080. if (newValue != null && newValue != ref.value) {
  3081. ref.value = newValue;
  3082. current = newValue;
  3083. onChanged(keys[values.indexOf(newValue)]);
  3084. }
  3085. }
  3086. : null,
  3087. items: values.map<DropdownMenuItem<String>>((String value) {
  3088. return DropdownMenuItem<String>(
  3089. value: value,
  3090. child: Text(
  3091. value,
  3092. style: const TextStyle(fontSize: 15),
  3093. overflow: TextOverflow.ellipsis,
  3094. ).marginOnly(left: 15),
  3095. );
  3096. }).toList(),
  3097. )),
  3098. ).marginOnly(bottom: 5);
  3099. }
  3100. }
  3101. Color? disabledTextColor(BuildContext context, bool enabled) {
  3102. return enabled
  3103. ? null
  3104. : Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6);
  3105. }
  3106. Widget loadPowered(BuildContext context) {
  3107. return MouseRegion(
  3108. cursor: SystemMouseCursors.click,
  3109. child: GestureDetector(
  3110. onTap: () {
  3111. launchUrl(Uri.parse('https://rustdesk.com'));
  3112. },
  3113. child: Opacity(
  3114. opacity: 0.5,
  3115. child: Text(
  3116. translate("powered_by_me"),
  3117. overflow: TextOverflow.clip,
  3118. style: Theme.of(context)
  3119. .textTheme
  3120. .bodySmall
  3121. ?.copyWith(fontSize: 9, decoration: TextDecoration.underline),
  3122. )),
  3123. ),
  3124. ).marginOnly(top: 6);
  3125. }
  3126. // max 300 x 60
  3127. Widget loadLogo() {
  3128. return FutureBuilder<ByteData>(
  3129. future: rootBundle.load('assets/logo.png'),
  3130. builder: (BuildContext context, AsyncSnapshot<ByteData> snapshot) {
  3131. if (snapshot.hasData) {
  3132. final image = Image.asset(
  3133. 'assets/logo.png',
  3134. fit: BoxFit.contain,
  3135. errorBuilder: (ctx, error, stackTrace) {
  3136. return Container();
  3137. },
  3138. );
  3139. return Container(
  3140. constraints: BoxConstraints(maxWidth: 300, maxHeight: 60),
  3141. child: image,
  3142. ).marginOnly(left: 12, right: 12, top: 12);
  3143. }
  3144. return const Offstage();
  3145. });
  3146. }
  3147. Widget loadIcon(double size) {
  3148. return Image.asset('assets/icon.png',
  3149. width: size,
  3150. height: size,
  3151. errorBuilder: (ctx, error, stackTrace) => SvgPicture.asset(
  3152. 'assets/icon.svg',
  3153. width: size,
  3154. height: size,
  3155. ));
  3156. }
  3157. var imcomingOnlyHomeSize = Size(280, 300);
  3158. Size getIncomingOnlyHomeSize() {
  3159. final magicWidth = isWindows ? 11.0 : 2.0;
  3160. final magicHeight = 10.0;
  3161. return imcomingOnlyHomeSize +
  3162. Offset(magicWidth, kDesktopRemoteTabBarHeight + magicHeight);
  3163. }
  3164. Size getIncomingOnlySettingsSize() {
  3165. return Size(768, 600);
  3166. }
  3167. bool isInHomePage() {
  3168. final controller = Get.find<DesktopTabController>();
  3169. return controller.state.value.selected == 0;
  3170. }
  3171. Widget _buildPresetPasswordWarning() {
  3172. if (bind.mainGetBuildinOption(key: kOptionRemovePresetPasswordWarning) !=
  3173. 'N') {
  3174. return SizedBox.shrink();
  3175. }
  3176. return Container(
  3177. color: Colors.yellow,
  3178. child: Column(
  3179. children: [
  3180. Align(
  3181. child: Text(
  3182. translate("Security Alert"),
  3183. style: TextStyle(
  3184. color: Colors.red,
  3185. fontSize:
  3186. 18, // https://github.com/rustdesk/rustdesk-server-pro/issues/261
  3187. fontWeight: FontWeight.bold,
  3188. ),
  3189. )).paddingOnly(bottom: 8),
  3190. Text(
  3191. translate("preset_password_warning"),
  3192. style: TextStyle(color: Colors.red),
  3193. )
  3194. ],
  3195. ).paddingAll(8),
  3196. ); // Show a warning message if the Future completed with true
  3197. }
  3198. Widget buildPresetPasswordWarningMobile() {
  3199. if (bind.isPresetPasswordMobileOnly()) {
  3200. return _buildPresetPasswordWarning();
  3201. } else {
  3202. return SizedBox.shrink();
  3203. }
  3204. }
  3205. Widget buildPresetPasswordWarning() {
  3206. return FutureBuilder<bool>(
  3207. future: bind.isPresetPassword(),
  3208. builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
  3209. if (snapshot.connectionState == ConnectionState.waiting) {
  3210. return CircularProgressIndicator(); // Show a loading spinner while waiting for the Future to complete
  3211. } else if (snapshot.hasError) {
  3212. return Text(
  3213. 'Error: ${snapshot.error}'); // Show an error message if the Future completed with an error
  3214. } else if (snapshot.hasData && snapshot.data == true) {
  3215. return _buildPresetPasswordWarning();
  3216. } else {
  3217. return SizedBox
  3218. .shrink(); // Show nothing if the Future completed with false or null
  3219. }
  3220. },
  3221. );
  3222. }
  3223. // https://github.com/leanflutter/window_manager/blob/87dd7a50b4cb47a375b9fc697f05e56eea0a2ab3/lib/src/widgets/virtual_window_frame.dart#L44
  3224. Widget buildVirtualWindowFrame(BuildContext context, Widget child) {
  3225. boxShadow() => isMainDesktopWindow
  3226. ? <BoxShadow>[
  3227. if (stateGlobal.fullscreen.isFalse || stateGlobal.isMaximized.isFalse)
  3228. BoxShadow(
  3229. color: Colors.black.withOpacity(0.1),
  3230. offset: Offset(
  3231. 0.0,
  3232. stateGlobal.isFocused.isTrue
  3233. ? kFrameBoxShadowOffsetFocused
  3234. : kFrameBoxShadowOffsetUnfocused),
  3235. blurRadius: kFrameBoxShadowBlurRadius,
  3236. ),
  3237. ]
  3238. : null;
  3239. return Obx(
  3240. () => Container(
  3241. decoration: BoxDecoration(
  3242. color: isMainDesktopWindow
  3243. ? Colors.transparent
  3244. : Theme.of(context).colorScheme.background,
  3245. border: Border.all(
  3246. color: Theme.of(context).dividerColor,
  3247. width: stateGlobal.windowBorderWidth.value,
  3248. ),
  3249. borderRadius: BorderRadius.circular(
  3250. (stateGlobal.fullscreen.isTrue || stateGlobal.isMaximized.isTrue)
  3251. ? 0
  3252. : kFrameBorderRadius,
  3253. ),
  3254. boxShadow: boxShadow(),
  3255. ),
  3256. child: ClipRRect(
  3257. borderRadius: BorderRadius.circular(
  3258. (stateGlobal.fullscreen.isTrue || stateGlobal.isMaximized.isTrue)
  3259. ? 0
  3260. : kFrameClipRRectBorderRadius,
  3261. ),
  3262. child: child,
  3263. ),
  3264. ),
  3265. );
  3266. }
  3267. get windowResizeEdgeSize =>
  3268. isLinux && !_linuxWindowResizable ? 0.0 : kWindowResizeEdgeSize;
  3269. // `windowManager.setResizable(false)` will reset the window size to the default size on Linux and then set unresizable.
  3270. // See _linuxWindowResizable for more details.
  3271. // So we use `setResizable()` instead of `windowManager.setResizable()`.
  3272. //
  3273. // We can only call `windowManager.setResizable(false)` if we need the default size on Linux.
  3274. setResizable(bool resizable) {
  3275. if (isLinux) {
  3276. _linuxWindowResizable = resizable;
  3277. stateGlobal.refreshResizeEdgeSize();
  3278. } else {
  3279. windowManager.setResizable(resizable);
  3280. }
  3281. }
  3282. isOptionFixed(String key) => bind.mainIsOptionFixed(key: key);
  3283. bool? _isCustomClient;
  3284. bool get isCustomClient {
  3285. _isCustomClient ??= bind.isCustomClient();
  3286. return _isCustomClient!;
  3287. }
  3288. get defaultOptionLang => isCustomClient ? 'default' : '';
  3289. get defaultOptionTheme => isCustomClient ? 'system' : '';
  3290. get defaultOptionYes => isCustomClient ? 'Y' : '';
  3291. get defaultOptionNo => isCustomClient ? 'N' : '';
  3292. get defaultOptionWhitelist => isCustomClient ? ',' : '';
  3293. get defaultOptionAccessMode => isCustomClient ? 'custom' : '';
  3294. get defaultOptionApproveMode => isCustomClient ? 'password-click' : '';
  3295. bool whitelistNotEmpty() {
  3296. // https://rustdesk.com/docs/en/self-host/client-configuration/advanced-settings/#whitelist
  3297. final v = bind.mainGetOptionSync(key: kOptionWhitelist);
  3298. return v != '' && v != ',';
  3299. }
  3300. // `setMovable()` is only supported on macOS.
  3301. //
  3302. // On macOS, the window can be dragged by the tab bar by default.
  3303. // We need to disable the movable feature to prevent the window from being dragged by the tabs in the tab bar.
  3304. //
  3305. // When we drag the blank tab bar (not the tab), the window will be dragged normally by adding the `onPanStart` handle.
  3306. //
  3307. // See the following code for more details:
  3308. // https://github.com/rustdesk/rustdesk/blob/ce1dac3b8613596b4d8ae981275f9335489eb935/flutter/lib/desktop/widgets/tabbar_widget.dart#L385
  3309. // https://github.com/rustdesk/rustdesk/blob/ce1dac3b8613596b4d8ae981275f9335489eb935/flutter/lib/desktop/widgets/tabbar_widget.dart#L399
  3310. //
  3311. // @platforms macos
  3312. disableWindowMovable(int? windowId) {
  3313. if (!isMacOS) {
  3314. return;
  3315. }
  3316. if (windowId == null) {
  3317. windowManager.setMovable(false);
  3318. } else {
  3319. WindowController.fromWindowId(windowId).setMovable(false);
  3320. }
  3321. }
  3322. Widget netWorkErrorWidget() {
  3323. return Center(
  3324. child: Column(
  3325. mainAxisAlignment: MainAxisAlignment.center,
  3326. crossAxisAlignment: CrossAxisAlignment.center,
  3327. children: [
  3328. Text(translate("network_error_tip")),
  3329. ElevatedButton(
  3330. onPressed: gFFI.userModel.refreshCurrentUser,
  3331. child: Text(translate("Retry")))
  3332. .marginSymmetric(vertical: 16),
  3333. SelectableText(gFFI.userModel.networkError.value,
  3334. style: TextStyle(fontSize: 11, color: Colors.red)),
  3335. ],
  3336. ));
  3337. }
  3338. List<ResizeEdge>? get windowManagerEnableResizeEdges => isWindows
  3339. ? [
  3340. ResizeEdge.topLeft,
  3341. ResizeEdge.top,
  3342. ResizeEdge.topRight,
  3343. ]
  3344. : null;
  3345. List<SubWindowResizeEdge>? get subWindowManagerEnableResizeEdges => isWindows
  3346. ? [
  3347. SubWindowResizeEdge.topLeft,
  3348. SubWindowResizeEdge.top,
  3349. SubWindowResizeEdge.topRight,
  3350. ]
  3351. : null;
  3352. void earlyAssert() {
  3353. assert('\1' == '1');
  3354. }