dart format

This commit is contained in:
2026-05-08 20:12:40 +02:00
parent 9e139b5704
commit 3b8da1d3d6
295 changed files with 6404 additions and 4161 deletions
@@ -8,7 +8,12 @@ class AnswerReference extends StatelessWidget {
final BuildContext context;
final GetChatResponseObject referenceMessage;
final String? selfId;
const AnswerReference({required this.context, required this.referenceMessage, required this.selfId, super.key});
const AnswerReference({
required this.context,
required this.referenceMessage,
required this.selfId,
super.key,
});
@override
Widget build(BuildContext context) {
@@ -16,15 +21,25 @@ class AnswerReference extends StatelessWidget {
return DecoratedBox(
decoration: BoxDecoration(
color: referenceMessage.actorId == selfId
? style.getSelfStyle(false).color!.withGreen(200).withValues(alpha: 0.2)
: style.getRemoteStyle(false).color!.withWhite(200).withValues(alpha: 0.2),
? style
.getSelfStyle(false)
.color!
.withGreen(200)
.withValues(alpha: 0.2)
: style
.getRemoteStyle(false)
.color!
.withWhite(200)
.withValues(alpha: 0.2),
borderRadius: const BorderRadius.all(Radius.circular(5)),
border: Border(left: BorderSide(
border: Border(
left: BorderSide(
color: referenceMessage.actorId == selfId
? style.getSelfStyle(false).color!.withGreen(200)
: style.getRemoteStyle(false).color!.withWhite(200),
width: 5
)),
width: 5,
),
),
),
child: Padding(
padding: const EdgeInsets.all(5).add(const EdgeInsets.only(left: 5)),
@@ -43,7 +58,10 @@ class AnswerReference extends StatelessWidget {
),
),
Text(
RichObjectStringProcessor.parseToString(referenceMessage.message, referenceMessage.messageParameters),
RichObjectStringProcessor.parseToString(
referenceMessage.message,
referenceMessage.messageParameters,
),
maxLines: 2,
style: TextStyle(
overflow: TextOverflow.ellipsis,
+33 -9
View File
@@ -3,12 +3,17 @@ import 'package:flutter/material.dart';
enum BubbleNip { leftTop, rightBottom, none }
class BubbleEdges {
const BubbleEdges.only({this.top = 0, this.bottom = 0, this.left = 0, this.right = 0});
const BubbleEdges.only({
this.top = 0,
this.bottom = 0,
this.left = 0,
this.right = 0,
});
const BubbleEdges.all(double value)
: top = value,
bottom = value,
left = value,
right = value;
: top = value,
bottom = value,
left = value,
right = value;
final double top;
final double bottom;
@@ -53,9 +58,19 @@ class Bubble extends StatelessWidget {
final flat = Radius.zero;
switch (style.nip) {
case BubbleNip.leftTop:
return BorderRadius.only(topLeft: flat, topRight: r, bottomLeft: r, bottomRight: r);
return BorderRadius.only(
topLeft: flat,
topRight: r,
bottomLeft: r,
bottomRight: r,
);
case BubbleNip.rightBottom:
return BorderRadius.only(topLeft: r, topRight: r, bottomLeft: r, bottomRight: flat);
return BorderRadius.only(
topLeft: r,
topRight: r,
bottomLeft: r,
bottomRight: flat,
);
case BubbleNip.none:
return BorderRadius.all(r);
}
@@ -72,10 +87,19 @@ class Bubble extends StatelessWidget {
color: style.color,
borderRadius: radius,
border: style.borderWidth > 0
? Border.all(color: Theme.of(context).dividerColor, width: style.borderWidth)
? Border.all(
color: Theme.of(context).dividerColor,
width: style.borderWidth,
)
: null,
boxShadow: style.elevation > 0
? [BoxShadow(color: Colors.black26, blurRadius: style.elevation * 2, offset: Offset(0, style.elevation))]
? [
BoxShadow(
color: Colors.black26,
blurRadius: style.elevation * 2,
offset: Offset(0, style.elevation),
),
]
: null,
),
padding: style.padding.toEdgeInsets(),
+107 -80
View File
@@ -40,13 +40,15 @@ class ChatBubble extends StatefulWidget {
required this.refetch,
this.isRead = false,
this.selfId,
super.key});
super.key,
});
@override
State<ChatBubble> createState() => _ChatBubbleState();
}
class _ChatBubbleState extends State<ChatBubble> with SingleTickerProviderStateMixin {
class _ChatBubbleState extends State<ChatBubble>
with SingleTickerProviderStateMixin {
late ChatMessage message;
DownloadJob? _job;
@@ -109,7 +111,10 @@ class _ChatBubbleState extends State<ChatBubble> with SingleTickerProviderStateM
final file = message.file;
final filePath = file?.path;
if (file == null || filePath == null) return;
final job = await DownloadManager.instance.start(remotePath: filePath, name: file.name);
final job = await DownloadManager.instance.start(
remotePath: filePath,
name: file.name,
);
if (!mounted) return;
if (_job == job) return;
_detachJob();
@@ -129,19 +134,22 @@ class _ChatBubbleState extends State<ChatBubble> with SingleTickerProviderStateM
BubbleStyle _getStyle() {
final styles = ChatBubbleStyles(context);
if (widget.bubbleData.messageType != GetRoomResponseObjectMessageType.comment) {
if (widget.bubbleData.messageType !=
GetRoomResponseObjectMessageType.comment) {
return styles.getSystemStyle();
}
return widget.isSender ? styles.getSelfStyle(false) : styles.getRemoteStyle(false);
return widget.isSender
? styles.getSelfStyle(false)
: styles.getRemoteStyle(false);
}
void _showOptionsDialog() => showChatMessageOptionsDialog(
context,
chatData: widget.chatData,
bubbleData: widget.bubbleData,
isSender: widget.isSender,
onRefetch: widget.refetch,
);
context,
chatData: widget.chatData,
bubbleData: widget.bubbleData,
isSender: widget.isSender,
onRefetch: widget.refetch,
);
void _onTap() {
final obj = message.originalData?['object'];
@@ -165,24 +173,40 @@ class _ChatBubbleState extends State<ChatBubble> with SingleTickerProviderStateM
@override
Widget build(BuildContext context) {
message = ChatMessage(originalMessage: widget.bubbleData.message, originalData: widget.bubbleData.messageParameters);
final showActorDisplayName = widget.bubbleData.messageType == GetRoomResponseObjectMessageType.comment
&& widget.chatData.type != GetRoomResponseObjectConversationType.oneToOne;
final showBubbleTime = widget.bubbleData.messageType != GetRoomResponseObjectMessageType.system
&& widget.bubbleData.messageType != GetRoomResponseObjectMessageType.deletedComment;
message = ChatMessage(
originalMessage: widget.bubbleData.message,
originalData: widget.bubbleData.messageParameters,
);
final showActorDisplayName =
widget.bubbleData.messageType ==
GetRoomResponseObjectMessageType.comment &&
widget.chatData.type != GetRoomResponseObjectConversationType.oneToOne;
final showBubbleTime =
widget.bubbleData.messageType !=
GetRoomResponseObjectMessageType.system &&
widget.bubbleData.messageType !=
GetRoomResponseObjectMessageType.deletedComment;
final parent = widget.bubbleData.parent;
final actorText = Text(
widget.bubbleData.actorDisplayName,
textAlign: TextAlign.start,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: Theme.of(context).primaryColor, fontWeight: FontWeight.bold),
style: TextStyle(
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.bold,
),
);
final timeText = Text(
DateTime.fromMillisecondsSinceEpoch(widget.bubbleData.timestamp * 1000).formatHm(),
DateTime.fromMillisecondsSinceEpoch(
widget.bubbleData.timestamp * 1000,
).formatHm(),
textAlign: TextAlign.end,
style: TextStyle(color: widget.timeIconColor, fontSize: widget.timeIconSize),
style: TextStyle(
color: widget.timeIconColor,
fontSize: widget.timeIconSize,
),
);
return Column(
@@ -206,7 +230,9 @@ class _ChatBubbleState extends State<ChatBubble> with SingleTickerProviderStateM
final isAction = _position.dx.abs() > 50;
setState(() => _position = Offset.zero);
if (widget.bubbleData.isReplyable && isAction) {
context.read<ChatBloc>().setReferenceMessageId(widget.bubbleData.id);
context.read<ChatBloc>().setReferenceMessageId(
widget.bubbleData.id,
);
}
},
onLongPress: _showOptionsDialog,
@@ -281,67 +307,68 @@ class _BubbleContent extends StatelessWidget {
@override
Widget build(BuildContext context) => Container(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.9,
minWidth: showActorDisplayName
? actorText.size.width
: timeText.size.width + (isSender ? spacing + timeIconSize : 0) + 3,
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.9,
minWidth: showActorDisplayName
? actorText.size.width
: timeText.size.width + (isSender ? spacing + timeIconSize : 0) + 3,
),
child: Stack(
children: [
if (showActorDisplayName) Positioned(top: 0, left: 0, child: actorText),
Padding(
padding: EdgeInsets.only(
bottom: showBubbleTime ? 18 : 0,
top: showActorDisplayName ? 18 : 0,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (parent != null &&
bubbleData.messageType ==
GetRoomResponseObjectMessageType.comment) ...[
AnswerReference(
context: context,
referenceMessage: parent!,
selfId: selfId,
),
const SizedBox(height: 5),
],
messageWidget,
],
),
),
child: Stack(
children: [
if (showActorDisplayName)
Positioned(top: 0, left: 0, child: actorText),
Padding(
padding: EdgeInsets.only(
bottom: showBubbleTime ? 18 : 0,
top: showActorDisplayName ? 18 : 0,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (parent != null && bubbleData.messageType == GetRoomResponseObjectMessageType.comment) ...[
AnswerReference(
context: context,
referenceMessage: parent!,
selfId: selfId,
),
const SizedBox(height: 5),
],
messageWidget,
if (showBubbleTime)
Positioned(
bottom: 0,
right: 0,
child: Row(
children: [
timeText,
if (isSender) ...[
SizedBox(width: spacing),
Icon(
isRead ? Icons.done_all_outlined : Icons.done_outlined,
size: timeIconSize,
color: timeIconColor,
),
],
),
],
),
if (showBubbleTime)
Positioned(
bottom: 0,
right: 0,
child: Row(
children: [
timeText,
if (isSender) ...[
SizedBox(width: spacing),
Icon(
isRead ? Icons.done_all_outlined : Icons.done_outlined,
size: timeIconSize,
color: timeIconColor,
),
],
],
),
),
if (downloadJob?.status.value is DownloadInProgress)
Positioned(
bottom: 0,
right: 0,
left: 0,
child: LinearProgressIndicator(
value: () {
final s = downloadJob!.status.value as DownloadInProgress;
return s.percent <= 0 ? null : s.percent / 100;
}(),
),
),
],
),
);
),
if (downloadJob?.status.value is DownloadInProgress)
Positioned(
bottom: 0,
right: 0,
left: 0,
child: LinearProgressIndicator(
value: () {
final s = downloadJob!.status.value as DownloadInProgress;
return s.percent <= 0 ? null : s.percent / 100;
}(),
),
),
],
),
);
}
@@ -22,14 +22,14 @@ void showChatBubblePollDialog(
future: pollState,
builder: (_, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Column(mainAxisSize: MainAxisSize.min, children: [LoadingSpinner()]);
return const Column(
mainAxisSize: MainAxisSize.min,
children: [LoadingSpinner()],
);
}
final pollData = snapshot.data!.data;
return SingleChildScrollView(
child: PollOptionsList(
pollData: pollData,
chatToken: chatToken,
),
child: PollOptionsList(pollData: pollData, chatToken: chatToken),
);
},
),
@@ -37,14 +37,20 @@ class ChatBubbleReactions extends StatelessWidget {
alignment: isSender ? WrapAlignment.end : WrapAlignment.start,
crossAxisAlignment: WrapCrossAlignment.start,
children: reactions.entries.map<Widget>((e) {
final hasSelfReacted = bubbleData.reactionsSelf?.contains(e.key) ?? false;
final hasSelfReacted =
bubbleData.reactionsSelf?.contains(e.key) ?? false;
return Container(
margin: const EdgeInsets.only(right: 2.5, left: 2.5),
child: ActionChip(
label: Text('${e.key} ${e.value}'),
visualDensity: const VisualDensity(vertical: VisualDensity.minimumDensity, horizontal: VisualDensity.minimumDensity),
visualDensity: const VisualDensity(
vertical: VisualDensity.minimumDensity,
horizontal: VisualDensity.minimumDensity,
),
padding: EdgeInsets.zero,
backgroundColor: hasSelfReacted ? Theme.of(context).primaryColor : null,
backgroundColor: hasSelfReacted
? Theme.of(context).primaryColor
: null,
onPressed: () {
runWithErrorDialog(context, () async {
if (hasSelfReacted) {
@@ -29,11 +29,13 @@ void showChatMessageOptionsDialog(
required void Function({bool renew}) onRefetch,
}) {
final parentContext = context;
final canReact = bubbleData.messageType == GetRoomResponseObjectMessageType.comment;
final canDelete = isSender &&
DateTime.fromMillisecondsSinceEpoch(bubbleData.timestamp * 1000)
.add(const Duration(hours: 6))
.isAfter(DateTime.now());
final canReact =
bubbleData.messageType == GetRoomResponseObjectMessageType.comment;
final canDelete =
isSender &&
DateTime.fromMillisecondsSinceEpoch(
bubbleData.timestamp * 1000,
).add(const Duration(hours: 6)).isAfter(DateTime.now());
showDetailsBottomSheet(
context,
@@ -61,7 +63,11 @@ void showChatMessageOptionsDialog(
onTap: () {
Navigator.of(sheetCtx).pop();
if (!parentContext.mounted) return;
AppRoutes.openMessageReactions(parentContext, chatData.token, bubbleData.id);
AppRoutes.openMessageReactions(
parentContext,
chatData.token,
bubbleData.id,
);
},
),
if (bubbleData.message != '{file}')
@@ -73,7 +79,9 @@ void showChatMessageOptionsDialog(
Navigator.of(sheetCtx).pop();
},
),
if (!kReleaseMode && !isSender && chatData.type != GetRoomResponseObjectConversationType.oneToOne)
if (!kReleaseMode &&
!isSender &&
chatData.type != GetRoomResponseObjectConversationType.oneToOne)
ListTile(
leading: const Icon(Icons.sms_outlined),
title: Text("Private Nachricht an '${bubbleData.actorDisplayName}'"),
@@ -136,54 +144,57 @@ class _ReactionsRowState extends State<_ReactionsRow> {
@override
Widget build(BuildContext context) => AnimatedBuilder(
animation: _controller,
builder: (context, _) {
final busy = _controller.busy;
final err = _controller.error;
return Column(
mainAxisSize: MainAxisSize.min,
animation: _controller,
builder: (context, _) {
final busy = _controller.busy;
final err = _controller.error;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Wrap(
alignment: WrapAlignment.center,
children: [
Wrap(
alignment: WrapAlignment.center,
children: [
..._commonReactions.map(
(emoji) => TextButton(
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
minimumSize: const Size(40, 40),
),
onPressed: busy ? null : () => _react(emoji),
child: Text(emoji),
),
),
IconButton(
onPressed: busy ? null : () => _showEmojiPicker(context),
style: IconButton.styleFrom(
padding: EdgeInsets.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
minimumSize: const Size(40, 40),
),
icon: busy
? const AppProgressIndicator.small()
: const Icon(Icons.add_circle_outline_outlined),
),
],
),
if (err != null)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: Text(
err,
textAlign: TextAlign.center,
style: TextStyle(color: Theme.of(context).colorScheme.error, fontSize: 12),
..._commonReactions.map(
(emoji) => TextButton(
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
minimumSize: const Size(40, 40),
),
onPressed: busy ? null : () => _react(emoji),
child: Text(emoji),
),
const Divider(),
),
IconButton(
onPressed: busy ? null : () => _showEmojiPicker(context),
style: IconButton.styleFrom(
padding: EdgeInsets.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
minimumSize: const Size(40, 40),
),
icon: busy
? const AppProgressIndicator.small()
: const Icon(Icons.add_circle_outline_outlined),
),
],
);
},
),
if (err != null)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: Text(
err,
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).colorScheme.error,
fontSize: 12,
),
),
),
const Divider(),
],
);
},
);
void _showEmojiPicker(BuildContext rowContext) {
showDialog(
@@ -214,7 +225,9 @@ class _ReactionsRowState extends State<_ReactionsRow> {
noRecents: const Text('Keine zuletzt verwendeten Emojis'),
columns: 7,
),
bottomActionBarConfig: const emojis.BottomActionBarConfig(enabled: false),
bottomActionBarConfig: const emojis.BottomActionBarConfig(
enabled: false,
),
categoryViewConfig: emojis.CategoryViewConfig(
backgroundColor: Theme.of(pickerCtx).hoverColor,
iconColorSelected: Theme.of(pickerCtx).primaryColor,
+178 -130
View File
@@ -39,13 +39,17 @@ class _ChatTextfieldState extends State<ChatTextfield> {
void share(String shareFolder, List<String> filePaths) {
for (final element in filePaths) {
final fileName = element.split(Platform.pathSeparator).last;
FileSharingApi().share(FileSharingApiParams(
shareType: 10,
shareWith: widget.sendToToken,
path: '$shareFolder/$fileName',
)).then((_) {
if (mounted) context.read<ChatBloc>().refresh();
});
FileSharingApi()
.share(
FileSharingApiParams(
shareType: 10,
shareWith: widget.sendToToken,
path: '$shareFolder/$fileName',
),
)
.then((_) {
if (mounted) context.read<ChatBloc>().refresh();
});
}
}
@@ -53,19 +57,25 @@ class _ChatTextfieldState extends State<ChatTextfield> {
if (paths == null) return;
const shareFolder = 'MarianumMobile';
unawaited(WebdavApi.webdav.then((webdav) => webdav.mkcol(PathUri.parse('/$shareFolder'))));
unawaited(
WebdavApi.webdav.then(
(webdav) => webdav.mkcol(PathUri.parse('/$shareFolder')),
),
);
if (!mounted) return;
unawaited(pushScreen(
context,
withNavBar: false,
screen: FilesUploadDialog(
filePaths: paths,
remotePath: shareFolder,
onUploadFinished: (uploaded) => share(shareFolder, uploaded),
uniqueNames: true,
unawaited(
pushScreen(
context,
withNavBar: false,
screen: FilesUploadDialog(
filePaths: paths,
remotePath: shareFolder,
onUploadFinished: (uploaded) => share(shareFolder, uploaded),
uniqueNames: true,
),
),
));
);
}
void _setDraft(String text) {
@@ -82,7 +92,9 @@ class _ChatTextfieldState extends State<ChatTextfield> {
if (messageId != null) {
talkSettings.draftReplies[widget.sendToToken] = messageId;
} else {
talkSettings.draftReplies.removeWhere((key, _) => key == widget.sendToToken);
talkSettings.draftReplies.removeWhere(
(key, _) => key == widget.sendToToken,
);
}
}
@@ -90,7 +102,10 @@ class _ChatTextfieldState extends State<ChatTextfield> {
void initState() {
super.initState();
settings = context.read<SettingsCubit>();
final draftReply = settings.val().talkSettings.draftReplies[widget.sendToToken];
final draftReply = settings
.val()
.talkSettings
.draftReplies[widget.sendToToken];
if (draftReply != null) {
context.read<ChatBloc>().setReferenceMessageId(draftReply);
}
@@ -121,16 +136,19 @@ class _ChatTextfieldState extends State<ChatTextfield> {
@override
Widget build(BuildContext context) {
_textBoxController.text = settings.val().talkSettings.drafts[widget.sendToToken] ?? '';
_textBoxController.text =
settings.val().talkSettings.drafts[widget.sendToToken] ?? '';
final chatBloc = context.watch<ChatBloc>();
final chatState = chatBloc.state.data;
Widget replyBanner = const SizedBox.shrink();
if (chatState != null && chatState.referenceMessageId != null && chatState.chatResponse != null) {
if (chatState != null &&
chatState.referenceMessageId != null &&
chatState.chatResponse != null) {
try {
final referenceMessage = chatState.chatResponse!.sortByTimestamp().firstWhere(
(e) => e.id == chatState.referenceMessageId,
);
final referenceMessage = chatState.chatResponse!
.sortByTimestamp()
.firstWhere((e) => e.id == chatState.referenceMessageId);
replyBanner = Row(
children: [
Expanded(
@@ -150,120 +168,150 @@ class _ChatTextfieldState extends State<ChatTextfield> {
),
],
);
} catch (_) {/* reference no longer in current chat data */}
} catch (_) {
/* reference no longer in current chat data */
}
}
return Stack(children: <Widget>[
Align(
alignment: Alignment.bottomLeft,
child: Container(
padding: const EdgeInsets.only(left: 10, bottom: 3, top: 3, right: 10),
width: double.infinity,
child: Column(
children: [
replyBanner,
if (_sendError != null)
Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Text(
_sendError!,
style: TextStyle(color: Theme.of(context).colorScheme.error, fontSize: 12),
),
),
Row(children: <Widget>[
GestureDetector(
onTap: () {
showDetailsBottomSheet(
context,
children: (sheetCtx) => [
ListTile(
leading: const Icon(Icons.file_open),
title: const Text('Aus Dateien auswählen'),
onTap: () {
FilePick.documentPick().then(mediaUpload);
Navigator.of(sheetCtx).pop();
},
),
ListTile(
leading: const Icon(Icons.image),
title: const Text('Aus Galerie auswählen'),
onTap: () {
FilePick.multipleGalleryPick().then((value) {
if (value != null) mediaUpload(value.map((e) => e.path).toList());
});
Navigator.of(sheetCtx).pop();
},
),
ListTile(
leading: const Icon(Icons.camera_alt_outlined),
title: const Text('Foto aufnehmen'),
onTap: () {
FilePick.cameraPick().then((image) {
if (image != null) mediaUpload([image.path]);
});
Navigator.of(sheetCtx).pop();
},
),
],
);
},
child: Material(
elevation: 5,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(30)),
child: Container(
height: 30,
width: 30,
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(30),
return Stack(
children: <Widget>[
Align(
alignment: Alignment.bottomLeft,
child: Container(
padding: const EdgeInsets.only(
left: 10,
bottom: 3,
top: 3,
right: 10,
),
width: double.infinity,
child: Column(
children: [
replyBanner,
if (_sendError != null)
Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Text(
_sendError!,
style: TextStyle(
color: Theme.of(context).colorScheme.error,
fontSize: 12,
),
child: const Icon(Icons.attach_file_outlined, color: Colors.white, size: 20),
),
),
),
const SizedBox(width: 15),
Expanded(
child: TextField(
autocorrect: true,
textCapitalization: TextCapitalization.sentences,
controller: _textBoxController,
maxLines: 7,
minLines: 1,
decoration: const InputDecoration(
hintText: 'Nachricht schreiben...',
border: InputBorder.none,
Row(
children: <Widget>[
GestureDetector(
onTap: () {
showDetailsBottomSheet(
context,
children: (sheetCtx) => [
ListTile(
leading: const Icon(Icons.file_open),
title: const Text('Aus Dateien auswählen'),
onTap: () {
FilePick.documentPick().then(mediaUpload);
Navigator.of(sheetCtx).pop();
},
),
ListTile(
leading: const Icon(Icons.image),
title: const Text('Aus Galerie auswählen'),
onTap: () {
FilePick.multipleGalleryPick().then((value) {
if (value != null) {
mediaUpload(
value.map((e) => e.path).toList(),
);
}
});
Navigator.of(sheetCtx).pop();
},
),
ListTile(
leading: const Icon(Icons.camera_alt_outlined),
title: const Text('Foto aufnehmen'),
onTap: () {
FilePick.cameraPick().then((image) {
if (image != null) mediaUpload([image.path]);
});
Navigator.of(sheetCtx).pop();
},
),
],
);
},
child: Material(
elevation: 5,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
child: Container(
height: 30,
width: 30,
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(30),
),
child: const Icon(
Icons.attach_file_outlined,
color: Colors.white,
size: 20,
),
),
),
),
onChanged: (text) {
if (text.trim().toLowerCase() == 'marbot marbot marbot') {
const newText = 'Roboter sind cool und so, aber Marbots sind besser!';
_textBoxController.text = newText;
text = newText;
}
_setDraft(text);
},
onTapOutside: (_) => FocusBehaviour.textFieldTapOutside(context),
),
const SizedBox(width: 15),
Expanded(
child: TextField(
autocorrect: true,
textCapitalization: TextCapitalization.sentences,
controller: _textBoxController,
maxLines: 7,
minLines: 1,
decoration: const InputDecoration(
hintText: 'Nachricht schreiben...',
border: InputBorder.none,
),
onChanged: (text) {
if (text.trim().toLowerCase() ==
'marbot marbot marbot') {
const newText =
'Roboter sind cool und so, aber Marbots sind besser!';
_textBoxController.text = newText;
text = newText;
}
_setDraft(text);
},
onTapOutside: (_) =>
FocusBehaviour.textFieldTapOutside(context),
),
),
const SizedBox(width: 15),
ValueListenableBuilder<TextEditingValue>(
valueListenable: _textBoxController,
builder: (context, value, _) => AsyncFab(
mini: true,
heroTag: 'chatSend_${widget.sendToToken}',
icon: Icons.send,
backgroundColor: Theme.of(context).primaryColor,
foregroundColor: Colors.white,
controller: _sendController,
onPressed: value.text.trim().isEmpty
? null
: () => _sendMessage(chatBloc),
onError: (message) =>
setState(() => _sendError = message),
onSuccess: () => setState(() => _sendError = null),
),
),
],
),
const SizedBox(width: 15),
ValueListenableBuilder<TextEditingValue>(
valueListenable: _textBoxController,
builder: (context, value, _) => AsyncFab(
mini: true,
heroTag: 'chatSend_${widget.sendToToken}',
icon: Icons.send,
backgroundColor: Theme.of(context).primaryColor,
foregroundColor: Colors.white,
controller: _sendController,
onPressed: value.text.trim().isEmpty ? null : () => _sendMessage(chatBloc),
onError: (message) => setState(() => _sendError = message),
onSuccess: () => setState(() => _sendError = null),
),
),
]),
],
],
),
),
),
),
]);
],
);
}
}
+47 -12
View File
@@ -25,7 +25,12 @@ class ChatTile extends StatefulWidget {
final bool disableContextActions;
final bool hasDraft;
const ChatTile({super.key, required this.data, this.disableContextActions = false, this.hasDraft = false});
const ChatTile({
super.key,
required this.data,
this.disableContextActions = false,
this.hasDraft = false,
});
@override
State<ChatTile> createState() => _ChatTileState();
@@ -39,7 +44,11 @@ class _ChatTileState extends State<ChatTile> {
super.initState();
AccountData().waitForPopulation().then((_) {
if (!mounted) return;
setState(() => selfUsername = AccountData().isPopulated() ? AccountData().getUsername() : null);
setState(
() => selfUsername = AccountData().isPopulated()
? AccountData().getUsername()
: null,
);
});
}
@@ -49,7 +58,9 @@ class _ChatTileState extends State<ChatTile> {
await SetReadMarker(
widget.data.token,
true,
setReadMarkerParams: SetReadMarkerParams(lastReadMessage: widget.data.lastMessage.id),
setReadMarkerParams: SetReadMarkerParams(
lastReadMessage: widget.data.lastMessage.id,
),
).run();
if (!mounted) return;
_refreshList();
@@ -58,12 +69,18 @@ class _ChatTileState extends State<ChatTile> {
@override
Widget build(BuildContext context) {
final chatBloc = context.watch<ChatBloc>();
final isGroup = widget.data.type != GetRoomResponseObjectConversationType.oneToOne;
final circleAvatar = UserAvatar(id: isGroup ? widget.data.token : widget.data.name, isGroup: isGroup);
final isGroup =
widget.data.type != GetRoomResponseObjectConversationType.oneToOne;
final circleAvatar = UserAvatar(
id: isGroup ? widget.data.token : widget.data.name,
isGroup: isGroup,
);
return ListTile(
style: ListTileStyle.list,
tileColor: chatBloc.state.data?.currentToken == widget.data.token && TalkNavigator.isSecondaryVisible(context)
tileColor:
chatBloc.state.data?.currentToken == widget.data.token &&
TalkNavigator.isSecondaryVisible(context)
? Theme.of(context).primaryColor.withAlpha(100)
: null,
leading: Stack(
@@ -80,16 +97,25 @@ class _ChatTileState extends State<ChatTile> {
color: Theme.of(context).primaryColor.withAlpha(200),
borderRadius: BorderRadius.circular(90.0),
),
child: const Icon(Icons.star, color: Colors.amberAccent, size: 15),
child: const Icon(
Icons.star,
color: Colors.amberAccent,
size: 15,
),
),
),
)
),
],
),
title: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(child: Text(widget.data.displayName, overflow: TextOverflow.ellipsis)),
Flexible(
child: Text(
widget.data.displayName,
overflow: TextOverflow.ellipsis,
),
),
if (widget.hasDraft) ...[
const SizedBox(width: 5),
const Icon(Icons.edit_outlined, size: 15),
@@ -119,8 +145,16 @@ class _ChatTileState extends State<ChatTile> {
onTap: () {
if (selfUsername == null) return;
unawaited(_setCurrentAsRead());
final view = ChatView(room: widget.data, selfId: selfUsername!, avatar: circleAvatar);
TalkNavigator.pushSplitView(context, view, overrideToSingleSubScreen: true);
final view = ChatView(
room: widget.data,
selfId: selfUsername!,
avatar: circleAvatar,
);
TalkNavigator.pushSplitView(
context,
view,
overrideToSingleSubScreen: true,
);
context.read<ChatBloc>().setToken(widget.data.token);
},
onLongPress: () {
@@ -168,7 +202,8 @@ class _ChatTileState extends State<ChatTile> {
Navigator.of(sheetCtx).pop();
ConfirmDialog(
title: 'Chat verlassen',
content: 'Du benötigst ggf. eine Einladung um erneut beizutreten.',
content:
'Du benötigst ggf. eine Einladung um erneut beizutreten.',
confirmButton: 'Verlassen',
onConfirmAsync: () async {
await LeaveRoom(widget.data.token).run();
@@ -1,4 +1,3 @@
import 'package:flutter/material.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
@@ -8,7 +7,11 @@ import '../../../../utils/url_opener.dart';
class PollOptionsList extends StatefulWidget {
final GetPollStateResponseObject pollData;
final String chatToken;
const PollOptionsList({super.key, required this.pollData, required this.chatToken});
const PollOptionsList({
super.key,
required this.pollData,
required this.chatToken,
});
@override
State<PollOptionsList> createState() => _PollOptionsListState();
@@ -23,44 +26,48 @@ class _PollOptionsListState extends State<PollOptionsList> {
var votedSelf = widget.pollData.votedSelf.contains(optionId);
var portionsVisible = widget.pollData.votes is Map<String, dynamic>;
var votes = portionsVisible
? (widget.pollData.votes['option-$optionId'] as num?) ?? 0
: 0;
? (widget.pollData.votes['option-$optionId'] as num?) ?? 0
: 0;
var numVoters = widget.pollData.numVoters ?? 0;
final portion = numVoters == 0 ? 0.0 : (votes / numVoters);
return ListTile(
isThreeLine: portionsVisible,
dense: true,
title: Text(
option,
style: Theme.of(context).textTheme.bodyLarge,
),
title: Text(option, style: Theme.of(context).textTheme.bodyLarge),
leading: Icon(
votedSelf ? Icons.check_circle_outlined : Icons.circle_outlined,
color: votedSelf
? Theme.of(context).colorScheme.primary.withValues(alpha: 0.6)
: Theme.of(context).colorScheme.onSurfaceVariant.withValues(alpha: 0.6),
: Theme.of(
context,
).colorScheme.onSurfaceVariant.withValues(alpha: 0.6),
),
subtitle: portionsVisible ? Row(
children: [
Expanded(
child: LinearProgressIndicator(value: portion.clamp(0.0, 1.0)),
),
Container(
margin: const EdgeInsets.only(left: 10),
child: Text('${(portion * 100).round()}%'),
),
],
) : null,
subtitle: portionsVisible
? Row(
children: [
Expanded(
child: LinearProgressIndicator(
value: portion.clamp(0.0, 1.0),
),
),
Container(
margin: const EdgeInsets.only(left: 10),
child: Text('${(portion * 100).round()}%'),
),
],
)
: null,
);
}),
ListTile(
title: Linkify(
text: 'Wenn du abstimmen möchtest, verwende die Webversion unter https://cloud.marianum-fulda.de/call/${widget.chatToken}',
text:
'Wenn du abstimmen möchtest, verwende die Webversion unter https://cloud.marianum-fulda.de/call/${widget.chatToken}',
onOpen: UrlOpener.onOpen,
style: Theme.of(context).textTheme.bodySmall,
),
)
),
],
);
}
@@ -7,21 +7,25 @@ class SplitViewPlaceholder extends StatelessWidget {
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
MediaQuery(
data: MediaQuery.of(context).copyWith(
invertColors: !AppTheme.isDarkMode(context),
),
child: Image.asset('assets/logo/icon.png', height: 200),
),
const SizedBox(height: 30),
const Text('Marianum Fulda\nTalk', textAlign: TextAlign.center, style: TextStyle(fontSize: 30)),
],
),
)
);
appBar: AppBar(),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
MediaQuery(
data: MediaQuery.of(
context,
).copyWith(invertColors: !AppTheme.isDarkMode(context)),
child: Image.asset('assets/logo/icon.png', height: 200),
),
const SizedBox(height: 30),
const Text(
'Marianum Fulda\nTalk',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 30),
),
],
),
),
);
}