design: Column mode auto padding

This commit is contained in:
Christian Pauly 2021-04-09 18:26:44 +02:00
parent 0146767e8a
commit 75258271af
13 changed files with 1344 additions and 1239 deletions

View File

@ -10,6 +10,7 @@ import 'package:famedlysdk/famedlysdk.dart';
import 'package:file_picker_cross/file_picker_cross.dart'; import 'package:file_picker_cross/file_picker_cross.dart';
import 'package:fluffychat/views/widgets/chat_settings_popup_menu.dart'; import 'package:fluffychat/views/widgets/chat_settings_popup_menu.dart';
import 'package:fluffychat/views/widgets/content_banner.dart'; import 'package:fluffychat/views/widgets/content_banner.dart';
import 'package:fluffychat/views/widgets/max_width_body.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:fluffychat/views/widgets/list_items/participant_list_item.dart'; import 'package:fluffychat/views/widgets/list_items/participant_list_item.dart';
import 'package:fluffychat/utils/matrix_locals.dart'; import 'package:fluffychat/utils/matrix_locals.dart';
@ -234,223 +235,188 @@ class _ChatDetailsState extends State<ChatDetails> {
), ),
), ),
], ],
body: ListView.builder( body: MaxWidthBody(
itemCount: members.length + 1 + (canRequestMoreMembers ? 1 : 0), child: ListView.builder(
itemBuilder: (BuildContext context, int i) => i == 0 itemCount:
? Column( members.length + 1 + (canRequestMoreMembers ? 1 : 0),
crossAxisAlignment: CrossAxisAlignment.stretch, itemBuilder: (BuildContext context, int i) => i == 0
children: <Widget>[ ? Column(
ListTile( crossAxisAlignment: CrossAxisAlignment.stretch,
leading: room.canSendEvent('m.room.topic') children: <Widget>[
? CircleAvatar(
backgroundColor: Theme.of(context)
.scaffoldBackgroundColor,
foregroundColor: Colors.grey,
radius: Avatar.defaultSize / 2,
child: Icon(Icons.edit_outlined),
)
: null,
title: Text('${L10n.of(context).groupDescription}:',
style: TextStyle(
color: Theme.of(context).accentColor,
fontWeight: FontWeight.bold)),
subtitle: LinkText(
text: room.topic?.isEmpty ?? true
? L10n.of(context).addGroupDescription
: room.topic,
linkStyle: TextStyle(color: Colors.blueAccent),
textStyle: TextStyle(
fontSize: 14,
color:
Theme.of(context).textTheme.bodyText2.color,
),
onLinkTap: (url) =>
UrlLauncher(context, url).launchUrl(),
),
onTap: room.canSendEvent('m.room.topic')
? () => setTopicAction(context)
: null,
),
Divider(thickness: 1),
ListTile(
title: Text(
L10n.of(context).settings,
style: TextStyle(
color: Theme.of(context).accentColor,
fontWeight: FontWeight.bold,
),
),
),
if (room.canSendEvent('m.room.name'))
ListTile( ListTile(
leading: CircleAvatar( leading: room.canSendEvent('m.room.topic')
backgroundColor: ? CircleAvatar(
Theme.of(context).scaffoldBackgroundColor, backgroundColor: Theme.of(context)
foregroundColor: Colors.grey, .scaffoldBackgroundColor,
child: Icon(Icons.people_outlined), foregroundColor: Colors.grey,
), radius: Avatar.defaultSize / 2,
child: Icon(Icons.edit_outlined),
)
: null,
title: Text( title: Text(
L10n.of(context).changeTheNameOfTheGroup), '${L10n.of(context).groupDescription}:',
subtitle: Text(room.getLocalizedDisplayname( style: TextStyle(
MatrixLocals(L10n.of(context)))), color: Theme.of(context).accentColor,
onTap: () => setDisplaynameAction(context), fontWeight: FontWeight.bold)),
), subtitle: LinkText(
if (room.canSendEvent('m.room.canonical_alias') && text: room.topic?.isEmpty ?? true
room.joinRules == JoinRules.public) ? L10n.of(context).addGroupDescription
ListTile( : room.topic,
leading: CircleAvatar( linkStyle: TextStyle(color: Colors.blueAccent),
backgroundColor: textStyle: TextStyle(
Theme.of(context).scaffoldBackgroundColor, fontSize: 14,
foregroundColor: Colors.grey, color: Theme.of(context)
child: Icon(Icons.link_outlined), .textTheme
.bodyText2
.color,
),
onLinkTap: (url) =>
UrlLauncher(context, url).launchUrl(),
), ),
onTap: () => setCanonicalAliasAction(context), onTap: room.canSendEvent('m.room.topic')
title: Text(L10n.of(context).setInvitationLink), ? () => setTopicAction(context)
subtitle: Text( : null,
(room.canonicalAlias?.isNotEmpty ?? false)
? room.canonicalAlias
: L10n.of(context).none),
), ),
ListTile( Divider(thickness: 1),
leading: CircleAvatar( ListTile(
backgroundColor: title: Text(
Theme.of(context).scaffoldBackgroundColor, L10n.of(context).settings,
foregroundColor: Colors.grey, style: TextStyle(
child: Icon(Icons.insert_emoticon_outlined), color: Theme.of(context).accentColor,
), fontWeight: FontWeight.bold,
title: Text(L10n.of(context).emoteSettings),
subtitle: Text(L10n.of(context).setCustomEmotes),
onTap: () async {
// okay, we need to test if there are any emote state events other than the default one
// if so, we need to be directed to a selection screen for which pack we want to look at
// otherwise, we just open the normal one.
if ((room.states['im.ponies.room_emotes'] ??
<String, Event>{})
.keys
.any((String s) => s.isNotEmpty)) {
await AdaptivePageLayout.of(context)
.pushNamed('/rooms/${room.id}/emotes');
} else {
await AdaptivePageLayout.of(context).pushNamed(
'/settings/emotes',
arguments: {'room': room});
}
},
),
PopupMenuButton(
onSelected: (JoinRules joinRule) =>
showFutureLoadingDialog(
context: context,
future: () => room.setJoinRules(joinRule),
),
itemBuilder: (BuildContext context) =>
<PopupMenuEntry<JoinRules>>[
if (room.canChangeJoinRules)
PopupMenuItem<JoinRules>(
value: JoinRules.public,
child: Text(JoinRules.public
.getLocalizedString(
MatrixLocals(L10n.of(context)))),
), ),
if (room.canChangeJoinRules) ),
PopupMenuItem<JoinRules>( ),
value: JoinRules.invite, if (room.canSendEvent('m.room.name'))
child: Text(JoinRules.invite ListTile(
.getLocalizedString( leading: CircleAvatar(
MatrixLocals(L10n.of(context)))),
),
],
child: ListTile(
leading: CircleAvatar(
backgroundColor: backgroundColor:
Theme.of(context).scaffoldBackgroundColor, Theme.of(context).scaffoldBackgroundColor,
foregroundColor: Colors.grey, foregroundColor: Colors.grey,
child: Icon(Icons.public_outlined)), child: Icon(Icons.people_outlined),
title: Text( ),
L10n.of(context).whoIsAllowedToJoinThisGroup), title: Text(
subtitle: Text( L10n.of(context).changeTheNameOfTheGroup),
room.joinRules.getLocalizedString( subtitle: Text(room.getLocalizedDisplayname(
MatrixLocals(L10n.of(context))), MatrixLocals(L10n.of(context)))),
onTap: () => setDisplaynameAction(context),
), ),
), if (room.canSendEvent('m.room.canonical_alias') &&
), room.joinRules == JoinRules.public)
PopupMenuButton( ListTile(
onSelected: (HistoryVisibility historyVisibility) => leading: CircleAvatar(
showFutureLoadingDialog( backgroundColor:
context: context, Theme.of(context).scaffoldBackgroundColor,
future: () => foregroundColor: Colors.grey,
room.setHistoryVisibility(historyVisibility), child: Icon(Icons.link_outlined),
),
itemBuilder: (BuildContext context) =>
<PopupMenuEntry<HistoryVisibility>>[
if (room.canChangeHistoryVisibility)
PopupMenuItem<HistoryVisibility>(
value: HistoryVisibility.invited,
child: Text(HistoryVisibility.invited
.getLocalizedString(
MatrixLocals(L10n.of(context)))),
), ),
if (room.canChangeHistoryVisibility) onTap: () => setCanonicalAliasAction(context),
PopupMenuItem<HistoryVisibility>( title: Text(L10n.of(context).setInvitationLink),
value: HistoryVisibility.joined, subtitle: Text(
child: Text(HistoryVisibility.joined (room.canonicalAlias?.isNotEmpty ?? false)
.getLocalizedString( ? room.canonicalAlias
MatrixLocals(L10n.of(context)))), : L10n.of(context).none),
), ),
if (room.canChangeHistoryVisibility) ListTile(
PopupMenuItem<HistoryVisibility>(
value: HistoryVisibility.shared,
child: Text(HistoryVisibility.shared
.getLocalizedString(
MatrixLocals(L10n.of(context)))),
),
if (room.canChangeHistoryVisibility)
PopupMenuItem<HistoryVisibility>(
value: HistoryVisibility.world_readable,
child: Text(HistoryVisibility.world_readable
.getLocalizedString(
MatrixLocals(L10n.of(context)))),
),
],
child: ListTile(
leading: CircleAvatar( leading: CircleAvatar(
backgroundColor: backgroundColor:
Theme.of(context).scaffoldBackgroundColor, Theme.of(context).scaffoldBackgroundColor,
foregroundColor: Colors.grey, foregroundColor: Colors.grey,
child: Icon(Icons.visibility_outlined), child: Icon(Icons.insert_emoticon_outlined),
),
title: Text(
L10n.of(context).visibilityOfTheChatHistory),
subtitle: Text(
room.historyVisibility.getLocalizedString(
MatrixLocals(L10n.of(context))),
), ),
title: Text(L10n.of(context).emoteSettings),
subtitle: Text(L10n.of(context).setCustomEmotes),
onTap: () async {
// okay, we need to test if there are any emote state events other than the default one
// if so, we need to be directed to a selection screen for which pack we want to look at
// otherwise, we just open the normal one.
if ((room.states['im.ponies.room_emotes'] ??
<String, Event>{})
.keys
.any((String s) => s.isNotEmpty)) {
await AdaptivePageLayout.of(context)
.pushNamed('/rooms/${room.id}/emotes');
} else {
await AdaptivePageLayout.of(context)
.pushNamed('/settings/emotes',
arguments: {'room': room});
}
},
), ),
),
if (room.joinRules == JoinRules.public)
PopupMenuButton( PopupMenuButton(
onSelected: (GuestAccess guestAccess) => onSelected: (JoinRules joinRule) =>
showFutureLoadingDialog( showFutureLoadingDialog(
context: context, context: context,
future: () => room.setGuestAccess(guestAccess), future: () => room.setJoinRules(joinRule),
), ),
itemBuilder: (BuildContext context) => itemBuilder: (BuildContext context) =>
<PopupMenuEntry<GuestAccess>>[ <PopupMenuEntry<JoinRules>>[
if (room.canChangeGuestAccess) if (room.canChangeJoinRules)
PopupMenuItem<GuestAccess>( PopupMenuItem<JoinRules>(
value: GuestAccess.can_join, value: JoinRules.public,
child: Text( child: Text(JoinRules.public
GuestAccess.can_join.getLocalizedString( .getLocalizedString(
MatrixLocals(L10n.of(context))), MatrixLocals(L10n.of(context)))),
),
), ),
if (room.canChangeGuestAccess) if (room.canChangeJoinRules)
PopupMenuItem<GuestAccess>( PopupMenuItem<JoinRules>(
value: GuestAccess.forbidden, value: JoinRules.invite,
child: Text( child: Text(JoinRules.invite
GuestAccess.forbidden.getLocalizedString( .getLocalizedString(
MatrixLocals(L10n.of(context))), MatrixLocals(L10n.of(context)))),
), ),
],
child: ListTile(
leading: CircleAvatar(
backgroundColor: Theme.of(context)
.scaffoldBackgroundColor,
foregroundColor: Colors.grey,
child: Icon(Icons.public_outlined)),
title: Text(L10n.of(context)
.whoIsAllowedToJoinThisGroup),
subtitle: Text(
room.joinRules.getLocalizedString(
MatrixLocals(L10n.of(context))),
),
),
),
PopupMenuButton(
onSelected:
(HistoryVisibility historyVisibility) =>
showFutureLoadingDialog(
context: context,
future: () => room
.setHistoryVisibility(historyVisibility),
),
itemBuilder: (BuildContext context) =>
<PopupMenuEntry<HistoryVisibility>>[
if (room.canChangeHistoryVisibility)
PopupMenuItem<HistoryVisibility>(
value: HistoryVisibility.invited,
child: Text(HistoryVisibility.invited
.getLocalizedString(
MatrixLocals(L10n.of(context)))),
),
if (room.canChangeHistoryVisibility)
PopupMenuItem<HistoryVisibility>(
value: HistoryVisibility.joined,
child: Text(HistoryVisibility.joined
.getLocalizedString(
MatrixLocals(L10n.of(context)))),
),
if (room.canChangeHistoryVisibility)
PopupMenuItem<HistoryVisibility>(
value: HistoryVisibility.shared,
child: Text(HistoryVisibility.shared
.getLocalizedString(
MatrixLocals(L10n.of(context)))),
),
if (room.canChangeHistoryVisibility)
PopupMenuItem<HistoryVisibility>(
value: HistoryVisibility.world_readable,
child: Text(HistoryVisibility.world_readable
.getLocalizedString(
MatrixLocals(L10n.of(context)))),
), ),
], ],
child: ListTile( child: ListTile(
@ -458,75 +424,119 @@ class _ChatDetailsState extends State<ChatDetails> {
backgroundColor: backgroundColor:
Theme.of(context).scaffoldBackgroundColor, Theme.of(context).scaffoldBackgroundColor,
foregroundColor: Colors.grey, foregroundColor: Colors.grey,
child: Icon(Icons.info_outline), child: Icon(Icons.visibility_outlined),
), ),
title: Text( title: Text(L10n.of(context)
L10n.of(context).areGuestsAllowedToJoin), .visibilityOfTheChatHistory),
subtitle: Text( subtitle: Text(
room.guestAccess.getLocalizedString( room.historyVisibility.getLocalizedString(
MatrixLocals(L10n.of(context))), MatrixLocals(L10n.of(context))),
), ),
), ),
), ),
ListTile( if (room.joinRules == JoinRules.public)
title: Text(L10n.of(context).editChatPermissions), PopupMenuButton(
subtitle: onSelected: (GuestAccess guestAccess) =>
Text(L10n.of(context).whoCanPerformWhichAction), showFutureLoadingDialog(
leading: CircleAvatar( context: context,
backgroundColor: future: () =>
Theme.of(context).scaffoldBackgroundColor, room.setGuestAccess(guestAccess),
foregroundColor: Colors.grey, ),
child: Icon(Icons.edit_attributes_outlined), itemBuilder: (BuildContext context) =>
), <PopupMenuEntry<GuestAccess>>[
onTap: () => AdaptivePageLayout.of(context) if (room.canChangeGuestAccess)
.pushNamed('/rooms/${room.id}/permissions'), PopupMenuItem<GuestAccess>(
), value: GuestAccess.can_join,
Divider(thickness: 1), child: Text(
ListTile( GuestAccess.can_join.getLocalizedString(
title: Text( MatrixLocals(L10n.of(context))),
actualMembersCount > 1 ),
? L10n.of(context).countParticipants( ),
actualMembersCount.toString()) if (room.canChangeGuestAccess)
: L10n.of(context).emptyChat, PopupMenuItem<GuestAccess>(
style: TextStyle( value: GuestAccess.forbidden,
color: Theme.of(context).accentColor, child: Text(
fontWeight: FontWeight.bold, GuestAccess.forbidden
), .getLocalizedString(
), MatrixLocals(L10n.of(context))),
), ),
room.canInvite ),
? ListTile( ],
title: Text(L10n.of(context).inviteContact), child: ListTile(
leading: CircleAvatar( leading: CircleAvatar(
backgroundColor: backgroundColor: Theme.of(context)
Theme.of(context).primaryColor, .scaffoldBackgroundColor,
foregroundColor: Colors.white, foregroundColor: Colors.grey,
radius: Avatar.defaultSize / 2, child: Icon(Icons.info_outline),
child: Icon(Icons.add_outlined),
), ),
onTap: () => AdaptivePageLayout.of(context) title: Text(
.pushNamed('/rooms/${room.id}/invite'), L10n.of(context).areGuestsAllowedToJoin),
) subtitle: Text(
: Container(), room.guestAccess.getLocalizedString(
], MatrixLocals(L10n.of(context))),
) ),
: i < members.length + 1 ),
? ParticipantListItem(members[i - 1]) ),
: ListTile( ListTile(
title: Text(L10n.of(context) title: Text(L10n.of(context).editChatPermissions),
.loadCountMoreParticipants( subtitle: Text(
(actualMembersCount - members.length) L10n.of(context).whoCanPerformWhichAction),
.toString())), leading: CircleAvatar(
leading: CircleAvatar( backgroundColor:
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
Theme.of(context).scaffoldBackgroundColor, foregroundColor: Colors.grey,
child: Icon( child: Icon(Icons.edit_attributes_outlined),
Icons.refresh, ),
color: Colors.grey, onTap: () => AdaptivePageLayout.of(context)
.pushNamed('/rooms/${room.id}/permissions'),
),
Divider(thickness: 1),
ListTile(
title: Text(
actualMembersCount > 1
? L10n.of(context).countParticipants(
actualMembersCount.toString())
: L10n.of(context).emptyChat,
style: TextStyle(
color: Theme.of(context).accentColor,
fontWeight: FontWeight.bold,
),
), ),
), ),
onTap: () => requestMoreMembersAction(context), room.canInvite
), ? ListTile(
title: Text(L10n.of(context).inviteContact),
leading: CircleAvatar(
backgroundColor:
Theme.of(context).primaryColor,
foregroundColor: Colors.white,
radius: Avatar.defaultSize / 2,
child: Icon(Icons.add_outlined),
),
onTap: () => AdaptivePageLayout.of(context)
.pushNamed('/rooms/${room.id}/invite'),
)
: Container(),
],
)
: i < members.length + 1
? ParticipantListItem(members[i - 1])
: ListTile(
title: Text(L10n.of(context)
.loadCountMoreParticipants(
(actualMembersCount - members.length)
.toString())),
leading: CircleAvatar(
backgroundColor:
Theme.of(context).scaffoldBackgroundColor,
child: Icon(
Icons.refresh,
color: Colors.grey,
),
),
onTap: () => requestMoreMembersAction(context),
),
),
), ),
), ),
); );

