diff --git a/mobile-next/lib/company/brand/apply_official_styles.dart b/mobile-next/lib/company/brand/apply_official_styles.dart index 69bd4db..3846948 100644 --- a/mobile-next/lib/company/brand/apply_official_styles.dart +++ b/mobile-next/lib/company/brand/apply_official_styles.dart @@ -1,3 +1,4 @@ +import 'package:flutter/material.dart'; import 'package:openim_common/openim_common.dart'; import 'app_tokens.dart'; @@ -34,4 +35,73 @@ void applyOfficialStylesFromTokens() { Styles.c_0C1C33_opacity60 = AppColors.textPrimary.withOpacity(.6); Styles.c_0C1C33_opacity85 = AppColors.textPrimary.withOpacity(.85); Styles.c_0C1C33_opacity80 = AppColors.textPrimary.withOpacity(.8); + + Styles.ts_0C1C33_17sp = TextStyle( + color: AppColors.textPrimary, + fontSize: AppFont.body, + ); + Styles.ts_0C1C33_17sp_medium = TextStyle( + color: AppColors.textPrimary, + fontSize: AppFont.body, + fontWeight: FontWeight.w500, + ); + Styles.ts_0C1C33_17sp_semibold = TextStyle( + color: AppColors.textPrimary, + fontSize: AppFont.body, + fontWeight: FontWeight.w600, + ); + Styles.ts_0C1C33_20sp = TextStyle( + color: AppColors.textPrimary, + fontSize: AppFont.title, + ); + Styles.ts_0C1C33_20sp_medium = TextStyle( + color: AppColors.textPrimary, + fontSize: AppFont.title, + fontWeight: FontWeight.w500, + ); + Styles.ts_0C1C33_20sp_semibold = TextStyle( + color: AppColors.textPrimary, + fontSize: AppFont.title, + fontWeight: FontWeight.w600, + ); + Styles.ts_0089FF_10sp_semibold = TextStyle( + color: AppColors.primary, + fontSize: AppFont.small, + fontWeight: FontWeight.w600, + ); + Styles.ts_0089FF_10sp = TextStyle( + color: AppColors.primary, + fontSize: AppFont.small, + ); + Styles.ts_0089FF_17sp = TextStyle( + color: AppColors.primary, + fontSize: AppFont.body, + ); + Styles.ts_0089FF_17sp_semibold = TextStyle( + color: AppColors.primary, + fontSize: AppFont.body, + fontWeight: FontWeight.w600, + ); + Styles.ts_0089FF_17sp_medium = TextStyle( + color: AppColors.primary, + fontSize: AppFont.body, + fontWeight: FontWeight.w500, + ); + Styles.ts_FF381F_17sp = TextStyle( + color: AppColors.danger, + fontSize: AppFont.body, + ); + Styles.ts_FF381F_10sp = TextStyle( + color: AppColors.danger, + fontSize: AppFont.small, + ); + Styles.ts_6085B1_17sp_medium = TextStyle( + color: AppColors.avatarGray, + fontSize: AppFont.body, + fontWeight: FontWeight.w500, + ); + Styles.ts_6085B1_17sp = TextStyle( + color: AppColors.avatarGray, + fontSize: AppFont.body, + ); } diff --git a/mobile-next/openim_common/lib/src/widgets/chat/chat_input_box.dart b/mobile-next/openim_common/lib/src/widgets/chat/chat_input_box.dart index 1abd21d..d72cdeb 100644 --- a/mobile-next/openim_common/lib/src/widgets/chat/chat_input_box.dart +++ b/mobile-next/openim_common/lib/src/widgets/chat/chat_input_box.dart @@ -105,15 +105,20 @@ class _ChatInputBoxState extends State /*with TickerProviderStateM children: [ Container( constraints: BoxConstraints(minHeight: kInputBoxMinHeight), - color: Styles.c_F0F2F6, + decoration: BoxDecoration( + color: Styles.c_FFFFFF, + border: Border( + top: BorderSide(color: Styles.c_E8EAEF, width: 0.5), + ), + ), child: Row( children: [ 12.horizontalSpace, (_leftKeyboardButton ? (ImageRes.openKeyboard.toImage..onTap = onTapLeftKeyboard) : (ImageRes.openVoice.toImage..onTap = onTapSpeak)) - ..width = 32.w - ..height = 32.h + ..width = 24.w + ..height = 24.h ..opacity = _opacity, 12.horizontalSpace, Expanded( @@ -134,15 +139,33 @@ class _ChatInputBoxState extends State /*with TickerProviderStateM (_rightKeyboardButton ? (ImageRes.openKeyboard.toImage..onTap = onTapRightKeyboard) : (ImageRes.openEmoji.toImage..onTap = onTapEmoji)) - ..width = 32.w - ..height = 32.h + ..width = 24.w + ..height = 24.h ..opacity = _opacity, 12.horizontalSpace, - (_sendButtonVisible ? ImageRes.sendMessage : ImageRes.openToolbox).toImage - ..width = 32.w - ..height = 32.h - ..opacity = _opacity - ..onTap = _sendButtonVisible ? send : toggleToolbox, + _sendButtonVisible + ? GestureDetector( + onTap: send, + child: Opacity( + opacity: _opacity, + child: Container( + height: 32, + padding: EdgeInsets.symmetric(horizontal: 12.w), + alignment: Alignment.center, + decoration: BoxDecoration( + color: Styles.c_0089FF, + borderRadius: BorderRadius.circular(8.r), + ), + child: StrRes.send.toText + ..style = Styles.ts_FFFFFF_14sp_medium, + ), + ), + ) + : ((ImageRes.openToolbox).toImage + ..width = 24.w + ..height = 24.h + ..opacity = _opacity + ..onTap = toggleToolbox), 12.horizontalSpace, ], ), @@ -178,10 +201,11 @@ class _ChatInputBoxState extends State /*with TickerProviderStateM } Widget get _textFiled => Container( - margin: EdgeInsets.only(top: 10.h, bottom: _showQuoteView ? 4.h : 10.h), + constraints: const BoxConstraints(minHeight: 40), + margin: EdgeInsets.only(top: 8.h, bottom: _showQuoteView ? 4.h : 8.h), decoration: BoxDecoration( - color: Styles.c_FFFFFF, - borderRadius: BorderRadius.circular(4.r), + color: Styles.c_F0F2F6, + borderRadius: BorderRadius.circular(8.r), ), child: ChatTextField( controller: widget.controller, diff --git a/mobile-next/openim_common/lib/src/widgets/chat/chat_item_view.dart b/mobile-next/openim_common/lib/src/widgets/chat/chat_item_view.dart index 3915720..7807eaa 100644 --- a/mobile-next/openim_common/lib/src/widgets/chat/chat_item_view.dart +++ b/mobile-next/openim_common/lib/src/widgets/chat/chat_item_view.dart @@ -8,8 +8,8 @@ import 'package:focus_detector_v2/focus_detector_v2.dart'; import 'package:openim_common/openim_common.dart'; import 'package:rxdart/rxdart.dart'; -double maxWidth = 247.w; -double pictureWidth = 120.w; +double maxWidth = 0.65.sw; +double pictureWidth = 200; double videoWidth = 120.w; double locationWidth = 220.w; diff --git a/mobile-next/openim_common/lib/src/widgets/chat/chat_picture_preview.dart b/mobile-next/openim_common/lib/src/widgets/chat/chat_picture_preview.dart index acf3aae..bd53e24 100644 --- a/mobile-next/openim_common/lib/src/widgets/chat/chat_picture_preview.dart +++ b/mobile-next/openim_common/lib/src/widgets/chat/chat_picture_preview.dart @@ -30,14 +30,28 @@ class ChatPicturePreview extends StatelessWidget { offset: offset, pageSize: pageSize, ), - child: MetaHero( - heroTag: heroTag, - onTap: onTap ?? () => Get.back(), - onLongPress: () { - final index = controller?.page?.round() ?? 0; - onLongPress?.call(images[index].url!); - }, - child: _childView, + child: Stack( + children: [ + MetaHero( + heroTag: heroTag, + onTap: onTap ?? () => Get.back(), + onLongPress: () { + final index = controller?.page?.round() ?? currentIndex; + onLongPress?.call(images[index].url!); + }, + child: _childView, + ), + if (images.length > 1) + Positioned( + top: MediaQuery.paddingOf(context).top + 12, + left: 16, + child: _PreviewIndex( + controller: controller, + total: images.length, + initial: currentIndex, + ), + ), + ], ), ); } @@ -79,19 +93,14 @@ class ChatPicturePreview extends StatelessWidget { if (source.url?.isVideoFileName == true) { return null; } - final ImageChunkEvent? loadingProgress = state.loadingProgress; - final double? progress = loadingProgress?.expectedTotalBytes != null - ? loadingProgress!.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! - : null; - return SizedBox( - width: 15.0, - height: 15.0, - child: Center( + return const Center( + child: SizedBox( + width: 24, + height: 24, child: CircularProgressIndicator( color: Styles.c_0089FF, - strokeWidth: 1.5, - value: progress ?? 0, + strokeWidth: 2, ), ), ); @@ -110,7 +119,23 @@ class ChatPicturePreview extends StatelessWidget { ); case LoadState.failed: state.imageProvider.evict(); - return ImageRes.pictureError.toImage; + return GestureDetector( + onTap: () => state.reLoadImage(), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.broken_image_outlined, + size: 64, color: Styles.c_FFFFFF_opacity70), + const SizedBox(height: 16), + const Text('图片加载失败', + style: TextStyle(color: Colors.white, fontSize: 16)), + const SizedBox(height: 4), + Text('点击重试', + style: TextStyle( + color: Styles.c_FFFFFF_opacity70, fontSize: 14)), + ], + ), + ); } }, ); @@ -140,3 +165,50 @@ class MetaHero extends StatelessWidget { return heroTag == null ? view : Hero(tag: heroTag!, child: view); } } + +class _PreviewIndex extends StatefulWidget { + const _PreviewIndex({ + required this.controller, + required this.total, + required this.initial, + }); + + final ExtendedPageController? controller; + final int total; + final int initial; + + @override + State<_PreviewIndex> createState() => _PreviewIndexState(); +} + +class _PreviewIndexState extends State<_PreviewIndex> { + late int _index; + + @override + void initState() { + super.initState(); + _index = widget.initial; + widget.controller?.addListener(_onPage); + } + + void _onPage() { + final page = widget.controller?.page?.round(); + if (page != null && page != _index) { + setState(() => _index = page); + } + } + + @override + void dispose() { + widget.controller?.removeListener(_onPage); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Text( + '${_index + 1}/${widget.total}', + style: const TextStyle(color: Colors.white, fontSize: 14), + ); + } +} diff --git a/mobile-next/openim_common/lib/src/widgets/chat/chat_picture_view.dart b/mobile-next/openim_common/lib/src/widgets/chat/chat_picture_view.dart index e600b5d..0defb59 100644 --- a/mobile-next/openim_common/lib/src/widgets/chat/chat_picture_view.dart +++ b/mobile-next/openim_common/lib/src/widgets/chat/chat_picture_view.dart @@ -44,12 +44,20 @@ class _ChatPictureViewState extends State { var h = picture?.sourcePicture?.height?.toDouble() ?? 1.0; if (pictureWidth > w) { - _trulyWidth = w; - _trulyHeight = h; + _trulyWidth = w < 80 ? 80 : w; + _trulyHeight = _trulyWidth * h / w; } else { _trulyWidth = pictureWidth; _trulyHeight = _trulyWidth * h / w; } + if (_trulyHeight < 80) { + _trulyHeight = 80; + _trulyWidth = _trulyHeight * w / h; + if (_trulyWidth > pictureWidth) { + _trulyWidth = pictureWidth; + _trulyHeight = _trulyWidth * h / w; + } + } final height = pictureWidth * 1.sh / 1.sw; diff --git a/mobile-next/openim_common/lib/src/widgets/chat/chat_voice_record_bar.dart b/mobile-next/openim_common/lib/src/widgets/chat/chat_voice_record_bar.dart index 756efe0..f82ecde 100644 --- a/mobile-next/openim_common/lib/src/widgets/chat/chat_voice_record_bar.dart +++ b/mobile-next/openim_common/lib/src/widgets/chat/chat_voice_record_bar.dart @@ -5,7 +5,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:openim_common/openim_common.dart'; -double kVoiceRecordBarHeight = 36.h; +double kVoiceRecordBarHeight = 40; enum RecordBarStatus { holdTalk, @@ -39,7 +39,7 @@ class ChatVoiceRecordBar extends StatefulWidget { class _ChatVoiceRecordBarState extends State { bool _pressing = false; bool _canCancel = false; - final double _offset = 40.h; + final double _offset = 60; StreamSubscription? _sub; @override @@ -106,12 +106,15 @@ class _ChatVoiceRecordBarState extends State { height: kVoiceRecordBarHeight, alignment: Alignment.center, decoration: BoxDecoration( - color: widget.speakBarColor ?? (_pressing ? Styles.c_8E9AB0_opacity30 : Styles.c_FFFFFF), - borderRadius: BorderRadius.circular(4.r), + color: widget.speakBarColor ?? + (_pressing ? Styles.c_F2F8FF : Styles.c_F0F2F6), + borderRadius: BorderRadius.circular(8.r), ), child: Text( - _pressing ? (_canCancel ? StrRes.liftFingerToCancelSend : StrRes.releaseToSend) : StrRes.holdTalk, - style: widget.speakTextStyle ?? Styles.ts_0C1C33_14sp_medium, + _pressing + ? (_canCancel ? StrRes.liftFingerToCancelSend : StrRes.releaseToSend) + : StrRes.holdTalk, + style: widget.speakTextStyle ?? Styles.ts_0C1C33_17sp, ), ), ); diff --git a/mobile-next/openim_common/lib/src/widgets/chat/chat_voice_record_layout.dart b/mobile-next/openim_common/lib/src/widgets/chat/chat_voice_record_layout.dart index b926a41..45b472c 100644 --- a/mobile-next/openim_common/lib/src/widgets/chat/chat_voice_record_layout.dart +++ b/mobile-next/openim_common/lib/src/widgets/chat/chat_voice_record_layout.dart @@ -106,20 +106,34 @@ class _ChatVoiceRecordLayoutState extends State { builder: (_, sec) => Material( color: Colors.transparent, child: Center( - child: Container( - width: 138.w, - height: 124.h, - padding: EdgeInsets.symmetric(horizontal: 6.w, vertical: 6.h), + child: AnimatedContainer( + duration: const Duration(milliseconds: 100), + curve: Curves.easeOut, + width: 160.w, + height: 140.h, + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 12.h), decoration: BoxDecoration( - color: _isCancelSend ? Styles.c_FF381F_opacity70 : Styles.c_0C1C33_opacity60, - borderRadius: BorderRadius.circular(6.r), + color: _isCancelSend + ? Styles.c_FF381F_opacity70 + : Styles.c_000000_opacity70, + borderRadius: BorderRadius.circular(12.r), ), child: Column( children: [ - IMUtils.seconds2HMS(sec).toText..style = Styles.ts_FFFFFF_12sp, - Expanded(child: _lottieAnimWidget), - (_isCancelSend ? StrRes.liftFingerToCancelSend : StrRes.releaseToSendSwipeUpToCancel).toText + IMUtils.seconds2HMS(sec).toText ..style = Styles.ts_FFFFFF_12sp, + Expanded( + child: _isCancelSend + ? Icon(Icons.delete_outline, + size: 48.w, color: Styles.c_FFFFFF) + : _lottieAnimWidget, + ), + (_isCancelSend + ? StrRes.liftFingerToCancelSend + : StrRes.releaseToSendSwipeUpToCancel) + .toText + ..style = Styles.ts_FFFFFF_12sp + ..maxLines = 1, ], ), ), diff --git a/mobile-next/openim_common/lib/src/widgets/chat/chat_voice_view.dart b/mobile-next/openim_common/lib/src/widgets/chat/chat_voice_view.dart index 00f6273..6170b3e 100644 --- a/mobile-next/openim_common/lib/src/widgets/chat/chat_voice_view.dart +++ b/mobile-next/openim_common/lib/src/widgets/chat/chat_voice_view.dart @@ -18,58 +18,47 @@ class ChatVoiceView extends StatelessWidget { this.isPlaying = false, }) : super(key: key); + double get _bubbleWidth { + final sec = duration ?? 0; + final width = 64.0 + (sec > 0 ? (sec - 1) : 0) * 8.0; + return width.clamp(64.0, 200.0); + } + Widget _buildVoiceAnimView() { + final icon = isPlaying + ? (ImageRes.voiceBlueAnim.toLottie + ..height = 24.h + ..width = 24.w + ..fit = BoxFit.fitHeight) + : (ImageRes.voiceBlue.toImage + ..width = 24.w + ..height = 24.h); + final seconds = '${duration ?? 0}″'.toText + ..style = Styles.ts_0C1C33_14sp; return isISend ? Row( + mainAxisAlignment: MainAxisAlignment.end, children: [ - '${duration ?? 0}``'.toText..style = Styles.ts_0089FF_17sp, - 4.horizontalSpace, - RotatedBox( - quarterTurns: 90, - child: isPlaying - ? (ImageRes.voiceBlueAnim.toLottie - ..height = 25.h - ..width = 25.w - ..fit = BoxFit.fitHeight) - : (ImageRes.voiceBlue.toImage - ..width = 25.w - ..height = 25.h), - ), + seconds, + 8.horizontalSpace, + RotatedBox(quarterTurns: 90, child: icon), ], ) : Row( mainAxisSize: MainAxisSize.min, children: [ - if (isPlaying) - ImageRes.voiceBlueAnim.toLottie - ..width = 24.w - ..height = 24.h - ..fit = BoxFit.fitHeight - else - ImageRes.voiceBlue.toImage - ..width = 24.w - ..height = 24.h, - 4.horizontalSpace, - '${duration ?? 0}``'.toText..style = Styles.ts_0089FF_17sp, + icon, + 8.horizontalSpace, + seconds, ], ); } @override Widget build(BuildContext context) { - return Container( - margin: EdgeInsets.only( - left: isISend ? _margin : 0, - right: !isISend ? _margin : 0, - ), + return SizedBox( + width: _bubbleWidth, child: _buildVoiceAnimView(), ); } - - double get _margin { - final maxWidth = 100.w; - const maxDuration = 60; - double diff = (duration ?? 0) * maxWidth / maxDuration; - return diff > maxWidth ? maxWidth : diff; - } }