Line data Source code
1 : import 'dart:io';
2 :
3 : import 'package:cwtch/constants.dart';
4 : import 'package:cwtch/controllers/enter_password.dart';
5 : import 'package:cwtch/controllers/filesharing.dart';
6 : import 'package:cwtch/cwtch_icons_icons.dart';
7 : import 'package:cwtch/models/appstate.dart';
8 : import 'package:cwtch/models/profile.dart';
9 : import 'package:cwtch/models/profilelist.dart';
10 : import 'package:flutter/material.dart';
11 : import 'package:cwtch/settings.dart';
12 : import 'package:cwtch/views/torstatusview.dart';
13 : import 'package:cwtch/widgets/passwordfield.dart';
14 : import 'package:cwtch/widgets/tor_icon.dart';
15 : import 'package:flutter/services.dart';
16 : import 'package:cwtch/l10n/app_localizations.dart';
17 : import 'package:cwtch/widgets/profilerow.dart';
18 : import 'package:provider/provider.dart';
19 : import '../main.dart';
20 : import '../torstatus.dart';
21 : import 'addeditprofileview.dart';
22 : import 'globalsettingsview.dart';
23 : import 'serversview.dart';
24 : import 'hybridgroupsview.dart';
25 :
26 : class ProfileMgrView extends StatefulWidget {
27 0 : ProfileMgrView();
28 :
29 0 : @override
30 0 : _ProfileMgrViewState createState() => _ProfileMgrViewState();
31 : }
32 :
33 : class _ProfileMgrViewState extends State<ProfileMgrView> {
34 : final ctrlrPassword = TextEditingController();
35 :
36 0 : @override
37 : void dispose() {
38 0 : ctrlrPassword.dispose();
39 0 : super.dispose();
40 : }
41 :
42 0 : @override
43 : Widget build(BuildContext context) {
44 0 : return Consumer<Settings>(
45 : // Prevents Android back button from closing the app on the profile manager screen
46 : // (which would shutdown connections and all kinds of other expensive to generate things)
47 0 : builder: (context, settings, child) => WillPopScope(
48 0 : onWillPop: () async {
49 0 : _modalShutdown();
50 0 : return Provider.of<AppState>(context, listen: false).cwtchIsClosing;
51 : },
52 0 : child: Scaffold(
53 0 : key: Key("ProfileManagerView"),
54 0 : backgroundColor: settings.theme.backgroundMainColor,
55 0 : appBar: AppBar(
56 0 : title: Row(
57 0 : children: [
58 0 : Icon(CwtchIcons.cwtch_knott, size: 36, color: settings.theme.mainTextColor),
59 0 : SizedBox(width: 10),
60 0 : Expanded(
61 0 : child: Text(
62 0 : MediaQuery.of(context).size.width > 600 ? AppLocalizations.of(context)!.titleManageProfiles : AppLocalizations.of(context)!.titleManageProfilesShort,
63 0 : style: TextStyle(color: settings.current().mainTextColor),
64 : ),
65 : ),
66 : ],
67 : ),
68 0 : actions: getActions(),
69 : ),
70 0 : floatingActionButton: FloatingActionButton(
71 0 : onPressed: _modalAddImportProfiles,
72 0 : tooltip: AppLocalizations.of(context)!.addNewProfileBtn,
73 0 : child: Icon(Icons.add, semanticLabel: AppLocalizations.of(context)!.addNewProfileBtn, color: Provider.of<Settings>(context).theme.defaultButtonTextColor),
74 : ),
75 0 : body: _buildProfileManager(),
76 : ),
77 : ),
78 : );
79 : }
80 :
81 0 : List<Widget> getActions() {
82 0 : List<Widget> actions = new List<Widget>.empty(growable: true);
83 :
84 : // Tor Status
85 0 : actions.add(
86 0 : IconButton(
87 0 : icon: TorIcon(),
88 0 : onPressed: _pushTorStatus,
89 0 : splashRadius: Material.defaultSplashRadius / 2,
90 0 : color: Provider.of<ProfileListState>(context).profiles.isEmpty ? Provider.of<Settings>(context).theme.defaultButtonColor : Provider.of<Settings>(context).theme.mainTextColor,
91 0 : tooltip: Provider.of<TorStatus>(context).progress == 100
92 0 : ? AppLocalizations.of(context)!.networkStatusOnline
93 0 : : (Provider.of<TorStatus>(context).progress == 0 ? AppLocalizations.of(context)!.networkStatusDisconnected : AppLocalizations.of(context)!.networkStatusAttemptingTor),
94 : ),
95 : );
96 :
97 : // Unlock Profiles
98 0 : actions.add(
99 0 : IconButton(
100 0 : icon: Icon(CwtchIcons.lock_open_24px),
101 0 : splashRadius: Material.defaultSplashRadius / 2,
102 0 : color: Provider.of<ProfileListState>(context).profiles.isEmpty ? Provider.of<Settings>(context).theme.defaultButtonColor : Provider.of<Settings>(context).theme.mainTextColor,
103 0 : tooltip: AppLocalizations.of(context)!.tooltipUnlockProfiles,
104 0 : onPressed: _modalUnlockProfiles,
105 : ),
106 : );
107 :
108 : // Hybrid groups
109 0 : if (Provider.of<FlwtchState>(context, listen: false).cwtch.IsManagedGroupsCompiled() &&
110 0 : Provider.of<Settings>(context).isExperimentEnabled(GroupManagerExperiment) &&
111 0 : !Platform.isAndroid &&
112 0 : !Platform.isIOS) {
113 0 : actions.add(
114 0 : IconButton(icon: Icon(Icons.diversity_1), splashRadius: Material.defaultSplashRadius / 2, tooltip: AppLocalizations.of(context)!.hybridGroupsManagerTitleShort, onPressed: _pushHybridGroups),
115 : );
116 : }
117 :
118 : // Servers
119 0 : if (Provider.of<FlwtchState>(context, listen: false).cwtch.IsServersCompiled() &&
120 0 : Provider.of<Settings>(context).isExperimentEnabled(ServerManagementExperiment) &&
121 0 : !Platform.isAndroid &&
122 0 : !Platform.isIOS) {
123 0 : actions.add(
124 0 : IconButton(icon: Icon(CwtchIcons.dns_24px), splashRadius: Material.defaultSplashRadius / 2, tooltip: AppLocalizations.of(context)!.serversManagerTitleShort, onPressed: _pushServers),
125 : );
126 : }
127 :
128 : // Global Settings
129 0 : actions.add(
130 0 : IconButton(
131 0 : key: Key("OpenSettingsView"),
132 0 : icon: Icon(Icons.settings),
133 0 : tooltip: AppLocalizations.of(context)!.tooltipOpenSettings,
134 0 : splashRadius: Material.defaultSplashRadius / 2,
135 0 : onPressed: _pushGlobalSettings,
136 : ),
137 : );
138 :
139 : // shutdown cwtch
140 0 : actions.add(
141 0 : IconButton(
142 0 : key: Key("shutdownCwtch"),
143 0 : icon: Icon(Icons.close),
144 0 : tooltip: AppLocalizations.of(context)!.shutdownCwtchTooltip,
145 0 : splashRadius: Material.defaultSplashRadius / 2,
146 0 : onPressed: _modalShutdown,
147 : ),
148 : );
149 :
150 : return actions;
151 : }
152 :
153 0 : void _modalShutdown() {
154 0 : Provider.of<FlwtchState>(context, listen: false).modalShutdown(MethodCall(""));
155 : }
156 :
157 0 : void _pushGlobalSettings() {
158 0 : Navigator.of(context).push(
159 0 : PageRouteBuilder(
160 0 : pageBuilder: (bcontext, a1, a2) {
161 0 : return Provider(create: (_) => Provider.of<FlwtchState>(bcontext, listen: false), child: GlobalSettingsView());
162 : },
163 0 : transitionsBuilder: (c, anim, a2, child) => FadeTransition(opacity: anim, child: child),
164 0 : transitionDuration: Duration(milliseconds: 200),
165 : ),
166 : );
167 : }
168 :
169 0 : void _pushHybridGroups() {
170 0 : Navigator.of(context).push(
171 0 : PageRouteBuilder(
172 0 : settings: RouteSettings(name: "hybridgroups"),
173 0 : pageBuilder: (bcontext, a1, a2) {
174 0 : return MultiProvider(
175 0 : providers: [
176 0 : ChangeNotifierProvider.value(value: globalHybridGroupsList),
177 0 : Provider.value(value: Provider.of<FlwtchState>(context)),
178 : ],
179 0 : child: HybridGroupsView(),
180 : );
181 : },
182 0 : transitionsBuilder: (c, anim, a2, child) => FadeTransition(opacity: anim, child: child),
183 0 : transitionDuration: Duration(milliseconds: 200),
184 : ),
185 : );
186 : }
187 :
188 0 : void _pushServers() {
189 0 : Navigator.of(context).push(
190 0 : PageRouteBuilder(
191 0 : settings: RouteSettings(name: "servers"),
192 0 : pageBuilder: (bcontext, a1, a2) {
193 0 : return MultiProvider(
194 0 : providers: [
195 0 : ChangeNotifierProvider.value(value: globalServersList),
196 0 : Provider.value(value: Provider.of<FlwtchState>(context)),
197 : ],
198 0 : child: ServersView(),
199 : );
200 : },
201 0 : transitionsBuilder: (c, anim, a2, child) => FadeTransition(opacity: anim, child: child),
202 0 : transitionDuration: Duration(milliseconds: 200),
203 : ),
204 : );
205 : }
206 :
207 0 : void _pushTorStatus() {
208 0 : Navigator.of(context).push(
209 0 : PageRouteBuilder(
210 0 : settings: RouteSettings(name: "torconfig"),
211 0 : pageBuilder: (bcontext, a1, a2) {
212 0 : return MultiProvider(
213 0 : providers: [Provider.value(value: Provider.of<FlwtchState>(context))],
214 0 : child: TorStatusView(),
215 : );
216 : },
217 0 : transitionsBuilder: (c, anim, a2, child) => FadeTransition(opacity: anim, child: child),
218 0 : transitionDuration: Duration(milliseconds: 200),
219 : ),
220 : );
221 : }
222 :
223 0 : void _pushAddProfile(bcontext, {onion = ""}) {
224 0 : Navigator.popUntil(bcontext, (route) => route.isFirst);
225 :
226 0 : Navigator.of(context).push(
227 0 : PageRouteBuilder(
228 0 : pageBuilder: (bcontext, a1, a2) {
229 0 : return MultiProvider(
230 0 : providers: [ChangeNotifierProvider<ProfileInfoState>(create: (_) => ProfileInfoState(onion: onion))],
231 0 : builder: (context, widget) => AddEditProfileView(key: Key('addprofile')),
232 : );
233 : },
234 0 : transitionsBuilder: (c, anim, a2, child) => FadeTransition(opacity: anim, child: child),
235 0 : transitionDuration: Duration(milliseconds: 200),
236 : ),
237 : );
238 : }
239 :
240 0 : void _modalAddImportProfiles() {
241 0 : showModalBottomSheet<void>(
242 0 : context: context,
243 : isScrollControlled: true,
244 0 : builder: (BuildContext context) {
245 0 : return Padding(
246 0 : padding: MediaQuery.of(context).viewInsets,
247 0 : child: RepaintBoundary(
248 0 : child: Container(
249 0 : height: Platform.isAndroid ? 250 : 200, // bespoke value courtesy of the [TextField] docs
250 0 : child: Center(
251 0 : child: Padding(
252 0 : padding: EdgeInsets.all(10.0),
253 0 : child: Column(
254 : mainAxisAlignment: MainAxisAlignment.center,
255 : crossAxisAlignment: CrossAxisAlignment.center,
256 : mainAxisSize: MainAxisSize.min,
257 0 : children: <Widget>[
258 0 : SizedBox(height: 20),
259 0 : Expanded(
260 0 : child: ElevatedButton(
261 0 : style: ElevatedButton.styleFrom(
262 0 : minimumSize: Size(399, 20),
263 0 : maximumSize: Size(400, 20),
264 0 : shape: RoundedRectangleBorder(
265 0 : borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180)),
266 : ),
267 : ),
268 0 : child: Text(
269 0 : key: Key("addNewProfileActual"),
270 0 : AppLocalizations.of(context)!.addProfileTitle,
271 0 : semanticsLabel: AppLocalizations.of(context)!.addProfileTitle,
272 0 : style: TextStyle(fontWeight: FontWeight.bold),
273 : ),
274 0 : onPressed: () {
275 0 : _pushAddProfile(context);
276 : },
277 : ),
278 : ),
279 0 : SizedBox(height: 20),
280 0 : Expanded(
281 0 : child: Tooltip(
282 0 : message: AppLocalizations.of(context)!.importProfileTooltip,
283 0 : child: OutlinedButton(
284 0 : style: ElevatedButton.styleFrom(
285 0 : minimumSize: Size(399, 20),
286 0 : maximumSize: Size(400, 20),
287 0 : shape: RoundedRectangleBorder(
288 0 : side: BorderSide(color: Provider.of<Settings>(context).theme.defaultButtonActiveColor, width: 2.0),
289 0 : borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180)),
290 : ),
291 : ),
292 0 : child: Text(AppLocalizations.of(context)!.importProfile, semanticsLabel: AppLocalizations.of(context)!.importProfile),
293 0 : onPressed: () {
294 : // 10GB profiles should be enough for anyone?
295 0 : showFilePicker(
296 : context,
297 : MaxGeneralFileSharingSize,
298 0 : (file) {
299 0 : showPasswordDialog(context, AppLocalizations.of(context)!.importProfile, AppLocalizations.of(context)!.importProfile, (password) {
300 0 : Navigator.popUntil(context, (route) => route.isFirst);
301 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.ImportProfile(file.path, password).then((value) {
302 0 : if (value == "") {
303 0 : final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.successfullyImportedProfile.replaceFirst("%profile", file.path)));
304 0 : ScaffoldMessenger.of(context).showSnackBar(snackBar);
305 : } else {
306 0 : final snackBar = SnackBar(content: Text(AppLocalizations.of(context)!.failedToImportProfile));
307 0 : ScaffoldMessenger.of(context).showSnackBar(snackBar);
308 : }
309 : });
310 : });
311 : },
312 0 : () {},
313 0 : () {},
314 : );
315 : },
316 : ),
317 : ),
318 : ),
319 0 : SizedBox(height: 20),
320 : ],
321 : ),
322 : ),
323 : ),
324 : ),
325 : ),
326 : );
327 : },
328 : );
329 : }
330 :
331 0 : void _modalUnlockProfiles() {
332 0 : showModalBottomSheet<void>(
333 0 : context: context,
334 : isScrollControlled: true,
335 0 : builder: (BuildContext context) {
336 0 : return Padding(
337 0 : padding: MediaQuery.of(context).viewInsets,
338 0 : child: RepaintBoundary(
339 0 : child: Container(
340 0 : height: Platform.isAndroid ? 250 : 200, // bespoke value courtesy of the [TextField] docs
341 0 : child: Center(
342 0 : child: Padding(
343 0 : padding: EdgeInsets.all(10.0),
344 0 : child: Column(
345 : mainAxisAlignment: MainAxisAlignment.center,
346 : mainAxisSize: MainAxisSize.min,
347 0 : children: <Widget>[
348 0 : Text(AppLocalizations.of(context)!.enterProfilePassword),
349 0 : SizedBox(height: 20),
350 0 : CwtchPasswordField(
351 0 : key: Key("unlockPasswordProfileElement"),
352 : autofocus: true,
353 0 : controller: ctrlrPassword,
354 0 : action: unlock,
355 0 : validator: (value) {
356 : return null;
357 : },
358 : ),
359 0 : SizedBox(height: 20),
360 0 : Row(
361 : mainAxisAlignment: MainAxisAlignment.spaceEvenly,
362 0 : children: [
363 0 : Spacer(),
364 0 : Expanded(
365 0 : child: ElevatedButton(
366 0 : child: Text(AppLocalizations.of(context)!.unlock, semanticsLabel: AppLocalizations.of(context)!.unlock),
367 0 : onPressed: () {
368 0 : unlock(ctrlrPassword.value.text);
369 : },
370 : ),
371 : ),
372 0 : Spacer(),
373 : ],
374 : ),
375 : ],
376 : ),
377 : ),
378 : ),
379 : ),
380 : ),
381 : );
382 : },
383 : );
384 : }
385 :
386 0 : void unlock(String password) {
387 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.LoadProfiles(password);
388 0 : ctrlrPassword.text = "";
389 0 : Navigator.pop(context);
390 : }
391 :
392 0 : Widget _buildProfileManager() {
393 0 : return Consumer<ProfileListState>(
394 0 : builder: (context, pls, child) {
395 0 : var tiles = pls.profiles.map((ProfileInfoState profile) {
396 0 : return ChangeNotifierProvider<ProfileInfoState>.value(value: profile, builder: (context, child) => ProfileRow());
397 : });
398 :
399 0 : List<ChangeNotifierProvider<ProfileInfoState>> widgetTiles = tiles.toList(growable: true);
400 0 : widgetTiles.add(
401 0 : ChangeNotifierProvider<ProfileInfoState>.value(
402 0 : value: ProfileInfoState(onion: ""),
403 0 : builder: (context, child) {
404 0 : return Container(
405 0 : margin: EdgeInsets.only(top: 20),
406 0 : width: MediaQuery.of(context).size.width,
407 0 : child: Row(
408 : mainAxisAlignment: MainAxisAlignment.spaceAround,
409 0 : children: [
410 0 : Tooltip(
411 0 : message: AppLocalizations.of(context)!.tooltipUnlockProfiles,
412 0 : child: FilledButton.icon(
413 0 : icon: Icon(CwtchIcons.lock_open_24px),
414 0 : style: TextButton.styleFrom(
415 0 : minimumSize: Size(MediaQuery.of(context).size.width * 0.79, 80),
416 0 : maximumSize: Size(MediaQuery.of(context).size.width * 0.8, 80),
417 0 : shape: RoundedRectangleBorder(
418 0 : borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180)),
419 : ),
420 : ),
421 0 : label: Text(
422 0 : AppLocalizations.of(context)!.unlock,
423 0 : semanticsLabel: AppLocalizations.of(context)!.unlock,
424 0 : style: TextStyle(fontWeight: FontWeight.bold),
425 : ),
426 0 : onPressed: () {
427 0 : _modalUnlockProfiles();
428 : },
429 : ),
430 : ),
431 : ],
432 : ),
433 : );
434 : },
435 : ),
436 : );
437 :
438 0 : final divided = ListTile.divideTiles(context: context, color: Provider.of<Settings>(context).theme.backgroundMainColor, tiles: widgetTiles).toList();
439 :
440 : // Display the welcome message / unlock profiles button to new accounts
441 0 : if (tiles.isEmpty) {
442 0 : return Center(
443 0 : child: Column(
444 : mainAxisSize: MainAxisSize.min,
445 : mainAxisAlignment: MainAxisAlignment.center,
446 : crossAxisAlignment: CrossAxisAlignment.center,
447 0 : children: [
448 0 : Text(AppLocalizations.of(context)!.unlockProfileTip, textAlign: TextAlign.center),
449 0 : Container(
450 0 : width: MediaQuery.of(context).size.width,
451 0 : margin: EdgeInsets.only(top: 20),
452 0 : child: Row(
453 : mainAxisAlignment: MainAxisAlignment.spaceAround,
454 0 : children: [
455 0 : Tooltip(
456 0 : message: AppLocalizations.of(context)!.addProfileTitle,
457 0 : child: FilledButton.icon(
458 0 : icon: Icon(Icons.add),
459 0 : style: TextButton.styleFrom(
460 0 : minimumSize: Size(MediaQuery.of(context).size.width * 0.79, 80),
461 0 : maximumSize: Size(MediaQuery.of(context).size.width * 0.8, 80),
462 0 : shape: RoundedRectangleBorder(
463 0 : borderRadius: BorderRadius.horizontal(left: Radius.circular(180), right: Radius.circular(180)),
464 : ),
465 : ),
466 0 : label: Text(
467 0 : AppLocalizations.of(context)!.addProfileTitle,
468 0 : semanticsLabel: AppLocalizations.of(context)!.addProfileTitle,
469 0 : style: TextStyle(fontWeight: FontWeight.bold),
470 : ),
471 0 : onPressed: () {
472 0 : _modalAddImportProfiles();
473 : },
474 : ),
475 : ),
476 : ],
477 : ),
478 : ),
479 0 : widgetTiles[0],
480 : ],
481 : ),
482 : );
483 : }
484 :
485 0 : return ListView(children: divided);
486 : },
487 : );
488 : }
489 : }
|