View File

@ -2,6 +2,7 @@ import 'package:famedlysdk/encryption.dart';
import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/famedlysdk.dart';
import 'package:fluffychat/views/widgets/avatar.dart'; import 'package:fluffychat/views/widgets/avatar.dart';
import 'package:fluffychat/views/widgets/matrix.dart'; import 'package:fluffychat/views/widgets/matrix.dart';
import 'package:fluffychat/views/widgets/max_width_body.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import '../views/widgets/dialogs/key_verification_dialog.dart'; import '../views/widgets/dialogs/key_verification_dialog.dart';
@ -81,65 +82,132 @@ class _ChatEncryptionSettingsState extends State<ChatEncryptionSettings> {
), ),
), ),
), ),
body: StreamBuilder( body: MaxWidthBody(
stream: room.onUpdate.stream, withScrolling: true,
builder: (context, snapshot) { child: StreamBuilder(
return FutureBuilder<List<DeviceKeys>>( stream: room.onUpdate.stream,
future: room.getUserDeviceKeys(), builder: (context, snapshot) {
builder: (BuildContext context, snapshot) { return FutureBuilder<List<DeviceKeys>>(
if (snapshot.hasError) { future: room.getUserDeviceKeys(),
return Center( builder: (BuildContext context, snapshot) {
child: Text(L10n.of(context).oopsSomethingWentWrong + if (snapshot.hasError) {
': ' + return Center(
snapshot.error.toString()), child: Text(L10n.of(context).oopsSomethingWentWrong +
); ': ' +
} snapshot.error.toString()),
if (!snapshot.hasData) { );
return Center(child: CircularProgressIndicator()); }
} if (!snapshot.hasData) {
final deviceKeys = snapshot.data; return Center(child: CircularProgressIndicator());
return ListView.builder( }
itemCount: deviceKeys.length, final deviceKeys = snapshot.data;
itemBuilder: (BuildContext context, int i) => Column( return ListView.builder(
mainAxisSize: MainAxisSize.min, shrinkWrap: true,
children: <Widget>[ physics: NeverScrollableScrollPhysics(),
if (i == 0 || itemCount: deviceKeys.length,
deviceKeys[i].userId != deviceKeys[i - 1].userId) ...{ itemBuilder: (BuildContext context, int i) => Column(
Divider(height: 1, thickness: 1), mainAxisSize: MainAxisSize.min,
children: <Widget>[
if (i == 0 ||
deviceKeys[i].userId !=
deviceKeys[i - 1].userId) ...{
Divider(height: 1, thickness: 1),
PopupMenuButton(
onSelected: (action) =>
onSelected(context, action, deviceKeys[i]),
itemBuilder: (c) {
var 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(
room
.getUserByMXIDSync(deviceKeys[i].userId)
.avatarUrl,
room
.getUserByMXIDSync(deviceKeys[i].userId)
.calcDisplayname(),
),
title: Row(
children: [
Text(
room
.getUserByMXIDSync(deviceKeys[i].userId)
.calcDisplayname(),
),
Spacer(),
Text(
deviceKeys[i].userId,
style: TextStyle(
fontSize: 14,
color: Theme.of(context)
.textTheme
.bodyText1
.color
.withAlpha(150),
),
),
],
),
),
),
},
PopupMenuButton( PopupMenuButton(
onSelected: (action) => onSelected: (action) =>
onSelected(context, action, deviceKeys[i]), onSelected(context, action, deviceKeys[i]),
itemBuilder: (c) { itemBuilder: (c) {
var items = <PopupMenuEntry<String>>[]; var items = <PopupMenuEntry<String>>[];
if (room.client.userDeviceKeys[deviceKeys[i].userId] if (deviceKeys[i].blocked ||
.verified == !deviceKeys[i].verified) {
UserVerifiedStatus.unknown) {
items.add(PopupMenuItem( items.add(PopupMenuItem(
value: 'verify_user', value:
child: Text(L10n.of(context).verifyUser), 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; return items;
}, },
child: ListTile( child: ListTile(
leading: Avatar( leading: CircleAvatar(
room foregroundColor:
.getUserByMXIDSync(deviceKeys[i].userId) Theme.of(context).textTheme.bodyText1.color,
.avatarUrl, backgroundColor:
room Theme.of(context).secondaryHeaderColor,
.getUserByMXIDSync(deviceKeys[i].userId) child: Icon(deviceKeys[i].icon),
.calcDisplayname(),
), ),
title: Row( title: Text(
deviceKeys[i].displayname,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Row(
children: [ children: [
Text( Text(
room deviceKeys[i].deviceId,
.getUserByMXIDSync(deviceKeys[i].userId)
.calcDisplayname(),
),
Spacer(),
Text(
deviceKeys[i].userId,
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
color: Theme.of(context) color: Theme.of(context)
@ -149,91 +217,33 @@ class _ChatEncryptionSettingsState extends State<ChatEncryptionSettings> {
.withAlpha(150), .withAlpha(150),
), ),
), ),
Spacer(),
Text(
deviceKeys[i].blocked
? L10n.of(context).blocked
: deviceKeys[i].verified
? L10n.of(context).verified
: L10n.of(context).unknownDevice,
style: TextStyle(
fontSize: 14,
color: deviceKeys[i].blocked
? Colors.red
: deviceKeys[i].verified
? Colors.green
: Colors.orange,
),
),
], ],
), ),
), ),
), ),
}, ],
PopupMenuButton( ),
onSelected: (action) => );
onSelected(context, action, deviceKeys[i]), },
itemBuilder: (c) { );
var 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:
Theme.of(context).textTheme.bodyText1.color,
backgroundColor:
Theme.of(context).secondaryHeaderColor,
child: Icon(deviceKeys[i].icon),
),
title: Text(
deviceKeys[i].displayname,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Row(
children: [
Text(
deviceKeys[i].deviceId,
style: TextStyle(
fontSize: 14,
color: Theme.of(context)
.textTheme
.bodyText1
.color
.withAlpha(150),
),
),
Spacer(),
Text(
deviceKeys[i].blocked
? L10n.of(context).blocked
: deviceKeys[i].verified
? L10n.of(context).verified
: L10n.of(context).unknownDevice,
style: TextStyle(
fontSize: 14,
color: deviceKeys[i].blocked
? Colors.red
: deviceKeys[i].verified
? Colors.green
: Colors.orange,
),
),
],
),
),
),
],
),
);
},
);
}),
); );
} }
} }

View File

@ -3,6 +3,7 @@ import 'dart:developer';
import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:adaptive_page_layout/adaptive_page_layout.dart'; import 'package:adaptive_page_layout/adaptive_page_layout.dart';
import 'package:fluffychat/views/widgets/dialogs/permission_slider_dialog.dart'; import 'package:fluffychat/views/widgets/dialogs/permission_slider_dialog.dart';
import 'package:fluffychat/views/widgets/max_width_body.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:fluffychat/views/widgets/matrix.dart'; import 'package:fluffychat/views/widgets/matrix.dart';
@ -52,133 +53,137 @@ class ChatPermissionsSettings extends StatelessWidget {
leading: BackButton(), leading: BackButton(),
title: Text(L10n.of(context).editChatPermissions), title: Text(L10n.of(context).editChatPermissions),
), ),
body: StreamBuilder( body: MaxWidthBody(
stream: Matrix.of(context).client.onSync.stream.where( withScrolling: true,
(e) => child: StreamBuilder(
(e?.rooms?.join?.containsKey(roomId) ?? false) && stream: Matrix.of(context).client.onSync.stream.where(
(e.rooms.join[roomId]?.timeline?.events (e) =>
?.any((s) => s.type == EventTypes.RoomPowerLevels) ?? (e?.rooms?.join?.containsKey(roomId) ?? false) &&
false), (e.rooms.join[roomId]?.timeline?.events?.any(
), (s) => s.type == EventTypes.RoomPowerLevels) ??
builder: (context, _) { false),
final room = Matrix.of(context).client.getRoomById(roomId); ),
final powerLevelsContent = Map<String, dynamic>.from( builder: (context, _) {
room.getState(EventTypes.RoomPowerLevels).content); final room = Matrix.of(context).client.getRoomById(roomId);
final powerLevels = Map<String, dynamic>.from(powerLevelsContent) final powerLevelsContent = Map<String, dynamic>.from(
..removeWhere((k, v) => !(v is int)); room.getState(EventTypes.RoomPowerLevels).content);
final eventsPowerLevels = final powerLevels = Map<String, dynamic>.from(powerLevelsContent)
Map<String, dynamic>.from(powerLevelsContent['events']) ..removeWhere((k, v) => !(v is int));
..removeWhere((k, v) => !(v is int)); final eventsPowerLevels =
Map<String, dynamic>.from(powerLevelsContent['events'])
..removeWhere((k, v) => !(v is int));
return ListView( return Column(
children: [ children: [
Column( Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
for (var entry in powerLevels.entries) for (var entry in powerLevels.entries)
PermissionsListTile(
permissionKey: entry.key,
permission: entry.value,
onTap: () =>
_editPowerLevel(context, entry.key, entry.value),
),
Divider(thickness: 1),
ListTile(
title: Text(
L10n.of(context).notifications,
style: TextStyle(
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.bold,
),
),
),
Builder(builder: (context) {
final key = 'rooms';
final int value =
powerLevelsContent.containsKey('notifications')
? powerLevelsContent['notifications']['rooms'] ?? 0
: 0;
return PermissionsListTile(
permissionKey: key,
permission: value,
category: 'notifications',
onTap: () => _editPowerLevel(context, key, value,
category: 'notifications'),
);
}),
Divider(thickness: 1),
ListTile(
title: Text(
L10n.of(context).configureChat,
style: TextStyle(
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.bold,
),
),
),
if (eventsPowerLevels != null)
for (var entry in eventsPowerLevels.entries)
PermissionsListTile( PermissionsListTile(
permissionKey: entry.key, permissionKey: entry.key,
category: 'events',
permission: entry.value, permission: entry.value,
onTap: () => _editPowerLevel( onTap: () =>
context, entry.key, entry.value, _editPowerLevel(context, entry.key, entry.value),
category: 'events'),
), ),
if (room.ownPowerLevel >= 100) ...{
Divider(thickness: 1), Divider(thickness: 1),
FutureBuilder<ServerCapabilities>( ListTile(
future: room.client.requestServerCapabilities(), title: Text(
builder: (context, snapshot) { L10n.of(context).notifications,
if (!snapshot.hasData) { style: TextStyle(
return Center(child: CircularProgressIndicator()); color: Theme.of(context).primaryColor,
} fontWeight: FontWeight.bold,
final String roomVersion = room ),
.getState(EventTypes.RoomCreate) ),
.content['room_version'] ??
'1';
final shouldHaveVersion =
snapshot.data.mRoomVersions.defaultVersion;
return ListTile(
title: Text('Current room version: $roomVersion'),
subtitle: roomVersion == shouldHaveVersion
? null
: Text(
'Upgrade to $shouldHaveVersion available!',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Theme.of(context).accentColor),
),
onTap: () async {
final newVersion =
await showConfirmationDialog<String>(
context: context,
title: 'Choose Room Version',
actions: snapshot
.data.mRoomVersions.available.entries
.where((r) => r.key != roomVersion)
.map((version) => AlertDialogAction(
key: version.key,
label:
'${version.key} (${version.value.toString().split('.').last})')),
);
await showFutureLoadingDialog(
context: context,
future: () =>
room.client.upgradeRoom(roomId, newVersion),
).then((_) => AdaptivePageLayout.of(context).pop());
},
);
},
), ),
}, Builder(builder: (context) {
], final key = 'rooms';
), final int value = powerLevelsContent
], .containsKey('notifications')
); ? powerLevelsContent['notifications']['rooms'] ?? 0
}, : 0;
return PermissionsListTile(
permissionKey: key,
permission: value,
category: 'notifications',
onTap: () => _editPowerLevel(context, key, value,
category: 'notifications'),
);
}),
Divider(thickness: 1),
ListTile(
title: Text(
L10n.of(context).configureChat,
style: TextStyle(
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.bold,
),
),
),
if (eventsPowerLevels != null)
for (var entry in eventsPowerLevels.entries)
PermissionsListTile(
permissionKey: entry.key,
category: 'events',
permission: entry.value,
onTap: () => _editPowerLevel(
context, entry.key, entry.value,
category: 'events'),
),
if (room.ownPowerLevel >= 100) ...{
Divider(thickness: 1),
FutureBuilder<ServerCapabilities>(
future: room.client.requestServerCapabilities(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(child: CircularProgressIndicator());
}
final String roomVersion = room
.getState(EventTypes.RoomCreate)
.content['room_version'] ??
'1';
final shouldHaveVersion =
snapshot.data.mRoomVersions.defaultVersion;
return ListTile(
title: Text('Current room version: $roomVersion'),
subtitle: roomVersion == shouldHaveVersion
? null
: Text(
'Upgrade to $shouldHaveVersion available!',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Theme.of(context).accentColor),
),
onTap: () async {
final newVersion =
await showConfirmationDialog<String>(
context: context,
title: 'Choose Room Version',
actions: snapshot
.data.mRoomVersions.available.entries
.where((r) => r.key != roomVersion)
.map((version) => AlertDialogAction(
key: version.key,
label:
'${version.key} (${version.value.toString().split('.').last})')),
);
await showFutureLoadingDialog(
context: context,
future: () =>
room.client.upgradeRoom(roomId, newVersion),
).then(
(_) => AdaptivePageLayout.of(context).pop());
},
);
},
),
},
],
),
],
);
},
),
), ),
); );
} }

