claude refactorings, flutter best practices, platform dependent changes, general cleanup

This commit is contained in:
2026-05-06 11:58:50 +02:00
parent 4b1d4379a0
commit 4e1272aba9
281 changed files with 1948 additions and 1041 deletions
+8 -8
View File
@@ -3,20 +3,20 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_split_view/flutter_split_view.dart';
import '../../../routing/app_routes.dart';
import '../../../state/app/infrastructure/loadableState/loadable_state.dart';
import '../../../state/app/infrastructure/loadableState/view/loadable_state_consumer.dart';
import '../../../state/app/infrastructure/utilityWidgets/bloc_module.dart';
import '../../../state/app/modules/chatList/bloc/chat_list_bloc.dart';
import '../../../state/app/modules/chatList/bloc/chat_list_state.dart';
import '../../../notification/notify_updater.dart';
import '../../../routing/app_routes.dart';
import '../../../state/app/infrastructure/loadable_state/loadable_state.dart';
import '../../../state/app/infrastructure/loadable_state/view/loadable_state_consumer.dart';
import '../../../state/app/infrastructure/utility_widgets/bloc_module.dart';
import '../../../state/app/modules/chat_list/bloc/chat_list_bloc.dart';
import '../../../state/app/modules/chat_list/bloc/chat_list_state.dart';
import '../../../state/app/modules/settings/bloc/settings_cubit.dart';
import '../../../widget/confirm_dialog.dart';
import '../../../widget/placeholder_view.dart';
import 'widgets/chat_tile.dart';
import 'widgets/split_view_placeholder.dart';
import 'join_chat.dart';
import 'search_chat.dart';
import 'widgets/chat_tile.dart';
import 'widgets/split_view_placeholder.dart';
class ChatList extends StatelessWidget {
const ChatList({super.key});
+6 -6
View File
@@ -1,19 +1,19 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../api/marianumcloud/talk/chat/getChatResponse.dart';
import '../../../api/marianumcloud/talk/room/getRoomResponse.dart';
import '../../../api/marianumcloud/talk/chat/get_chat_response.dart';
import '../../../api/marianumcloud/talk/room/get_room_response.dart';
import '../../../extensions/date_time.dart';
import '../../../state/app/infrastructure/loadableState/view/loadable_state_consumer.dart';
import '../../../state/app/infrastructure/loadable_state/view/loadable_state_consumer.dart';
import '../../../state/app/modules/chat/bloc/chat_bloc.dart';
import '../../../state/app/modules/chat/bloc/chat_state.dart';
import '../../../theming/app_theme.dart';
import '../../../widget/clickable_app_bar.dart';
import '../../../widget/user_avatar.dart';
import 'details/chat_info.dart';
import 'talk_navigator.dart';
import 'widgets/chat_bubble.dart';
import 'widgets/chat_textfield.dart';
import 'talk_navigator.dart';
class ChatView extends StatefulWidget {
final GetRoomResponseObject room;
@@ -99,7 +99,7 @@ class _ChatViewState extends State<ChatView> {
),
),
),
body: Container(
body: DecoratedBox(
decoration: BoxDecoration(
image: DecorationImage(
image: const AssetImage('assets/background/chat.png'),
@@ -122,7 +122,7 @@ class _ChatViewState extends State<ChatView> {
),
),
),
Container(
ColoredBox(
color: Theme.of(context).colorScheme.surface,
child: TalkNavigator.isSecondaryVisible(context)
? ChatTextfield(widget.room.token, selfId: widget.selfId)
@@ -1,7 +1,7 @@
import 'package:bubble/bubble.dart';
import 'package:flutter/material.dart';
import '../../../../theming/app_theme.dart';
import '../widgets/bubble.dart';
extension ColorExtensions on Color {
Color invert() {
+2 -2
View File
@@ -3,8 +3,8 @@ 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 '../../../../api/marianumcloud/talk/chat/get_chat_response.dart';
import '../../../../api/marianumcloud/talk/chat/rich_object_string_processor.dart';
import '../../../../model/account_data.dart';
import '../../../../model/endpoint_data.dart';
import '../../../../utils/url_opener.dart';
+3 -3
View File
@@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import '../../../../api/marianumcloud/talk/getParticipants/getParticipantsCache.dart';
import '../../../../api/marianumcloud/talk/getParticipants/getParticipantsResponse.dart';
import '../../../../api/marianumcloud/talk/room/getRoomResponse.dart';
import '../../../../api/marianumcloud/talk/get_participants/get_participants_cache.dart';
import '../../../../api/marianumcloud/talk/get_participants/get_participants_response.dart';
import '../../../../api/marianumcloud/talk/room/get_room_response.dart';
import '../../../../widget/large_profile_picture_view.dart';
import '../../../../widget/loading_spinner.dart';
import '../../../../widget/user_avatar.dart';
@@ -1,8 +1,8 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import '../../../../api/marianumcloud/talk/getReactions/getReactions.dart';
import '../../../../api/marianumcloud/talk/getReactions/getReactionsResponse.dart';
import '../../../../api/marianumcloud/talk/get_reactions/get_reactions.dart';
import '../../../../api/marianumcloud/talk/get_reactions/get_reactions_response.dart';
import '../../../../model/account_data.dart';
import '../../../../widget/centered_leading.dart';
import '../../../../widget/loading_spinner.dart';
@@ -1,7 +1,7 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import '../../../../api/marianumcloud/talk/getParticipants/getParticipantsResponse.dart';
import '../../../../api/marianumcloud/talk/get_participants/get_participants_response.dart';
import '../../../../widget/user_avatar.dart';
class ParticipantsListView extends StatelessWidget {
@@ -10,7 +10,7 @@ class ParticipantsListView extends StatelessWidget {
@override
Widget build(BuildContext context) {
lastname(participant) => participant.displayName.toString().split(' ').last;
String lastname(participant) => participant.displayName.toString().split(' ').last;
final participants = participantsResponse.data
.sorted((a, b) {
+2 -2
View File
@@ -3,8 +3,8 @@ import 'package:async/async.dart';
import 'package:flutter/material.dart';
import '../../../api/errors/error_mapper.dart';
import '../../../api/marianumcloud/autocomplete/autocompleteApi.dart';
import '../../../api/marianumcloud/autocomplete/autocompleteResponse.dart';
import '../../../api/marianumcloud/autocomplete/autocomplete_api.dart';
import '../../../api/marianumcloud/autocomplete/autocomplete_response.dart';
import '../../../model/endpoint_data.dart';
import '../../../widget/app_progress_indicator.dart';
import '../../../widget/placeholder_view.dart';
+2 -2
View File
@@ -1,9 +1,9 @@
import 'package:flutter/material.dart';
import '../../../api/marianumcloud/talk/room/getRoomResponse.dart';
import '../../../api/marianumcloud/talk/room/get_room_response.dart';
import 'widgets/chat_tile.dart';
class SearchChat extends SearchDelegate {
class SearchChat extends SearchDelegate<GetRoomResponseObject?> {
List<GetRoomResponseObject> chats;
SearchChat(this.chats);
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import '../../../../api/marianumcloud/talk/chat/getChatResponse.dart';
import '../../../../api/marianumcloud/talk/chat/richObjectStringProcessor.dart';
import '../../../../api/marianumcloud/talk/chat/get_chat_response.dart';
import '../../../../api/marianumcloud/talk/chat/rich_object_string_processor.dart';
import '../data/chat_bubble_styles.dart';
class AnswerReference extends StatelessWidget {
+87
View File
@@ -0,0 +1,87 @@
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.all(double value)
: top = value,
bottom = value,
left = value,
right = value;
final double top;
final double bottom;
final double left;
final double right;
EdgeInsets toEdgeInsets() => EdgeInsets.fromLTRB(left, top, right, bottom);
}
class BubbleStyle {
const BubbleStyle({
this.color,
this.borderWidth = 0,
this.elevation = 0,
this.margin = const BubbleEdges.only(),
this.padding = const BubbleEdges.all(8),
this.alignment = Alignment.centerLeft,
this.nip = BubbleNip.none,
this.borderRadius = 12,
});
final Color? color;
final double borderWidth;
final double elevation;
final BubbleEdges margin;
final BubbleEdges padding;
final Alignment alignment;
final BubbleNip nip;
final double borderRadius;
}
/// Lightweight chat bubble. Replaces the abandoned `bubble` package: renders a
/// rounded container with optional shadow / border. The nip is conveyed by
/// flattening one corner so the bubble visually anchors to the speaker side.
class Bubble extends StatelessWidget {
const Bubble({required this.child, required this.style, super.key});
final Widget child;
final BubbleStyle style;
BorderRadius _radius() {
final r = Radius.circular(style.borderRadius);
final flat = Radius.zero;
switch (style.nip) {
case BubbleNip.leftTop:
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);
case BubbleNip.none:
return BorderRadius.all(r);
}
}
@override
Widget build(BuildContext context) {
final radius = _radius();
return Align(
alignment: style.alignment,
child: Container(
margin: style.margin.toEdgeInsets(),
decoration: BoxDecoration(
color: style.color,
borderRadius: radius,
border: style.borderWidth > 0
? 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))]
: null,
),
padding: style.padding.toEdgeInsets(),
child: child,
),
);
}
}
+108 -64
View File
@@ -1,25 +1,24 @@
import 'package:bubble/bubble.dart';
import 'package:flowder/flowder.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:jiffy/jiffy.dart';
import 'package:open_filex/open_filex.dart';
import '../../../../api/marianumcloud/talk/chat/getChatResponse.dart';
import '../../../../api/marianumcloud/talk/deleteReactMessage/deleteReactMessage.dart';
import '../../../../api/marianumcloud/talk/deleteReactMessage/deleteReactMessageParams.dart';
import '../../../../api/marianumcloud/talk/getPoll/getPollState.dart';
import '../../../../api/marianumcloud/talk/reactMessage/reactMessage.dart';
import '../../../../api/marianumcloud/talk/reactMessage/reactMessageParams.dart';
import '../../../../api/marianumcloud/talk/room/getRoomResponse.dart';
import '../../../../api/marianumcloud/talk/chat/get_chat_response.dart';
import '../../../../api/marianumcloud/talk/delete_react_message/delete_react_message.dart';
import '../../../../api/marianumcloud/talk/delete_react_message/delete_react_message_params.dart';
import '../../../../api/marianumcloud/talk/get_poll/get_poll_state.dart';
import '../../../../api/marianumcloud/talk/react_message/react_message.dart';
import '../../../../api/marianumcloud/talk/react_message/react_message_params.dart';
import '../../../../api/marianumcloud/talk/room/get_room_response.dart';
import '../../../../extensions/text.dart';
import '../../../../routing/app_routes.dart';
import '../../../../state/app/modules/chat/bloc/chat_bloc.dart';
import '../../../../utils/download_manager.dart';
import '../../../../widget/async_action_button.dart';
import '../../../../widget/loading_spinner.dart';
import '../../files/widgets/file_element.dart';
import '../data/chat_bubble_styles.dart';
import '../data/chat_message.dart';
import 'answer_reference.dart';
import 'bubble.dart';
import 'chat_message_options_dialog.dart';
import 'poll_options_list.dart';
@@ -53,12 +52,95 @@ class ChatBubble extends StatefulWidget {
class _ChatBubbleState extends State<ChatBubble> with SingleTickerProviderStateMixin {
late ChatMessage message;
double downloadProgress = 0;
Future<DownloaderCore>? downloadCore;
DownloadJob? _job;
late Offset _position = const Offset(0, 0);
late Offset _dragStartPosition = Offset.zero;
@override
void initState() {
super.initState();
final filePath = widget.bubbleData.messageParameters?['file']?.path;
if (filePath != null) _attachJob(DownloadManager.instance.jobFor(filePath));
}
@override
void dispose() {
_detachJob();
super.dispose();
}
void _attachJob(DownloadJob? job) {
_job = job;
if (job == null) return;
job.status.addListener(_onStatusChange);
if (job.isFinished) {
WidgetsBinding.instance.addPostFrameCallback((_) => _onStatusChange());
}
}
void _detachJob() {
_job?.status.removeListener(_onStatusChange);
_job = null;
}
void _onStatusChange() {
if (!mounted) return;
final job = _job;
if (job == null) return;
final status = job.status.value;
if (status is DownloadDone) {
DownloadManager.instance.clear(job.remotePath);
_detachJob();
AppRoutes.openFileViewer(context, status.localPath);
setState(() {});
} else if (status is DownloadFailed) {
final message = status.message;
DownloadManager.instance.clear(job.remotePath);
_detachJob();
setState(() {});
showDialog<void>(context: context, builder: (context) => AlertDialog(content: Text(message)));
} else if (status is DownloadCancelled) {
DownloadManager.instance.clear(job.remotePath);
_detachJob();
setState(() {});
} else {
setState(() {});
}
}
Future<void> _startFileDownload() async {
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);
if (!mounted) return;
if (_job == job) return;
_detachJob();
_attachJob(job);
setState(() {});
}
void _confirmCancel() {
showDialog<void>(
context: context,
builder: (dialogContext) => AlertDialog(
title: const Text('Download abbrechen?'),
content: const Text('Möchtest du den Download abbrechen?'),
actions: [
TextButton(onPressed: () => Navigator.of(dialogContext).pop(), child: const Text('Nein')),
TextButton(
onPressed: () {
Navigator.of(dialogContext).pop();
_job?.cancel();
},
child: const Text('Ja, Abbrechen'),
),
],
),
);
}
BubbleStyle getStyle() {
var styles = ChatBubbleStyles(context);
if(widget.bubbleData.messageType == GetRoomResponseObjectMessageType.comment) {
@@ -162,53 +244,12 @@ class _ChatBubbleState extends State<ChatBubble> with SingleTickerProviderStateM
));
}
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;
if (message.file == null) return;
if (_job?.status.value is DownloadInProgress) {
_confirmCancel();
} else {
_startFileDownload();
}
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,
@@ -270,15 +311,18 @@ class _ChatBubbleState extends State<ChatBubble> with SingleTickerProviderStateM
)
),
),
Visibility(
visible: downloadProgress > 0,
child: Positioned(
if (_job?.status.value is DownloadInProgress)
Positioned(
bottom: 0,
right: 0,
left: 0,
child: LinearProgressIndicator(value: downloadProgress == 1 ? null : downloadProgress/100),
child: LinearProgressIndicator(
value: () {
final s = _job!.status.value as DownloadInProgress;
return s.percent <= 0 ? null : s.percent / 100;
}(),
),
),
),
],
),
),
@@ -4,11 +4,11 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../api/marianumcloud/talk/chat/getChatResponse.dart';
import '../../../../api/marianumcloud/talk/actions/talk_actions.dart';
import '../../../../api/marianumcloud/talk/reactMessage/reactMessage.dart';
import '../../../../api/marianumcloud/talk/reactMessage/reactMessageParams.dart';
import '../../../../api/marianumcloud/talk/room/getRoomResponse.dart';
import '../../../../api/marianumcloud/talk/chat/get_chat_response.dart';
import '../../../../api/marianumcloud/talk/react_message/react_message.dart';
import '../../../../api/marianumcloud/talk/react_message/react_message_params.dart';
import '../../../../api/marianumcloud/talk/room/get_room_response.dart';
import '../../../../routing/app_routes.dart';
import '../../../../state/app/modules/chat/bloc/chat_bloc.dart';
import '../../../../widget/app_progress_indicator.dart';
@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
@@ -5,11 +6,11 @@ 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 '../../../../api/marianumcloud/files_sharing/file_sharing_api.dart';
import '../../../../api/marianumcloud/files_sharing/file_sharing_api_params.dart';
import '../../../../api/marianumcloud/talk/send_message/send_message.dart';
import '../../../../api/marianumcloud/talk/send_message/send_message_params.dart';
import '../../../../api/marianumcloud/webdav/webdav_api.dart';
import '../../../../state/app/modules/chat/bloc/chat_bloc.dart';
import '../../../../state/app/modules/settings/bloc/settings_cubit.dart';
import '../../../../widget/async_action_button.dart';
@@ -51,10 +52,10 @@ class _ChatTextfieldState extends State<ChatTextfield> {
if (paths == null) return;
const shareFolder = 'MarianumMobile';
WebdavApi.webdav.then((webdav) => webdav.mkcol(PathUri.parse('/$shareFolder')));
unawaited(WebdavApi.webdav.then((webdav) => webdav.mkcol(PathUri.parse('/$shareFolder'))));
if (!mounted) return;
pushScreen(
unawaited(pushScreen(
context,
withNavBar: false,
screen: FilesUploadDialog(
@@ -63,7 +64,7 @@ class _ChatTextfieldState extends State<ChatTextfield> {
onUploadFinished: (uploaded) => share(shareFolder, uploaded),
uniqueNames: true,
),
);
));
}
void _setDraft(String text) {
+5 -5
View File
@@ -5,13 +5,13 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:jiffy/jiffy.dart';
import '../../../../api/marianumcloud/talk/actions/talk_actions.dart';
import '../../../../api/marianumcloud/talk/chat/richObjectStringProcessor.dart';
import '../../../../api/marianumcloud/talk/room/getRoomResponse.dart';
import '../../../../api/marianumcloud/talk/setReadMarker/setReadMarker.dart';
import '../../../../api/marianumcloud/talk/setReadMarker/setReadMarkerParams.dart';
import '../../../../api/marianumcloud/talk/chat/rich_object_string_processor.dart';
import '../../../../api/marianumcloud/talk/room/get_room_response.dart';
import '../../../../api/marianumcloud/talk/set_read_marker/set_read_marker.dart';
import '../../../../api/marianumcloud/talk/set_read_marker/set_read_marker_params.dart';
import '../../../../model/account_data.dart';
import '../../../../state/app/modules/chat/bloc/chat_bloc.dart';
import '../../../../state/app/modules/chatList/bloc/chat_list_bloc.dart';
import '../../../../state/app/modules/chat_list/bloc/chat_list_bloc.dart';
import '../../../../widget/async_action_button.dart';
import '../../../../widget/confirm_dialog.dart';
import '../../../../widget/debug/debug_tile.dart';
@@ -2,7 +2,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import '../../../../api/marianumcloud/talk/getPoll/getPollStateResponse.dart';
import '../../../../api/marianumcloud/talk/get_poll/get_poll_state_response.dart';
import '../../../../utils/url_opener.dart';
class PollOptionsList extends StatefulWidget {