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