View File

@ -5,6 +5,7 @@ import 'package:fluffychat/views/widgets/default_app_bar_search_field.dart';
import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/famedlysdk.dart';
import 'package:fluffychat/views/widgets/avatar.dart'; import 'package:fluffychat/views/widgets/avatar.dart';
import 'package:fluffychat/views/widgets/max_width_body.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:fluffychat/views/widgets/matrix.dart'; import 'package:fluffychat/views/widgets/matrix.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -128,45 +129,52 @@ class _InvitationSelectionState extends State<InvitationSelection> {
onChanged: (String text) => searchUserWithCoolDown(context, text), onChanged: (String text) => searchUserWithCoolDown(context, text),
), ),
), ),
body: foundProfiles.isNotEmpty body: MaxWidthBody(
? ListView.builder( withScrolling: true,
itemCount: foundProfiles.length, child: foundProfiles.isNotEmpty
itemBuilder: (BuildContext context, int i) => ListTile( ? ListView.builder(
leading: Avatar( physics: NeverScrollableScrollPhysics(),
foundProfiles[i].avatarUrl, shrinkWrap: true,
foundProfiles[i].displayname ?? foundProfiles[i].userId, itemCount: foundProfiles.length,
), itemBuilder: (BuildContext context, int i) => ListTile(
title: Text( leading: Avatar(
foundProfiles[i].displayname ?? foundProfiles[i].avatarUrl,
foundProfiles[i].userId.localpart, foundProfiles[i].displayname ?? foundProfiles[i].userId,
),
subtitle: Text(foundProfiles[i].userId),
onTap: () => inviteAction(context, foundProfiles[i].userId),
),
)
: FutureBuilder<List<User>>(
future: getContacts(context),
builder: (BuildContext context, snapshot) {
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(),
);
}
var contacts = snapshot.data;
return ListView.builder(
itemCount: contacts.length,
itemBuilder: (BuildContext context, int i) => ListTile(
leading: Avatar(
contacts[i].avatarUrl,
contacts[i].calcDisplayname(),
),
title: Text(contacts[i].calcDisplayname()),
subtitle: Text(contacts[i].id),
onTap: () => inviteAction(context, contacts[i].id),
), ),
); title: Text(
}, foundProfiles[i].displayname ??
), foundProfiles[i].userId.localpart,
),
subtitle: Text(foundProfiles[i].userId),
onTap: () => inviteAction(context, foundProfiles[i].userId),
),
)
: FutureBuilder<List<User>>(
future: getContacts(context),
builder: (BuildContext context, snapshot) {
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(),
);
}
var contacts = snapshot.data;
return ListView.builder(
physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: contacts.length,
itemBuilder: (BuildContext context, int i) => ListTile(
leading: Avatar(
contacts[i].avatarUrl,
contacts[i].calcDisplayname(),
),
title: Text(contacts[i].calcDisplayname()),
subtitle: Text(contacts[i].id),
onTap: () => inviteAction(context, contacts[i].id),
),
);
},
),
),
); );
} }
} }

