12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704 |
- import 'dart:async';
- import 'dart:convert';
- import 'dart:math';
- import 'package:back_button_interceptor/back_button_interceptor.dart';
- import 'package:desktop_multi_window/desktop_multi_window.dart';
- import 'package:flutter/foundation.dart';
- import 'package:flutter/gestures.dart';
- import 'package:flutter/material.dart';
- import 'package:flutter/services.dart';
- import 'package:flutter_hbb/common/formatter/id_formatter.dart';
- import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart';
- import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
- import 'package:flutter_hbb/main.dart';
- import 'package:flutter_hbb/models/peer_model.dart';
- import 'package:flutter_hbb/models/state_model.dart';
- import 'package:flutter_hbb/utils/multi_window_manager.dart';
- import 'package:flutter_hbb/utils/platform_channel.dart';
- import 'package:flutter_svg/flutter_svg.dart';
- import 'package:get/get.dart';
- import 'package:provider/provider.dart';
- import 'package:uni_links/uni_links.dart';
- import 'package:url_launcher/url_launcher.dart';
- import 'package:uuid/uuid.dart';
- import 'package:window_manager/window_manager.dart';
- import 'package:window_size/window_size.dart' as window_size;
- import '../consts.dart';
- import 'common/widgets/overlay.dart';
- import 'mobile/pages/file_manager_page.dart';
- import 'mobile/pages/remote_page.dart';
- import 'desktop/pages/remote_page.dart' as desktop_remote;
- import 'desktop/pages/file_manager_page.dart' as desktop_file_manager;
- import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
- import 'models/model.dart';
- import 'models/platform_model.dart';
- import 'package:flutter_hbb/native/win32.dart'
- if (dart.library.html) 'package:flutter_hbb/web/win32.dart';
- import 'package:flutter_hbb/native/common.dart'
- if (dart.library.html) 'package:flutter_hbb/web/common.dart';
- final globalKey = GlobalKey<NavigatorState>();
- final navigationBarKey = GlobalKey();
- final isAndroid = isAndroid_;
- final isIOS = isIOS_;
- final isWindows = isWindows_;
- final isMacOS = isMacOS_;
- final isLinux = isLinux_;
- final isDesktop = isDesktop_;
- final isWeb = isWeb_;
- final isWebDesktop = isWebDesktop_;
- final isWebOnWindows = isWebOnWindows_;
- final isWebOnLinux = isWebOnLinux_;
- final isWebOnMacOs = isWebOnMacOS_;
- var isMobile = isAndroid || isIOS;
- var version = '';
- int androidVersion = 0;
- // Only used on Linux.
- // `windowManager.setResizable(false)` will reset the window size to the default size on Linux.
- // https://stackoverflow.com/questions/8193613/gtk-window-resize-disable-without-going-back-to-default
- // So we need to use this flag to enable/disable resizable.
- bool _linuxWindowResizable = true;
- // Only used on Windows(window manager).
- bool _ignoreDevicePixelRatio = true;
- /// only available for Windows target
- int windowsBuildNumber = 0;
- DesktopType? desktopType;
- bool get isMainDesktopWindow =>
- desktopType == DesktopType.main || desktopType == DesktopType.cm;
- String get screenInfo => screenInfo_;
- /// Check if the app is running with single view mode.
- bool isSingleViewApp() {
- return desktopType == DesktopType.cm;
- }
- /// * debug or test only, DO NOT enable in release build
- bool isTest = false;
- typedef F = String Function(String);
- typedef FMethod = String Function(String, dynamic);
- typedef StreamEventHandler = Future<void> Function(Map<String, dynamic>);
- typedef SessionID = UuidValue;
- final iconHardDrive = MemoryImage(Uint8List.fromList(base64Decode(
- '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==')));
- enum DesktopType {
- main,
- remote,
- fileTransfer,
- cm,
- portForward,
- }
- class IconFont {
- static const _family1 = 'Tabbar';
- static const _family2 = 'PeerSearchbar';
- static const _family3 = 'AddressBook';
- static const _family4 = 'DeviceGroup';
- IconFont._();
- static const IconData max = IconData(0xe606, fontFamily: _family1);
- static const IconData restore = IconData(0xe607, fontFamily: _family1);
- static const IconData close = IconData(0xe668, fontFamily: _family1);
- static const IconData min = IconData(0xe609, fontFamily: _family1);
- static const IconData add = IconData(0xe664, fontFamily: _family1);
- static const IconData menu = IconData(0xe628, fontFamily: _family1);
- static const IconData search = IconData(0xe6a4, fontFamily: _family2);
- static const IconData roundClose = IconData(0xe6ed, fontFamily: _family2);
- static const IconData addressBook = IconData(0xe602, fontFamily: _family3);
- static const IconData deviceGroupOutline =
- IconData(0xe623, fontFamily: _family4);
- static const IconData deviceGroupFill =
- IconData(0xe748, fontFamily: _family4);
- }
- class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {
- const ColorThemeExtension({
- required this.border,
- required this.border2,
- required this.border3,
- required this.highlight,
- required this.drag_indicator,
- required this.shadow,
- required this.errorBannerBg,
- required this.me,
- required this.toastBg,
- required this.toastText,
- required this.divider,
- });
- final Color? border;
- final Color? border2;
- final Color? border3;
- final Color? highlight;
- final Color? drag_indicator;
- final Color? shadow;
- final Color? errorBannerBg;
- final Color? me;
- final Color? toastBg;
- final Color? toastText;
- final Color? divider;
- static final light = ColorThemeExtension(
- border: Color(0xFFCCCCCC),
- border2: Color(0xFFBBBBBB),
- border3: Colors.black26,
- highlight: Color(0xFFE5E5E5),
- drag_indicator: Colors.grey[800],
- shadow: Colors.black,
- errorBannerBg: Color(0xFFFDEEEB),
- me: Colors.green,
- toastBg: Colors.black.withOpacity(0.6),
- toastText: Colors.white,
- divider: Colors.black38,
- );
- static final dark = ColorThemeExtension(
- border: Color(0xFF555555),
- border2: Color(0xFFE5E5E5),
- border3: Colors.white24,
- highlight: Color(0xFF3F3F3F),
- drag_indicator: Colors.grey,
- shadow: Colors.grey,
- errorBannerBg: Color(0xFF470F2D),
- me: Colors.greenAccent,
- toastBg: Colors.white.withOpacity(0.6),
- toastText: Colors.black,
- divider: Colors.white38,
- );
- @override
- ThemeExtension<ColorThemeExtension> copyWith({
- Color? border,
- Color? border2,
- Color? border3,
- Color? highlight,
- Color? drag_indicator,
- Color? shadow,
- Color? errorBannerBg,
- Color? me,
- Color? toastBg,
- Color? toastText,
- Color? divider,
- }) {
- return ColorThemeExtension(
- border: border ?? this.border,
- border2: border2 ?? this.border2,
- border3: border3 ?? this.border3,
- highlight: highlight ?? this.highlight,
- drag_indicator: drag_indicator ?? this.drag_indicator,
- shadow: shadow ?? this.shadow,
- errorBannerBg: errorBannerBg ?? this.errorBannerBg,
- me: me ?? this.me,
- toastBg: toastBg ?? this.toastBg,
- toastText: toastText ?? this.toastText,
- divider: divider ?? this.divider,
- );
- }
- @override
- ThemeExtension<ColorThemeExtension> lerp(
- ThemeExtension<ColorThemeExtension>? other, double t) {
- if (other is! ColorThemeExtension) {
- return this;
- }
- return ColorThemeExtension(
- border: Color.lerp(border, other.border, t),
- border2: Color.lerp(border2, other.border2, t),
- border3: Color.lerp(border3, other.border3, t),
- highlight: Color.lerp(highlight, other.highlight, t),
- drag_indicator: Color.lerp(drag_indicator, other.drag_indicator, t),
- shadow: Color.lerp(shadow, other.shadow, t),
- errorBannerBg: Color.lerp(shadow, other.errorBannerBg, t),
- me: Color.lerp(shadow, other.me, t),
- toastBg: Color.lerp(shadow, other.toastBg, t),
- toastText: Color.lerp(shadow, other.toastText, t),
- divider: Color.lerp(shadow, other.divider, t),
- );
- }
- }
- class MyTheme {
- MyTheme._();
- static const Color grayBg = Color(0xFFEFEFF2);
- static const Color accent = Color(0xFF0071FF);
- static const Color accent50 = Color(0x770071FF);
- static const Color accent80 = Color(0xAA0071FF);
- static const Color canvasColor = Color(0xFF212121);
- static const Color border = Color(0xFFCCCCCC);
- static const Color idColor = Color(0xFF00B6F0);
- static const Color darkGray = Color.fromARGB(255, 148, 148, 148);
- static const Color cmIdColor = Color(0xFF21790B);
- static const Color dark = Colors.black87;
- static const Color button = Color(0xFF2C8CFF);
- static const Color hoverBorder = Color(0xFF999999);
- // ListTile
- static const ListTileThemeData listTileTheme = ListTileThemeData(
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.all(
- Radius.circular(5),
- ),
- ),
- );
- static SwitchThemeData switchTheme() {
- return SwitchThemeData(
- splashRadius: (isDesktop || isWebDesktop) ? 0 : kRadialReactionRadius);
- }
- static RadioThemeData radioTheme() {
- return RadioThemeData(
- splashRadius: (isDesktop || isWebDesktop) ? 0 : kRadialReactionRadius);
- }
- // Checkbox
- static const CheckboxThemeData checkboxTheme = CheckboxThemeData(
- splashRadius: 0,
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.all(
- Radius.circular(5),
- ),
- ),
- );
- // TextButton
- // Value is used to calculate "dialog.actionsPadding"
- static const double mobileTextButtonPaddingLR = 20;
- // TextButton on mobile needs a fixed padding, otherwise small buttons
- // like "OK" has a larger left/right padding.
- static TextButtonThemeData mobileTextButtonTheme = TextButtonThemeData(
- style: TextButton.styleFrom(
- padding: EdgeInsets.symmetric(horizontal: mobileTextButtonPaddingLR),
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(8.0),
- ),
- ),
- );
- //tooltip
- static TooltipThemeData tooltipTheme() {
- return TooltipThemeData(
- waitDuration: Duration(seconds: 1, milliseconds: 500),
- );
- }
- // Dialogs
- static const double dialogPadding = 24;
- // padding bottom depends on content (some dialogs has no content)
- static EdgeInsets dialogTitlePadding({bool content = true}) {
- final double p = dialogPadding;
- return EdgeInsets.fromLTRB(p, p, p, content ? 0 : p);
- }
- // padding bottom depends on actions (mobile has dialogs without actions)
- static EdgeInsets dialogContentPadding({bool actions = true}) {
- final double p = dialogPadding;
- return (isDesktop || isWebDesktop)
- ? EdgeInsets.fromLTRB(p, p, p, actions ? (p - 4) : p)
- : EdgeInsets.fromLTRB(p, p, p, actions ? (p / 2) : p);
- }
- static EdgeInsets dialogActionsPadding() {
- final double p = dialogPadding;
- return (isDesktop || isWebDesktop)
- ? EdgeInsets.fromLTRB(p, 0, p, (p - 4))
- : EdgeInsets.fromLTRB(p, 0, (p - mobileTextButtonPaddingLR), (p / 2));
- }
- static EdgeInsets dialogButtonPadding = (isDesktop || isWebDesktop)
- ? EdgeInsets.only(left: dialogPadding)
- : EdgeInsets.only(left: dialogPadding / 3);
- static ScrollbarThemeData scrollbarTheme = ScrollbarThemeData(
- thickness: MaterialStateProperty.all(6),
- thumbColor: MaterialStateProperty.resolveWith<Color?>((states) {
- if (states.contains(MaterialState.dragged)) {
- return Colors.grey[900];
- } else if (states.contains(MaterialState.hovered)) {
- return Colors.grey[700];
- } else {
- return Colors.grey[500];
- }
- }),
- crossAxisMargin: 4,
- );
- static ScrollbarThemeData scrollbarThemeDark = scrollbarTheme.copyWith(
- thumbColor: MaterialStateProperty.resolveWith<Color?>((states) {
- if (states.contains(MaterialState.dragged)) {
- return Colors.grey[100];
- } else if (states.contains(MaterialState.hovered)) {
- return Colors.grey[300];
- } else {
- return Colors.grey[500];
- }
- }),
- );
- static ThemeData lightTheme = ThemeData(
- // https://stackoverflow.com/questions/77537315/after-upgrading-to-flutter-3-16-the-app-bar-background-color-button-size-and
- useMaterial3: false,
- brightness: Brightness.light,
- hoverColor: Color.fromARGB(255, 224, 224, 224),
- scaffoldBackgroundColor: Colors.white,
- dialogBackgroundColor: Colors.white,
- appBarTheme: AppBarTheme(
- shadowColor: Colors.transparent,
- ),
- dialogTheme: DialogTheme(
- elevation: 15,
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(18.0),
- side: BorderSide(
- width: 1,
- color: grayBg,
- ),
- ),
- ),
- scrollbarTheme: scrollbarTheme,
- inputDecorationTheme: isDesktop
- ? InputDecorationTheme(
- fillColor: grayBg,
- filled: true,
- isDense: true,
- border: OutlineInputBorder(
- borderRadius: BorderRadius.circular(8),
- ),
- )
- : null,
- textTheme: const TextTheme(
- titleLarge: TextStyle(fontSize: 19, color: Colors.black87),
- titleSmall: TextStyle(fontSize: 14, color: Colors.black87),
- bodySmall: TextStyle(fontSize: 12, color: Colors.black87, height: 1.25),
- bodyMedium:
- TextStyle(fontSize: 14, color: Colors.black87, height: 1.25),
- labelLarge: TextStyle(fontSize: 16.0, color: MyTheme.accent80)),
- cardColor: grayBg,
- hintColor: Color(0xFFAAAAAA),
- visualDensity: VisualDensity.adaptivePlatformDensity,
- tabBarTheme: const TabBarTheme(
- labelColor: Colors.black87,
- ),
- tooltipTheme: tooltipTheme(),
- splashColor: (isDesktop || isWebDesktop) ? Colors.transparent : null,
- highlightColor: (isDesktop || isWebDesktop) ? Colors.transparent : null,
- splashFactory: (isDesktop || isWebDesktop) ? NoSplash.splashFactory : null,
- textButtonTheme: (isDesktop || isWebDesktop)
- ? TextButtonThemeData(
- style: TextButton.styleFrom(
- splashFactory: NoSplash.splashFactory,
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(18.0),
- ),
- ),
- )
- : mobileTextButtonTheme,
- elevatedButtonTheme: ElevatedButtonThemeData(
- style: ElevatedButton.styleFrom(
- backgroundColor: MyTheme.accent,
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(8.0),
- ),
- ),
- ),
- outlinedButtonTheme: OutlinedButtonThemeData(
- style: OutlinedButton.styleFrom(
- backgroundColor: grayBg,
- foregroundColor: Colors.black87,
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(8.0),
- ),
- ),
- ),
- switchTheme: switchTheme(),
- radioTheme: radioTheme(),
- checkboxTheme: checkboxTheme,
- listTileTheme: listTileTheme,
- menuBarTheme: MenuBarThemeData(
- style:
- MenuStyle(backgroundColor: MaterialStatePropertyAll(Colors.white))),
- colorScheme: ColorScheme.light(
- primary: Colors.blue, secondary: accent, background: grayBg),
- popupMenuTheme: PopupMenuThemeData(
- color: Colors.white,
- shape: RoundedRectangleBorder(
- side: BorderSide(
- color: (isDesktop || isWebDesktop)
- ? Color(0xFFECECEC)
- : Colors.transparent),
- borderRadius: BorderRadius.all(Radius.circular(8.0)),
- )),
- ).copyWith(
- extensions: <ThemeExtension<dynamic>>[
- ColorThemeExtension.light,
- TabbarTheme.light,
- ],
- );
- static ThemeData darkTheme = ThemeData(
- useMaterial3: false,
- brightness: Brightness.dark,
- hoverColor: Color.fromARGB(255, 45, 46, 53),
- scaffoldBackgroundColor: Color(0xFF18191E),
- dialogBackgroundColor: Color(0xFF18191E),
- appBarTheme: AppBarTheme(
- shadowColor: Colors.transparent,
- ),
- dialogTheme: DialogTheme(
- elevation: 15,
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(18.0),
- side: BorderSide(
- width: 1,
- color: Color(0xFF24252B),
- ),
- ),
- ),
- scrollbarTheme: scrollbarThemeDark,
- inputDecorationTheme: (isDesktop || isWebDesktop)
- ? InputDecorationTheme(
- fillColor: Color(0xFF24252B),
- filled: true,
- isDense: true,
- border: OutlineInputBorder(
- borderRadius: BorderRadius.circular(8),
- ),
- )
- : null,
- textTheme: const TextTheme(
- titleLarge: TextStyle(fontSize: 19),
- titleSmall: TextStyle(fontSize: 14),
- bodySmall: TextStyle(fontSize: 12, height: 1.25),
- bodyMedium: TextStyle(fontSize: 14, height: 1.25),
- labelLarge: TextStyle(
- fontSize: 16.0,
- fontWeight: FontWeight.bold,
- color: accent80,
- ),
- ),
- cardColor: Color(0xFF24252B),
- visualDensity: VisualDensity.adaptivePlatformDensity,
- tabBarTheme: const TabBarTheme(
- labelColor: Colors.white70,
- ),
- tooltipTheme: tooltipTheme(),
- splashColor: (isDesktop || isWebDesktop) ? Colors.transparent : null,
- highlightColor: (isDesktop || isWebDesktop) ? Colors.transparent : null,
- splashFactory: (isDesktop || isWebDesktop) ? NoSplash.splashFactory : null,
- textButtonTheme: (isDesktop || isWebDesktop)
- ? TextButtonThemeData(
- style: TextButton.styleFrom(
- splashFactory: NoSplash.splashFactory,
- disabledForegroundColor: Colors.white70,
- foregroundColor: Colors.white70,
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(18.0),
- ),
- ),
- )
- : mobileTextButtonTheme,
- elevatedButtonTheme: ElevatedButtonThemeData(
- style: ElevatedButton.styleFrom(
- backgroundColor: MyTheme.accent,
- foregroundColor: Colors.white,
- disabledForegroundColor: Colors.white70,
- disabledBackgroundColor: Colors.white10,
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(8.0),
- ),
- ),
- ),
- outlinedButtonTheme: OutlinedButtonThemeData(
- style: OutlinedButton.styleFrom(
- backgroundColor: Color(0xFF24252B),
- side: BorderSide(color: Colors.white12, width: 0.5),
- disabledForegroundColor: Colors.white70,
- foregroundColor: Colors.white70,
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(8.0),
- ),
- ),
- ),
- switchTheme: switchTheme(),
- radioTheme: radioTheme(),
- checkboxTheme: checkboxTheme,
- listTileTheme: listTileTheme,
- menuBarTheme: MenuBarThemeData(
- style: MenuStyle(
- backgroundColor: MaterialStatePropertyAll(Color(0xFF121212)))),
- colorScheme: ColorScheme.dark(
- primary: Colors.blue,
- secondary: accent,
- background: Color(0xFF24252B),
- ),
- popupMenuTheme: PopupMenuThemeData(
- shape: RoundedRectangleBorder(
- side: BorderSide(color: Colors.white24),
- borderRadius: BorderRadius.all(Radius.circular(8.0)),
- )),
- ).copyWith(
- extensions: <ThemeExtension<dynamic>>[
- ColorThemeExtension.dark,
- TabbarTheme.dark,
- ],
- );
- static ThemeMode getThemeModePreference() {
- return themeModeFromString(bind.mainGetLocalOption(key: kCommConfKeyTheme));
- }
- static Future<void> changeDarkMode(ThemeMode mode) async {
- Get.changeThemeMode(mode);
- if (desktopType == DesktopType.main || isAndroid || isIOS || isWeb) {
- if (mode == ThemeMode.system) {
- await bind.mainSetLocalOption(
- key: kCommConfKeyTheme, value: defaultOptionTheme);
- } else {
- await bind.mainSetLocalOption(
- key: kCommConfKeyTheme, value: mode.toShortString());
- }
- if (!isWeb) await bind.mainChangeTheme(dark: mode.toShortString());
- // Synchronize the window theme of the system.
- updateSystemWindowTheme();
- }
- }
- static ThemeMode currentThemeMode() {
- final preference = getThemeModePreference();
- if (preference == ThemeMode.system) {
- if (WidgetsBinding.instance.platformDispatcher.platformBrightness ==
- Brightness.light) {
- return ThemeMode.light;
- } else {
- return ThemeMode.dark;
- }
- } else {
- return preference;
- }
- }
- static ColorThemeExtension color(BuildContext context) {
- return Theme.of(context).extension<ColorThemeExtension>()!;
- }
- static TabbarTheme tabbar(BuildContext context) {
- return Theme.of(context).extension<TabbarTheme>()!;
- }
- static ThemeMode themeModeFromString(String v) {
- switch (v) {
- case "light":
- return ThemeMode.light;
- case "dark":
- return ThemeMode.dark;
- default:
- return ThemeMode.system;
- }
- }
- }
- extension ParseToString on ThemeMode {
- String toShortString() {
- return toString().split('.').last;
- }
- }
- final ButtonStyle flatButtonStyle = TextButton.styleFrom(
- minimumSize: Size(0, 36),
- padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 10.0),
- shape: const RoundedRectangleBorder(
- borderRadius: BorderRadius.all(Radius.circular(2.0)),
- ),
- );
- List<Locale> supportedLocales = const [
- Locale('en', 'US'),
- Locale('zh', 'CN'),
- Locale('zh', 'TW'),
- Locale('zh', 'SG'),
- Locale('fr'),
- Locale('de'),
- Locale('it'),
- Locale('ja'),
- Locale('cs'),
- Locale('pl'),
- Locale('ko'),
- Locale('hu'),
- Locale('pt'),
- Locale('ru'),
- Locale('sk'),
- Locale('id'),
- Locale('da'),
- Locale('eo'),
- Locale('tr'),
- Locale('kz'),
- Locale('es'),
- Locale('nl'),
- Locale('nb'),
- Locale('et'),
- Locale('eu'),
- Locale('bg'),
- Locale('be'),
- Locale('vn'),
- Locale('uk'),
- Locale('fa'),
- Locale('ca'),
- Locale('el'),
- Locale('sv'),
- Locale('sq'),
- Locale('sr'),
- Locale('th'),
- Locale('sl'),
- Locale('ro'),
- Locale('lt'),
- Locale('lv'),
- Locale('ar'),
- Locale('he'),
- Locale('hr'),
- ];
- String formatDurationToTime(Duration duration) {
- var totalTime = duration.inSeconds;
- final secs = totalTime % 60;
- totalTime = (totalTime - secs) ~/ 60;
- final mins = totalTime % 60;
- totalTime = (totalTime - mins) ~/ 60;
- return "${totalTime.toString().padLeft(2, "0")}:${mins.toString().padLeft(2, "0")}:${secs.toString().padLeft(2, "0")}";
- }
- closeConnection({String? id}) {
- if (isAndroid || isIOS) {
- () async {
- await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
- overlays: SystemUiOverlay.values);
- gFFI.chatModel.hideChatOverlay();
- Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/"));
- stateGlobal.isInMainPage = true;
- }();
- } else {
- if (isWeb) {
- Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/"));
- stateGlobal.isInMainPage = true;
- } else {
- final controller = Get.find<DesktopTabController>();
- controller.closeBy(id);
- }
- }
- }
- Future<void> windowOnTop(int? id) async {
- if (!isDesktop) {
- return;
- }
- print("Bring window '$id' on top");
- if (id == null) {
- // main window
- if (stateGlobal.isMinimized) {
- await windowManager.restore();
- }
- await windowManager.show();
- await windowManager.focus();
- await rustDeskWinManager.registerActiveWindow(kWindowMainId);
- } else {
- WindowController.fromWindowId(id)
- ..focus()
- ..show();
- rustDeskWinManager.call(WindowType.Main, kWindowEventShow, {"id": id});
- }
- }
- typedef DialogBuilder = CustomAlertDialog Function(
- StateSetter setState, void Function([dynamic]) close, BuildContext context);
- class Dialog<T> {
- OverlayEntry? entry;
- Completer<T?> completer = Completer<T?>();
- Dialog();
- void complete(T? res) {
- try {
- if (!completer.isCompleted) {
- completer.complete(res);
- }
- } catch (e) {
- debugPrint("Dialog complete catch error: $e");
- } finally {
- entry?.remove();
- }
- }
- }
- class OverlayKeyState {
- final _overlayKey = GlobalKey<OverlayState>();
- /// use global overlay by default
- OverlayState? get state =>
- _overlayKey.currentState ?? globalKey.currentState?.overlay;
- GlobalKey<OverlayState>? get key => _overlayKey;
- }
- class OverlayDialogManager {
- final Map<String, Dialog> _dialogs = {};
- var _overlayKeyState = OverlayKeyState();
- int _tagCount = 0;
- OverlayEntry? _mobileActionsOverlayEntry;
- RxBool mobileActionsOverlayVisible = true.obs;
- setMobileActionsOverlayVisible(bool v, {store = true}) {
- if (store) {
- bind.setLocalFlutterOption(k: kOptionShowMobileAction, v: v ? 'Y' : 'N');
- }
- // No need to read the value from local storage after setting it.
- // It better to toggle the value directly.
- mobileActionsOverlayVisible.value = v;
- }
- loadMobileActionsOverlayVisible() {
- mobileActionsOverlayVisible.value =
- bind.getLocalFlutterOption(k: kOptionShowMobileAction) != 'N';
- }
- void setOverlayState(OverlayKeyState overlayKeyState) {
- _overlayKeyState = overlayKeyState;
- }
- void dismissAll() {
- _dialogs.forEach((key, value) {
- value.complete(null);
- BackButtonInterceptor.removeByName(key);
- });
- _dialogs.clear();
- }
- void dismissByTag(String tag) {
- _dialogs[tag]?.complete(null);
- _dialogs.remove(tag);
- BackButtonInterceptor.removeByName(tag);
- }
- Future<T?> show<T>(DialogBuilder builder,
- {bool clickMaskDismiss = false,
- bool backDismiss = false,
- String? tag,
- bool useAnimation = true,
- bool forceGlobal = false}) {
- final overlayState =
- forceGlobal ? globalKey.currentState?.overlay : _overlayKeyState.state;
- if (overlayState == null) {
- return Future.error(
- "[OverlayDialogManager] Failed to show dialog, _overlayState is null, call [setOverlayState] first");
- }
- final String dialogTag;
- if (tag != null) {
- dialogTag = tag;
- } else {
- dialogTag = _tagCount.toString();
- _tagCount++;
- }
- final dialog = Dialog<T>();
- _dialogs[dialogTag] = dialog;
- close([res]) {
- _dialogs.remove(dialogTag);
- dialog.complete(res);
- BackButtonInterceptor.removeByName(dialogTag);
- }
- dialog.entry = OverlayEntry(builder: (context) {
- bool innerClicked = false;
- return Listener(
- onPointerUp: (_) {
- if (!innerClicked && clickMaskDismiss) {
- close();
- }
- innerClicked = false;
- },
- child: Container(
- color: Theme.of(context).brightness == Brightness.light
- ? Colors.black12
- : Colors.black45,
- child: StatefulBuilder(builder: (context, setState) {
- return Listener(
- onPointerUp: (_) => innerClicked = true,
- child: builder(setState, close, overlayState.context),
- );
- })));
- });
- overlayState.insert(dialog.entry!);
- BackButtonInterceptor.add((stopDefaultButtonEvent, routeInfo) {
- if (backDismiss) {
- close();
- }
- return true;
- }, name: dialogTag);
- return dialog.completer.future;
- }
- String showLoading(String text,
- {bool clickMaskDismiss = false,
- bool showCancel = true,
- VoidCallback? onCancel,
- String? tag}) {
- if (tag == null) {
- tag = _tagCount.toString();
- _tagCount++;
- }
- show((setState, close, context) {
- cancel() {
- dismissAll();
- if (onCancel != null) {
- onCancel();
- }
- }
- return CustomAlertDialog(
- content: Container(
- constraints: const BoxConstraints(maxWidth: 240),
- child: Column(
- mainAxisSize: MainAxisSize.min,
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- const SizedBox(height: 30),
- const Center(child: CircularProgressIndicator()),
- const SizedBox(height: 20),
- Center(
- child: Text(translate(text),
- style: const TextStyle(fontSize: 15))),
- const SizedBox(height: 20),
- Offstage(
- offstage: !showCancel,
- child: Center(
- child: (isDesktop || isWebDesktop)
- ? dialogButton('Cancel', onPressed: cancel)
- : TextButton(
- style: flatButtonStyle,
- onPressed: cancel,
- child: Text(translate('Cancel'),
- style: const TextStyle(
- color: MyTheme.accent)))))
- ])),
- onCancel: showCancel ? cancel : null,
- );
- }, tag: tag);
- return tag;
- }
- void resetMobileActionsOverlay({FFI? ffi}) {
- if (_mobileActionsOverlayEntry == null) return;
- hideMobileActionsOverlay();
- showMobileActionsOverlay(ffi: ffi);
- }
- void showMobileActionsOverlay({FFI? ffi}) {
- if (_mobileActionsOverlayEntry != null) return;
- final overlayState = _overlayKeyState.state;
- if (overlayState == null) return;
- final overlay = makeMobileActionsOverlayEntry(
- () => hideMobileActionsOverlay(),
- ffi: ffi,
- );
- overlayState.insert(overlay);
- _mobileActionsOverlayEntry = overlay;
- setMobileActionsOverlayVisible(true);
- }
- void hideMobileActionsOverlay({store = true}) {
- if (_mobileActionsOverlayEntry != null) {
- _mobileActionsOverlayEntry!.remove();
- _mobileActionsOverlayEntry = null;
- setMobileActionsOverlayVisible(false, store: store);
- return;
- }
- }
- void toggleMobileActionsOverlay({FFI? ffi}) {
- if (_mobileActionsOverlayEntry == null) {
- showMobileActionsOverlay(ffi: ffi);
- } else {
- hideMobileActionsOverlay();
- }
- }
- bool existing(String tag) {
- return _dialogs.keys.contains(tag);
- }
- }
- makeMobileActionsOverlayEntry(VoidCallback? onHide, {FFI? ffi}) {
- makeMobileActions(BuildContext context, double s) {
- final scale = s < 0.85 ? 0.85 : s;
- final session = ffi ?? gFFI;
- const double overlayW = 200;
- const double overlayH = 45;
- computeOverlayPosition() {
- final screenW = MediaQuery.of(context).size.width;
- final screenH = MediaQuery.of(context).size.height;
- final left = (screenW - overlayW * scale) / 2;
- final top = screenH - (overlayH + 80) * scale;
- return Offset(left, top);
- }
- if (draggablePositions.mobileActions.isInvalid()) {
- draggablePositions.mobileActions.update(computeOverlayPosition());
- } else {
- draggablePositions.mobileActions.tryAdjust(overlayW, overlayH, scale);
- }
- return DraggableMobileActions(
- scale: scale,
- position: draggablePositions.mobileActions,
- width: overlayW,
- height: overlayH,
- onBackPressed: session.inputModel.onMobileBack,
- onHomePressed: session.inputModel.onMobileHome,
- onRecentPressed: session.inputModel.onMobileApps,
- onHidePressed: onHide,
- );
- }
- return OverlayEntry(builder: (context) {
- if (isDesktop) {
- final c = Provider.of<CanvasModel>(context);
- return makeMobileActions(context, c.scale * 2.0);
- } else {
- return makeMobileActions(globalKey.currentContext!, 1.0);
- }
- });
- }
- void showToast(String text, {Duration timeout = const Duration(seconds: 3)}) {
- final overlayState = globalKey.currentState?.overlay;
- if (overlayState == null) return;
- final entry = OverlayEntry(builder: (context) {
- return IgnorePointer(
- child: Align(
- alignment: const Alignment(0.0, 0.8),
- child: Container(
- decoration: BoxDecoration(
- color: MyTheme.color(context).toastBg,
- borderRadius: const BorderRadius.all(
- Radius.circular(20),
- ),
- ),
- padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 5),
- child: Text(
- text,
- textAlign: TextAlign.center,
- style: TextStyle(
- decoration: TextDecoration.none,
- fontWeight: FontWeight.w300,
- fontSize: 18,
- color: MyTheme.color(context).toastText),
- ),
- )));
- });
- overlayState.insert(entry);
- Future.delayed(timeout, () {
- entry.remove();
- });
- }
- // TODO
- // - Remove argument "contentPadding", no need for it, all should look the same.
- // - Remove "required" for argument "content". See simple confirm dialog "delete peer", only title and actions are used. No need to "content: SizedBox.shrink()".
- // - Make dead code alive, transform arguments "onSubmit" and "onCancel" into correspondenting buttons "ConfirmOkButton", "CancelButton".
- class CustomAlertDialog extends StatelessWidget {
- const CustomAlertDialog(
- {Key? key,
- this.title,
- this.titlePadding,
- required this.content,
- this.actions,
- this.contentPadding,
- this.contentBoxConstraints = const BoxConstraints(maxWidth: 500),
- this.onSubmit,
- this.onCancel})
- : super(key: key);
- final Widget? title;
- final EdgeInsetsGeometry? titlePadding;
- final Widget content;
- final List<Widget>? actions;
- final double? contentPadding;
- final BoxConstraints contentBoxConstraints;
- final Function()? onSubmit;
- final Function()? onCancel;
- @override
- Widget build(BuildContext context) {
- // request focus
- FocusScopeNode scopeNode = FocusScopeNode();
- Future.delayed(Duration.zero, () {
- if (!scopeNode.hasFocus) scopeNode.requestFocus();
- });
- bool tabTapped = false;
- if (isAndroid) gFFI.invokeMethod("enable_soft_keyboard", true);
- return FocusScope(
- node: scopeNode,
- autofocus: true,
- onKey: (node, key) {
- if (key.logicalKey == LogicalKeyboardKey.escape) {
- if (key is RawKeyDownEvent) {
- onCancel?.call();
- }
- return KeyEventResult.handled; // avoid TextField exception on escape
- } else if (!tabTapped &&
- onSubmit != null &&
- (key.logicalKey == LogicalKeyboardKey.enter ||
- key.logicalKey == LogicalKeyboardKey.numpadEnter)) {
- if (key is RawKeyDownEvent) onSubmit?.call();
- return KeyEventResult.handled;
- } else if (key.logicalKey == LogicalKeyboardKey.tab) {
- if (key is RawKeyDownEvent) {
- scopeNode.nextFocus();
- tabTapped = true;
- }
- return KeyEventResult.handled;
- }
- return KeyEventResult.ignored;
- },
- child: AlertDialog(
- scrollable: true,
- title: title,
- content: ConstrainedBox(
- constraints: contentBoxConstraints,
- child: content,
- ),
- actions: actions,
- titlePadding: titlePadding ?? MyTheme.dialogTitlePadding(),
- contentPadding:
- MyTheme.dialogContentPadding(actions: actions is List),
- actionsPadding: MyTheme.dialogActionsPadding(),
- buttonPadding: MyTheme.dialogButtonPadding),
- );
- }
- }
- Widget createDialogContent(String text) {
- final RegExp linkRegExp = RegExp(r'(https?://[^\s]+)');
- final List<TextSpan> spans = [];
- int start = 0;
- bool hasLink = false;
- linkRegExp.allMatches(text).forEach((match) {
- hasLink = true;
- if (match.start > start) {
- spans.add(TextSpan(text: text.substring(start, match.start)));
- }
- spans.add(TextSpan(
- text: match.group(0) ?? '',
- style: TextStyle(
- color: Colors.blue,
- decoration: TextDecoration.underline,
- ),
- recognizer: TapGestureRecognizer()
- ..onTap = () {
- String linkText = match.group(0) ?? '';
- linkText = linkText.replaceAll(RegExp(r'[.,;!?]+$'), '');
- launchUrl(Uri.parse(linkText));
- },
- ));
- start = match.end;
- });
- if (start < text.length) {
- spans.add(TextSpan(text: text.substring(start)));
- }
- if (!hasLink) {
- return SelectableText(text, style: const TextStyle(fontSize: 15));
- }
- return SelectableText.rich(
- TextSpan(
- style: TextStyle(color: Colors.black, fontSize: 15),
- children: spans,
- ),
- );
- }
- void msgBox(SessionID sessionId, String type, String title, String text,
- String link, OverlayDialogManager dialogManager,
- {bool? hasCancel, ReconnectHandle? reconnect, int? reconnectTimeout}) {
- dialogManager.dismissAll();
- List<Widget> buttons = [];
- bool hasOk = false;
- submit() {
- dialogManager.dismissAll();
- // https://github.com/rustdesk/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263
- if (!type.contains("custom") && desktopType != DesktopType.portForward) {
- closeConnection();
- }
- }
- cancel() {
- dialogManager.dismissAll();
- }
- jumplink() {
- if (link.startsWith('http')) {
- launchUrl(Uri.parse(link));
- }
- }
- if (type != "connecting" && type != "success" && !type.contains("nook")) {
- hasOk = true;
- buttons.insert(0, dialogButton('OK', onPressed: submit));
- }
- hasCancel ??= !type.contains("error") &&
- !type.contains("nocancel") &&
- type != "restarting";
- if (hasCancel) {
- buttons.insert(
- 0, dialogButton('Cancel', onPressed: cancel, isOutline: true));
- }
- if (type.contains("hasclose")) {
- buttons.insert(
- 0,
- dialogButton('Close', onPressed: () {
- dialogManager.dismissAll();
- }));
- }
- if (reconnect != null &&
- title == "Connection Error" &&
- reconnectTimeout != null) {
- // `enabled` is used to disable the dialog button once the button is clicked.
- final enabled = true.obs;
- final button = Obx(() => _ReconnectCountDownButton(
- second: reconnectTimeout,
- onPressed: enabled.isTrue
- ? () {
- // Disable the button
- enabled.value = false;
- reconnect(dialogManager, sessionId, false);
- }
- : null,
- ));
- buttons.insert(0, button);
- }
- if (link.isNotEmpty) {
- buttons.insert(0, dialogButton('JumpLink', onPressed: jumplink));
- }
- dialogManager.show(
- (setState, close, context) => CustomAlertDialog(
- title: null,
- content: SelectionArea(child: msgboxContent(type, title, text)),
- actions: buttons,
- onSubmit: hasOk ? submit : null,
- onCancel: hasCancel == true ? cancel : null,
- ),
- tag: '$sessionId-$type-$title-$text-$link',
- );
- }
- Color? _msgboxColor(String type) {
- if (type == "input-password" || type == "custom-os-password") {
- return Color(0xFFAD448E);
- }
- if (type.contains("success")) {
- return Color(0xFF32bea6);
- }
- if (type.contains("error") || type == "re-input-password") {
- return Color(0xFFE04F5F);
- }
- return Color(0xFF2C8CFF);
- }
- Widget msgboxIcon(String type) {
- IconData? iconData;
- if (type.contains("error") || type == "re-input-password") {
- iconData = Icons.cancel;
- }
- if (type.contains("success")) {
- iconData = Icons.check_circle;
- }
- if (type == "wait-uac" || type == "wait-remote-accept-nook") {
- iconData = Icons.hourglass_top;
- }
- if (type == 'on-uac' || type == 'on-foreground-elevated') {
- iconData = Icons.admin_panel_settings;
- }
- if (type.contains('info')) {
- iconData = Icons.info;
- }
- if (iconData != null) {
- return Icon(iconData, size: 50, color: _msgboxColor(type))
- .marginOnly(right: 16);
- }
- return Offstage();
- }
- // title should be null
- Widget msgboxContent(String type, String title, String text) {
- String translateText(String text) {
- if (text.indexOf('Failed') == 0 && text.indexOf(': ') > 0) {
- List<String> words = text.split(': ');
- for (var i = 0; i < words.length; ++i) {
- words[i] = translate(words[i]);
- }
- text = words.join(': ');
- } else {
- List<String> words = text.split(' ');
- if (words.length > 1 && words[0].endsWith('_tip')) {
- words[0] = translate(words[0]);
- final rest = text.substring(words[0].length + 1);
- text = '${words[0]} ${translate(rest)}';
- } else {
- text = translate(text);
- }
- }
- return text;
- }
- return Row(
- children: [
- msgboxIcon(type),
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- translate(title),
- style: TextStyle(fontSize: 21),
- ).marginOnly(bottom: 10),
- createDialogContent(translateText(text)),
- ],
- ),
- ),
- ],
- ).marginOnly(bottom: 12);
- }
- void msgBoxCommon(OverlayDialogManager dialogManager, String title,
- Widget content, List<Widget> buttons,
- {bool hasCancel = true}) {
- dialogManager.show((setState, close, context) => CustomAlertDialog(
- title: Text(
- translate(title),
- style: TextStyle(fontSize: 21),
- ),
- content: content,
- actions: buttons,
- onCancel: hasCancel ? close : null,
- ));
- }
- Color str2color(String str, [alpha = 0xFF]) {
- var hash = 160 << 16 + 114 << 8 + 91;
- for (var i = 0; i < str.length; i += 1) {
- hash = str.codeUnitAt(i) + ((hash << 5) - hash);
- }
- hash = hash % 16777216;
- return Color((hash & 0xFF7FFF) | (alpha << 24));
- }
- Color str2color2(String str, {List<int> existing = const []}) {
- Map<String, Color> colorMap = {
- "red": Colors.red,
- "green": Colors.green,
- "blue": Colors.blue,
- "orange": Colors.orange,
- "purple": Colors.purple,
- "grey": Colors.grey,
- "cyan": Colors.cyan,
- "lime": Colors.lime,
- "teal": Colors.teal,
- "pink": Colors.pink[200]!,
- "indigo": Colors.indigo,
- "brown": Colors.brown,
- };
- final color = colorMap[str.toLowerCase()];
- if (color != null) {
- return color.withAlpha(0xFF);
- }
- if (str.toLowerCase() == 'yellow') {
- return Colors.yellow.withAlpha(0xFF);
- }
- var hash = 0;
- for (var i = 0; i < str.length; i++) {
- hash += str.codeUnitAt(i);
- }
- List<Color> colorList = colorMap.values.toList();
- hash = hash % colorList.length;
- var result = colorList[hash].withAlpha(0xFF);
- if (existing.contains(result.value)) {
- Color? notUsed =
- colorList.firstWhereOrNull((e) => !existing.contains(e.value));
- if (notUsed != null) {
- result = notUsed;
- }
- }
- return result;
- }
- const K = 1024;
- const M = K * K;
- const G = M * K;
- String readableFileSize(double size) {
- if (size < K) {
- return "${size.toStringAsFixed(2)} B";
- } else if (size < M) {
- return "${(size / K).toStringAsFixed(2)} KB";
- } else if (size < G) {
- return "${(size / M).toStringAsFixed(2)} MB";
- } else {
- return "${(size / G).toStringAsFixed(2)} GB";
- }
- }
- /// Flutter can't not catch PointerMoveEvent when size is 1
- /// This will happen in Android AccessibilityService Input
- /// android can't init dispatching size yet ,see: https://stackoverflow.com/questions/59960451/android-accessibility-dispatchgesture-is-it-possible-to-specify-pressure-for-a
- /// use this temporary solution until flutter or android fixes the bug
- class AccessibilityListener extends StatelessWidget {
- final Widget? child;
- static final offset = 100;
- AccessibilityListener({this.child});
- @override
- Widget build(BuildContext context) {
- return Listener(
- onPointerDown: (evt) {
- if (evt.size == 1) {
- GestureBinding.instance.handlePointerEvent(PointerAddedEvent(
- pointer: evt.pointer + offset, position: evt.position));
- GestureBinding.instance.handlePointerEvent(PointerDownEvent(
- pointer: evt.pointer + offset,
- size: 0.1,
- position: evt.position));
- }
- },
- onPointerUp: (evt) {
- if (evt.size == 1) {
- GestureBinding.instance.handlePointerEvent(PointerUpEvent(
- pointer: evt.pointer + offset,
- size: 0.1,
- position: evt.position));
- GestureBinding.instance.handlePointerEvent(PointerRemovedEvent(
- pointer: evt.pointer + offset, position: evt.position));
- }
- },
- onPointerMove: (evt) {
- if (evt.size == 1) {
- GestureBinding.instance.handlePointerEvent(PointerMoveEvent(
- pointer: evt.pointer + offset,
- size: 0.1,
- delta: evt.delta,
- position: evt.position));
- }
- },
- child: child);
- }
- }
- class AndroidPermissionManager {
- static Completer<bool>? _completer;
- static Timer? _timer;
- static var _current = "";
- static bool isWaitingFile() {
- if (_completer != null) {
- return !_completer!.isCompleted && _current == kManageExternalStorage;
- }
- return false;
- }
- static Future<bool> check(String type) {
- if (isDesktop || isWeb) {
- return Future.value(true);
- }
- return gFFI.invokeMethod("check_permission", type);
- }
- // startActivity goto Android Setting's page to request permission manually by user
- static void startAction(String action) {
- gFFI.invokeMethod(AndroidChannel.kStartAction, action);
- }
- /// We use XXPermissions to request permissions,
- /// for supported types, see https://github.com/getActivity/XXPermissions/blob/e46caea32a64ad7819df62d448fb1c825481cd28/library/src/main/java/com/hjq/permissions/Permission.java
- static Future<bool> request(String type) {
- if (isDesktop || isWeb) {
- return Future.value(true);
- }
- gFFI.invokeMethod("request_permission", type);
- // clear last task
- if (_completer?.isCompleted == false) {
- _completer?.complete(false);
- }
- _timer?.cancel();
- _current = type;
- _completer = Completer<bool>();
- _timer = Timer(Duration(seconds: 120), () {
- if (_completer == null) return;
- if (!_completer!.isCompleted) {
- _completer!.complete(false);
- }
- _completer = null;
- _current = "";
- });
- return _completer!.future;
- }
- static complete(String type, bool res) {
- if (type != _current) {
- res = false;
- }
- _timer?.cancel();
- _completer?.complete(res);
- _current = "";
- }
- }
- RadioListTile<T> getRadio<T>(
- Widget title, T toValue, T curValue, ValueChanged<T?>? onChange,
- {bool? dense}) {
- return RadioListTile<T>(
- visualDensity: VisualDensity.compact,
- controlAffinity: ListTileControlAffinity.trailing,
- title: title,
- value: toValue,
- groupValue: curValue,
- onChanged: onChange,
- dense: dense,
- );
- }
- /// find ffi, tag is Remote ID
- /// for session specific usage
- FFI ffi(String? tag) {
- return Get.find<FFI>(tag: tag);
- }
- /// Global FFI object
- late FFI _globalFFI;
- FFI get gFFI => _globalFFI;
- Future<void> initGlobalFFI() async {
- debugPrint("_globalFFI init");
- _globalFFI = FFI(null);
- debugPrint("_globalFFI init end");
- // after `put`, can also be globally found by Get.find<FFI>();
- Get.put<FFI>(_globalFFI, permanent: true);
- }
- String translate(String name) {
- if (name.startsWith('Failed to') && name.contains(': ')) {
- return name.split(': ').map((x) => translate(x)).join(': ');
- }
- return platformFFI.translate(name, localeName);
- }
- // This function must be kept the same as the one in rust and sciter code.
- // rust: libs/hbb_common/src/config.rs -> option2bool()
- // sciter: Does not have the function, but it should be kept the same.
- bool option2bool(String option, String value) {
- bool res;
- if (option.startsWith("enable-")) {
- res = value != "N";
- } else if (option.startsWith("allow-") ||
- option == kOptionStopService ||
- option == kOptionDirectServer ||
- option == kOptionForceAlwaysRelay) {
- res = value == "Y";
- } else {
- assert(false);
- res = value != "N";
- }
- return res;
- }
- String bool2option(String option, bool b) {
- String res;
- if (option.startsWith('enable-')) {
- res = b ? defaultOptionYes : 'N';
- } else if (option.startsWith('allow-') ||
- option == kOptionStopService ||
- option == kOptionDirectServer ||
- option == kOptionForceAlwaysRelay) {
- res = b ? 'Y' : defaultOptionNo;
- } else {
- assert(false);
- res = b ? 'Y' : 'N';
- }
- return res;
- }
- mainSetBoolOption(String key, bool value) async {
- String v = bool2option(key, value);
- await bind.mainSetOption(key: key, value: v);
- }
- Future<bool> mainGetBoolOption(String key) async {
- return option2bool(key, await bind.mainGetOption(key: key));
- }
- bool mainGetBoolOptionSync(String key) {
- return option2bool(key, bind.mainGetOptionSync(key: key));
- }
- mainSetLocalBoolOption(String key, bool value) async {
- String v = bool2option(key, value);
- await bind.mainSetLocalOption(key: key, value: v);
- }
- bool mainGetLocalBoolOptionSync(String key) {
- return option2bool(key, bind.mainGetLocalOption(key: key));
- }
- bool mainGetPeerBoolOptionSync(String id, String key) {
- return option2bool(key, bind.mainGetPeerOptionSync(id: id, key: key));
- }
- // Don't use `option2bool()` and `bool2option()` to convert the session option.
- // Use `sessionGetToggleOption()` and `sessionToggleOption()` instead.
- // Because all session options use `Y` and `<Empty>` as values.
- Future<bool> matchPeer(String searchText, Peer peer) async {
- if (searchText.isEmpty) {
- return true;
- }
- if (peer.id.toLowerCase().contains(searchText)) {
- return true;
- }
- if (peer.hostname.toLowerCase().contains(searchText) ||
- peer.username.toLowerCase().contains(searchText)) {
- return true;
- }
- final alias = peer.alias;
- if (alias.isEmpty) {
- return false;
- }
- return alias.toLowerCase().contains(searchText);
- }
- /// Get the image for the current [platform].
- Widget getPlatformImage(String platform, {double size = 50}) {
- if (platform.isEmpty) {
- return Container(width: size, height: size);
- }
- if (platform == kPeerPlatformMacOS) {
- platform = 'mac';
- } else if (platform != kPeerPlatformLinux &&
- platform != kPeerPlatformAndroid) {
- platform = 'win';
- } else {
- platform = platform.toLowerCase();
- }
- return SvgPicture.asset('assets/$platform.svg', height: size, width: size);
- }
- class LastWindowPosition {
- double? width;
- double? height;
- double? offsetWidth;
- double? offsetHeight;
- bool? isMaximized;
- bool? isFullscreen;
- LastWindowPosition(this.width, this.height, this.offsetWidth,
- this.offsetHeight, this.isMaximized, this.isFullscreen);
- Map<String, dynamic> toJson() {
- return <String, dynamic>{
- "width": width,
- "height": height,
- "offsetWidth": offsetWidth,
- "offsetHeight": offsetHeight,
- "isMaximized": isMaximized,
- "isFullscreen": isFullscreen,
- };
- }
- @override
- String toString() {
- return jsonEncode(toJson());
- }
- static LastWindowPosition? loadFromString(String content) {
- if (content.isEmpty) {
- return null;
- }
- try {
- final m = jsonDecode(content);
- return LastWindowPosition(m["width"], m["height"], m["offsetWidth"],
- m["offsetHeight"], m["isMaximized"], m["isFullscreen"]);
- } catch (e) {
- debugPrintStack(
- label:
- 'Failed to load LastWindowPosition "$content" ${e.toString()}');
- return null;
- }
- }
- }
- String get windowFramePrefix =>
- kWindowPrefix +
- (bind.isIncomingOnly()
- ? "incoming_"
- : (bind.isOutgoingOnly() ? "outgoing_" : ""));
- /// Save window position and size on exit
- /// Note that windowId must be provided if it's subwindow
- Future<void> saveWindowPosition(WindowType type, {int? windowId}) async {
- if (type != WindowType.Main && windowId == null) {
- debugPrint(
- "Error: windowId cannot be null when saving positions for sub window");
- }
- late Offset position;
- late Size sz;
- late bool isMaximized;
- bool isFullscreen = stateGlobal.fullscreen.isTrue;
- setPreFrame() {
- final pos = bind.getLocalFlutterOption(k: windowFramePrefix + type.name);
- var lpos = LastWindowPosition.loadFromString(pos);
- position = Offset(
- lpos?.offsetWidth ?? position.dx, lpos?.offsetHeight ?? position.dy);
- sz = Size(lpos?.width ?? sz.width, lpos?.height ?? sz.height);
- }
- switch (type) {
- case WindowType.Main:
- // Checking `bind.isIncomingOnly()` is a simple workaround for MacOS.
- // `await windowManager.isMaximized()` will always return true
- // if is not resizable. The reason is unknown.
- //
- // `setResizable(!bind.isIncomingOnly());` in main.dart
- isMaximized =
- bind.isIncomingOnly() ? false : await windowManager.isMaximized();
- if (isFullscreen || isMaximized) {
- setPreFrame();
- } else {
- position = await windowManager.getPosition(
- ignoreDevicePixelRatio: _ignoreDevicePixelRatio);
- sz = await windowManager.getSize(
- ignoreDevicePixelRatio: _ignoreDevicePixelRatio);
- }
- break;
- default:
- final wc = WindowController.fromWindowId(windowId!);
- isMaximized = await wc.isMaximized();
- if (isFullscreen || isMaximized) {
- setPreFrame();
- } else {
- final Rect frame;
- try {
- frame = await wc.getFrame();
- } catch (e) {
- debugPrint(
- "Failed to get frame of window $windowId, it may be hidden");
- return;
- }
- position = frame.topLeft;
- sz = frame.size;
- }
- break;
- }
- if (isWindows) {
- const kMinOffset = -10000;
- const kMaxOffset = 10000;
- if (position.dx < kMinOffset ||
- position.dy < kMinOffset ||
- position.dx > kMaxOffset ||
- position.dy > kMaxOffset) {
- debugPrint("Invalid position: $position, ignore saving position");
- return;
- }
- }
- final pos = LastWindowPosition(
- sz.width, sz.height, position.dx, position.dy, isMaximized, isFullscreen);
- debugPrint(
- "Saving frame: $windowId: ${pos.width}/${pos.height}, offset:${pos.offsetWidth}/${pos.offsetHeight}, isMaximized:${pos.isMaximized}, isFullscreen:${pos.isFullscreen}");
- await bind.setLocalFlutterOption(
- k: windowFramePrefix + type.name, v: pos.toString());
- if (type == WindowType.RemoteDesktop && windowId != null) {
- await _saveSessionWindowPosition(
- type, windowId, isMaximized, isFullscreen, pos);
- }
- }
- Future _saveSessionWindowPosition(WindowType windowType, int windowId,
- bool isMaximized, bool isFullscreen, LastWindowPosition pos) async {
- final remoteList = await DesktopMultiWindow.invokeMethod(
- windowId, kWindowEventGetRemoteList, null);
- getPeerPos(String peerId) {
- if (isMaximized || isFullscreen) {
- final peerPos = bind.mainGetPeerFlutterOptionSync(
- id: peerId, k: windowFramePrefix + windowType.name);
- var lpos = LastWindowPosition.loadFromString(peerPos);
- return LastWindowPosition(
- lpos?.width ?? pos.offsetWidth,
- lpos?.height ?? pos.offsetHeight,
- lpos?.offsetWidth ?? pos.offsetWidth,
- lpos?.offsetHeight ?? pos.offsetHeight,
- isMaximized,
- isFullscreen)
- .toString();
- } else {
- return pos.toString();
- }
- }
- if (remoteList != null) {
- for (final peerId in remoteList.split(',')) {
- bind.mainSetPeerFlutterOptionSync(
- id: peerId,
- k: windowFramePrefix + windowType.name,
- v: getPeerPos(peerId));
- }
- }
- }
- Future<Size> _adjustRestoreMainWindowSize(double? width, double? height) async {
- const double minWidth = 1;
- const double minHeight = 1;
- const double maxWidth = 6480;
- const double maxHeight = 6480;
- final defaultWidth =
- ((isDesktop || isWebDesktop) ? 1280 : kMobileDefaultDisplayWidth)
- .toDouble();
- final defaultHeight =
- ((isDesktop || isWebDesktop) ? 720 : kMobileDefaultDisplayHeight)
- .toDouble();
- double restoreWidth = width ?? defaultWidth;
- double restoreHeight = height ?? defaultHeight;
- if (restoreWidth < minWidth) {
- restoreWidth = defaultWidth;
- }
- if (restoreHeight < minHeight) {
- restoreHeight = defaultHeight;
- }
- if (restoreWidth > maxWidth) {
- restoreWidth = defaultWidth;
- }
- if (restoreHeight > maxHeight) {
- restoreHeight = defaultHeight;
- }
- return Size(restoreWidth, restoreHeight);
- }
- bool isPointInRect(Offset point, Rect rect) {
- return point.dx >= rect.left &&
- point.dx <= rect.right &&
- point.dy >= rect.top &&
- point.dy <= rect.bottom;
- }
- /// return null means center
- Future<Offset?> _adjustRestoreMainWindowOffset(
- double? left,
- double? top,
- double? width,
- double? height,
- ) async {
- if (left == null || top == null || width == null || height == null) {
- return null;
- }
- double? frameLeft;
- double? frameTop;
- double? frameRight;
- double? frameBottom;
- if (isDesktop || isWebDesktop) {
- for (final screen in await window_size.getScreenList()) {
- frameLeft = frameLeft == null
- ? screen.visibleFrame.left
- : min(screen.visibleFrame.left, frameLeft);
- frameTop = frameTop == null
- ? screen.visibleFrame.top
- : min(screen.visibleFrame.top, frameTop);
- frameRight = frameRight == null
- ? screen.visibleFrame.right
- : max(screen.visibleFrame.right, frameRight);
- frameBottom = frameBottom == null
- ? screen.visibleFrame.bottom
- : max(screen.visibleFrame.bottom, frameBottom);
- }
- }
- if (frameLeft == null) {
- frameLeft = 0.0;
- frameTop = 0.0;
- frameRight = ((isDesktop || isWebDesktop)
- ? kDesktopMaxDisplaySize
- : kMobileMaxDisplaySize)
- .toDouble();
- frameBottom = ((isDesktop || isWebDesktop)
- ? kDesktopMaxDisplaySize
- : kMobileMaxDisplaySize)
- .toDouble();
- }
- final minWidth = 10.0;
- if ((left + minWidth) > frameRight! ||
- (top + minWidth) > frameBottom! ||
- (left + width - minWidth) < frameLeft ||
- top < frameTop!) {
- return null;
- } else {
- return Offset(left, top);
- }
- }
- /// Restore window position and size on start
- /// Note that windowId must be provided if it's subwindow
- //
- // display is used to set the offset of the window in individual display mode.
- Future<bool> restoreWindowPosition(WindowType type,
- {int? windowId, String? peerId, int? display}) async {
- if (bind
- .mainGetEnv(key: "DISABLE_RUSTDESK_RESTORE_WINDOW_POSITION")
- .isNotEmpty) {
- return false;
- }
- if (type != WindowType.Main && windowId == null) {
- debugPrint(
- "Error: windowId cannot be null when saving positions for sub window");
- return false;
- }
- bool isRemotePeerPos = false;
- String? pos;
- // No need to check mainGetLocalBoolOptionSync(kOptionOpenNewConnInTabs)
- // Though "open in tabs" is true and the new window restore peer position, it's ok.
- if (type == WindowType.RemoteDesktop && windowId != null && peerId != null) {
- final peerPos = bind.mainGetPeerFlutterOptionSync(
- id: peerId, k: windowFramePrefix + type.name);
- if (peerPos.isNotEmpty) {
- pos = peerPos;
- }
- isRemotePeerPos = pos != null;
- }
- pos ??= bind.getLocalFlutterOption(k: windowFramePrefix + type.name);
- var lpos = LastWindowPosition.loadFromString(pos);
- if (lpos == null) {
- debugPrint("no window position saved, ignoring position restoration");
- return false;
- }
- if (type == WindowType.RemoteDesktop) {
- if (!isRemotePeerPos && windowId != null) {
- if (lpos.offsetWidth != null) {
- lpos.offsetWidth = lpos.offsetWidth! + windowId * kNewWindowOffset;
- }
- if (lpos.offsetHeight != null) {
- lpos.offsetHeight = lpos.offsetHeight! + windowId * kNewWindowOffset;
- }
- }
- if (display != null) {
- if (lpos.offsetWidth != null) {
- lpos.offsetWidth = lpos.offsetWidth! + display * kNewWindowOffset;
- }
- if (lpos.offsetHeight != null) {
- lpos.offsetHeight = lpos.offsetHeight! + display * kNewWindowOffset;
- }
- }
- }
- final size = await _adjustRestoreMainWindowSize(lpos.width, lpos.height);
- final offsetLeftTop = await _adjustRestoreMainWindowOffset(
- lpos.offsetWidth,
- lpos.offsetHeight,
- size.width,
- size.height,
- );
- debugPrint(
- "restore lpos: ${size.width}/${size.height}, offset:${offsetLeftTop?.dx}/${offsetLeftTop?.dy}, isMaximized: ${lpos.isMaximized}, isFullscreen: ${lpos.isFullscreen}");
- switch (type) {
- case WindowType.Main:
- restorePos() async {
- if (offsetLeftTop == null) {
- await windowManager.center();
- } else {
- await windowManager.setPosition(offsetLeftTop,
- ignoreDevicePixelRatio: _ignoreDevicePixelRatio);
- }
- }
- if (lpos.isMaximized == true) {
- await restorePos();
- if (!(bind.isIncomingOnly() || bind.isOutgoingOnly())) {
- await windowManager.maximize();
- }
- } else {
- final storeSize = !bind.isIncomingOnly() || bind.isOutgoingOnly();
- if (isWindows) {
- if (storeSize) {
- // We need to set the window size first to avoid the incorrect size in some special cases.
- // E.g. There are two monitors, the left one is 100% DPI and the right one is 175% DPI.
- // The window belongs to the left monitor, but if it is moved a little to the right, it will belong to the right monitor.
- // After restoring, the size will be incorrect.
- // See known issue in https://github.com/rustdesk/rustdesk/pull/9840
- await windowManager.setSize(size,
- ignoreDevicePixelRatio: _ignoreDevicePixelRatio);
- }
- await restorePos();
- if (storeSize) {
- await windowManager.setSize(size,
- ignoreDevicePixelRatio: _ignoreDevicePixelRatio);
- }
- } else {
- if (storeSize) {
- await windowManager.setSize(size,
- ignoreDevicePixelRatio: _ignoreDevicePixelRatio);
- }
- await restorePos();
- }
- }
- return true;
- default:
- final wc = WindowController.fromWindowId(windowId!);
- restoreFrame() async {
- if (offsetLeftTop == null) {
- await wc.center();
- } else {
- final frame = Rect.fromLTWH(
- offsetLeftTop.dx, offsetLeftTop.dy, size.width, size.height);
- await wc.setFrame(frame);
- }
- }
- if (lpos.isFullscreen == true) {
- if (!isMacOS) {
- await restoreFrame();
- }
- // An duration is needed to avoid the window being restored after fullscreen.
- Future.delayed(Duration(milliseconds: 300), () async {
- if (kWindowId == windowId) {
- stateGlobal.setFullscreen(true);
- } else {
- // If is not current window, we need to send a fullscreen message to `windowId`
- DesktopMultiWindow.invokeMethod(
- windowId, kWindowEventSetFullscreen, 'true');
- }
- });
- } else if (lpos.isMaximized == true) {
- await restoreFrame();
- // An duration is needed to avoid the window being restored after maximized.
- Future.delayed(Duration(milliseconds: 300), () async {
- await wc.maximize();
- });
- } else {
- await restoreFrame();
- }
- break;
- }
- return false;
- }
- var webInitialLink = "";
- /// Initialize uni links for macos/windows
- ///
- /// [Availability]
- /// initUniLinks should only be used on macos/windows.
- /// we use dbus for linux currently.
- Future<bool> initUniLinks() async {
- if (isLinux) {
- return false;
- }
- // check cold boot
- try {
- final initialLink = await getInitialLink();
- print("initialLink: $initialLink");
- if (initialLink == null || initialLink.isEmpty) {
- return false;
- }
- if (isWeb) {
- webInitialLink = initialLink;
- return false;
- } else {
- return handleUriLink(uriString: initialLink);
- }
- } catch (err) {
- debugPrintStack(label: "$err");
- return false;
- }
- }
- /// Listen for uni links.
- ///
- /// * handleByFlutter: Should uni links be handled by Flutter.
- ///
- /// Returns a [StreamSubscription] which can listen the uni links.
- StreamSubscription? listenUniLinks({handleByFlutter = true}) {
- if (isLinux || isWeb) {
- return null;
- }
- final sub = uriLinkStream.listen((Uri? uri) {
- debugPrint("A uri was received: $uri. handleByFlutter $handleByFlutter");
- if (uri != null) {
- if (handleByFlutter) {
- handleUriLink(uri: uri);
- } else {
- bind.sendUrlScheme(url: uri.toString());
- }
- } else {
- print("uni listen error: uri is empty.");
- }
- }, onError: (err) {
- print("uni links error: $err");
- });
- return sub;
- }
- enum UriLinkType {
- remoteDesktop,
- fileTransfer,
- portForward,
- rdp,
- }
- // uri link handler
- bool handleUriLink({List<String>? cmdArgs, Uri? uri, String? uriString}) {
- List<String>? args;
- if (cmdArgs != null && cmdArgs.isNotEmpty) {
- args = cmdArgs;
- // rustdesk <uri link>
- if (args[0].startsWith(bind.mainUriPrefixSync())) {
- final uri = Uri.tryParse(args[0]);
- if (uri != null) {
- args = urlLinkToCmdArgs(uri);
- }
- }
- } else if (uri != null) {
- args = urlLinkToCmdArgs(uri);
- } else if (uriString != null) {
- final uri = Uri.tryParse(uriString);
- if (uri != null) {
- args = urlLinkToCmdArgs(uri);
- }
- }
- if (args == null) {
- return false;
- }
- if (args.isEmpty) {
- windowOnTop(null);
- return true;
- }
- UriLinkType? type;
- String? id;
- String? password;
- String? switchUuid;
- bool? forceRelay;
- for (int i = 0; i < args.length; i++) {
- switch (args[i]) {
- case '--connect':
- case '--play':
- type = UriLinkType.remoteDesktop;
- id = args[i + 1];
- i++;
- break;
- case '--file-transfer':
- type = UriLinkType.fileTransfer;
- id = args[i + 1];
- i++;
- break;
- case '--port-forward':
- type = UriLinkType.portForward;
- id = args[i + 1];
- i++;
- break;
- case '--rdp':
- type = UriLinkType.rdp;
- id = args[i + 1];
- i++;
- break;
- case '--password':
- password = args[i + 1];
- i++;
- break;
- case '--switch_uuid':
- switchUuid = args[i + 1];
- i++;
- break;
- case '--relay':
- forceRelay = true;
- break;
- default:
- break;
- }
- }
- if (type != null && id != null) {
- switch (type) {
- case UriLinkType.remoteDesktop:
- Future.delayed(Duration.zero, () {
- rustDeskWinManager.newRemoteDesktop(id!,
- password: password,
- switchUuid: switchUuid,
- forceRelay: forceRelay);
- });
- break;
- case UriLinkType.fileTransfer:
- Future.delayed(Duration.zero, () {
- rustDeskWinManager.newFileTransfer(id!,
- password: password, forceRelay: forceRelay);
- });
- break;
- case UriLinkType.portForward:
- Future.delayed(Duration.zero, () {
- rustDeskWinManager.newPortForward(id!, false,
- password: password, forceRelay: forceRelay);
- });
- break;
- case UriLinkType.rdp:
- Future.delayed(Duration.zero, () {
- rustDeskWinManager.newPortForward(id!, true,
- password: password, forceRelay: forceRelay);
- });
- break;
- }
- return true;
- }
- return false;
- }
- List<String>? urlLinkToCmdArgs(Uri uri) {
- String? command;
- String? id;
- final options = ["connect", "play", "file-transfer", "port-forward", "rdp"];
- if (uri.authority.isEmpty &&
- uri.path.split('').every((char) => char == '/')) {
- return [];
- } else if (uri.authority == "connection" && uri.path.startsWith("/new/")) {
- // For compatibility
- command = '--connect';
- id = uri.path.substring("/new/".length);
- } else if (uri.authority == "config") {
- if (isAndroid || isIOS) {
- final config = uri.path.substring("/".length);
- // add a timer to make showToast work
- Timer(Duration(seconds: 1), () {
- importConfig(null, null, config);
- });
- }
- return null;
- } else if (uri.authority == "password") {
- if (isAndroid || isIOS) {
- final password = uri.path.substring("/".length);
- if (password.isNotEmpty) {
- Timer(Duration(seconds: 1), () async {
- await bind.mainSetPermanentPassword(password: password);
- showToast(translate('Successful'));
- });
- }
- }
- } else if (options.contains(uri.authority)) {
- final optionIndex = options.indexOf(uri.authority);
- command = '--${uri.authority}';
- if (uri.path.length > 1) {
- id = uri.path.substring(1);
- }
- if (isMobile && id != null) {
- if (optionIndex == 0 || optionIndex == 1) {
- connect(Get.context!, id);
- } else if (optionIndex == 2) {
- connect(Get.context!, id, isFileTransfer: true);
- }
- return null;
- }
- } else if (uri.authority.length > 2 &&
- (uri.path.length <= 1 ||
- (uri.path == '/r' || uri.path.startsWith('/r@')))) {
- // rustdesk://<connect-id>
- // rustdesk://<connect-id>/r
- // rustdesk://<connect-id>/r@<server>
- command = '--connect';
- id = uri.authority;
- if (uri.path.length > 1) {
- id = id + uri.path;
- }
- }
- var queryParameters =
- uri.queryParameters.map((k, v) => MapEntry(k.toLowerCase(), v));
- var key = queryParameters["key"];
- if (id != null) {
- if (key != null) {
- id = "$id?key=$key";
- }
- }
- if (isMobile) {
- if (id != null) {
- final forceRelay = queryParameters["relay"] != null;
- connect(Get.context!, id, forceRelay: forceRelay);
- return null;
- }
- }
- List<String> args = List.empty(growable: true);
- if (command != null && id != null) {
- args.add(command);
- args.add(id);
- var param = queryParameters;
- String? password = param["password"];
- if (password != null) args.addAll(['--password', password]);
- String? switch_uuid = param["switch_uuid"];
- if (switch_uuid != null) args.addAll(['--switch_uuid', switch_uuid]);
- if (param["relay"] != null) args.add("--relay");
- return args;
- }
- return null;
- }
- connectMainDesktop(String id,
- {required bool isFileTransfer,
- required bool isTcpTunneling,
- required bool isRDP,
- bool? forceRelay,
- String? password,
- String? connToken,
- bool? isSharedPassword}) async {
- if (isFileTransfer) {
- await rustDeskWinManager.newFileTransfer(id,
- password: password,
- isSharedPassword: isSharedPassword,
- connToken: connToken,
- forceRelay: forceRelay);
- } else if (isTcpTunneling || isRDP) {
- await rustDeskWinManager.newPortForward(id, isRDP,
- password: password,
- isSharedPassword: isSharedPassword,
- connToken: connToken,
- forceRelay: forceRelay);
- } else {
- await rustDeskWinManager.newRemoteDesktop(id,
- password: password,
- isSharedPassword: isSharedPassword,
- forceRelay: forceRelay);
- }
- }
- /// Connect to a peer with [id].
- /// If [isFileTransfer], starts a session only for file transfer.
- /// If [isTcpTunneling], starts a session only for tcp tunneling.
- /// If [isRDP], starts a session only for rdp.
- connect(BuildContext context, String id,
- {bool isFileTransfer = false,
- bool isTcpTunneling = false,
- bool isRDP = false,
- bool forceRelay = false,
- String? password,
- String? connToken,
- bool? isSharedPassword}) async {
- if (id == '') return;
- if (!isDesktop || desktopType == DesktopType.main) {
- try {
- if (Get.isRegistered<IDTextEditingController>()) {
- final idController = Get.find<IDTextEditingController>();
- idController.text = formatID(id);
- }
- if (Get.isRegistered<TextEditingController>()) {
- final fieldTextEditingController = Get.find<TextEditingController>();
- fieldTextEditingController.text = formatID(id);
- }
- } catch (_) {}
- }
- id = id.replaceAll(' ', '');
- final oldId = id;
- id = await bind.mainHandleRelayId(id: id);
- final forceRelay2 = id != oldId || forceRelay;
- assert(!(isFileTransfer && isTcpTunneling && isRDP),
- "more than one connect type");
- if (isDesktop) {
- if (desktopType == DesktopType.main) {
- await connectMainDesktop(
- id,
- isFileTransfer: isFileTransfer,
- isTcpTunneling: isTcpTunneling,
- isRDP: isRDP,
- password: password,
- isSharedPassword: isSharedPassword,
- forceRelay: forceRelay2,
- );
- } else {
- await rustDeskWinManager.call(WindowType.Main, kWindowConnect, {
- 'id': id,
- 'isFileTransfer': isFileTransfer,
- 'isTcpTunneling': isTcpTunneling,
- 'isRDP': isRDP,
- 'password': password,
- 'isSharedPassword': isSharedPassword,
- 'forceRelay': forceRelay,
- 'connToken': connToken,
- });
- }
- } else {
- if (isFileTransfer) {
- if (isAndroid) {
- if (!await AndroidPermissionManager.check(kManageExternalStorage)) {
- if (!await AndroidPermissionManager.request(kManageExternalStorage)) {
- return;
- }
- }
- }
- if (isWeb) {
- Navigator.push(
- context,
- MaterialPageRoute(
- builder: (BuildContext context) =>
- desktop_file_manager.FileManagerPage(
- id: id,
- password: password,
- isSharedPassword: isSharedPassword),
- ),
- );
- } else {
- Navigator.push(
- context,
- MaterialPageRoute(
- builder: (BuildContext context) => FileManagerPage(
- id: id, password: password, isSharedPassword: isSharedPassword),
- ),
- );
- }
- } else {
- if (isWeb) {
- Navigator.push(
- context,
- MaterialPageRoute(
- builder: (BuildContext context) => desktop_remote.RemotePage(
- key: ValueKey(id),
- id: id,
- toolbarState: ToolbarState(),
- password: password,
- forceRelay: forceRelay,
- isSharedPassword: isSharedPassword,
- ),
- ),
- );
- } else {
- Navigator.push(
- context,
- MaterialPageRoute(
- builder: (BuildContext context) => RemotePage(
- id: id, password: password, isSharedPassword: isSharedPassword),
- ),
- );
- }
- }
- stateGlobal.isInMainPage = false;
- }
- FocusScopeNode currentFocus = FocusScope.of(context);
- if (!currentFocus.hasPrimaryFocus) {
- currentFocus.unfocus();
- }
- }
- Map<String, String> getHttpHeaders() {
- return {
- 'Authorization': 'Bearer ${bind.mainGetLocalOption(key: 'access_token')}'
- };
- }
- // Simple wrapper of built-in types for reference use.
- class SimpleWrapper<T> {
- T value;
- SimpleWrapper(this.value);
- }
- /// call this to reload current window.
- ///
- /// [Note]
- /// Must have [RefreshWrapper] on the top of widget tree.
- void reloadCurrentWindow() {
- if (Get.context != null) {
- // reload self window
- RefreshWrapper.of(Get.context!)?.rebuild();
- } else {
- debugPrint(
- "reload current window failed, global BuildContext does not exist");
- }
- }
- /// call this to reload all windows, including main + all sub windows.
- Future<void> reloadAllWindows() async {
- reloadCurrentWindow();
- try {
- final ids = await DesktopMultiWindow.getAllSubWindowIds();
- for (final id in ids) {
- DesktopMultiWindow.invokeMethod(id, kWindowActionRebuild);
- }
- } on AssertionError {
- // ignore
- }
- }
- /// Indicate the flutter app is running in portable mode.
- ///
- /// [Note]
- /// Portable build is only available on Windows.
- bool isRunningInPortableMode() {
- if (!isWindows) {
- return false;
- }
- return bool.hasEnvironment(kEnvPortableExecutable);
- }
- /// Window status callback
- Future<void> onActiveWindowChanged() async {
- print(
- "[MultiWindowHandler] active window changed: ${rustDeskWinManager.getActiveWindows()}");
- if (rustDeskWinManager.getActiveWindows().isEmpty) {
- // close all sub windows
- try {
- if (isLinux) {
- await Future.wait([
- saveWindowPosition(WindowType.Main),
- rustDeskWinManager.closeAllSubWindows()
- ]);
- } else {
- await rustDeskWinManager.closeAllSubWindows();
- }
- } catch (err) {
- debugPrintStack(label: "$err");
- } finally {
- debugPrint("Start closing RustDesk...");
- await windowManager.setPreventClose(false);
- await windowManager.close();
- if (isMacOS) {
- // If we call without delay, `flutter/macos/Runner/MainFlutterWindow.swift` can handle the "terminate" event.
- // But the app will not close.
- //
- // No idea why we need to delay here, `terminate()` itself is also an async function.
- //
- // A quick workaround, use `Timer.periodic` to avoid the app not closing.
- // Because `await windowManager.close()` and `RdPlatformChannel.instance.terminate()`
- // may not work since `Flutter 3.24.4`, see the following logs.
- // A delay will allow the app to close.
- //
- //```
- // embedder.cc (2725): 'FlutterPlatformMessageCreateResponseHandle' returned 'kInvalidArguments'. Engine handle was invalid.
- // 2024-11-11 11:41:11.546 RustDesk[90272:2567686] Failed to create a FlutterPlatformMessageResponseHandle (2)
- // embedder.cc (2672): 'FlutterEngineSendPlatformMessage' returned 'kInvalidArguments'. Invalid engine handle.
- // 2024-11-11 11:41:11.565 RustDesk[90272:2567686] Failed to send message to Flutter engine on channel 'flutter/lifecycle' (2).
- // ```
- periodic_immediate(
- Duration(milliseconds: 30), RdPlatformChannel.instance.terminate);
- }
- }
- }
- }
- Timer periodic_immediate(Duration duration, Future<void> Function() callback) {
- Future.delayed(Duration.zero, callback);
- return Timer.periodic(duration, (timer) async {
- await callback();
- });
- }
- /// return a human readable windows version
- WindowsTarget getWindowsTarget(int buildNumber) {
- if (!isWindows) {
- return WindowsTarget.naw;
- }
- if (buildNumber >= 22000) {
- return WindowsTarget.w11;
- } else if (buildNumber >= 10240) {
- return WindowsTarget.w10;
- } else if (buildNumber >= 9600) {
- return WindowsTarget.w8_1;
- } else if (buildNumber >= 9200) {
- return WindowsTarget.w8;
- } else if (buildNumber >= 7601) {
- return WindowsTarget.w7;
- } else if (buildNumber >= 6002) {
- return WindowsTarget.vista;
- } else {
- // minimum support
- return WindowsTarget.xp;
- }
- }
- /// Get windows target build number.
- ///
- /// [Note]
- /// Please use this function wrapped with `Platform.isWindows`.
- int getWindowsTargetBuildNumber() {
- return getWindowsTargetBuildNumber_();
- }
- /// Indicating we need to use compatible ui mode.
- ///
- /// [Conditions]
- /// - Windows 7, window will overflow when we use frameless ui.
- bool get kUseCompatibleUiMode =>
- isWindows &&
- const [WindowsTarget.w7].contains(windowsBuildNumber.windowsVersion);
- bool get isWin10 => windowsBuildNumber.windowsVersion == WindowsTarget.w10;
- class ServerConfig {
- late String idServer;
- late String relayServer;
- late String apiServer;
- late String key;
- ServerConfig(
- {String? idServer, String? relayServer, String? apiServer, String? key}) {
- this.idServer = idServer?.trim() ?? '';
- this.relayServer = relayServer?.trim() ?? '';
- this.apiServer = apiServer?.trim() ?? '';
- this.key = key?.trim() ?? '';
- }
- /// decode from shared string (from user shared or rustdesk-server generated)
- /// also see [encode]
- /// throw when decoding failure
- ServerConfig.decode(String msg) {
- var json = {};
- try {
- // back compatible
- json = jsonDecode(msg);
- } catch (err) {
- final input = msg.split('').reversed.join('');
- final bytes = base64Decode(base64.normalize(input));
- json = jsonDecode(utf8.decode(bytes));
- }
- idServer = json['host'] ?? '';
- relayServer = json['relay'] ?? '';
- apiServer = json['api'] ?? '';
- key = json['key'] ?? '';
- }
- /// encode to shared string
- /// also see [ServerConfig.decode]
- String encode() {
- Map<String, String> config = {};
- config['host'] = idServer.trim();
- config['relay'] = relayServer.trim();
- config['api'] = apiServer.trim();
- config['key'] = key.trim();
- return base64UrlEncode(Uint8List.fromList(jsonEncode(config).codeUnits))
- .split('')
- .reversed
- .join();
- }
- /// from local options
- ServerConfig.fromOptions(Map<String, dynamic> options)
- : idServer = options['custom-rendezvous-server'] ?? "",
- relayServer = options['relay-server'] ?? "",
- apiServer = options['api-server'] ?? "",
- key = options['key'] ?? "";
- }
- Widget dialogButton(String text,
- {required VoidCallback? onPressed,
- bool isOutline = false,
- Widget? icon,
- TextStyle? style,
- ButtonStyle? buttonStyle}) {
- if (isDesktop || isWebDesktop) {
- if (isOutline) {
- return icon == null
- ? OutlinedButton(
- onPressed: onPressed,
- child: Text(translate(text), style: style),
- )
- : OutlinedButton.icon(
- icon: icon,
- onPressed: onPressed,
- label: Text(translate(text), style: style),
- );
- } else {
- return icon == null
- ? ElevatedButton(
- style: ElevatedButton.styleFrom(elevation: 0).merge(buttonStyle),
- onPressed: onPressed,
- child: Text(translate(text), style: style),
- )
- : ElevatedButton.icon(
- icon: icon,
- style: ElevatedButton.styleFrom(elevation: 0).merge(buttonStyle),
- onPressed: onPressed,
- label: Text(translate(text), style: style),
- );
- }
- } else {
- return TextButton(
- onPressed: onPressed,
- child: Text(
- translate(text),
- style: style,
- ),
- );
- }
- }
- int versionCmp(String v1, String v2) {
- return bind.versionToNumber(v: v1) - bind.versionToNumber(v: v2);
- }
- String getWindowName({WindowType? overrideType}) {
- final name = bind.mainGetAppNameSync();
- switch (overrideType ?? kWindowType) {
- case WindowType.Main:
- return name;
- case WindowType.FileTransfer:
- return "File Transfer - $name";
- case WindowType.PortForward:
- return "Port Forward - $name";
- case WindowType.RemoteDesktop:
- return "Remote Desktop - $name";
- default:
- break;
- }
- return name;
- }
- String getWindowNameWithId(String id, {WindowType? overrideType}) {
- return "${DesktopTab.tablabelGetter(id).value} - ${getWindowName(overrideType: overrideType)}";
- }
- Future<void> updateSystemWindowTheme() async {
- // Set system window theme for macOS.
- final userPreference = MyTheme.getThemeModePreference();
- if (userPreference != ThemeMode.system) {
- if (isMacOS) {
- await RdPlatformChannel.instance.changeSystemWindowTheme(
- userPreference == ThemeMode.light
- ? SystemWindowTheme.light
- : SystemWindowTheme.dark);
- }
- }
- }
- /// macOS only
- ///
- /// Note: not found a general solution for rust based AVFoundation bingding.
- /// [AVFoundation] crate has compile error.
- const kMacOSPermChannel = MethodChannel("org.rustdesk.rustdesk/macos");
- enum PermissionAuthorizeType {
- undetermined,
- authorized,
- denied, // and restricted
- }
- Future<PermissionAuthorizeType> osxCanRecordAudio() async {
- int res = await kMacOSPermChannel.invokeMethod("canRecordAudio");
- print(res);
- if (res > 0) {
- return PermissionAuthorizeType.authorized;
- } else if (res == 0) {
- return PermissionAuthorizeType.undetermined;
- } else {
- return PermissionAuthorizeType.denied;
- }
- }
- Future<bool> osxRequestAudio() async {
- return await kMacOSPermChannel.invokeMethod("requestRecordAudio");
- }
- Widget futureBuilder(
- {required Future? future, required Widget Function(dynamic data) hasData}) {
- return FutureBuilder(
- future: future,
- builder: (BuildContext context, AsyncSnapshot snapshot) {
- if (snapshot.hasData) {
- return hasData(snapshot.data!);
- } else {
- if (snapshot.hasError) {
- debugPrint(snapshot.error.toString());
- }
- return Container();
- }
- });
- }
- void onCopyFingerprint(String value) {
- if (value.isNotEmpty) {
- Clipboard.setData(ClipboardData(text: value));
- showToast('$value\n${translate("Copied")}');
- } else {
- showToast(translate("no fingerprints"));
- }
- }
- Future<bool> callMainCheckSuperUserPermission() async {
- bool checked = await bind.mainCheckSuperUserPermission();
- if (isMacOS) {
- await windowManager.show();
- }
- return checked;
- }
- Future<void> start_service(bool is_start) async {
- bool checked = !bind.mainIsInstalled() ||
- !isMacOS ||
- await callMainCheckSuperUserPermission();
- if (checked) {
- mainSetBoolOption(kOptionStopService, !is_start);
- }
- }
- Future<bool> canBeBlocked() async {
- var access_mode = await bind.mainGetOption(key: kOptionAccessMode);
- var option = option2bool(kOptionAllowRemoteConfigModification,
- await bind.mainGetOption(key: kOptionAllowRemoteConfigModification));
- return access_mode == 'view' || (access_mode.isEmpty && !option);
- }
- Future<void> shouldBeBlocked(RxBool block, WhetherUseRemoteBlock? use) async {
- if (use != null && !await use()) {
- block.value = false;
- return;
- }
- var time0 = DateTime.now().millisecondsSinceEpoch;
- await bind.mainCheckMouseTime();
- Timer(const Duration(milliseconds: 120), () async {
- var d = time0 - await bind.mainGetMouseTime();
- if (d < 120) {
- block.value = true;
- } else {
- block.value = false;
- }
- });
- }
- typedef WhetherUseRemoteBlock = Future<bool> Function();
- Widget buildRemoteBlock(
- {required Widget child,
- required RxBool block,
- required bool mask,
- WhetherUseRemoteBlock? use}) {
- return Obx(() => MouseRegion(
- onEnter: (_) async {
- await shouldBeBlocked(block, use);
- },
- onExit: (event) => block.value = false,
- child: Stack(children: [
- // scope block tab
- preventMouseKeyBuilder(child: child, block: block.value),
- // mask block click, cm not block click and still use check_click_time to avoid block local click
- if (mask)
- Offstage(
- offstage: !block.value,
- child: Container(
- color: Colors.black.withOpacity(0.5),
- )),
- ]),
- ));
- }
- Widget preventMouseKeyBuilder({required Widget child, required bool block}) {
- return ExcludeFocus(
- excluding: block, child: AbsorbPointer(child: child, absorbing: block));
- }
- Widget unreadMessageCountBuilder(RxInt? count,
- {double? size, double? fontSize}) {
- return Obx(() => Offstage(
- offstage: !((count?.value ?? 0) > 0),
- child: Container(
- width: size ?? 16,
- height: size ?? 16,
- decoration: BoxDecoration(
- color: Colors.red,
- shape: BoxShape.circle,
- ),
- child: Center(
- child: Text("${count?.value ?? 0}",
- maxLines: 1,
- style: TextStyle(color: Colors.white, fontSize: fontSize ?? 10)),
- ),
- )));
- }
- Widget unreadTopRightBuilder(RxInt? count, {Widget? icon}) {
- return Stack(
- children: [
- icon ?? Icon(Icons.chat),
- Positioned(
- top: 0,
- right: 0,
- child: unreadMessageCountBuilder(count, size: 12, fontSize: 8))
- ],
- );
- }
- String toCapitalized(String s) {
- if (s.isEmpty) {
- return s;
- }
- return s.substring(0, 1).toUpperCase() + s.substring(1);
- }
- Widget buildErrorBanner(BuildContext context,
- {required RxBool loading,
- required RxString err,
- required Function? retry,
- required Function close}) {
- return Obx(() => Offstage(
- offstage: !(!loading.value && err.value.isNotEmpty),
- child: Center(
- child: Container(
- color: MyTheme.color(context).errorBannerBg,
- child: Row(
- mainAxisAlignment: MainAxisAlignment.center,
- crossAxisAlignment: CrossAxisAlignment.center,
- children: [
- FittedBox(
- child: Icon(
- Icons.info,
- color: Color.fromARGB(255, 249, 81, 81),
- ),
- ).marginAll(4),
- Flexible(
- child: Align(
- alignment: Alignment.centerLeft,
- child: Tooltip(
- message: translate(err.value),
- child: SelectableText(
- translate(err.value),
- ),
- )).marginSymmetric(vertical: 2),
- ),
- if (retry != null)
- InkWell(
- onTap: () {
- retry.call();
- },
- child: Text(
- translate("Retry"),
- style: TextStyle(color: MyTheme.accent),
- )).marginSymmetric(horizontal: 5),
- FittedBox(
- child: InkWell(
- onTap: () {
- close.call();
- },
- child: Icon(Icons.close).marginSymmetric(horizontal: 5),
- ),
- ).marginAll(4)
- ],
- ),
- )).marginOnly(bottom: 14),
- ));
- }
- String getDesktopTabLabel(String peerId, String alias) {
- String label = alias.isEmpty ? peerId : alias;
- try {
- String peer = bind.mainGetPeerSync(id: peerId);
- Map<String, dynamic> config = jsonDecode(peer);
- if (config['info']['hostname'] is String) {
- String hostname = config['info']['hostname'];
- if (hostname.isNotEmpty &&
- !label.toLowerCase().contains(hostname.toLowerCase())) {
- label += "@$hostname";
- }
- }
- } catch (e) {
- debugPrint("Failed to get hostname:$e");
- }
- return label;
- }
- sessionRefreshVideo(SessionID sessionId, PeerInfo pi) async {
- if (pi.currentDisplay == kAllDisplayValue) {
- for (int i = 0; i < pi.displays.length; i++) {
- await bind.sessionRefresh(sessionId: sessionId, display: i);
- }
- } else {
- await bind.sessionRefresh(sessionId: sessionId, display: pi.currentDisplay);
- }
- }
- Future<List<Rect>> getScreenListWayland() async {
- final screenRectList = <Rect>[];
- if (isMainDesktopWindow) {
- for (var screen in await window_size.getScreenList()) {
- final scale = kIgnoreDpi ? 1.0 : screen.scaleFactor;
- double l = screen.frame.left;
- double t = screen.frame.top;
- double r = screen.frame.right;
- double b = screen.frame.bottom;
- final rect = Rect.fromLTRB(l / scale, t / scale, r / scale, b / scale);
- screenRectList.add(rect);
- }
- } else {
- final screenList = await rustDeskWinManager.call(
- WindowType.Main, kWindowGetScreenList, '');
- try {
- for (var screen in jsonDecode(screenList.result) as List<dynamic>) {
- final scale = kIgnoreDpi ? 1.0 : screen['scaleFactor'];
- double l = screen['frame']['l'];
- double t = screen['frame']['t'];
- double r = screen['frame']['r'];
- double b = screen['frame']['b'];
- final rect = Rect.fromLTRB(l / scale, t / scale, r / scale, b / scale);
- screenRectList.add(rect);
- }
- } catch (e) {
- debugPrint('Failed to parse screenList: $e');
- }
- }
- return screenRectList;
- }
- Future<List<Rect>> getScreenListNotWayland() async {
- final screenRectList = <Rect>[];
- final displays = bind.mainGetDisplays();
- if (displays.isEmpty) {
- return screenRectList;
- }
- try {
- for (var display in jsonDecode(displays) as List<dynamic>) {
- // to-do: scale factor ?
- // final scale = kIgnoreDpi ? 1.0 : screen.scaleFactor;
- double l = display['x'].toDouble();
- double t = display['y'].toDouble();
- double r = (display['x'] + display['w']).toDouble();
- double b = (display['y'] + display['h']).toDouble();
- screenRectList.add(Rect.fromLTRB(l, t, r, b));
- }
- } catch (e) {
- debugPrint('Failed to parse displays: $e');
- }
- return screenRectList;
- }
- Future<List<Rect>> getScreenRectList() async {
- return bind.mainCurrentIsWayland()
- ? await getScreenListWayland()
- : await getScreenListNotWayland();
- }
- openMonitorInTheSameTab(int i, FFI ffi, PeerInfo pi,
- {bool updateCursorPos = true}) {
- final displays = i == kAllDisplayValue
- ? List.generate(pi.displays.length, (index) => index)
- : [i];
- // Try clear image model before switching from all displays
- // 1. The remote side has multiple displays.
- // 2. Do not use texture render.
- // 3. Connect to Display 1.
- // 4. Switch to multi-displays `kAllDisplayValue`
- // 5. Switch to Display 2.
- // Then the remote page will display last picture of Display 1 at the beginning.
- if (pi.forceTextureRender && i != kAllDisplayValue) {
- ffi.imageModel.clearImage();
- }
- bind.sessionSwitchDisplay(
- isDesktop: isDesktop,
- sessionId: ffi.sessionId,
- value: Int32List.fromList(displays),
- );
- ffi.ffiModel.switchToNewDisplay(i, ffi.sessionId, ffi.id,
- updateCursorPos: updateCursorPos);
- }
- // Open new tab or window to show this monitor.
- // For now just open new window.
- //
- // screenRect is used to move the new window to the specified screen and set fullscreen.
- openMonitorInNewTabOrWindow(int i, String peerId, PeerInfo pi,
- {Rect? screenRect}) {
- final args = {
- 'window_id': stateGlobal.windowId,
- 'peer_id': peerId,
- 'display': i,
- 'display_count': pi.displays.length,
- };
- if (screenRect != null) {
- args['screen_rect'] = {
- 'l': screenRect.left,
- 't': screenRect.top,
- 'r': screenRect.right,
- 'b': screenRect.bottom,
- };
- }
- DesktopMultiWindow.invokeMethod(
- kMainWindowId, kWindowEventOpenMonitorSession, jsonEncode(args));
- }
- setNewConnectWindowFrame(int windowId, String peerId, int preSessionCount,
- int? display, Rect? screenRect) async {
- if (screenRect == null) {
- // Do not restore window position to new connection if there's a pre-session.
- // https://github.com/rustdesk/rustdesk/discussions/8825
- if (preSessionCount == 0) {
- await restoreWindowPosition(WindowType.RemoteDesktop,
- windowId: windowId, display: display, peerId: peerId);
- }
- } else {
- await tryMoveToScreenAndSetFullscreen(screenRect);
- }
- }
- tryMoveToScreenAndSetFullscreen(Rect? screenRect) async {
- if (screenRect == null) {
- return;
- }
- final wc = WindowController.fromWindowId(stateGlobal.windowId);
- final curFrame = await wc.getFrame();
- final frame =
- Rect.fromLTWH(screenRect.left + 30, screenRect.top + 30, 600, 400);
- if (stateGlobal.fullscreen.isTrue &&
- curFrame.left <= frame.left &&
- curFrame.top <= frame.top &&
- curFrame.width >= frame.width &&
- curFrame.height >= frame.height) {
- return;
- }
- await wc.setFrame(frame);
- // An duration is needed to avoid the window being restored after fullscreen.
- Future.delayed(Duration(milliseconds: 300), () async {
- stateGlobal.setFullscreen(true);
- });
- }
- parseParamScreenRect(Map<String, dynamic> params) {
- Rect? screenRect;
- if (params['screen_rect'] != null) {
- double l = params['screen_rect']['l'];
- double t = params['screen_rect']['t'];
- double r = params['screen_rect']['r'];
- double b = params['screen_rect']['b'];
- screenRect = Rect.fromLTRB(l, t, r, b);
- }
- return screenRect;
- }
- get isInputSourceFlutter => stateGlobal.getInputSource() == "Input source 2";
- class _ReconnectCountDownButton extends StatefulWidget {
- _ReconnectCountDownButton({
- Key? key,
- required this.second,
- required this.onPressed,
- }) : super(key: key);
- final VoidCallback? onPressed;
- final int second;
- @override
- State<_ReconnectCountDownButton> createState() =>
- _ReconnectCountDownButtonState();
- }
- class _ReconnectCountDownButtonState extends State<_ReconnectCountDownButton> {
- late int _countdownSeconds = widget.second;
- Timer? _timer;
- @override
- void initState() {
- super.initState();
- _startCountdownTimer();
- }
- @override
- void dispose() {
- _timer?.cancel();
- super.dispose();
- }
- void _startCountdownTimer() {
- _timer = Timer.periodic(Duration(seconds: 1), (timer) {
- if (_countdownSeconds <= 0) {
- timer.cancel();
- } else {
- setState(() {
- _countdownSeconds--;
- });
- }
- });
- }
- @override
- Widget build(BuildContext context) {
- return dialogButton(
- '${translate('Reconnect')} (${_countdownSeconds}s)',
- onPressed: widget.onPressed,
- isOutline: true,
- );
- }
- }
- importConfig(List<TextEditingController>? controllers, List<RxString>? errMsgs,
- String? text) {
- text = text?.trim();
- if (text != null && text.isNotEmpty) {
- try {
- final sc = ServerConfig.decode(text);
- if (isWeb || isIOS) {
- sc.relayServer = '';
- }
- if (sc.idServer.isNotEmpty) {
- Future<bool> success = setServerConfig(controllers, errMsgs, sc);
- success.then((value) {
- if (value) {
- showToast(translate('Import server configuration successfully'));
- } else {
- showToast(translate('Invalid server configuration'));
- }
- });
- } else {
- showToast(translate('Invalid server configuration'));
- }
- return sc;
- } catch (e) {
- showToast(translate('Invalid server configuration'));
- }
- } else {
- showToast(translate('Clipboard is empty'));
- }
- }
- Future<bool> setServerConfig(
- List<TextEditingController>? controllers,
- List<RxString>? errMsgs,
- ServerConfig config,
- ) async {
- String removeEndSlash(String input) {
- if (input.endsWith('/')) {
- return input.substring(0, input.length - 1);
- }
- return input;
- }
- config.idServer = removeEndSlash(config.idServer.trim());
- config.relayServer = removeEndSlash(config.relayServer.trim());
- config.apiServer = removeEndSlash(config.apiServer.trim());
- config.key = config.key.trim();
- if (controllers != null) {
- controllers[0].text = config.idServer;
- controllers[1].text = config.relayServer;
- controllers[2].text = config.apiServer;
- controllers[3].text = config.key;
- }
- // id
- if (config.idServer.isNotEmpty && errMsgs != null) {
- errMsgs[0].value = translate(await bind.mainTestIfValidServer(
- server: config.idServer, testWithProxy: true));
- if (errMsgs[0].isNotEmpty) {
- return false;
- }
- }
- // relay
- if (config.relayServer.isNotEmpty && errMsgs != null) {
- errMsgs[1].value = translate(await bind.mainTestIfValidServer(
- server: config.relayServer, testWithProxy: true));
- if (errMsgs[1].isNotEmpty) {
- return false;
- }
- }
- // api
- if (config.apiServer.isNotEmpty && errMsgs != null) {
- if (!config.apiServer.startsWith('http://') &&
- !config.apiServer.startsWith('https://')) {
- errMsgs[2].value =
- '${translate("API Server")}: ${translate("invalid_http")}';
- return false;
- }
- }
- final oldApiServer = await bind.mainGetApiServer();
- // should set one by one
- await bind.mainSetOption(
- key: 'custom-rendezvous-server', value: config.idServer);
- await bind.mainSetOption(key: 'relay-server', value: config.relayServer);
- await bind.mainSetOption(key: 'api-server', value: config.apiServer);
- await bind.mainSetOption(key: 'key', value: config.key);
- final newApiServer = await bind.mainGetApiServer();
- if (oldApiServer.isNotEmpty &&
- oldApiServer != newApiServer &&
- gFFI.userModel.isLogin) {
- gFFI.userModel.logOut(apiServer: oldApiServer);
- }
- return true;
- }
- ColorFilter? svgColor(Color? color) {
- if (color == null) {
- return null;
- } else {
- return ColorFilter.mode(color, BlendMode.srcIn);
- }
- }
- // ignore: must_be_immutable
- class ComboBox extends StatelessWidget {
- late final List<String> keys;
- late final List<String> values;
- late final String initialKey;
- late final Function(String key) onChanged;
- late final bool enabled;
- late String current;
- ComboBox({
- Key? key,
- required this.keys,
- required this.values,
- required this.initialKey,
- required this.onChanged,
- this.enabled = true,
- }) : super(key: key);
- @override
- Widget build(BuildContext context) {
- var index = keys.indexOf(initialKey);
- if (index < 0) {
- index = 0;
- }
- var ref = values[index].obs;
- current = keys[index];
- return Container(
- decoration: BoxDecoration(
- border: Border.all(
- color: enabled
- ? MyTheme.color(context).border2 ?? MyTheme.border
- : MyTheme.border,
- ),
- borderRadius:
- BorderRadius.circular(8), //border raiuds of dropdown button
- ),
- height: 42, // should be the height of a TextField
- child: Obx(() => DropdownButton<String>(
- isExpanded: true,
- value: ref.value,
- elevation: 16,
- underline: Container(),
- style: TextStyle(
- color: enabled
- ? Theme.of(context).textTheme.titleMedium?.color
- : disabledTextColor(context, enabled)),
- icon: const Icon(
- Icons.expand_more_sharp,
- size: 20,
- ).marginOnly(right: 15),
- onChanged: enabled
- ? (String? newValue) {
- if (newValue != null && newValue != ref.value) {
- ref.value = newValue;
- current = newValue;
- onChanged(keys[values.indexOf(newValue)]);
- }
- }
- : null,
- items: values.map<DropdownMenuItem<String>>((String value) {
- return DropdownMenuItem<String>(
- value: value,
- child: Text(
- value,
- style: const TextStyle(fontSize: 15),
- overflow: TextOverflow.ellipsis,
- ).marginOnly(left: 15),
- );
- }).toList(),
- )),
- ).marginOnly(bottom: 5);
- }
- }
- Color? disabledTextColor(BuildContext context, bool enabled) {
- return enabled
- ? null
- : Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6);
- }
- Widget loadPowered(BuildContext context) {
- return MouseRegion(
- cursor: SystemMouseCursors.click,
- child: GestureDetector(
- onTap: () {
- launchUrl(Uri.parse('https://rustdesk.com'));
- },
- child: Opacity(
- opacity: 0.5,
- child: Text(
- translate("powered_by_me"),
- overflow: TextOverflow.clip,
- style: Theme.of(context)
- .textTheme
- .bodySmall
- ?.copyWith(fontSize: 9, decoration: TextDecoration.underline),
- )),
- ),
- ).marginOnly(top: 6);
- }
- // max 300 x 60
- Widget loadLogo() {
- return FutureBuilder<ByteData>(
- future: rootBundle.load('assets/logo.png'),
- builder: (BuildContext context, AsyncSnapshot<ByteData> snapshot) {
- if (snapshot.hasData) {
- final image = Image.asset(
- 'assets/logo.png',
- fit: BoxFit.contain,
- errorBuilder: (ctx, error, stackTrace) {
- return Container();
- },
- );
- return Container(
- constraints: BoxConstraints(maxWidth: 300, maxHeight: 60),
- child: image,
- ).marginOnly(left: 12, right: 12, top: 12);
- }
- return const Offstage();
- });
- }
- Widget loadIcon(double size) {
- return Image.asset('assets/icon.png',
- width: size,
- height: size,
- errorBuilder: (ctx, error, stackTrace) => SvgPicture.asset(
- 'assets/icon.svg',
- width: size,
- height: size,
- ));
- }
- var imcomingOnlyHomeSize = Size(280, 300);
- Size getIncomingOnlyHomeSize() {
- final magicWidth = isWindows ? 11.0 : 2.0;
- final magicHeight = 10.0;
- return imcomingOnlyHomeSize +
- Offset(magicWidth, kDesktopRemoteTabBarHeight + magicHeight);
- }
- Size getIncomingOnlySettingsSize() {
- return Size(768, 600);
- }
- bool isInHomePage() {
- final controller = Get.find<DesktopTabController>();
- return controller.state.value.selected == 0;
- }
- Widget _buildPresetPasswordWarning() {
- if (bind.mainGetBuildinOption(key: kOptionRemovePresetPasswordWarning) !=
- 'N') {
- return SizedBox.shrink();
- }
- return Container(
- color: Colors.yellow,
- child: Column(
- children: [
- Align(
- child: Text(
- translate("Security Alert"),
- style: TextStyle(
- color: Colors.red,
- fontSize:
- 18, // https://github.com/rustdesk/rustdesk-server-pro/issues/261
- fontWeight: FontWeight.bold,
- ),
- )).paddingOnly(bottom: 8),
- Text(
- translate("preset_password_warning"),
- style: TextStyle(color: Colors.red),
- )
- ],
- ).paddingAll(8),
- ); // Show a warning message if the Future completed with true
- }
- Widget buildPresetPasswordWarningMobile() {
- if (bind.isPresetPasswordMobileOnly()) {
- return _buildPresetPasswordWarning();
- } else {
- return SizedBox.shrink();
- }
- }
- Widget buildPresetPasswordWarning() {
- return FutureBuilder<bool>(
- future: bind.isPresetPassword(),
- builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
- if (snapshot.connectionState == ConnectionState.waiting) {
- return CircularProgressIndicator(); // Show a loading spinner while waiting for the Future to complete
- } else if (snapshot.hasError) {
- return Text(
- 'Error: ${snapshot.error}'); // Show an error message if the Future completed with an error
- } else if (snapshot.hasData && snapshot.data == true) {
- return _buildPresetPasswordWarning();
- } else {
- return SizedBox
- .shrink(); // Show nothing if the Future completed with false or null
- }
- },
- );
- }
- // https://github.com/leanflutter/window_manager/blob/87dd7a50b4cb47a375b9fc697f05e56eea0a2ab3/lib/src/widgets/virtual_window_frame.dart#L44
- Widget buildVirtualWindowFrame(BuildContext context, Widget child) {
- boxShadow() => isMainDesktopWindow
- ? <BoxShadow>[
- if (stateGlobal.fullscreen.isFalse || stateGlobal.isMaximized.isFalse)
- BoxShadow(
- color: Colors.black.withOpacity(0.1),
- offset: Offset(
- 0.0,
- stateGlobal.isFocused.isTrue
- ? kFrameBoxShadowOffsetFocused
- : kFrameBoxShadowOffsetUnfocused),
- blurRadius: kFrameBoxShadowBlurRadius,
- ),
- ]
- : null;
- return Obx(
- () => Container(
- decoration: BoxDecoration(
- color: isMainDesktopWindow
- ? Colors.transparent
- : Theme.of(context).colorScheme.background,
- border: Border.all(
- color: Theme.of(context).dividerColor,
- width: stateGlobal.windowBorderWidth.value,
- ),
- borderRadius: BorderRadius.circular(
- (stateGlobal.fullscreen.isTrue || stateGlobal.isMaximized.isTrue)
- ? 0
- : kFrameBorderRadius,
- ),
- boxShadow: boxShadow(),
- ),
- child: ClipRRect(
- borderRadius: BorderRadius.circular(
- (stateGlobal.fullscreen.isTrue || stateGlobal.isMaximized.isTrue)
- ? 0
- : kFrameClipRRectBorderRadius,
- ),
- child: child,
- ),
- ),
- );
- }
- get windowResizeEdgeSize =>
- isLinux && !_linuxWindowResizable ? 0.0 : kWindowResizeEdgeSize;
- // `windowManager.setResizable(false)` will reset the window size to the default size on Linux and then set unresizable.
- // See _linuxWindowResizable for more details.
- // So we use `setResizable()` instead of `windowManager.setResizable()`.
- //
- // We can only call `windowManager.setResizable(false)` if we need the default size on Linux.
- setResizable(bool resizable) {
- if (isLinux) {
- _linuxWindowResizable = resizable;
- stateGlobal.refreshResizeEdgeSize();
- } else {
- windowManager.setResizable(resizable);
- }
- }
- isOptionFixed(String key) => bind.mainIsOptionFixed(key: key);
- bool? _isCustomClient;
- bool get isCustomClient {
- _isCustomClient ??= bind.isCustomClient();
- return _isCustomClient!;
- }
- get defaultOptionLang => isCustomClient ? 'default' : '';
- get defaultOptionTheme => isCustomClient ? 'system' : '';
- get defaultOptionYes => isCustomClient ? 'Y' : '';
- get defaultOptionNo => isCustomClient ? 'N' : '';
- get defaultOptionWhitelist => isCustomClient ? ',' : '';
- get defaultOptionAccessMode => isCustomClient ? 'custom' : '';
- get defaultOptionApproveMode => isCustomClient ? 'password-click' : '';
- bool whitelistNotEmpty() {
- // https://rustdesk.com/docs/en/self-host/client-configuration/advanced-settings/#whitelist
- final v = bind.mainGetOptionSync(key: kOptionWhitelist);
- return v != '' && v != ',';
- }
- // `setMovable()` is only supported on macOS.
- //
- // On macOS, the window can be dragged by the tab bar by default.
- // We need to disable the movable feature to prevent the window from being dragged by the tabs in the tab bar.
- //
- // When we drag the blank tab bar (not the tab), the window will be dragged normally by adding the `onPanStart` handle.
- //
- // See the following code for more details:
- // https://github.com/rustdesk/rustdesk/blob/ce1dac3b8613596b4d8ae981275f9335489eb935/flutter/lib/desktop/widgets/tabbar_widget.dart#L385
- // https://github.com/rustdesk/rustdesk/blob/ce1dac3b8613596b4d8ae981275f9335489eb935/flutter/lib/desktop/widgets/tabbar_widget.dart#L399
- //
- // @platforms macos
- disableWindowMovable(int? windowId) {
- if (!isMacOS) {
- return;
- }
- if (windowId == null) {
- windowManager.setMovable(false);
- } else {
- WindowController.fromWindowId(windowId).setMovable(false);
- }
- }
- Widget netWorkErrorWidget() {
- return Center(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- crossAxisAlignment: CrossAxisAlignment.center,
- children: [
- Text(translate("network_error_tip")),
- ElevatedButton(
- onPressed: gFFI.userModel.refreshCurrentUser,
- child: Text(translate("Retry")))
- .marginSymmetric(vertical: 16),
- SelectableText(gFFI.userModel.networkError.value,
- style: TextStyle(fontSize: 11, color: Colors.red)),
- ],
- ));
- }
- List<ResizeEdge>? get windowManagerEnableResizeEdges => isWindows
- ? [
- ResizeEdge.topLeft,
- ResizeEdge.top,
- ResizeEdge.topRight,
- ]
- : null;
- List<SubWindowResizeEdge>? get subWindowManagerEnableResizeEdges => isWindows
- ? [
- SubWindowResizeEdge.topLeft,
- SubWindowResizeEdge.top,
- SubWindowResizeEdge.topRight,
- ]
- : null;
- void earlyAssert() {
- assert('\1' == '1');
- }
- void checkUpdate() {
- if (!isWeb) {
- if (!bind.isCustomClient()) {
- platformFFI.registerEventHandler(
- kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish,
- (Map<String, dynamic> evt) async {
- if (evt['url'] is String) {
- stateGlobal.updateUrl.value = evt['url'];
- }
- });
- Timer(const Duration(seconds: 1), () async {
- bind.mainGetSoftwareUpdateUrl();
- });
- }
- }
- }
- // https://github.com/flutter/flutter/issues/153560#issuecomment-2497160535
- // For TextField, TextFormField
- extension WorkaroundFreezeLinuxMint on Widget {
- Widget workaroundFreezeLinuxMint() {
- // No need to check if is Linux Mint, because this workaround is harmless on other platforms.
- if (isLinux) {
- return ExcludeSemantics(child: this);
- } else {
- return this;
- }
- }
- }
- // Don't use `extension` here, the border looks weird if using `extension` in my test.
- Widget workaroundWindowBorder(BuildContext context, Widget child) {
- if (!isWin10) {
- return child;
- }
- final isLight = Theme.of(context).brightness == Brightness.light;
- final borderColor = isLight ? Colors.black87 : Colors.grey;
- final width = isLight ? 0.5 : 0.1;
- getBorderWidget(Widget child) {
- return Obx(() =>
- (stateGlobal.isMaximized.isTrue || stateGlobal.fullscreen.isTrue)
- ? Offstage()
- : child);
- }
- final List<Widget> borders = [
- getBorderWidget(Container(
- color: borderColor,
- height: width + 0.1,
- ))
- ];
- if (kWindowType == WindowType.Main && !isLight) {
- borders.addAll([
- getBorderWidget(Align(
- alignment: Alignment.topLeft,
- child: Container(
- color: borderColor,
- width: width,
- ),
- )),
- getBorderWidget(Align(
- alignment: Alignment.topRight,
- child: Container(
- color: borderColor,
- width: width,
- ),
- )),
- getBorderWidget(Align(
- alignment: Alignment.bottomCenter,
- child: Container(
- color: borderColor,
- height: width,
- ),
- )),
- ]);
- }
- return Stack(
- children: [
- child,
- ...borders,
- ],
- );
- }
|