import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'package:Capsule/components/file_picker.dart'; import 'package:Capsule/components/flash_message.dart'; import 'package:Capsule/utils/encryption/aes_helper.dart'; import 'package:Capsule/utils/storage/session_cookie.dart'; import 'package:Capsule/utils/storage/write_file.dart'; import 'package:flutter/material.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:image_picker/image_picker.dart'; import 'package:mime/mime.dart'; import 'package:qr_flutter/qr_flutter.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:sliding_up_panel/sliding_up_panel.dart'; import 'package:http/http.dart' as http; import '/components/select_message_ttl.dart'; import '/components/custom_circle_avatar.dart'; import '/components/custom_title_bar.dart'; import '/models/my_profile.dart'; import '/utils/encryption/crypto_utils.dart'; import '/utils/storage/database.dart'; import '/views/main/profile/change_password.dart'; import '/views/main/profile/change_server_url.dart'; class Profile extends StatefulWidget { final MyProfile profile; const Profile({ Key? key, required this.profile, }) : super(key: key); @override State createState() => _ProfileState(); } class _ProfileState extends State { final PanelController _panelController = PanelController(); bool showFileSelector = false; @override Widget build(BuildContext context) { return Scaffold( appBar: const CustomTitleBar( title: Text( 'Profile', style: TextStyle( fontSize: 32, fontWeight: FontWeight.bold ) ), showBack: false, backgroundColor: Colors.transparent, ), body: SlidingUpPanel( controller: _panelController, slideDirection: SlideDirection.DOWN, defaultPanelState: PanelState.CLOSED, color: Theme.of(context).scaffoldBackgroundColor, backdropTapClosesPanel: true, backdropEnabled: true, backdropOpacity: 0.2, minHeight: 0, maxHeight: 450, panel: Center( child: _profileQrCode(), ), body: Padding( padding: const EdgeInsets.only(top: 16,left: 16,right: 16), child: Column( children: [ usernameHeading(), fileSelector(), SizedBox(height: showFileSelector ? 10 : 30), settings(), const SizedBox(height: 30), logout(), ], ) ), ), ); } Widget usernameHeading() { return Row( children: [ CustomCircleAvatar( image: widget.profile.image, icon: const Icon(Icons.person, size: 40), radius: 30, editImageCallback: () { setState(() { showFileSelector = true; }); }, ), const SizedBox(width: 20), Expanded( flex: 1, child: Text( widget.profile.username, style: const TextStyle( fontSize: 25, fontWeight: FontWeight.w500, ), ), ), IconButton( onPressed: () => _panelController.open(), icon: const Icon(Icons.qr_code_2), ), ], ); } Widget fileSelector() { if (!showFileSelector) { return const SizedBox.shrink(); } return Padding( padding: const EdgeInsets.only(top: 10), child: FilePicker( cameraHandle: _setProfileImage, galleryHandleSingle: _setProfileImage, ) ); } Future _setProfileImage(XFile image) async { widget.profile.image = await writeImage( widget.profile.id, File(image.path).readAsBytesSync(), ); setState(() { showFileSelector = false; }); saveProfile(); Map payload = { 'data': AesHelper.aesEncrypt( widget.profile.symmetricKey!, Uint8List.fromList(widget.profile.image!.readAsBytesSync()) ), 'mimetype': lookupMimeType(widget.profile.image!.path), 'extension': getExtension(widget.profile.image!.path), }; http.post( await MyProfile.getServerUrl('api/v1/auth/image'), headers: { 'cookie': await getSessionCookie(), }, body: jsonEncode(payload), ).then((http.Response response) { if (response.statusCode == 204) { return; } showMessage( 'Could not change your default message expiry, please try again later.', context, ); }); } Widget logout() { bool isTesting = dotenv.env['ENVIRONMENT'] == 'development'; return Align( alignment: Alignment.centerLeft, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ TextButton.icon( label: const Text( 'Logout', style: TextStyle(fontSize: 16) ), icon: const Icon(Icons.exit_to_app), style: const ButtonStyle( alignment: Alignment.centerLeft, ), onPressed: () { deleteDb(); MyProfile.logout(); Navigator.pushNamedAndRemoveUntil(context, '/landing', ModalRoute.withName('/landing')); } ), isTesting ? TextButton.icon( label: const Text( 'Delete Database', style: TextStyle(fontSize: 16) ), icon: const Icon(Icons.delete_forever), style: const ButtonStyle( alignment: Alignment.centerLeft, ), onPressed: () { deleteDb(); } ) : const SizedBox.shrink(), ], ), ); } Widget settings() { return Align( alignment: Alignment.centerLeft, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const SizedBox(height: 5), TextButton.icon( label: const Text( 'Disappearing Messages', style: TextStyle(fontSize: 16) ), icon: const Icon(Icons.timer), style: ButtonStyle( alignment: Alignment.centerLeft, foregroundColor: MaterialStateProperty.resolveWith( (Set states) { return Theme.of(context).colorScheme.onBackground; }, ) ), onPressed: () { Navigator.of(context).push( MaterialPageRoute(builder: (context) => SelectMessageTTL( widgetTitle: 'Message Expiry', currentSelected: widget.profile.messageExpiryDefault, backCallback: (String messageExpiry) async { widget.profile.messageExpiryDefault = messageExpiry; http.post( await MyProfile.getServerUrl('api/v1/auth/message_expiry'), headers: { 'cookie': await getSessionCookie(), }, body: jsonEncode({ 'message_expiry': messageExpiry, }), ).then((http.Response response) { if (response.statusCode == 200) { return; } showMessage( 'Could not change your default message expiry, please try again later.', context, ); }); saveProfile(); }, )) ); } ), const SizedBox(height: 5), TextButton.icon( label: const Text( 'Server URL', style: TextStyle(fontSize: 16) ), icon: const Icon(Icons.dataset_linked_outlined), style: ButtonStyle( alignment: Alignment.centerLeft, foregroundColor: MaterialStateProperty.resolveWith( (Set states) { return Theme.of(context).colorScheme.onBackground; }, ) ), onPressed: () { Navigator.of(context).push( MaterialPageRoute(builder: (context) => const ChangeServerUrl()) ); } ), const SizedBox(height: 5), TextButton.icon( label: const Text( 'Change Password', style: TextStyle(fontSize: 16) ), icon: const Icon(Icons.password), style: ButtonStyle( alignment: Alignment.centerLeft, foregroundColor: MaterialStateProperty.resolveWith( (Set states) { return Theme.of(context).colorScheme.onBackground; }, ) ), onPressed: () { Navigator.of(context).push( MaterialPageRoute(builder: (context) => ChangePassword( privateKey: widget.profile.privateKey!, )) ); saveProfile(); } ), ], ), ); } Widget _profileQrCode() { String payload = jsonEncode({ 'i': widget.profile.id, 'u': widget.profile.username, 'k': base64.encode( CryptoUtils.encodeRSAPublicKeyToPem(widget.profile.publicKey!).codeUnits ), }); return Column( children: [ Padding( padding: const EdgeInsets.all(20), child: QrImage( backgroundColor: Theme.of(context).colorScheme.primary, data: payload, version: QrVersions.auto, gapless: true, ), ), Align( alignment: Alignment.centerRight, child: Padding( padding: const EdgeInsets.only(right: 20), child: IconButton( onPressed: () => _panelController.close(), icon: const Icon(Icons.arrow_upward), ), ), ), ] ); } Future saveProfile() async { final preferences = await SharedPreferences.getInstance(); preferences.setString('profile', widget.profile.toJson()); } }