folder restructuring
This commit is contained in:
@@ -1,59 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../api/marianumcloud/talk/chat/getChatResponse.dart';
|
||||
import '../../../../api/marianumcloud/talk/chat/richObjectStringProcessor.dart';
|
||||
import 'chatBubbleStyles.dart';
|
||||
|
||||
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});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var style = ChatBubbleStyles(context);
|
||||
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),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(5)),
|
||||
border: Border(left: BorderSide(
|
||||
color: referenceMessage.actorId == selfId
|
||||
? style.getSelfStyle(false).color!.withGreen(200)
|
||||
: style.getRemoteStyle(false).color!.withWhite(200),
|
||||
width: 5
|
||||
)),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(5).add(const EdgeInsets.only(left: 5)),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
referenceMessage.actorDisplayName,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: referenceMessage.actorId == selfId
|
||||
? style.getSelfStyle(false).color!.withGreen(200)
|
||||
: style.getRemoteStyle(false).color!.withWhite(200),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
RichObjectStringProcessor.parseToString(referenceMessage.message, referenceMessage.messageParameters),
|
||||
maxLines: 2,
|
||||
style: TextStyle(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,504 +0,0 @@
|
||||
import 'package:bubble/bubble.dart';
|
||||
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart' as emojis;
|
||||
import 'package:flowder/flowder.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:jiffy/jiffy.dart';
|
||||
import 'package:open_filex/open_filex.dart';
|
||||
import '../../../../api/marianumcloud/talk/getPoll/getPollState.dart';
|
||||
import '../../../../extensions/text.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../../../api/marianumcloud/talk/chat/getChatResponse.dart';
|
||||
import '../../../../api/marianumcloud/talk/deleteMessage/deleteMessage.dart';
|
||||
import '../../../../api/marianumcloud/talk/deleteReactMessage/deleteReactMessage.dart';
|
||||
import '../../../../api/marianumcloud/talk/deleteReactMessage/deleteReactMessageParams.dart';
|
||||
import '../../../../api/marianumcloud/talk/reactMessage/reactMessage.dart';
|
||||
import '../../../../api/marianumcloud/talk/reactMessage/reactMessageParams.dart';
|
||||
import '../../../../api/marianumcloud/talk/room/getRoomResponse.dart';
|
||||
import '../../../../state/app/modules/chat/bloc/chat_bloc.dart';
|
||||
import '../../../../widget/debug/debugTile.dart';
|
||||
import '../../../../widget/loadingSpinner.dart';
|
||||
import '../../files/fileElement.dart';
|
||||
import 'answerReference.dart';
|
||||
import 'chatBubbleStyles.dart';
|
||||
import 'chatMessage.dart';
|
||||
import '../messageReactions.dart';
|
||||
import 'pollOptionsList.dart';
|
||||
|
||||
class ChatBubble extends StatefulWidget {
|
||||
final BuildContext context;
|
||||
final bool isSender;
|
||||
final GetChatResponseObject bubbleData;
|
||||
final GetRoomResponseObject chatData;
|
||||
final bool isRead;
|
||||
final String? selfId;
|
||||
|
||||
final double spacing = 3;
|
||||
final double timeIconSize = 11;
|
||||
final Color timeIconColor = Colors.grey;
|
||||
|
||||
final void Function({bool renew}) refetch;
|
||||
|
||||
const ChatBubble({
|
||||
required this.context,
|
||||
required this.isSender,
|
||||
required this.bubbleData,
|
||||
required this.chatData,
|
||||
required this.refetch,
|
||||
this.isRead = false,
|
||||
this.selfId,
|
||||
super.key});
|
||||
|
||||
@override
|
||||
State<ChatBubble> createState() => _ChatBubbleState();
|
||||
}
|
||||
|
||||
class _ChatBubbleState extends State<ChatBubble> with SingleTickerProviderStateMixin {
|
||||
late ChatMessage message;
|
||||
double downloadProgress = 0;
|
||||
Future<DownloaderCore>? downloadCore;
|
||||
|
||||
late Offset _position = const Offset(0, 0);
|
||||
late Offset _dragStartPosition = Offset.zero;
|
||||
|
||||
BubbleStyle getStyle() {
|
||||
var styles = ChatBubbleStyles(context);
|
||||
if(widget.bubbleData.messageType == GetRoomResponseObjectMessageType.comment) {
|
||||
if(widget.isSender) {
|
||||
return styles.getSelfStyle(false);
|
||||
} else {
|
||||
return styles.getRemoteStyle(false);
|
||||
}
|
||||
} else {
|
||||
return styles.getSystemStyle();
|
||||
}
|
||||
}
|
||||
|
||||
void showOptionsDialog() {
|
||||
showDialog(context: context, builder: (context) {
|
||||
var commonReactions = <String>['👍', '👎', '😆', '❤️', '👀'];
|
||||
var canReact = widget.bubbleData.messageType == GetRoomResponseObjectMessageType.comment;
|
||||
return SimpleDialog(
|
||||
children: [
|
||||
Visibility(
|
||||
visible: canReact,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
children: [
|
||||
...commonReactions.map((e) => TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
minimumSize: const Size(40, 40)
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
ReactMessage(
|
||||
chatToken: widget.chatData.token,
|
||||
messageId: widget.bubbleData.id,
|
||||
params: ReactMessageParams(e),
|
||||
).run().then((value) => widget.refetch(renew: true));
|
||||
},
|
||||
child: Text(e),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
showDialog(context: context, builder: (context) => AlertDialog(
|
||||
contentPadding: const EdgeInsets.all(15),
|
||||
titlePadding: const EdgeInsets.only(left: 6, top: 15),
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
const Text('Reagieren'),
|
||||
],
|
||||
),
|
||||
content: SizedBox(
|
||||
width: 256,
|
||||
height: 270,
|
||||
child: Column(
|
||||
children: [
|
||||
emojis.EmojiPicker(
|
||||
config: emojis.Config(
|
||||
height: 256,
|
||||
// swapCategoryAndBottomBar: true, // TODO this property is no longer supported, need to find an replacement
|
||||
emojiViewConfig: emojis.EmojiViewConfig(
|
||||
backgroundColor: Theme.of(context).canvasColor,
|
||||
recentsLimit: 67,
|
||||
emojiSizeMax: 25,
|
||||
noRecents: const Text('Keine zuletzt verwendeten Emojis'),
|
||||
columns: 7,
|
||||
),
|
||||
bottomActionBarConfig: const emojis.BottomActionBarConfig(
|
||||
enabled: false,
|
||||
),
|
||||
categoryViewConfig: emojis.CategoryViewConfig(
|
||||
backgroundColor: Theme.of(context).hoverColor,
|
||||
iconColorSelected: Theme.of(context).primaryColor,
|
||||
indicatorColor: Theme.of(context).primaryColor,
|
||||
),
|
||||
searchViewConfig: emojis.SearchViewConfig(
|
||||
backgroundColor: Theme.of(context).dividerColor,
|
||||
// buttonColor: Theme.of(context).dividerColor, // TODO property no longer supported
|
||||
hintText: 'Suchen',
|
||||
buttonIconColor: Colors.white,
|
||||
),
|
||||
),
|
||||
onEmojiSelected: (emojis.Category? category, emojis.Emoji emoji) {
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).pop();
|
||||
ReactMessage(
|
||||
chatToken: widget.chatData.token,
|
||||
messageId: widget.bubbleData.id,
|
||||
params: ReactMessageParams(emoji.emoji),
|
||||
).run().then((value) => widget.refetch(renew: true));
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
));
|
||||
},
|
||||
style: IconButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
minimumSize: const Size(40, 40),
|
||||
),
|
||||
icon: const Icon(Icons.add_circle_outline_outlined),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(),
|
||||
],
|
||||
),
|
||||
),
|
||||
Visibility(
|
||||
visible: widget.bubbleData.isReplyable,
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.reply_outlined),
|
||||
title: const Text('Antworten'),
|
||||
onTap: () {
|
||||
context.read<ChatBloc>().setReferenceMessageId(widget.bubbleData.id);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
),
|
||||
Visibility(
|
||||
visible: canReact,
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.emoji_emotions_outlined),
|
||||
title: const Text('Reaktionen'),
|
||||
onTap: () {
|
||||
Navigator.of(context).push(MaterialPageRoute(builder: (context) => MessageReactions(
|
||||
token: widget.chatData.token,
|
||||
messageId: widget.bubbleData.id,
|
||||
)));
|
||||
},
|
||||
),
|
||||
),
|
||||
Visibility(
|
||||
visible: widget.bubbleData.message != '{file}',
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.copy),
|
||||
title: const Text('Nachricht kopieren'),
|
||||
onTap: () => {
|
||||
Clipboard.setData(ClipboardData(text: widget.bubbleData.message)),
|
||||
Navigator.of(context).pop(),
|
||||
},
|
||||
),
|
||||
),
|
||||
Visibility(
|
||||
visible: !kReleaseMode && !widget.isSender && widget.chatData.type != GetRoomResponseObjectConversationType.oneToOne,
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.sms_outlined),
|
||||
title: Text("Private Nachricht an '${widget.bubbleData.actorDisplayName}'"),
|
||||
onTap: () => {
|
||||
Navigator.of(context).pop()
|
||||
},
|
||||
),
|
||||
),
|
||||
Visibility(
|
||||
visible: widget.isSender && DateTime.fromMillisecondsSinceEpoch(widget.bubbleData.timestamp * 1000).add(const Duration(hours: 6)).isAfter(DateTime.now()),
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.delete_outline),
|
||||
title: const Text('Nachricht löschen'),
|
||||
onTap: () {
|
||||
DeleteMessage(widget.chatData.token, widget.bubbleData.id).run().then((value) {
|
||||
if (!context.mounted) return;
|
||||
context.read<ChatBloc>().refresh();
|
||||
Navigator.of(context).pop();
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
DebugTile(context).jsonData(widget.bubbleData.toJson()),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
message = ChatMessage(originalMessage: widget.bubbleData.message, originalData: widget.bubbleData.messageParameters);
|
||||
var showActorDisplayName = widget.bubbleData.messageType == GetRoomResponseObjectMessageType.comment && widget.chatData.type != GetRoomResponseObjectConversationType.oneToOne;
|
||||
var showBubbleTime = widget.bubbleData.messageType != GetRoomResponseObjectMessageType.system
|
||||
&& widget.bubbleData.messageType != GetRoomResponseObjectMessageType.deletedComment;
|
||||
|
||||
var parent = widget.bubbleData.parent;
|
||||
var actorText = Text(
|
||||
widget.bubbleData.actorDisplayName,
|
||||
textAlign: TextAlign.start,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(color: Theme.of(context).primaryColor, fontWeight: FontWeight.bold),
|
||||
);
|
||||
|
||||
var timeText = Text(
|
||||
Jiffy.parseFromMillisecondsSinceEpoch(widget.bubbleData.timestamp * 1000).format(pattern: 'HH:mm'),
|
||||
textAlign: TextAlign.end,
|
||||
style: TextStyle(color: widget.timeIconColor, fontSize: widget.timeIconSize),
|
||||
);
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
textDirection: TextDirection.ltr,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
|
||||
children: [
|
||||
GestureDetector(
|
||||
onHorizontalDragStart: (details) {
|
||||
_dragStartPosition = _position;
|
||||
},
|
||||
onHorizontalDragUpdate: (details) {
|
||||
if(!widget.bubbleData.isReplyable) return;
|
||||
var dx = details.delta.dx - _dragStartPosition.dx;
|
||||
setState(() {
|
||||
_position = (_position.dx + dx).abs() > 60 ? Offset(_position.dx, 0) : Offset(_position.dx + dx, 0);
|
||||
});
|
||||
},
|
||||
onHorizontalDragEnd: (DragEndDetails details) {
|
||||
var isAction = _position.dx.abs() > 50;
|
||||
setState(() {
|
||||
_position = const Offset(0, 0);
|
||||
});
|
||||
if(widget.bubbleData.isReplyable && isAction) {
|
||||
context.read<ChatBloc>().setReferenceMessageId(widget.bubbleData.id);
|
||||
}
|
||||
},
|
||||
onLongPress: showOptionsDialog,
|
||||
onDoubleTap: showOptionsDialog,
|
||||
onTap: () {
|
||||
if(message.originalData?['object']?.type == RichObjectStringObjectType.talkPoll) {
|
||||
var pollId = int.parse(message.originalData!['object']!.id);
|
||||
var pollState = GetPollState(token: widget.bubbleData.token, pollId: pollId).run();
|
||||
showDialog(context: context, builder: (context) => AlertDialog(
|
||||
title: Text(message.originalData!['object']!.name, overflow: TextOverflow.ellipsis),
|
||||
content: FutureBuilder(
|
||||
future: pollState,
|
||||
builder: (context, snapshot) {
|
||||
if(snapshot.connectionState == ConnectionState.waiting) return const Column(mainAxisSize: MainAxisSize.min, children: [LoadingSpinner()]);
|
||||
|
||||
var pollData = snapshot.data!.data;
|
||||
return SingleChildScrollView(
|
||||
child: PollOptionsList(
|
||||
pollData: pollData,
|
||||
chatToken: widget.chatData.token,
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Zurück')
|
||||
),
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
if(message.file == null) return;
|
||||
|
||||
if(downloadProgress > 0) {
|
||||
showDialog(context: context, builder: (context) => AlertDialog(
|
||||
title: const Text('Download abbrechen?'),
|
||||
content: const Text('Möchtest du den Download abbrechen?'),
|
||||
actions: [
|
||||
TextButton(onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
}, child: const Text('Nein')),
|
||||
TextButton(onPressed: () {
|
||||
downloadCore?.then((value) {
|
||||
if(!value.isCancelled) value.cancel();
|
||||
if (!context.mounted) return;
|
||||
Navigator.of(context).pop();
|
||||
});
|
||||
setState(() {
|
||||
downloadProgress = 0;
|
||||
downloadCore = null;
|
||||
});
|
||||
}, child: const Text('Ja, Abbrechen'))
|
||||
],
|
||||
));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
downloadProgress = 1;
|
||||
});
|
||||
downloadCore = FileElement.download(context, message.file!.path!, message.file!.name, (progress) {
|
||||
if(progress > 1) {
|
||||
setState(() {
|
||||
downloadProgress = progress;
|
||||
});
|
||||
}
|
||||
}, (result) {
|
||||
setState(() {
|
||||
downloadProgress = 0;
|
||||
});
|
||||
|
||||
if(result.type != ResultType.done) {
|
||||
showDialog(context: context, builder: (context) => AlertDialog(
|
||||
content: Text(result.message),
|
||||
));
|
||||
}
|
||||
});
|
||||
},
|
||||
child: Transform.translate(
|
||||
offset: _position,
|
||||
child: Bubble(
|
||||
style: getStyle(),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: MediaQuery.of(context).size.width * 0.9,
|
||||
minWidth: showActorDisplayName
|
||||
? actorText.size.width
|
||||
: timeText.size.width + (widget.isSender ? widget.spacing + widget.timeIconSize : 0) + 3,
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
Visibility(
|
||||
visible: showActorDisplayName,
|
||||
child: 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 && widget.bubbleData.messageType == GetRoomResponseObjectMessageType.comment) ...[
|
||||
AnswerReference(
|
||||
context: context,
|
||||
referenceMessage: parent,
|
||||
selfId: widget.selfId,
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
],
|
||||
message.getWidget(),
|
||||
],
|
||||
),
|
||||
),
|
||||
Visibility(
|
||||
visible: showBubbleTime,
|
||||
child: Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
child: Row(
|
||||
children: [
|
||||
timeText,
|
||||
if(widget.isSender) ...[
|
||||
SizedBox(width: widget.spacing),
|
||||
Icon(
|
||||
widget.isRead ? Icons.done_all_outlined: Icons.done_outlined,
|
||||
size: widget.timeIconSize,
|
||||
color: widget.timeIconColor
|
||||
)
|
||||
]
|
||||
],
|
||||
)
|
||||
),
|
||||
),
|
||||
Visibility(
|
||||
visible: downloadProgress > 0,
|
||||
child: Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
left: 0,
|
||||
child: LinearProgressIndicator(value: downloadProgress == 1 ? null : downloadProgress/100),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Visibility(
|
||||
visible: widget.bubbleData.reactions != null,
|
||||
child: Transform.translate(
|
||||
offset: const Offset(0, -10),
|
||||
child: Container(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
margin: const EdgeInsets.only(left: 15, right: 15),
|
||||
child: Wrap(
|
||||
alignment: widget.isSender ? WrapAlignment.end : WrapAlignment.start,
|
||||
crossAxisAlignment: WrapCrossAlignment.start,
|
||||
children: widget.bubbleData.reactions?.entries.map<Widget>((e) {
|
||||
var hasSelfReacted = widget.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),
|
||||
padding: EdgeInsets.zero,
|
||||
backgroundColor: hasSelfReacted ? Theme.of(context).primaryColor : null,
|
||||
onPressed: () {
|
||||
if(hasSelfReacted) {
|
||||
// Delete existing reaction
|
||||
DeleteReactMessage(
|
||||
chatToken: widget.chatData.token,
|
||||
messageId: widget.bubbleData.id,
|
||||
params: DeleteReactMessageParams(e.key),
|
||||
).run().then((value) => widget.refetch(renew: true));
|
||||
|
||||
} else {
|
||||
// Add reaction
|
||||
ReactMessage(
|
||||
chatToken: widget.chatData.token,
|
||||
messageId: widget.bubbleData.id,
|
||||
params: ReactMessageParams(e.key)
|
||||
).run().then((value) => widget.refetch(renew: true));
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}).toList() ?? [],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
import 'package:bubble/bubble.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../theming/appTheme.dart';
|
||||
|
||||
extension ColorExtensions on Color {
|
||||
Color invert() {
|
||||
final invertedR = 1.0 - r;
|
||||
final invertedG = 1.0 - g;
|
||||
final invertedB = 1.0 - b;
|
||||
return Color.from(alpha: a, red: invertedR, green: invertedG, blue: invertedB);
|
||||
}
|
||||
|
||||
Color withWhite(int whiteValue) {
|
||||
final value = whiteValue / 255.0;
|
||||
return Color.from(alpha: a, red: value, green: value, blue: value);
|
||||
}
|
||||
}
|
||||
|
||||
class ChatBubbleStyles {
|
||||
final BuildContext context;
|
||||
|
||||
ChatBubbleStyles(this.context);
|
||||
|
||||
BubbleStyle getSystemStyle() => BubbleStyle(
|
||||
color: AppTheme.isDarkMode(context) ? const Color(0xff182229) : Colors.white,
|
||||
borderWidth: 1,
|
||||
elevation: 2,
|
||||
margin: const BubbleEdges.only(bottom: 20, top: 10),
|
||||
alignment: Alignment.center,
|
||||
);
|
||||
|
||||
BubbleStyle getRemoteStyle(bool seamless) {
|
||||
var color = AppTheme.isDarkMode(context) ? const Color(0xff202c33) : Colors.white;
|
||||
return BubbleStyle(
|
||||
nip: BubbleNip.leftTop,
|
||||
color: seamless ? Colors.transparent : color,
|
||||
borderWidth: seamless ? 0 : 1,
|
||||
elevation: seamless ? 0 : 1,
|
||||
margin: const BubbleEdges.only(bottom: 10, left: 10, right: 50),
|
||||
alignment: Alignment.topLeft,
|
||||
);
|
||||
}
|
||||
|
||||
BubbleStyle getSelfStyle(bool seamless) {
|
||||
var color = AppTheme.isDarkMode(context) ? const Color(0xff005c4b) : const Color(0xffd3d3d3);
|
||||
return BubbleStyle(
|
||||
nip: BubbleNip.rightBottom,
|
||||
color: seamless ? Colors.transparent : color,
|
||||
borderWidth: seamless ? 0 : 1,
|
||||
elevation: seamless ? 0 : 1,
|
||||
margin: const BubbleEdges.only(bottom: 10, right: 10, left: 50),
|
||||
alignment: Alignment.topRight,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||
|
||||
import '../../../../api/marianumcloud/talk/chat/getChatResponse.dart';
|
||||
import '../../../../api/marianumcloud/talk/chat/richObjectStringProcessor.dart';
|
||||
import '../../../../model/accountData.dart';
|
||||
import '../../../../model/endpointData.dart';
|
||||
import '../../../../utils/UrlOpener.dart';
|
||||
|
||||
class ChatMessage {
|
||||
String originalMessage;
|
||||
Map<String, RichObjectString>? originalData;
|
||||
|
||||
RichObjectString? file;
|
||||
String content = '';
|
||||
|
||||
bool get containsFile => file != null;
|
||||
|
||||
ChatMessage({required this.originalMessage, this.originalData}) {
|
||||
if(originalData?.containsKey('file') ?? false) {
|
||||
file = originalData?['file'];
|
||||
}
|
||||
content = RichObjectStringProcessor.parseToString(originalMessage, originalData);
|
||||
}
|
||||
|
||||
Widget getWidget() {
|
||||
|
||||
var contentWidget = Linkify(
|
||||
text: content,
|
||||
onOpen: UrlOpener.onOpen,
|
||||
);
|
||||
|
||||
if(originalData?['object']?.type == RichObjectStringObjectType.talkPoll) {
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.poll_outlined),
|
||||
title: Text(originalData!['object']!.name),
|
||||
contentPadding: const EdgeInsets.only(left: 10),
|
||||
);
|
||||
}
|
||||
|
||||
if(file == null) return contentWidget;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 5),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CachedNetworkImage(
|
||||
errorWidget: (context, url, error) => Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.file_open_outlined, size: 35),
|
||||
const SizedBox(width: 10),
|
||||
Flexible(child: Text(file!.name, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle(fontWeight: FontWeight.bold))),
|
||||
const SizedBox(width: 10),
|
||||
],
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
placeholder: (context, url) => const Padding(padding: EdgeInsets.all(15), child: SizedBox(width: 50, child: LinearProgressIndicator())),
|
||||
fadeInDuration: Duration.zero,
|
||||
fadeOutDuration: Duration.zero,
|
||||
errorListener: (value) {},
|
||||
httpHeaders: AccountData().authHeaders(),
|
||||
imageUrl: 'https://${EndpointData().nextcloud().full()}/index.php/core/preview?fileId=${file!.id}&x=130&y=-1&a=1',
|
||||
),
|
||||
if(originalMessage != '{file}') ...[
|
||||
SizedBox(height: 5),
|
||||
contentWidget
|
||||
]
|
||||
],
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,241 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:nextcloud/nextcloud.dart';
|
||||
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
|
||||
|
||||
import '../../../../api/marianumcloud/files-sharing/fileSharingApi.dart';
|
||||
import '../../../../api/marianumcloud/files-sharing/fileSharingApiParams.dart';
|
||||
import '../../../../api/marianumcloud/talk/sendMessage/sendMessage.dart';
|
||||
import '../../../../api/marianumcloud/talk/sendMessage/sendMessageParams.dart';
|
||||
import '../../../../api/marianumcloud/webdav/webdavApi.dart';
|
||||
import '../../../../state/app/modules/chat/bloc/chat_bloc.dart';
|
||||
import '../../../../state/app/modules/settings/bloc/settings_cubit.dart';
|
||||
import '../../../../widget/filePick.dart';
|
||||
import '../../../../widget/focusBehaviour.dart';
|
||||
import '../../files/filesUploadDialog.dart';
|
||||
import 'answerReference.dart';
|
||||
|
||||
class ChatTextfield extends StatefulWidget {
|
||||
final String sendToToken;
|
||||
final String? selfId;
|
||||
|
||||
const ChatTextfield(this.sendToToken, {this.selfId, super.key});
|
||||
|
||||
@override
|
||||
State<ChatTextfield> createState() => _ChatTextfieldState();
|
||||
}
|
||||
|
||||
class _ChatTextfieldState extends State<ChatTextfield> {
|
||||
late SettingsCubit settings;
|
||||
final TextEditingController _textBoxController = TextEditingController();
|
||||
bool isLoading = false;
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> mediaUpload(List<String>? paths) async {
|
||||
if (paths == null) return;
|
||||
|
||||
const shareFolder = 'MarianumMobile';
|
||||
WebdavApi.webdav.then((webdav) => webdav.mkcol(PathUri.parse('/$shareFolder')));
|
||||
|
||||
if (!mounted) return;
|
||||
pushScreen(
|
||||
context,
|
||||
withNavBar: false,
|
||||
screen: FilesUploadDialog(
|
||||
filePaths: paths,
|
||||
remotePath: shareFolder,
|
||||
onUploadFinished: (uploaded) => share(shareFolder, uploaded),
|
||||
uniqueNames: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _setDraft(String text) {
|
||||
final talkSettings = settings.val(write: true).talkSettings;
|
||||
if (text.isNotEmpty) {
|
||||
talkSettings.drafts[widget.sendToToken] = text;
|
||||
} else {
|
||||
talkSettings.drafts.removeWhere((key, _) => key == widget.sendToToken);
|
||||
}
|
||||
}
|
||||
|
||||
void _setDraftReply(int? messageId) {
|
||||
final talkSettings = settings.val(write: true).talkSettings;
|
||||
if (messageId != null) {
|
||||
talkSettings.draftReplies[widget.sendToToken] = messageId;
|
||||
} else {
|
||||
talkSettings.draftReplies.removeWhere((key, _) => key == widget.sendToToken);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
settings = context.read<SettingsCubit>();
|
||||
final draftReply = settings.val().talkSettings.draftReplies[widget.sendToToken];
|
||||
if (draftReply != null) {
|
||||
context.read<ChatBloc>().setReferenceMessageId(draftReply);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_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) {
|
||||
try {
|
||||
final referenceMessage = chatState.chatResponse!.sortByTimestamp().firstWhere(
|
||||
(e) => e.id == chatState.referenceMessageId,
|
||||
);
|
||||
replyBanner = Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: AnswerReference(
|
||||
context: context,
|
||||
referenceMessage: referenceMessage,
|
||||
selfId: widget.selfId,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
chatBloc.setReferenceMessageId(null);
|
||||
_setDraftReply(null);
|
||||
},
|
||||
icon: const Icon(Icons.close_outlined),
|
||||
padding: const EdgeInsets.only(left: 0),
|
||||
),
|
||||
],
|
||||
);
|
||||
} 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,
|
||||
Row(children: <Widget>[
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
showDialog(context: context, builder: (dialogCtx) => SimpleDialog(children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.file_open),
|
||||
title: const Text('Aus Dateien auswählen'),
|
||||
onTap: () {
|
||||
FilePick.documentPick().then(mediaUpload);
|
||||
Navigator.of(dialogCtx).pop();
|
||||
},
|
||||
),
|
||||
Visibility(
|
||||
visible: !Platform.isIOS,
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.image),
|
||||
title: const Text('Aus Gallerie auswählen'),
|
||||
onTap: () {
|
||||
FilePick.multipleGalleryPick().then((value) {
|
||||
if (value != null) mediaUpload(value.map((e) => e.path).toList());
|
||||
});
|
||||
Navigator.of(dialogCtx).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),
|
||||
),
|
||||
),
|
||||
),
|
||||
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),
|
||||
FloatingActionButton(
|
||||
mini: true,
|
||||
onPressed: () {
|
||||
if (_textBoxController.text.isEmpty || isLoading) return;
|
||||
|
||||
setState(() => isLoading = true);
|
||||
SendMessage(
|
||||
widget.sendToToken,
|
||||
SendMessageParams(
|
||||
_textBoxController.text,
|
||||
replyTo: chatBloc.state.data?.referenceMessageId?.toString(),
|
||||
),
|
||||
).run().then((_) {
|
||||
if (!mounted) return;
|
||||
chatBloc.refresh();
|
||||
setState(() => isLoading = false);
|
||||
_textBoxController.text = '';
|
||||
_setDraft('');
|
||||
chatBloc.setReferenceMessageId(null);
|
||||
_setDraftReply(null);
|
||||
});
|
||||
},
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
elevation: 5,
|
||||
child: isLoading
|
||||
? Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: const CircularProgressIndicator(color: Colors.white, strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.send, color: Colors.white, size: 18),
|
||||
),
|
||||
]),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:jiffy/jiffy.dart';
|
||||
|
||||
import '../../../../api/marianumcloud/talk/chat/richObjectStringProcessor.dart';
|
||||
import '../../../../api/marianumcloud/talk/leaveRoom/leaveRoom.dart';
|
||||
import '../../../../api/marianumcloud/talk/room/getRoomResponse.dart';
|
||||
import '../../../../api/marianumcloud/talk/setFavorite/setFavorite.dart';
|
||||
import '../../../../api/marianumcloud/talk/setReadMarker/setReadMarker.dart';
|
||||
import '../../../../api/marianumcloud/talk/setReadMarker/setReadMarkerParams.dart';
|
||||
import '../../../../model/accountData.dart';
|
||||
import '../../../../state/app/modules/chat/bloc/chat_bloc.dart';
|
||||
import '../../../../state/app/modules/chatList/bloc/chat_list_bloc.dart';
|
||||
import '../../../../widget/confirmDialog.dart';
|
||||
import '../../../../widget/debug/debugTile.dart';
|
||||
import '../../../../widget/userAvatar.dart';
|
||||
import '../chatView.dart';
|
||||
import '../talkNavigator.dart';
|
||||
|
||||
class ChatTile extends StatefulWidget {
|
||||
final GetRoomResponseObject data;
|
||||
final bool disableContextActions;
|
||||
final bool hasDraft;
|
||||
|
||||
const ChatTile({super.key, required this.data, this.disableContextActions = false, this.hasDraft = false});
|
||||
|
||||
@override
|
||||
State<ChatTile> createState() => _ChatTileState();
|
||||
}
|
||||
|
||||
class _ChatTileState extends State<ChatTile> {
|
||||
String? selfUsername;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
AccountData().waitForPopulation().then((_) {
|
||||
if (!mounted) return;
|
||||
setState(() => selfUsername = AccountData().isPopulated() ? AccountData().getUsername() : null);
|
||||
});
|
||||
}
|
||||
|
||||
void _refreshList() => context.read<ChatListBloc>().refresh();
|
||||
|
||||
void setCurrentAsRead() {
|
||||
SetReadMarker(
|
||||
widget.data.token,
|
||||
true,
|
||||
setReadMarkerParams: SetReadMarkerParams(lastReadMessage: widget.data.lastMessage.id),
|
||||
).run().then((_) {
|
||||
if (!mounted) return;
|
||||
_refreshList();
|
||||
});
|
||||
}
|
||||
|
||||
@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);
|
||||
|
||||
return ListTile(
|
||||
style: ListTileStyle.list,
|
||||
tileColor: chatBloc.state.data?.currentToken == widget.data.token && TalkNavigator.isSecondaryVisible(context)
|
||||
? Theme.of(context).primaryColor.withAlpha(100)
|
||||
: null,
|
||||
leading: Stack(
|
||||
children: [
|
||||
circleAvatar,
|
||||
Visibility(
|
||||
visible: widget.data.isFavorite,
|
||||
child: Positioned(
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(1),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).primaryColor.withAlpha(200),
|
||||
borderRadius: BorderRadius.circular(90.0),
|
||||
),
|
||||
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)),
|
||||
if (widget.hasDraft) ...[
|
||||
const SizedBox(width: 5),
|
||||
const Icon(Icons.edit_outlined, size: 15),
|
||||
],
|
||||
],
|
||||
),
|
||||
subtitle: Text(
|
||||
'${Jiffy.parseFromMillisecondsSinceEpoch(widget.data.lastMessage.timestamp * 1000).fromNow()}: '
|
||||
'${RichObjectStringProcessor.parseToString(widget.data.lastMessage.message.replaceAll("\n", " "), widget.data.lastMessage.messageParameters)}',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
trailing: widget.data.unreadMessages <= 0
|
||||
? null
|
||||
: Container(
|
||||
padding: const EdgeInsets.all(1),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).primaryColor,
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
constraints: const BoxConstraints(minWidth: 20, minHeight: 20),
|
||||
child: Text(
|
||||
'${widget.data.unreadMessages}',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 15),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
if (selfUsername == null) return;
|
||||
setCurrentAsRead();
|
||||
final view = ChatView(room: widget.data, selfId: selfUsername!, avatar: circleAvatar);
|
||||
TalkNavigator.pushSplitView(context, view, overrideToSingleSubScreen: true);
|
||||
context.read<ChatBloc>().setToken(widget.data.token);
|
||||
},
|
||||
onLongPress: () {
|
||||
if (widget.disableContextActions) return;
|
||||
showDialog(context: context, builder: (dialogCtx) => SimpleDialog(
|
||||
children: [
|
||||
Visibility(
|
||||
visible: widget.data.unreadMessages > 0,
|
||||
replacement: ListTile(
|
||||
leading: const Icon(Icons.mark_chat_unread_outlined),
|
||||
title: const Text('Als ungelesen markieren'),
|
||||
onTap: () {
|
||||
SetReadMarker(widget.data.token, false).run().then((_) {
|
||||
if (mounted) _refreshList();
|
||||
});
|
||||
Navigator.of(dialogCtx).pop();
|
||||
},
|
||||
),
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.mark_chat_read_outlined),
|
||||
title: const Text('Als gelesen markieren'),
|
||||
onTap: () {
|
||||
setCurrentAsRead();
|
||||
Navigator.of(dialogCtx).pop();
|
||||
},
|
||||
),
|
||||
),
|
||||
Visibility(
|
||||
visible: widget.data.isFavorite,
|
||||
replacement: ListTile(
|
||||
leading: const Icon(Icons.star_outline),
|
||||
title: const Text('Zu Favoriten hinzufügen'),
|
||||
onTap: () {
|
||||
SetFavorite(widget.data.token, true).run().then((_) {
|
||||
if (mounted) _refreshList();
|
||||
});
|
||||
Navigator.of(dialogCtx).pop();
|
||||
},
|
||||
),
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.stars_outlined),
|
||||
title: const Text('Von Favoriten entfernen'),
|
||||
onTap: () {
|
||||
SetFavorite(widget.data.token, false).run().then((_) {
|
||||
if (mounted) _refreshList();
|
||||
});
|
||||
Navigator.of(dialogCtx).pop();
|
||||
},
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.delete_outline),
|
||||
title: const Text('Konversation verlassen'),
|
||||
onTap: () {
|
||||
ConfirmDialog(
|
||||
title: 'Chat verlassen',
|
||||
content: 'Du benötigst ggf. eine Einladung um erneut beizutreten.',
|
||||
confirmButton: 'Löschen',
|
||||
onConfirm: () {
|
||||
LeaveRoom(widget.data.token).run().then((_) {
|
||||
if (mounted) _refreshList();
|
||||
});
|
||||
Navigator.of(dialogCtx).pop();
|
||||
},
|
||||
).asDialog(dialogCtx);
|
||||
},
|
||||
),
|
||||
DebugTile(dialogCtx).jsonData(widget.data.toJson()),
|
||||
],
|
||||
));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||
|
||||
import '../../../../api/marianumcloud/talk/getPoll/getPollStateResponse.dart';
|
||||
import '../../../../utils/UrlOpener.dart';
|
||||
|
||||
class PollOptionsList extends StatefulWidget {
|
||||
final GetPollStateResponseObject pollData;
|
||||
final String chatToken;
|
||||
const PollOptionsList({super.key, required this.pollData, required this.chatToken});
|
||||
|
||||
@override
|
||||
State<PollOptionsList> createState() => _PollOptionsListState();
|
||||
}
|
||||
|
||||
class _PollOptionsListState extends State<PollOptionsList> {
|
||||
@override
|
||||
Widget build(BuildContext context) => Column(
|
||||
children: [
|
||||
...widget.pollData.options.map<Widget>((option) {
|
||||
var optionId = widget.pollData.options.indexOf(option);
|
||||
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;
|
||||
var numVoters = widget.pollData.numVoters ?? 0;
|
||||
final portion = numVoters == 0 ? 0.0 : (votes / numVoters);
|
||||
|
||||
return ListTile(
|
||||
// enabled: false,
|
||||
isThreeLine: portionsVisible,
|
||||
dense: true,
|
||||
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),
|
||||
),
|
||||
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}',
|
||||
onOpen: UrlOpener.onOpen,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../theming/appTheme.dart';
|
||||
|
||||
class SplitViewPlaceholder extends StatelessWidget {
|
||||
const SplitViewPlaceholder({super.key});
|
||||
|
||||
@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)),
|
||||
],
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user