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<Profile> createState() => _ProfileState();
|
|
}
|
|
|
|
class _ProfileState extends State<Profile> {
|
|
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: <Widget>[
|
|
usernameHeading(),
|
|
fileSelector(),
|
|
SizedBox(height: showFileSelector ? 10 : 30),
|
|
settings(),
|
|
const SizedBox(height: 30),
|
|
logout(),
|
|
],
|
|
)
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget usernameHeading() {
|
|
return Row(
|
|
children: <Widget> [
|
|
|
|
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<void> _setProfileImage(XFile image) async {
|
|
widget.profile.image = await writeImage(
|
|
widget.profile.id,
|
|
File(image.path).readAsBytesSync(),
|
|
);
|
|
|
|
setState(() {
|
|
showFileSelector = false;
|
|
});
|
|
|
|
saveProfile();
|
|
|
|
Map<String, dynamic> 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<Color>(
|
|
(Set<MaterialState> 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<Color>(
|
|
(Set<MaterialState> 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<Color>(
|
|
(Set<MaterialState> 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<void> saveProfile() async {
|
|
final preferences = await SharedPreferences.getInstance();
|
|
preferences.setString('profile', widget.profile.toJson());
|
|
}
|
|
}
|