View File

@ -1,5 +1,6 @@
import 'package:adaptive_page_layout/adaptive_page_layout.dart'; import 'package:adaptive_page_layout/adaptive_page_layout.dart';
import 'package:famedlysdk/famedlysdk.dart' as sdk; import 'package:famedlysdk/famedlysdk.dart' as sdk;
import 'package:fluffychat/views/widgets/max_width_body.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:fluffychat/views/widgets/matrix.dart'; import 'package:fluffychat/views/widgets/matrix.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -45,32 +46,34 @@ class _NewGroupState extends State<NewGroup> {
title: Text(L10n.of(context).createNewGroup), title: Text(L10n.of(context).createNewGroup),
elevation: 0, elevation: 0,
), ),
body: Column( body: MaxWidthBody(
mainAxisSize: MainAxisSize.min, child: Column(
children: <Widget>[ mainAxisSize: MainAxisSize.min,
Padding( children: <Widget>[
padding: const EdgeInsets.all(12.0), Padding(
child: TextField( padding: const EdgeInsets.all(12.0),
controller: controller, child: TextField(
autofocus: true, controller: controller,
autocorrect: false, autofocus: true,
textInputAction: TextInputAction.go, autocorrect: false,
onSubmitted: (s) => submitAction(context), textInputAction: TextInputAction.go,
decoration: InputDecoration( onSubmitted: (s) => submitAction(context),
labelText: L10n.of(context).optionalGroupName, decoration: InputDecoration(
prefixIcon: Icon(Icons.people_outlined), labelText: L10n.of(context).optionalGroupName,
hintText: L10n.of(context).enterAGroupName), prefixIcon: Icon(Icons.people_outlined),
hintText: L10n.of(context).enterAGroupName),
),
), ),
), SwitchListTile(
SwitchListTile( title: Text(L10n.of(context).groupIsPublic),
title: Text(L10n.of(context).groupIsPublic), value: publicGroup,
value: publicGroup, onChanged: (bool b) => setState(() => publicGroup = b),
onChanged: (bool b) => setState(() => publicGroup = b), ),
), Expanded(
Expanded( child: Image.asset('assets/new_group_wallpaper.png'),
child: Image.asset('assets/new_group_wallpaper.png'), ),
), ],
], ),
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
onPressed: () => submitAction(context), onPressed: () => submitAction(context),

View File

@ -4,6 +4,7 @@ import 'package:adaptive_page_layout/adaptive_page_layout.dart';
import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/famedlysdk.dart';
import 'package:fluffychat/views/widgets/avatar.dart'; import 'package:fluffychat/views/widgets/avatar.dart';
import 'package:fluffychat/views/widgets/contacts_list.dart'; import 'package:fluffychat/views/widgets/contacts_list.dart';
import 'package:fluffychat/views/widgets/max_width_body.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:fluffychat/views/widgets/matrix.dart'; import 'package:fluffychat/views/widgets/matrix.dart';
import 'package:fluffychat/utils/fluffy_share.dart'; import 'package:fluffychat/utils/fluffy_share.dart';
@ -101,123 +102,127 @@ class _NewPrivateChatState extends State<NewPrivateChat> {
) )
], ],
), ),
body: Column( body: MaxWidthBody(
children: <Widget>[ child: Column(
Padding( children: <Widget>[
padding: const EdgeInsets.all(12.0), Padding(
child: Form( padding: const EdgeInsets.all(12.0),
key: _formKey, child: Form(
child: TextFormField( key: _formKey,
controller: controller, child: TextFormField(
//autofocus: true, controller: controller,
autocorrect: false, //autofocus: true,
onChanged: (String text) => searchUserWithCoolDown(context), autocorrect: false,
textInputAction: TextInputAction.go, onChanged: (String text) => searchUserWithCoolDown(context),
onFieldSubmitted: (s) => submitAction(context), textInputAction: TextInputAction.go,
validator: (value) { onFieldSubmitted: (s) => submitAction(context),
if (value.isEmpty) { validator: (value) {
return L10n.of(context).pleaseEnterAMatrixIdentifier; if (value.isEmpty) {
} return L10n.of(context).pleaseEnterAMatrixIdentifier;
final matrix = Matrix.of(context); }
var mxid = '@' + controller.text.trim(); final matrix = Matrix.of(context);
if (mxid == matrix.client.userID) { var mxid = '@' + controller.text.trim();
return L10n.of(context).youCannotInviteYourself; if (mxid == matrix.client.userID) {
} return L10n.of(context).youCannotInviteYourself;
if (!mxid.contains('@')) { }
return L10n.of(context).makeSureTheIdentifierIsValid; if (!mxid.contains('@')) {
} return L10n.of(context).makeSureTheIdentifierIsValid;
if (!mxid.contains(':')) { }
return L10n.of(context).makeSureTheIdentifierIsValid; if (!mxid.contains(':')) {
} return L10n.of(context).makeSureTheIdentifierIsValid;
return null; }
}, return null;
decoration: InputDecoration( },
labelText: L10n.of(context).enterAUsername, decoration: InputDecoration(
prefixIcon: loading labelText: L10n.of(context).enterAUsername,
? Container( prefixIcon: loading
padding: const EdgeInsets.all(8.0), ? Container(
width: 12, padding: const EdgeInsets.all(8.0),
height: 12, width: 12,
child: CircularProgressIndicator(), height: 12,
) child: CircularProgressIndicator(),
: correctMxId )
? Padding( : correctMxId
padding: const EdgeInsets.all(8.0), ? Padding(
child: Avatar( padding: const EdgeInsets.all(8.0),
foundProfile.avatarUrl, child: Avatar(
foundProfile.displayname ?? foundProfile.userId, foundProfile.avatarUrl,
size: 12, foundProfile.displayname ??
), foundProfile.userId,
) size: 12,
: Icon(Icons.account_circle_outlined), ),
prefixText: '@', )
suffixIcon: IconButton( : Icon(Icons.account_circle_outlined),
onPressed: () => submitAction(context), prefixText: '@',
icon: Icon(Icons.arrow_forward_outlined), suffixIcon: IconButton(
onPressed: () => submitAction(context),
icon: Icon(Icons.arrow_forward_outlined),
),
hintText: '${L10n.of(context).username.toLowerCase()}',
), ),
hintText: '${L10n.of(context).username.toLowerCase()}',
), ),
), ),
), ),
), Divider(height: 1),
Divider(height: 1), ListTile(
ListTile( leading: CircleAvatar(
leading: CircleAvatar( radius: Avatar.defaultSize / 2,
radius: Avatar.defaultSize / 2, foregroundColor: Theme.of(context).accentColor,
foregroundColor: Theme.of(context).accentColor, backgroundColor: Theme.of(context).secondaryHeaderColor,
backgroundColor: Theme.of(context).secondaryHeaderColor, child: Icon(Icons.share_outlined),
child: Icon(Icons.share_outlined), ),
), onTap: () => FluffyShare.share(
onTap: () => FluffyShare.share( L10n.of(context).inviteText(Matrix.of(context).client.userID,
L10n.of(context).inviteText(Matrix.of(context).client.userID, 'https://matrix.to/#/${Matrix.of(context).client.userID}'),
'https://matrix.to/#/${Matrix.of(context).client.userID}'), context),
context), title: Text('${L10n.of(context).yourOwnUsername}:'),
title: Text('${L10n.of(context).yourOwnUsername}:'), subtitle: Text(
subtitle: Text( Matrix.of(context).client.userID,
Matrix.of(context).client.userID, style: TextStyle(color: Theme.of(context).accentColor),
style: TextStyle(color: Theme.of(context).accentColor),
),
),
Divider(height: 1),
if (foundProfiles.isNotEmpty)
Expanded(
child: ListView.builder(
itemCount: foundProfiles.length,
itemBuilder: (BuildContext context, int i) {
var foundProfile = foundProfiles[i];
return ListTile(
onTap: () {
setState(() {
controller.text = currentSearchTerm =
foundProfile.userId.substring(1);
});
},
leading: Avatar(
foundProfile.avatarUrl,
foundProfile.displayname ?? foundProfile.userId,
//size: 24,
),
title: Text(
foundProfile.displayname ?? foundProfile.userId.localpart,
style: TextStyle(),
maxLines: 1,
),
subtitle: Text(
foundProfile.userId,
maxLines: 1,
style: TextStyle(
fontSize: 12,
),
),
);
},
), ),
), ),
if (foundProfiles.isEmpty) Divider(height: 1),
Expanded( if (foundProfiles.isNotEmpty)
child: ContactsList(searchController: controller), Expanded(
), child: ListView.builder(
], itemCount: foundProfiles.length,
itemBuilder: (BuildContext context, int i) {
var foundProfile = foundProfiles[i];
return ListTile(
onTap: () {
setState(() {
controller.text = currentSearchTerm =
foundProfile.userId.substring(1);
});
},
leading: Avatar(
foundProfile.avatarUrl,
foundProfile.displayname ?? foundProfile.userId,
//size: 24,
),
title: Text(
foundProfile.displayname ??
foundProfile.userId.localpart,
style: TextStyle(),
maxLines: 1,
),
subtitle: Text(
foundProfile.userId,
maxLines: 1,
style: TextStyle(
fontSize: 12,
),
),
);
},
),
),
if (foundProfiles.isEmpty)
Expanded(
child: ContactsList(searchController: controller),
),
],
),
), ),
); );
} }

