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