mirror of
https://gitlab.com/famedly/fluffychat.git
synced 2024-11-05 03:29:30 +01:00
Merge branch 'krille/nicer-encryption-page' into 'main'
design: New encryption page See merge request famedly/fluffychat!1059
This commit is contained in:
commit
1294479974
BIN
assets/encryption.png
Normal file
BIN
assets/encryption.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 51 KiB |
@ -2979,5 +2979,10 @@
|
||||
"oldDisplayName": {}
|
||||
}
|
||||
},
|
||||
"newSpaceDescription": "Spaces allows you to consolidate your chats and build private or public communities."
|
||||
"newSpaceDescription": "Spaces allows you to consolidate your chats and build private or public communities.",
|
||||
"encryptThisChat": "Encrypt this chat",
|
||||
"endToEndEncryption": "End to end encryption",
|
||||
"disableEncryptionWarning": "For security reasons you can not disable encryption in a chat, where it has been enabled before.",
|
||||
"sorryThatsNotPossible": "Sorry... that is not possible",
|
||||
"deviceKeys": "Device keys:"
|
||||
}
|
||||
|
@ -24,6 +24,8 @@ abstract class AppConfig {
|
||||
static String get privacyUrl => _privacyUrl;
|
||||
static const String enablePushTutorial =
|
||||
'https://gitlab.com/famedly/fluffychat/-/wikis/Push-Notifications-without-Google-Services';
|
||||
static const String encryptionTutorial =
|
||||
'https://gitlab.com/famedly/fluffychat/-/wikis/How-to-use-end-to-end-encryption-in-FluffyChat';
|
||||
static const String appId = 'im.fluffychat.FluffyChat';
|
||||
static const String appOpenUrlScheme = 'im.fluffychat';
|
||||
static String _webBaseUrl = 'https://fluffychat.im/web';
|
||||
|
@ -1,93 +1,45 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:vrouter/vrouter.dart';
|
||||
|
||||
import '../../widgets/matrix.dart';
|
||||
|
||||
class EncryptionButton extends StatefulWidget {
|
||||
class EncryptionButton extends StatelessWidget {
|
||||
final Room room;
|
||||
const EncryptionButton(this.room, {Key? key}) : super(key: key);
|
||||
@override
|
||||
EncryptionButtonState createState() => EncryptionButtonState();
|
||||
}
|
||||
|
||||
class EncryptionButtonState extends State<EncryptionButton> {
|
||||
StreamSubscription? _onSyncSub;
|
||||
|
||||
void _enableEncryptionAction() async {
|
||||
if (widget.room.encrypted) {
|
||||
VRouter.of(context).toSegments(['rooms', widget.room.id, 'encryption']);
|
||||
return;
|
||||
}
|
||||
if (widget.room.joinRules == JoinRules.public) {
|
||||
await showOkAlertDialog(
|
||||
useRootNavigator: false,
|
||||
context: context,
|
||||
okLabel: L10n.of(context)!.ok,
|
||||
message: L10n.of(context)!.noEncryptionForPublicRooms,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (await showOkCancelAlertDialog(
|
||||
useRootNavigator: false,
|
||||
context: context,
|
||||
title: L10n.of(context)!.enableEncryption,
|
||||
message: widget.room.client.encryptionEnabled
|
||||
? L10n.of(context)!.enableEncryptionWarning
|
||||
: L10n.of(context)!.needPantalaimonWarning,
|
||||
okLabel: L10n.of(context)!.yes,
|
||||
cancelLabel: L10n.of(context)!.cancel,
|
||||
) ==
|
||||
OkCancelResult.ok) {
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => widget.room.enableEncryption(),
|
||||
);
|
||||
// we want to enable the lock icon
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_onSyncSub?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.room.encrypted) {
|
||||
_onSyncSub ??= Matrix.of(context)
|
||||
.client
|
||||
.onSync
|
||||
.stream
|
||||
.where((s) => s.deviceLists != null)
|
||||
.listen((s) => setState(() {}));
|
||||
}
|
||||
return FutureBuilder<EncryptionHealthState>(
|
||||
future: widget.room.calcEncryptionHealthState(),
|
||||
builder: (BuildContext context, snapshot) => IconButton(
|
||||
tooltip: widget.room.encrypted
|
||||
? L10n.of(context)!.encrypted
|
||||
: L10n.of(context)!.encryptionNotEnabled,
|
||||
icon: Icon(
|
||||
widget.room.encrypted
|
||||
? Icons.lock_outlined
|
||||
: Icons.lock_open_outlined,
|
||||
size: 20,
|
||||
color: widget.room.joinRules != JoinRules.public &&
|
||||
!widget.room.encrypted
|
||||
? Colors.red
|
||||
: snapshot.data == EncryptionHealthState.unverifiedDevices
|
||||
? Colors.orange
|
||||
: null),
|
||||
onPressed: _enableEncryptionAction,
|
||||
));
|
||||
return StreamBuilder<SyncUpdate>(
|
||||
stream: Matrix.of(context)
|
||||
.client
|
||||
.onSync
|
||||
.stream
|
||||
.where((s) => s.deviceLists != null),
|
||||
builder: (context, snapshot) {
|
||||
return FutureBuilder<EncryptionHealthState>(
|
||||
future: room.calcEncryptionHealthState(),
|
||||
builder: (BuildContext context, snapshot) => IconButton(
|
||||
tooltip: room.encrypted
|
||||
? L10n.of(context)!.encrypted
|
||||
: L10n.of(context)!.encryptionNotEnabled,
|
||||
icon: Icon(
|
||||
room.encrypted
|
||||
? Icons.lock_outlined
|
||||
: Icons.lock_open_outlined,
|
||||
size: 20,
|
||||
color: room.joinRules != JoinRules.public &&
|
||||
!room.encrypted
|
||||
? Colors.red
|
||||
: snapshot.data ==
|
||||
EncryptionHealthState.unverifiedDevices
|
||||
? Colors.orange
|
||||
: null),
|
||||
onPressed: () => VRouter.of(context)
|
||||
.toSegments(['rooms', room.id, 'encryption']),
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:matrix/encryption.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:vrouter/vrouter.dart';
|
||||
@ -19,49 +22,68 @@ class ChatEncryptionSettings extends StatefulWidget {
|
||||
class ChatEncryptionSettingsController extends State<ChatEncryptionSettings> {
|
||||
String? get roomId => VRouter.of(context).pathParameters['roomid'];
|
||||
|
||||
Room get room => Matrix.of(context).client.getRoomById(roomId!)!;
|
||||
|
||||
Future<void> unblock(DeviceKeys key) async {
|
||||
if (key.blocked) {
|
||||
await key.setBlocked(false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> onSelected(
|
||||
BuildContext context, String action, DeviceKeys key) async {
|
||||
final room = Matrix.of(context).client.getRoomById(roomId!);
|
||||
switch (action) {
|
||||
case 'verify':
|
||||
await unblock(key);
|
||||
final req = key.startVerification();
|
||||
req.onUpdate = () {
|
||||
if (req.state == KeyVerificationState.done) {
|
||||
setState(() {});
|
||||
}
|
||||
};
|
||||
await KeyVerificationDialog(request: req).show(context);
|
||||
break;
|
||||
case 'verify_user':
|
||||
await unblock(key);
|
||||
final req =
|
||||
await room!.client.userDeviceKeys[key.userId]!.startVerification();
|
||||
req.onUpdate = () {
|
||||
if (req.state == KeyVerificationState.done) {
|
||||
setState(() {});
|
||||
}
|
||||
};
|
||||
await KeyVerificationDialog(request: req).show(context);
|
||||
break;
|
||||
case 'block':
|
||||
if (key.directVerified) {
|
||||
await key.setVerified(false);
|
||||
}
|
||||
await key.setBlocked(true);
|
||||
setState(() {});
|
||||
break;
|
||||
case 'unblock':
|
||||
await unblock(key);
|
||||
setState(() {});
|
||||
break;
|
||||
void enableEncryption(_) async {
|
||||
if (room.encrypted) {
|
||||
showOkAlertDialog(
|
||||
context: context,
|
||||
title: L10n.of(context)!.sorryThatsNotPossible,
|
||||
message: L10n.of(context)!.disableEncryptionWarning,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (room.joinRules == JoinRules.public) {
|
||||
showOkAlertDialog(
|
||||
context: context,
|
||||
title: L10n.of(context)!.sorryThatsNotPossible,
|
||||
message: L10n.of(context)!.noEncryptionForPublicRooms,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!room.canChangeStateEvent(EventTypes.Encryption)) {
|
||||
showOkAlertDialog(
|
||||
context: context,
|
||||
title: L10n.of(context)!.sorryThatsNotPossible,
|
||||
message: L10n.of(context)!.noPermission,
|
||||
);
|
||||
return;
|
||||
}
|
||||
final consent = await showOkCancelAlertDialog(
|
||||
context: context,
|
||||
title: L10n.of(context)!.areYouSure,
|
||||
message: L10n.of(context)!.enableEncryptionWarning,
|
||||
okLabel: L10n.of(context)!.yes,
|
||||
cancelLabel: L10n.of(context)!.cancel,
|
||||
);
|
||||
if (consent != OkCancelResult.ok) return;
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => room.enableEncryption(),
|
||||
);
|
||||
}
|
||||
|
||||
void startVerification() async {
|
||||
final req = await room.client.userDeviceKeys[room.directChatMatrixID]!
|
||||
.startVerification();
|
||||
req.onUpdate = () {
|
||||
if (req.state == KeyVerificationState.done) {
|
||||
setState(() {});
|
||||
}
|
||||
};
|
||||
await KeyVerificationDialog(request: req).show(context);
|
||||
}
|
||||
|
||||
void toggleDeviceKey(DeviceKeys key) {
|
||||
setState(() {
|
||||
key.setBlocked(!key.blocked);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -2,14 +2,11 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:vrouter/vrouter.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pages/chat_encryption_settings/chat_encryption_settings.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import '../../utils/matrix_sdk_extensions.dart/device_extension.dart';
|
||||
import '../../widgets/m2_popup_menu_button.dart';
|
||||
|
||||
class ChatEncryptionSettingsView extends StatelessWidget {
|
||||
final ChatEncryptionSettingsController controller;
|
||||
@ -19,184 +16,148 @@ class ChatEncryptionSettingsView extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final room = Matrix.of(context).client.getRoomById(controller.roomId!)!;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.close_outlined),
|
||||
onPressed: () =>
|
||||
VRouter.of(context).toSegments(['rooms', controller.roomId!]),
|
||||
),
|
||||
title: Text(L10n.of(context)!.tapOnDeviceToVerify),
|
||||
elevation: 0,
|
||||
),
|
||||
body: MaxWidthBody(
|
||||
withScrolling: true,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(L10n.of(context)!.deviceVerifyDescription),
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: Theme.of(context).secondaryHeaderColor,
|
||||
foregroundColor: Theme.of(context).colorScheme.secondary,
|
||||
child: const Icon(Icons.lock),
|
||||
final room = controller.room;
|
||||
return StreamBuilder<Object>(
|
||||
stream: room.client.onSync.stream.where(
|
||||
(s) => s.rooms?.join?[room.id] != null || s.deviceLists != null),
|
||||
builder: (context, _) => Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.close_outlined),
|
||||
onPressed: () => VRouter.of(context)
|
||||
.toSegments(['rooms', controller.roomId!]),
|
||||
),
|
||||
title: Text(L10n.of(context)!.endToEndEncryption),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => launch(AppConfig.encryptionTutorial),
|
||||
child: Text(L10n.of(context)!.help),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
StreamBuilder(
|
||||
stream: room.onUpdate.stream,
|
||||
builder: (context, snapshot) {
|
||||
return FutureBuilder<List<DeviceKeys>>(
|
||||
future: room.getUserDeviceKeys(),
|
||||
builder: (BuildContext context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return Center(
|
||||
child: Text(
|
||||
'${L10n.of(context)!.oopsSomethingWentWrong}: ${snapshot.error}'),
|
||||
);
|
||||
}
|
||||
if (!snapshot.hasData) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator.adaptive(
|
||||
strokeWidth: 2));
|
||||
}
|
||||
final deviceKeys = snapshot.data!;
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: deviceKeys.length,
|
||||
itemBuilder: (BuildContext context, int i) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
if (i == 0 ||
|
||||
deviceKeys[i].userId !=
|
||||
deviceKeys[i - 1].userId) ...{
|
||||
const Divider(height: 1, thickness: 1),
|
||||
M2PopupMenuButton(
|
||||
onSelected: (dynamic action) => controller
|
||||
.onSelected(context, action, deviceKeys[i]),
|
||||
itemBuilder: (c) {
|
||||
final items = <PopupMenuEntry<String>>[];
|
||||
if (room
|
||||
.client
|
||||
.userDeviceKeys[deviceKeys[i].userId]!
|
||||
.verified ==
|
||||
UserVerifiedStatus.unknown) {
|
||||
items.add(PopupMenuItem(
|
||||
value: 'verify_user',
|
||||
child: Text(L10n.of(context)!.verifyUser),
|
||||
));
|
||||
}
|
||||
return items;
|
||||
},
|
||||
child: ListTile(
|
||||
leading: Avatar(
|
||||
mxContent: room
|
||||
.unsafeGetUserFromMemoryOrFallback(
|
||||
deviceKeys[i].userId)
|
||||
.avatarUrl,
|
||||
name: room
|
||||
.unsafeGetUserFromMemoryOrFallback(
|
||||
deviceKeys[i].userId)
|
||||
.calcDisplayname(),
|
||||
),
|
||||
title: Text(
|
||||
room
|
||||
.unsafeGetUserFromMemoryOrFallback(
|
||||
deviceKeys[i].userId)
|
||||
.calcDisplayname(),
|
||||
),
|
||||
subtitle: Text(
|
||||
deviceKeys[i].userId,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w300),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
M2PopupMenuButton(
|
||||
onSelected: (dynamic action) => controller
|
||||
.onSelected(context, action, deviceKeys[i]),
|
||||
itemBuilder: (c) {
|
||||
final items = <PopupMenuEntry<String>>[];
|
||||
if (deviceKeys[i].blocked ||
|
||||
!deviceKeys[i].verified) {
|
||||
items.add(PopupMenuItem(
|
||||
value: deviceKeys[i].userId ==
|
||||
room.client.userID
|
||||
? 'verify'
|
||||
: 'verify_user',
|
||||
child: Text(L10n.of(context)!.verifyStart),
|
||||
));
|
||||
}
|
||||
if (deviceKeys[i].blocked) {
|
||||
items.add(PopupMenuItem(
|
||||
value: 'unblock',
|
||||
child:
|
||||
Text(L10n.of(context)!.unblockDevice),
|
||||
));
|
||||
}
|
||||
if (!deviceKeys[i].blocked) {
|
||||
items.add(PopupMenuItem(
|
||||
value: 'block',
|
||||
child: Text(L10n.of(context)!.blockDevice),
|
||||
));
|
||||
}
|
||||
return items;
|
||||
},
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: deviceKeys[i].color,
|
||||
child: Icon(deviceKeys[i].icon),
|
||||
),
|
||||
title: Text(
|
||||
deviceKeys[i].displayname,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: Row(
|
||||
children: [
|
||||
Text(
|
||||
deviceKeys[i].deviceId!,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w300),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
deviceKeys[i].blocked
|
||||
? L10n.of(context)!.blocked
|
||||
: deviceKeys[i].verified
|
||||
? L10n.of(context)!.verified
|
||||
: L10n.of(context)!.unverified,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: deviceKeys[i].color,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
body: ListView(
|
||||
children: [
|
||||
SwitchListTile(
|
||||
secondary: CircleAvatar(
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.primaryContainer,
|
||||
child: const Icon(Icons.lock_outlined)),
|
||||
title: Text(L10n.of(context)!.encryptThisChat),
|
||||
value: room.encrypted,
|
||||
onChanged: controller.enableEncryption,
|
||||
),
|
||||
Center(
|
||||
child: Image.asset(
|
||||
'assets/encryption.png',
|
||||
width: 212,
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
if (room.isDirectChat)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Center(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: controller.startVerification,
|
||||
icon: const Icon(Icons.verified_outlined),
|
||||
label: Text(L10n.of(context)!.verifyStart),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
),
|
||||
),
|
||||
if (room.encrypted) ...[
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
title: Text(
|
||||
L10n.of(context)!.deviceKeys,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
StreamBuilder(
|
||||
stream: room.onUpdate.stream,
|
||||
builder: (context, snapshot) =>
|
||||
FutureBuilder<List<DeviceKeys>>(
|
||||
future: room.getUserDeviceKeys(),
|
||||
builder: (BuildContext context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return Center(
|
||||
child: Text(
|
||||
'${L10n.of(context)!.oopsSomethingWentWrong}: ${snapshot.error}'),
|
||||
);
|
||||
}
|
||||
if (!snapshot.hasData) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator.adaptive(
|
||||
strokeWidth: 2));
|
||||
}
|
||||
final deviceKeys = snapshot.data!;
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: deviceKeys.length,
|
||||
itemBuilder: (BuildContext context, int i) =>
|
||||
SwitchListTile(
|
||||
value: !deviceKeys[i].blocked,
|
||||
activeColor: deviceKeys[i].verified
|
||||
? Colors.green
|
||||
: Colors.orange,
|
||||
onChanged: (_) => controller
|
||||
.toggleDeviceKey(deviceKeys[i]),
|
||||
title: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
deviceKeys[i].deviceId ??
|
||||
L10n.of(context)!.unknownDevice,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 4.0),
|
||||
child: Chip(
|
||||
label: Text(
|
||||
deviceKeys[i].userId,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Text(
|
||||
deviceKeys[i]
|
||||
.ed25519Key
|
||||
?.replaceAllMapped(
|
||||
RegExp(r'.{4}'),
|
||||
(s) => '${s.group(0)} ') ??
|
||||
L10n.of(context)!
|
||||
.unknownEncryptionAlgorithm,
|
||||
style:
|
||||
const TextStyle(fontFamily: 'Mono'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
] else
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Center(
|
||||
child: Text(
|
||||
L10n.of(context)!.encryptionNotEnabled,
|
||||
style: const TextStyle(
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
extension on DeviceKeys {
|
||||
Color get color => blocked
|
||||
? Colors.red
|
||||
: verified
|
||||
? Colors.green
|
||||
: Colors.orange;
|
||||
}
|
||||
|
@ -64,6 +64,7 @@ class FluffyChatAppState extends State<FluffyChatApp> {
|
||||
return VRouter(
|
||||
key: FluffyChatApp.routerKey,
|
||||
title: AppConfig.applicationName,
|
||||
debugShowCheckedModeBanner: false,
|
||||
themeMode: themeMode,
|
||||
theme: FluffyThemes.buildTheme(Brightness.light, primaryColor),
|
||||
darkTheme: FluffyThemes.buildTheme(Brightness.dark, primaryColor),
|
||||
|
Loading…
Reference in New Issue
Block a user