View File

@ -1,5 +1,6 @@
import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/famedlysdk.dart';
import 'package:fluffychat/views/widgets/max_width_body.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:fluffychat/views/widgets/matrix.dart'; import 'package:fluffychat/views/widgets/matrix.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -99,64 +100,67 @@ class _Settings3PidState extends State<Settings3Pid> {
) )
], ],
), ),
body: FutureBuilder<List<ThirdPartyIdentifier>>( body: MaxWidthBody(
future: _request, child: FutureBuilder<List<ThirdPartyIdentifier>>(
builder: (BuildContext context, future: _request,
AsyncSnapshot<List<ThirdPartyIdentifier>> snapshot) { builder: (BuildContext context,
if (snapshot.hasError) { AsyncSnapshot<List<ThirdPartyIdentifier>> snapshot) {
return Center( if (snapshot.hasError) {
child: Text( return Center(
snapshot.error.toString(), child: Text(
textAlign: TextAlign.center, snapshot.error.toString(),
), textAlign: TextAlign.center,
); ),
} );
if (!snapshot.hasData) { }
return Center(child: CircularProgressIndicator()); if (!snapshot.hasData) {
} return Center(child: CircularProgressIndicator());
final identifier = snapshot.data; }
return Column( final identifier = snapshot.data;
children: [ return Column(
ListTile( children: [
leading: CircleAvatar( ListTile(
backgroundColor: Theme.of(context).scaffoldBackgroundColor, leading: CircleAvatar(
foregroundColor: backgroundColor: Theme.of(context).scaffoldBackgroundColor,
identifier.isEmpty ? Colors.orange : Colors.grey, foregroundColor:
child: Icon( identifier.isEmpty ? Colors.orange : Colors.grey,
child: Icon(
identifier.isEmpty
? Icons.warning_outlined
: Icons.info_outlined,
),
),
title: Text(
identifier.isEmpty identifier.isEmpty
? Icons.warning_outlined ? L10n.of(context).noPasswordRecoveryDescription
: Icons.info_outlined, : L10n.of(context)
.withTheseAddressesRecoveryDescription,
), ),
), ),
title: Text( Divider(height: 1),
identifier.isEmpty Expanded(
? L10n.of(context).noPasswordRecoveryDescription child: ListView.builder(
: L10n.of(context).withTheseAddressesRecoveryDescription, itemCount: identifier.length,
), itemBuilder: (BuildContext context, int i) => ListTile(
), leading: CircleAvatar(
Divider(height: 1), backgroundColor:
Expanded( Theme.of(context).scaffoldBackgroundColor,
child: ListView.builder( foregroundColor: Colors.grey,
itemCount: identifier.length, child: Icon(identifier[i].iconData)),
itemBuilder: (BuildContext context, int i) => ListTile( title: Text(identifier[i].address),
leading: CircleAvatar( trailing: IconButton(
backgroundColor: tooltip: L10n.of(context).delete,
Theme.of(context).scaffoldBackgroundColor, icon: Icon(Icons.delete_forever_outlined),
foregroundColor: Colors.grey, color: Colors.red,
child: Icon(identifier[i].iconData)), onPressed: () => _delete3Pid(context, identifier[i]),
title: Text(identifier[i].address), ),
trailing: IconButton(
tooltip: L10n.of(context).delete,
icon: Icon(Icons.delete_forever_outlined),
color: Colors.red,
onPressed: () => _delete3Pid(context, identifier[i]),
), ),
), ),
), ),
), ],
], );
); },
}, ),
), ),
); );
} }

View File

