common.dart 116 KB

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