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