@ -2,6 +2,7 @@ import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:famedlysdk/encryption/utils/key_verification.dart'; import 'package:famedlysdk/encryption/utils/key_verification.dart';
import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/famedlysdk.dart';
import 'package:fluffychat/views/widgets/dialogs/key_verification_dialog.dart'; import 'package:fluffychat/views/widgets/dialogs/key_verification_dialog.dart';
import 'package:fluffychat/views/widgets/max_width_body.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
@ -131,83 +132,86 @@ class DevicesSettingsState extends State<DevicesSettings> {
leading: BackButton(), leading: BackButton(),
title: Text(L10n.of(context).devices), title: Text(L10n.of(context).devices),
), ),
body: FutureBuilder<bool>( body: MaxWidthBody(
future: _loadUserDevices(context), child: FutureBuilder<bool>(
builder: (BuildContext context, snapshot) { future: _loadUserDevices(context),
if (snapshot.hasError) { builder: (BuildContext context, snapshot) {
return Center( if (snapshot.hasError) {
child: Column( return Center(
mainAxisSize: MainAxisSize.min, child: Column(
children: <Widget>[ mainAxisSize: MainAxisSize.min,
Icon(Icons.error_outlined), children: <Widget>[
Text(snapshot.error.toString()), Icon(Icons.error_outlined),
], Text(snapshot.error.toString()),
), ],
);
}
if (!snapshot.hasData || this.devices == null) {
return Center(child: CircularProgressIndicator());
}
Function isOwnDevice = (Device userDevice) =>
userDevice.deviceId == Matrix.of(context).client.deviceID;
final devices = List<Device>.from(this.devices);
var thisDevice = devices.firstWhere(isOwnDevice, orElse: () => null);
devices.removeWhere(isOwnDevice);
devices.sort((a, b) => b.lastSeenTs.compareTo(a.lastSeenTs));
return Column(
children: <Widget>[
if (thisDevice != null)
UserDeviceListItem(
thisDevice,
rename: (d) => _renameDeviceAction(context, d),
remove: (d) => _removeDevicesAction(context, [d]),
verify: (d) => _verifyDeviceAction(context, d),
block: (d) => _blockDeviceAction(context, d),
unblock: (d) => _unblockDeviceAction(context, d),
), ),
Divider(height: 1), );
if (devices.isNotEmpty) }
ListTile( if (!snapshot.hasData || this.devices == null) {
title: Text( return Center(child: CircularProgressIndicator());
_errorDeletingDevices ?? }
L10n.of(context).removeAllOtherDevices, Function isOwnDevice = (Device userDevice) =>
style: TextStyle(color: Colors.red), userDevice.deviceId == Matrix.of(context).client.deviceID;
final devices = List<Device>.from(this.devices);
var thisDevice =
devices.firstWhere(isOwnDevice, orElse: () => null);
devices.removeWhere(isOwnDevice);
devices.sort((a, b) => b.lastSeenTs.compareTo(a.lastSeenTs));
return Column(
children: <Widget>[
if (thisDevice != null)
UserDeviceListItem(
thisDevice,
rename: (d) => _renameDeviceAction(context, d),
remove: (d) => _removeDevicesAction(context, [d]),
verify: (d) => _verifyDeviceAction(context, d),
block: (d) => _blockDeviceAction(context, d),
unblock: (d) => _unblockDeviceAction(context, d),
), ),
trailing: _loadingDeletingDevices Divider(height: 1),
? CircularProgressIndicator() if (devices.isNotEmpty)
: Icon(Icons.delete_outline), ListTile(
onTap: _loadingDeletingDevices title: Text(
? null _errorDeletingDevices ??
: () => _removeDevicesAction(context, devices), L10n.of(context).removeAllOtherDevices,
style: TextStyle(color: Colors.red),
),
trailing: _loadingDeletingDevices
? CircularProgressIndicator()
: Icon(Icons.delete_outline),
onTap: _loadingDeletingDevices
? null
: () => _removeDevicesAction(context, devices),
),
Divider(height: 1),
Expanded(
child: devices.isEmpty
? Center(
child: Icon(
Icons.devices_other,
size: 60,
color: Theme.of(context).secondaryHeaderColor,
),
)
: ListView.separated(
separatorBuilder: (BuildContext context, int i) =>
Divider(height: 1),
itemCount: devices.length,
itemBuilder: (BuildContext context, int i) =>
UserDeviceListItem(
devices[i],
rename: (d) => _renameDeviceAction(context, d),
remove: (d) => _removeDevicesAction(context, [d]),
verify: (d) => _verifyDeviceAction(context, d),
block: (d) => _blockDeviceAction(context, d),
unblock: (d) => _unblockDeviceAction(context, d),
),
),
), ),
Divider(height: 1), ],
Expanded( );
child: devices.isEmpty },
? Center( ),
child: Icon(
Icons.devices_other,
size: 60,
color: Theme.of(context).secondaryHeaderColor,
),
)
: ListView.separated(
separatorBuilder: (BuildContext context, int i) =>
Divider(height: 1),
itemCount: devices.length,
itemBuilder: (BuildContext context, int i) =>
UserDeviceListItem(
devices[i],
rename: (d) => _renameDeviceAction(context, d),
remove: (d) => _removeDevicesAction(context, [d]),
verify: (d) => _verifyDeviceAction(context, d),
block: (d) => _blockDeviceAction(context, d),
unblock: (d) => _unblockDeviceAction(context, d),
),
),
),
],
);
},
), ),
); );
} }

View File

@ -6,6 +6,7 @@ import 'package:famedlysdk/famedlysdk.dart';
import 'package:file_picker_cross/file_picker_cross.dart'; import 'package:file_picker_cross/file_picker_cross.dart';
import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/views/widgets/max_width_body.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
@ -184,223 +185,227 @@ class _EmotesSettingsState extends State<EmotesSettings> {
child: Icon(Icons.save_outlined, color: Colors.white), child: Icon(Icons.save_outlined, color: Colors.white),
) )
: null, : null,
body: StreamBuilder( body: MaxWidthBody(
stream: widget.room?.onUpdate?.stream, child: StreamBuilder(
builder: (context, snapshot) { stream: widget.room?.onUpdate?.stream,
return Column( builder: (context, snapshot) {
children: <Widget>[ return Column(
if (!readonly) children: <Widget>[
Container( if (!readonly)
padding: EdgeInsets.symmetric( Container(
vertical: 8.0, padding: EdgeInsets.symmetric(
), vertical: 8.0,
child: ListTile(
leading: Container(
width: 180.0,
height: 38,
padding: EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(10)),
color: Theme.of(context).secondaryHeaderColor,
),
child: TextField(
controller: newEmoteController,
autocorrect: false,
minLines: 1,
maxLines: 1,
decoration: InputDecoration(
hintText: L10n.of(context).emoteShortcode,
prefixText: ': ',
suffixText: ':',
prefixStyle: TextStyle(
color: Theme.of(context).accentColor,
fontWeight: FontWeight.bold,
),
suffixStyle: TextStyle(
color: Theme.of(context).accentColor,
fontWeight: FontWeight.bold,
),
border: InputBorder.none,
),
),
), ),
title: _EmoteImagePicker(newMxcController), child: ListTile(
trailing: InkWell( leading: Container(
onTap: () async { width: 180.0,
if (newEmoteController.text == null || height: 38,
newEmoteController.text.isEmpty || padding: EdgeInsets.symmetric(horizontal: 8),
newMxcController.text == null || decoration: BoxDecoration(
newMxcController.text.isEmpty) { borderRadius: BorderRadius.all(Radius.circular(10)),
await showOkAlertDialog( color: Theme.of(context).secondaryHeaderColor,
context: context,
message: L10n.of(context).emoteWarnNeedToPick,
okLabel: L10n.of(context).ok,
useRootNavigator: false,
);
return;
}
final emoteCode = ':${newEmoteController.text}:';
final mxc = newMxcController.text;
if (emotes.indexWhere((e) =>
e.emote == emoteCode && e.mxc != mxc) !=
-1) {
await showOkAlertDialog(
context: context,
message: L10n.of(context).emoteExists,
okLabel: L10n.of(context).ok,
useRootNavigator: false,
);
return;
}
if (!RegExp(r'^:[-\w]+:$').hasMatch(emoteCode)) {
await showOkAlertDialog(
context: context,
message: L10n.of(context).emoteInvalid,
okLabel: L10n.of(context).ok,
useRootNavigator: false,
);
return;
}
emotes.add(_EmoteEntry(emote: emoteCode, mxc: mxc));
await _save(context);
setState(() {
newEmoteController.text = '';
newMxcController.text = '';
showSave = false;
});
},
child: Icon(
Icons.add_outlined,
color: Colors.green,
size: 32.0,
),
),
),
),
if (widget.room != null)
ListTile(
title: Text(L10n.of(context).enableEmotesGlobally),
trailing: Switch(
value: isGloballyActive(client),
onChanged: (bool newValue) async {
await _setIsGloballyActive(context, newValue);
setState(() => null);
},
),
),
if (!readonly || widget.room != null)
Divider(
height: 2,
thickness: 2,
color: Theme.of(context).primaryColor,
),
Expanded(
child: emotes.isEmpty
? Center(
child: Padding(
padding: EdgeInsets.all(16),
child: Text(
L10n.of(context).noEmotesFound,
style: TextStyle(fontSize: 20),
),
), ),
) child: TextField(
: ListView.separated( controller: newEmoteController,
separatorBuilder: (BuildContext context, int i) => autocorrect: false,
Container(), minLines: 1,
itemCount: emotes.length + 1, maxLines: 1,
itemBuilder: (BuildContext context, int i) { decoration: InputDecoration(
if (i >= emotes.length) { hintText: L10n.of(context).emoteShortcode,
return Container(height: 70); prefixText: ': ',
} suffixText: ':',
final emote = emotes[i]; prefixStyle: TextStyle(
final controller = TextEditingController(); color: Theme.of(context).accentColor,
controller.text = emote.emoteClean; fontWeight: FontWeight.bold,
return ListTile(
leading: Container(
width: 180.0,
height: 38,
padding: EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
borderRadius:
BorderRadius.all(Radius.circular(10)),
color: Theme.of(context).secondaryHeaderColor,
),
child: TextField(
readOnly: readonly,
controller: controller,
autocorrect: false,
minLines: 1,
maxLines: 1,
decoration: InputDecoration(
hintText: L10n.of(context).emoteShortcode,
prefixText: ': ',
suffixText: ':',
prefixStyle: TextStyle(
color: Theme.of(context).accentColor,
fontWeight: FontWeight.bold,
),
suffixStyle: TextStyle(
color: Theme.of(context).accentColor,
fontWeight: FontWeight.bold,
),
border: InputBorder.none,
),
onSubmitted: (s) {
final emoteCode = ':$s:';
if (emotes.indexWhere((e) =>
e.emote == emoteCode &&
e.mxc != emote.mxc) !=
-1) {
controller.text = emote.emoteClean;
showOkAlertDialog(
context: context,
message: L10n.of(context).emoteExists,
okLabel: L10n.of(context).ok,
useRootNavigator: false,
);
return;
}
if (!RegExp(r'^:[-\w]+:$')
.hasMatch(emoteCode)) {
controller.text = emote.emoteClean;
showOkAlertDialog(
context: context,
message: L10n.of(context).emoteInvalid,
okLabel: L10n.of(context).ok,
useRootNavigator: false,
);
return;
}
setState(() {
emote.emote = emoteCode;
showSave = true;
});
},
),
), ),
title: _EmoteImage(emote.mxc), suffixStyle: TextStyle(
trailing: readonly color: Theme.of(context).accentColor,
? null fontWeight: FontWeight.bold,
: InkWell( ),
onTap: () => setState(() { border: InputBorder.none,
emotes.removeWhere( ),
(e) => e.emote == emote.emote); ),
showSave = true;
}),
child: Icon(
Icons.delete_forever_outlined,
color: Colors.red,
size: 32.0,
),
),
);
},
), ),
), title: _EmoteImagePicker(newMxcController),
], trailing: InkWell(
); onTap: () async {
}), if (newEmoteController.text == null ||
newEmoteController.text.isEmpty ||
newMxcController.text == null ||
newMxcController.text.isEmpty) {
await showOkAlertDialog(
context: context,
message: L10n.of(context).emoteWarnNeedToPick,
okLabel: L10n.of(context).ok,
useRootNavigator: false,
);
return;
}
final emoteCode = ':${newEmoteController.text}:';
final mxc = newMxcController.text;
if (emotes.indexWhere((e) =>
e.emote == emoteCode && e.mxc != mxc) !=
-1) {
await showOkAlertDialog(
context: context,
message: L10n.of(context).emoteExists,
okLabel: L10n.of(context).ok,
useRootNavigator: false,
);
return;
}
if (!RegExp(r'^:[-\w]+:$').hasMatch(emoteCode)) {
await showOkAlertDialog(
context: context,
message: L10n.of(context).emoteInvalid,
okLabel: L10n.of(context).ok,
useRootNavigator: false,
);
return;
}
emotes.add(_EmoteEntry(emote: emoteCode, mxc: mxc));
await _save(context);
setState(() {
newEmoteController.text = '';
newMxcController.text = '';
showSave = false;
});
},
child: Icon(
Icons.add_outlined,
color: Colors.green,
size: 32.0,
),
),
),
),
if (widget.room != null)
ListTile(
title: Text(L10n.of(context).enableEmotesGlobally),
trailing: Switch(
value: isGloballyActive(client),
onChanged: (bool newValue) async {
await _setIsGloballyActive(context, newValue);
setState(() => null);
},
),
),
if (!readonly || widget.room != null)
Divider(
height: 2,
thickness: 2,
color: Theme.of(context).primaryColor,
),
Expanded(
child: emotes.isEmpty
? Center(
child: Padding(
padding: EdgeInsets.all(16),
child: Text(
L10n.of(context).noEmotesFound,
style: TextStyle(fontSize: 20),
),
),
)
: ListView.separated(
separatorBuilder: (BuildContext context, int i) =>
Container(),
itemCount: emotes.length + 1,
itemBuilder: (BuildContext context, int i) {
if (i >= emotes.length) {
return Container(height: 70);
}
final emote = emotes[i];
final controller = TextEditingController();
controller.text = emote.emoteClean;
return ListTile(
leading: Container(
width: 180.0,
height: 38,
padding: EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
borderRadius:
BorderRadius.all(Radius.circular(10)),
color:
Theme.of(context).secondaryHeaderColor,
),
child: TextField(
readOnly: readonly,
controller: controller,
autocorrect: false,
minLines: 1,
maxLines: 1,
decoration: InputDecoration(
hintText: L10n.of(context).emoteShortcode,
prefixText: ': ',
suffixText: ':',
prefixStyle: TextStyle(
color: Theme.of(context).accentColor,
fontWeight: FontWeight.bold,
),
suffixStyle: TextStyle(
color: Theme.of(context).accentColor,
fontWeight: FontWeight.bold,
),
border: InputBorder.none,
),
onSubmitted: (s) {
final emoteCode = ':$s:';
if (emotes.indexWhere((e) =>
e.emote == emoteCode &&
e.mxc != emote.mxc) !=
-1) {
controller.text = emote.emoteClean;
showOkAlertDialog(
context: context,
message: L10n.of(context).emoteExists,
okLabel: L10n.of(context).ok,
useRootNavigator: false,
);
return;
}
if (!RegExp(r'^:[-\w]+:$')
.hasMatch(emoteCode)) {
controller.text = emote.emoteClean;
showOkAlertDialog(
context: context,
message:
L10n.of(context).emoteInvalid,
okLabel: L10n.of(context).ok,
useRootNavigator: false,
);
return;
}
setState(() {
emote.emote = emoteCode;
showSave = true;
});
},
),
),
title: _EmoteImage(emote.mxc),
trailing: readonly
? null
: InkWell(
onTap: () => setState(() {
emotes.removeWhere(
(e) => e.emote == emote.emote);
showSave = true;
}),
child: Icon(
Icons.delete_forever_outlined,
color: Colors.red,
size: 32.0,
),
),
);
},
),
),
],
);
}),
),
); );
} }
} }

