common.dart 117 KB

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