View File

@ -1,5 +1,6 @@
import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/famedlysdk.dart';
import 'package:fluffychat/views/widgets/avatar.dart'; import 'package:fluffychat/views/widgets/avatar.dart';
import 'package:fluffychat/views/widgets/max_width_body.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
@ -45,71 +46,73 @@ class _SettingsIgnoreListState extends State<SettingsIgnoreList> {
leading: BackButton(), leading: BackButton(),
title: Text(L10n.of(context).ignoredUsers), title: Text(L10n.of(context).ignoredUsers),
), ),
body: Column( body: MaxWidthBody(
children: [ child: Column(
Padding( children: [
padding: const EdgeInsets.all(16.0), Padding(
child: Column( padding: const EdgeInsets.all(16.0),
mainAxisSize: MainAxisSize.min, child: Column(
children: [ mainAxisSize: MainAxisSize.min,
TextField( children: [
controller: _controller, TextField(
autocorrect: false, controller: _controller,
textInputAction: TextInputAction.done, autocorrect: false,
onSubmitted: (_) => _ignoreUser(context), textInputAction: TextInputAction.done,
decoration: InputDecoration( onSubmitted: (_) => _ignoreUser(context),
border: OutlineInputBorder(), decoration: InputDecoration(
hintText: 'bad_guy:domain.abc', border: OutlineInputBorder(),
prefixText: '@', hintText: 'bad_guy:domain.abc',
labelText: L10n.of(context).ignoreUsername, prefixText: '@',
suffixIcon: IconButton( labelText: L10n.of(context).ignoreUsername,
tooltip: L10n.of(context).ignore, suffixIcon: IconButton(
icon: Icon(Icons.done_outlined), tooltip: L10n.of(context).ignore,
onPressed: () => _ignoreUser(context), icon: Icon(Icons.done_outlined),
onPressed: () => _ignoreUser(context),
),
), ),
), ),
), SizedBox(height: 16),
SizedBox(height: 16), Text(
Text( L10n.of(context).ignoreListDescription,
L10n.of(context).ignoreListDescription, style: TextStyle(color: Colors.orange),
style: TextStyle(color: Colors.orange), ),
), ],
], ),
), ),
), Divider(height: 1),
Divider(height: 1), Expanded(
Expanded( child: StreamBuilder<Object>(
child: StreamBuilder<Object>( stream: client.onAccountData.stream
stream: client.onAccountData.stream .where((a) => a.type == 'm.ignored_user_list'),
.where((a) => a.type == 'm.ignored_user_list'), builder: (context, snapshot) {
builder: (context, snapshot) { return ListView.builder(
return ListView.builder( itemCount: client.ignoredUsers.length,
itemCount: client.ignoredUsers.length, itemBuilder: (c, i) => FutureBuilder<Profile>(
itemBuilder: (c, i) => FutureBuilder<Profile>( future:
future: client.getProfileFromUserId(client.ignoredUsers[i]),
client.getProfileFromUserId(client.ignoredUsers[i]), builder: (c, s) => ListTile(
builder: (c, s) => ListTile( leading: Avatar(
leading: Avatar( s.data?.avatarUrl ?? Uri.parse(''),
s.data?.avatarUrl ?? Uri.parse(''), s.data?.displayname ?? client.ignoredUsers[i],
s.data?.displayname ?? client.ignoredUsers[i], ),
), title: Text(
title: s.data?.displayname ?? client.ignoredUsers[i]),
Text(s.data?.displayname ?? client.ignoredUsers[i]), trailing: IconButton(
trailing: IconButton( tooltip: L10n.of(context).delete,
tooltip: L10n.of(context).delete, icon: Icon(Icons.delete_forever_outlined),
icon: Icon(Icons.delete_forever_outlined), onPressed: () => showFutureLoadingDialog(
onPressed: () => showFutureLoadingDialog( context: context,
context: context, future: () =>
future: () => client.unignoreUser(client.ignoredUsers[i]),
client.unignoreUser(client.ignoredUsers[i]), ),
), ),
), ),
), ),
), );
); }),
}), ),
), ],
], ),
), ),
); );
} }

View File

@ -1,6 +1,7 @@
import 'dart:io'; import 'dart:io';
import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/famedlysdk.dart';
import 'package:fluffychat/views/widgets/max_width_body.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/app_config.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
@ -113,97 +114,101 @@ class SettingsNotifications extends StatelessWidget {
leading: BackButton(), leading: BackButton(),
title: Text(L10n.of(context).notifications), title: Text(L10n.of(context).notifications),
), ),
body: StreamBuilder( body: MaxWidthBody(
stream: Matrix.of(context) withScrolling: true,
.client child: StreamBuilder(
.onAccountData stream: Matrix.of(context)
.stream .client
.where((event) => event.type == 'm.push_rules'), .onAccountData
builder: (BuildContext context, _) { .stream
return ListView( .where((event) => event.type == 'm.push_rules'),
children: [ builder: (BuildContext context, _) {
SwitchListTile( return Column(
value: !Matrix.of(context).client.allPushNotificationsMuted, children: [
title: SwitchListTile(
Text(L10n.of(context).notificationsEnabledForThisAccount), value: !Matrix.of(context).client.allPushNotificationsMuted,
onChanged: (_) => showFutureLoadingDialog( title: Text(
context: context, L10n.of(context).notificationsEnabledForThisAccount),
future: () => Matrix.of(context) onChanged: (_) => showFutureLoadingDialog(
.client context: context,
.setMuteAllPushNotifications( future: () =>
!Matrix.of(context).client.allPushNotificationsMuted, Matrix.of(context).client.setMuteAllPushNotifications(
), !Matrix.of(context)
), .client
), .allPushNotificationsMuted,
if (!Matrix.of(context).client.allPushNotificationsMuted) ...{ ),
if (!kIsWeb && Platform.isAndroid)
ListTile(
title: Text(L10n.of(context).soundVibrationLedColor),
trailing: CircleAvatar(
backgroundColor:
Theme.of(context).scaffoldBackgroundColor,
foregroundColor: Colors.grey,
child: Icon(Icons.edit_outlined),
),
onTap: () => _openAndroidNotificationSettingsAction(),
), ),
),
if (!Matrix.of(context).client.allPushNotificationsMuted) ...{
if (!kIsWeb && Platform.isAndroid)
ListTile(
title: Text(L10n.of(context).soundVibrationLedColor),
trailing: CircleAvatar(
backgroundColor:
Theme.of(context).scaffoldBackgroundColor,
foregroundColor: Colors.grey,
child: Icon(Icons.edit_outlined),
),
onTap: () => _openAndroidNotificationSettingsAction(),
),
Divider(thickness: 1),
ListTile(
title: Text(
L10n.of(context).pushRules,
style: TextStyle(
color: Theme.of(context).accentColor,
fontWeight: FontWeight.bold,
),
),
),
for (var item in items)
SwitchListTile(
value: _getNotificationSetting(context, item) ?? true,
title: Text(item.title(context)),
onChanged: (bool enabled) =>
_setNotificationSetting(context, item, enabled),
),
},
Divider(thickness: 1), Divider(thickness: 1),
ListTile( ListTile(
title: Text( title: Text(
L10n.of(context).pushRules, L10n.of(context).devices,
style: TextStyle( style: TextStyle(
color: Theme.of(context).accentColor, color: Theme.of(context).accentColor,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
), ),
for (var item in items) FutureBuilder<List<Pusher>>(
SwitchListTile( future: Matrix.of(context).client.requestPushers(),
value: _getNotificationSetting(context, item) ?? true, builder: (context, snapshot) {
title: Text(item.title(context)), if (snapshot.hasError) {
onChanged: (bool enabled) => Center(
_setNotificationSetting(context, item, enabled), child: Text(
), snapshot.error.toLocalizedString(context),
}, ),
Divider(thickness: 1), );
ListTile( }
title: Text( if (!snapshot.hasData) {
L10n.of(context).devices, Center(child: CircularProgressIndicator());
style: TextStyle( }
color: Theme.of(context).accentColor, final pushers = snapshot.data;
fontWeight: FontWeight.bold, return ListView.builder(
), physics: NeverScrollableScrollPhysics(),
), shrinkWrap: true,
), itemCount: pushers.length,
FutureBuilder<List<Pusher>>( itemBuilder: (_, i) => ListTile(
future: Matrix.of(context).client.requestPushers(), title: Text(
builder: (context, snapshot) { '${pushers[i].appDisplayName} - ${pushers[i].appId}'),
if (snapshot.hasError) { subtitle: Text(pushers[i].data.url.toString()),
Center(
child: Text(
snapshot.error.toLocalizedString(context),
), ),
); );
} },
if (!snapshot.hasData) { ),
Center(child: CircularProgressIndicator()); ],
} );
final pushers = snapshot.data; }),
return ListView.builder( ),
physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: pushers.length,
itemBuilder: (_, i) => ListTile(
title: Text(
'${pushers[i].appDisplayName} - ${pushers[i].appId}'),
subtitle: Text(pushers[i].data.url.toString()),
),
);
},
),
],
);
}),
); );
} }
} }

View File

@ -2,6 +2,7 @@ import 'dart:io';
import 'package:adaptive_theme/adaptive_theme.dart'; import 'package:adaptive_theme/adaptive_theme.dart';
import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/config/setting_keys.dart';
import 'package:fluffychat/views/widgets/max_width_body.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
@ -56,100 +57,104 @@ class _SettingsStyleState extends State<SettingsStyle> {
leading: BackButton(), leading: BackButton(),
title: Text(L10n.of(context).changeTheme), title: Text(L10n.of(context).changeTheme),
), ),
body: ListView( body: MaxWidthBody(
children: [ withScrolling: true,
RadioListTile<AdaptiveThemeMode>( child: Column(
groupValue: _currentTheme, children: [
value: AdaptiveThemeMode.system, RadioListTile<AdaptiveThemeMode>(
title: Text(L10n.of(context).systemTheme), groupValue: _currentTheme,
onChanged: (t) => _switchTheme(t, context), value: AdaptiveThemeMode.system,
), title: Text(L10n.of(context).systemTheme),
RadioListTile<AdaptiveThemeMode>( onChanged: (t) => _switchTheme(t, context),
groupValue: _currentTheme,
value: AdaptiveThemeMode.light,
title: Text(L10n.of(context).lightTheme),
onChanged: (t) => _switchTheme(t, context),
),
RadioListTile<AdaptiveThemeMode>(
groupValue: _currentTheme,
value: AdaptiveThemeMode.dark,
title: Text(L10n.of(context).darkTheme),
onChanged: (t) => _switchTheme(t, context),
),
Divider(height: 1),
ListTile(
title: Text(
L10n.of(context).wallpaper,
style: TextStyle(
color: Theme.of(context).accentColor,
fontWeight: FontWeight.bold,
),
), ),
), RadioListTile<AdaptiveThemeMode>(
if (Matrix.of(context).wallpaper != null) groupValue: _currentTheme,
value: AdaptiveThemeMode.light,
title: Text(L10n.of(context).lightTheme),
onChanged: (t) => _switchTheme(t, context),
),
RadioListTile<AdaptiveThemeMode>(
groupValue: _currentTheme,
value: AdaptiveThemeMode.dark,
title: Text(L10n.of(context).darkTheme),
onChanged: (t) => _switchTheme(t, context),
),
Divider(height: 1),
ListTile( ListTile(
title: Image.file( title: Text(
Matrix.of(context).wallpaper, L10n.of(context).wallpaper,
height: 38,
fit: BoxFit.cover,
),
trailing: Icon(
Icons.delete_forever_outlined,
color: Colors.red,
),
onTap: () => deleteWallpaperAction(context),
),
Builder(builder: (context) {
return ListTile(
title: Text(L10n.of(context).changeWallpaper),
trailing: Icon(Icons.wallpaper_outlined),
onTap: () => setWallpaperAction(context),
);
}),
Divider(height: 1),
ListTile(
title: Text(
L10n.of(context).fontSize,
style: TextStyle(
color: Theme.of(context).accentColor,
fontWeight: FontWeight.bold,
),
),
subtitle: Text('(*${AppConfig.fontSizeFactor})'),
),
Container(
alignment: Alignment.centerLeft,
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 10),
decoration: BoxDecoration(
color: Theme.of(context).secondaryHeaderColor,
borderRadius: BorderRadius.circular(16),
),
child: Text(
'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor',
style: TextStyle( style: TextStyle(
fontSize: Theme.of(context).textTheme.bodyText1.fontSize * color: Theme.of(context).accentColor,
AppConfig.fontSizeFactor, fontWeight: FontWeight.bold,
), ),
), ),
), ),
), if (Matrix.of(context).wallpaper != null)
Slider( ListTile(
min: 0.5, title: Image.file(
max: 2.5, Matrix.of(context).wallpaper,
divisions: 4, height: 38,
value: AppConfig.fontSizeFactor, fit: BoxFit.cover,
semanticFormatterCallback: (d) => d.toString(), ),
onChanged: (d) { trailing: Icon(
setState(() => AppConfig.fontSizeFactor = d); Icons.delete_forever_outlined,
Matrix.of(context).store.setItem( color: Colors.red,
SettingKeys.fontSizeFactor, ),
AppConfig.fontSizeFactor.toString(), onTap: () => deleteWallpaperAction(context),
); ),
}, Builder(builder: (context) {
), return ListTile(
], title: Text(L10n.of(context).changeWallpaper),
trailing: Icon(Icons.wallpaper_outlined),
onTap: () => setWallpaperAction(context),
);
}),
Divider(height: 1),
ListTile(
title: Text(
L10n.of(context).fontSize,
style: TextStyle(
color: Theme.of(context).accentColor,
fontWeight: FontWeight.bold,
),
),
subtitle: Text('(*${AppConfig.fontSizeFactor})'),
),
Container(
alignment: Alignment.centerLeft,
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
padding:
const EdgeInsets.symmetric(vertical: 6, horizontal: 10),
decoration: BoxDecoration(
color: Theme.of(context).secondaryHeaderColor,
borderRadius: BorderRadius.circular(16),
),
child: Text(
'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor',
style: TextStyle(
fontSize: Theme.of(context).textTheme.bodyText1.fontSize *
AppConfig.fontSizeFactor,
),
),
),
),
Slider(
min: 0.5,
max: 2.5,
divisions: 4,
value: AppConfig.fontSizeFactor,
semanticFormatterCallback: (d) => d.toString(),
onChanged: (d) {
setState(() => AppConfig.fontSizeFactor = d);
Matrix.of(context).store.setItem(
SettingKeys.fontSizeFactor,
AppConfig.fontSizeFactor.toString(),
);
},
),
],
),
), ),
); );
} }

View File

@ -0,0 +1,38 @@
import 'dart:math';
import 'package:flutter/material.dart';
class MaxWidthBody extends StatelessWidget {
final Widget child;
final double maxWidth;
final bool withScrolling;
const MaxWidthBody({
this.child,
this.maxWidth = 600,
this.withScrolling = false,
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final padding = EdgeInsets.symmetric(
horizontal: max(0, (constraints.maxWidth - maxWidth) / 2),
);
return withScrolling
? SingleChildScrollView(
physics: ScrollPhysics(),
child: Padding(
padding: padding,
child: child,
),
)
: Padding(
padding: padding,
child: child,
);
},
);
}
}