mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-06-25 08:44:08 +00:00
update media view to new mybutton design
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
This commit is contained in:
parent
f22e9086ed
commit
d528913e84
6 changed files with 477 additions and 269 deletions
|
|
@ -85,9 +85,12 @@ final routerProvider = GoRouter(
|
|||
),
|
||||
GoRoute(
|
||||
path: 'media_viewer',
|
||||
builder: (context, state) {
|
||||
pageBuilder: (context, state) {
|
||||
final group = state.extra! as Group;
|
||||
return MediaViewerView(group);
|
||||
return MediaViewerView.buildPage(
|
||||
key: state.pageKey,
|
||||
group: group,
|
||||
);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
|
|
|
|||
145
lib/src/visual/elements/my_icon_button.element.dart
Normal file
145
lib/src/visual/elements/my_icon_button.element.dart
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/physics.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/themes/light.dart';
|
||||
|
||||
enum MyIconButtonVariant {
|
||||
primary,
|
||||
secondary,
|
||||
}
|
||||
|
||||
class MyIconButton extends StatefulWidget {
|
||||
const MyIconButton({
|
||||
required this.icon,
|
||||
required this.onPressed,
|
||||
this.onLongPress,
|
||||
this.variant = MyIconButtonVariant.primary,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Widget icon;
|
||||
final VoidCallback? onPressed;
|
||||
final VoidCallback? onLongPress;
|
||||
final MyIconButtonVariant variant;
|
||||
|
||||
@override
|
||||
State<MyIconButton> createState() => _MyIconButtonState();
|
||||
}
|
||||
|
||||
class _MyIconButtonState extends State<MyIconButton>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
lowerBound: double.negativeInfinity,
|
||||
upperBound: double.infinity,
|
||||
value: 0,
|
||||
)..addListener(() {
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onTapDown(TapDownDetails details) {
|
||||
if (widget.onPressed != null || widget.onLongPress != null) {
|
||||
_controller.animateTo(
|
||||
1,
|
||||
duration: const Duration(milliseconds: 60),
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onTapUp(TapUpDetails details) {
|
||||
if (widget.onPressed != null || widget.onLongPress != null) {
|
||||
_bounce();
|
||||
}
|
||||
}
|
||||
|
||||
void _onTapCancel() {
|
||||
if (widget.onPressed != null || widget.onLongPress != null) {
|
||||
_bounce();
|
||||
}
|
||||
}
|
||||
|
||||
void _bounce() {
|
||||
const spring = SpringDescription(
|
||||
mass: 1,
|
||||
stiffness: 400,
|
||||
damping: 15,
|
||||
);
|
||||
final simulation = SpringSimulation(
|
||||
spring,
|
||||
_controller.value,
|
||||
0,
|
||||
_controller.velocity,
|
||||
);
|
||||
_controller.animateWith(simulation);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scale = 1.0 - (_controller.value * 0.02);
|
||||
final isEnabled = widget.onPressed != null || widget.onLongPress != null;
|
||||
final isDark = isDarkMode(context);
|
||||
final disabledBgColor = isDark
|
||||
? const Color(0xFF353535)
|
||||
: const Color(0xFFE0E0E0);
|
||||
final disabledFgColor = isDark
|
||||
? const Color(0xFF757575)
|
||||
: const Color(0xFF9E9E9E);
|
||||
|
||||
late final Color bgColor;
|
||||
late final Color fgColor;
|
||||
|
||||
if (widget.variant == MyIconButtonVariant.primary) {
|
||||
bgColor = primaryColor;
|
||||
fgColor = Colors.black87;
|
||||
} else {
|
||||
bgColor = isDark ? Colors.grey[800]! : Colors.grey[200]!;
|
||||
fgColor = isDark ? Colors.white : Colors.black87;
|
||||
}
|
||||
|
||||
final childButton = FilledButton(
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: bgColor,
|
||||
foregroundColor: fgColor,
|
||||
disabledBackgroundColor: disabledBgColor,
|
||||
disabledForegroundColor: disabledFgColor,
|
||||
minimumSize: const Size(72, 52),
|
||||
fixedSize: const Size(72, 52),
|
||||
padding: EdgeInsets.zero,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
onPressed: isEnabled ? () {} : null,
|
||||
child: widget.icon,
|
||||
);
|
||||
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTapDown: isEnabled ? _onTapDown : null,
|
||||
onTapUp: isEnabled ? _onTapUp : null,
|
||||
onTapCancel: isEnabled ? _onTapCancel : null,
|
||||
onTap: widget.onPressed,
|
||||
onLongPress: widget.onLongPress,
|
||||
child: Transform.scale(
|
||||
scale: scale,
|
||||
child: AbsorbPointer(
|
||||
child: childButton,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ class MyInput extends StatefulWidget {
|
|||
this.autofocus = false,
|
||||
this.errorText,
|
||||
this.obscureText = false,
|
||||
this.dense = false,
|
||||
super.key,
|
||||
});
|
||||
|
||||
|
|
@ -31,6 +32,7 @@ class MyInput extends StatefulWidget {
|
|||
final bool autofocus;
|
||||
final String? errorText;
|
||||
final bool obscureText;
|
||||
final bool dense;
|
||||
|
||||
@override
|
||||
State<MyInput> createState() => _MyInputState();
|
||||
|
|
@ -165,14 +167,15 @@ class _MyInputState extends State<MyInput> with SingleTickerProviderStateMixin {
|
|||
color: isDark ? Colors.white : Colors.black87,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
isDense: widget.dense,
|
||||
hintText: widget.hintText,
|
||||
hintStyle: TextStyle(
|
||||
color: inputHintColor,
|
||||
),
|
||||
filled: true,
|
||||
fillColor: inputFillColor,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
vertical: 18,
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
vertical: widget.dense ? 14 : 18,
|
||||
horizontal: 24,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
|
|
|
|||
|
|
@ -95,14 +95,10 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
|
|||
if (!mounted) return;
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return MediaViewerView(
|
||||
widget.group,
|
||||
initialMessage: widget.message,
|
||||
);
|
||||
},
|
||||
),
|
||||
MediaViewerView.buildPage<void>(
|
||||
group: widget.group,
|
||||
initialMessage: widget.message,
|
||||
).createRoute(context),
|
||||
);
|
||||
} else if (widget.mediaService.mediaFile.downloadState ==
|
||||
DownloadState.pending) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:drift/drift.dart' show Value;
|
||||
|
|
@ -27,7 +28,8 @@ import 'package:twonly/src/services/notifications/background.notifications.dart'
|
|||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/visual/components/animate_icon.comp.dart';
|
||||
import 'package:twonly/src/visual/decorations/input_text.decoration.dart';
|
||||
import 'package:twonly/src/visual/elements/my_icon_button.element.dart';
|
||||
import 'package:twonly/src/visual/elements/my_input.element.dart';
|
||||
import 'package:twonly/src/visual/helpers/media_view_sizing.helper.dart';
|
||||
import 'package:twonly/src/visual/loader/three_rotating_dots.loader.dart';
|
||||
import 'package:twonly/src/visual/views/camera/camera_send_to.view.dart';
|
||||
|
|
@ -41,6 +43,30 @@ class MediaViewerView extends StatefulWidget {
|
|||
|
||||
final Message? initialMessage;
|
||||
|
||||
static Page<T> buildPage<T>({
|
||||
required Group group,
|
||||
LocalKey? key,
|
||||
Message? initialMessage,
|
||||
}) {
|
||||
return CustomTransitionPage<T>(
|
||||
key: key,
|
||||
opaque: false,
|
||||
barrierColor: Colors.transparent,
|
||||
transitionDuration: const Duration(milliseconds: 250),
|
||||
reverseTransitionDuration: const Duration(milliseconds: 250),
|
||||
child: MediaViewerView(
|
||||
group,
|
||||
initialMessage: initialMessage,
|
||||
),
|
||||
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
||||
return FadeTransition(
|
||||
opacity: animation,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
State<MediaViewerView> createState() => _MediaViewerViewState();
|
||||
}
|
||||
|
|
@ -81,6 +107,9 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
final HashSet<String> _alreadyOpenedMediaIds = HashSet();
|
||||
|
||||
bool _isTransitioning = false;
|
||||
bool _isZoomed = false;
|
||||
late PageController _verticalPager;
|
||||
final ValueNotifier<double> _backdropOpacityNotifier = ValueNotifier(1);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -91,6 +120,11 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
allMediaFiles = [widget.initialMessage!];
|
||||
}
|
||||
|
||||
_verticalPager = PageController(initialPage: 1);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) _verticalPager.addListener(_onVerticalScrollUpdated);
|
||||
});
|
||||
|
||||
asyncLoadNextMedia(true);
|
||||
}
|
||||
|
||||
|
|
@ -118,9 +152,29 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
);
|
||||
textMessageController.dispose();
|
||||
|
||||
_verticalPager
|
||||
..removeListener(_onVerticalScrollUpdated)
|
||||
..dispose();
|
||||
_backdropOpacityNotifier.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onVerticalScrollUpdated() {
|
||||
if (!_verticalPager.hasClients) return;
|
||||
final page = _verticalPager.page ?? 1.0;
|
||||
final linearFraction = min(1, max(0, page)).toDouble();
|
||||
_backdropOpacityNotifier.value = linearFraction * linearFraction;
|
||||
}
|
||||
|
||||
void _onPageSnapped(int index) {
|
||||
if (index == 0) {
|
||||
if (mounted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _disposeVideoController() {
|
||||
final listener = _videoListener;
|
||||
final controller = videoController;
|
||||
|
|
@ -549,30 +603,18 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
if (currentMedia != null &&
|
||||
!currentMedia!.mediaFile.requiresAuthentication &&
|
||||
currentMedia!.mediaFile.displayLimitInMilliseconds == null)
|
||||
OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
iconColor: imageSaved
|
||||
? Theme.of(context).colorScheme.outline
|
||||
: Theme.of(context).colorScheme.primary,
|
||||
foregroundColor: imageSaved
|
||||
? Theme.of(context).colorScheme.outline
|
||||
: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
MyIconButton(
|
||||
variant: MyIconButtonVariant.secondary,
|
||||
onPressed: (currentMedia == null) ? null : onPressedSaveToGallery,
|
||||
child: Row(
|
||||
children: [
|
||||
if (imageSaving)
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
height: 10,
|
||||
child: CircularProgressIndicator.adaptive(strokeWidth: 1),
|
||||
icon: imageSaving
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator.adaptive(strokeWidth: 2),
|
||||
)
|
||||
else
|
||||
imageSaved
|
||||
? const Icon(Icons.check)
|
||||
: const FaIcon(FontAwesomeIcons.floppyDisk),
|
||||
],
|
||||
),
|
||||
: imageSaved
|
||||
? const Icon(Icons.check)
|
||||
: const FaIcon(FontAwesomeIcons.floppyDisk, size: 20),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
IconButton(
|
||||
|
|
@ -614,23 +656,21 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
IconButton.outlined(
|
||||
icon: const FaIcon(FontAwesomeIcons.message),
|
||||
MyIconButton(
|
||||
variant: MyIconButtonVariant.secondary,
|
||||
onPressed: () async {
|
||||
displayShortReactions();
|
||||
setState(() {
|
||||
showSendTextMessageInput = true;
|
||||
});
|
||||
},
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all<EdgeInsets>(
|
||||
const EdgeInsets.symmetric(vertical: 10, horizontal: 20),
|
||||
),
|
||||
icon: const FaIcon(
|
||||
FontAwesomeIcons.message,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
IconButton.outlined(
|
||||
icon: const FaIcon(FontAwesomeIcons.camera),
|
||||
MyIconButton(
|
||||
onPressed: () async {
|
||||
nextMediaTimer?.cancel();
|
||||
progressTimer?.cancel();
|
||||
|
|
@ -651,11 +691,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
await videoController?.play();
|
||||
}
|
||||
},
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all<EdgeInsets>(
|
||||
const EdgeInsets.symmetric(vertical: 10, horizontal: 20),
|
||||
),
|
||||
),
|
||||
icon: const FaIcon(FontAwesomeIcons.camera, size: 24),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
|
@ -688,221 +724,256 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
backgroundColor: Colors.transparent,
|
||||
body: ValueListenableBuilder<double>(
|
||||
valueListenable: _backdropOpacityNotifier,
|
||||
builder: (context, opacity, child) {
|
||||
final baseColor = isDarkMode(context) ? Colors.black : Colors.white;
|
||||
return ColoredBox(
|
||||
color: baseColor.withValues(alpha: opacity),
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: PageView(
|
||||
controller: _verticalPager,
|
||||
scrollDirection: Axis.vertical,
|
||||
physics: _isZoomed
|
||||
? const NeverScrollableScrollPhysics()
|
||||
: const BouncingScrollPhysics(
|
||||
parent: AlwaysScrollableScrollPhysics(),
|
||||
),
|
||||
onPageChanged: _onPageSnapped,
|
||||
children: [
|
||||
if (_showDownloadingLoader) _loader(),
|
||||
if ((currentMedia != null || videoController != null) &&
|
||||
(canBeSeenUntil == null || progress.value >= 0))
|
||||
GestureDetector(
|
||||
onTap: onTap,
|
||||
onDoubleTap: (videoController == null) ? null : onTap,
|
||||
child: MediaViewSizingHelper(
|
||||
bottomNavigation: bottomNavigation(),
|
||||
requiredHeight: 55,
|
||||
child: Stack(
|
||||
children: [
|
||||
if (videoController != null)
|
||||
Positioned.fill(
|
||||
child: PhotoView.customChild(
|
||||
initialScale: PhotoViewComputedScale.contained,
|
||||
minScale: PhotoViewComputedScale.contained,
|
||||
child: VideoPlayer(
|
||||
videoController!,
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (currentMedia != null &&
|
||||
(currentMedia!.mediaFile.type == MediaType.image ||
|
||||
currentMedia!.mediaFile.type == MediaType.gif))
|
||||
Positioned.fill(
|
||||
child: PhotoView(
|
||||
imageProvider: FileImage(
|
||||
currentMedia!.tempPath,
|
||||
),
|
||||
loadingBuilder: (context, event) => _loader(),
|
||||
initialScale: PhotoViewComputedScale.contained,
|
||||
minScale: PhotoViewComputedScale.contained,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return const Center(
|
||||
child: Icon(
|
||||
Icons.broken_image_outlined,
|
||||
color: Colors.white38,
|
||||
size: 64,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (displayTwonlyPresent)
|
||||
Positioned.fill(
|
||||
child: GestureDetector(
|
||||
onTap: () => loadCurrentMediaFile(showTwonly: true),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Lottie.asset(
|
||||
'assets/animations/present.lottie.lottie',
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.only(bottom: 200),
|
||||
child: Text(context.lang.mediaViewerTwonlyTapToOpen),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 10,
|
||||
top: 10,
|
||||
child: Row(
|
||||
const SizedBox.expand(),
|
||||
SafeArea(
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, size: 30),
|
||||
color: Colors.white,
|
||||
onPressed: () => Navigator.pop(context),
|
||||
if (_showDownloadingLoader) _loader(),
|
||||
if ((currentMedia != null || videoController != null) &&
|
||||
(canBeSeenUntil == null || progress.value >= 0))
|
||||
GestureDetector(
|
||||
onTap: onTap,
|
||||
onDoubleTap: (videoController == null) ? null : onTap,
|
||||
child: MediaViewSizingHelper(
|
||||
bottomNavigation: bottomNavigation(),
|
||||
requiredHeight: 55,
|
||||
child: Stack(
|
||||
children: [
|
||||
if (videoController != null)
|
||||
Positioned.fill(
|
||||
child: PhotoView.customChild(
|
||||
initialScale:
|
||||
PhotoViewComputedScale.contained,
|
||||
minScale: PhotoViewComputedScale.contained,
|
||||
backgroundDecoration: const BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
),
|
||||
scaleStateChangedCallback: (state) {
|
||||
final zoomed =
|
||||
state != PhotoViewScaleState.initial;
|
||||
if (_isZoomed != zoomed) {
|
||||
setState(() {
|
||||
_isZoomed = zoomed;
|
||||
});
|
||||
}
|
||||
},
|
||||
child: VideoPlayer(
|
||||
videoController!,
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (currentMedia != null &&
|
||||
(currentMedia!.mediaFile.type ==
|
||||
MediaType.image ||
|
||||
currentMedia!.mediaFile.type ==
|
||||
MediaType.gif))
|
||||
Positioned.fill(
|
||||
child: PhotoView(
|
||||
imageProvider: FileImage(
|
||||
currentMedia!.tempPath,
|
||||
),
|
||||
loadingBuilder: (context, event) => _loader(),
|
||||
backgroundDecoration: const BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
),
|
||||
initialScale:
|
||||
PhotoViewComputedScale.contained,
|
||||
minScale: PhotoViewComputedScale.contained,
|
||||
scaleStateChangedCallback: (state) {
|
||||
final zoomed =
|
||||
state != PhotoViewScaleState.initial;
|
||||
if (_isZoomed != zoomed) {
|
||||
setState(() {
|
||||
_isZoomed = zoomed;
|
||||
});
|
||||
}
|
||||
},
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return const Center(
|
||||
child: Icon(
|
||||
Icons.broken_image_outlined,
|
||||
color: Colors.white38,
|
||||
size: 64,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (displayTwonlyPresent)
|
||||
Positioned.fill(
|
||||
child: GestureDetector(
|
||||
onTap: () => loadCurrentMediaFile(showTwonly: true),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Lottie.asset(
|
||||
'assets/animations/present.lottie.lottie',
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.only(bottom: 200),
|
||||
child: Text(
|
||||
context.lang.mediaViewerTwonlyTapToOpen,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (currentMedia != null &&
|
||||
currentMedia?.mediaFile.downloadState !=
|
||||
DownloadState.ready)
|
||||
Positioned.fill(child: _loader()),
|
||||
if (canBeSeenUntil != null || progress.value >= 0)
|
||||
Positioned(
|
||||
right: 20,
|
||||
top: 27,
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: ValueListenableBuilder<double>(
|
||||
valueListenable: progress,
|
||||
builder: (context, value, child) {
|
||||
return CircularProgressIndicator(
|
||||
value: value,
|
||||
strokeWidth: 2,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 10,
|
||||
left: showSendTextMessageInput ? 0 : null,
|
||||
right: showSendTextMessageInput ? 0 : 15,
|
||||
child: Text(
|
||||
_currentMediaSender,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: showSendTextMessageInput ? 24 : 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: showSendTextMessageInput
|
||||
? null
|
||||
: const Color.fromARGB(255, 126, 126, 126),
|
||||
shadows: const [
|
||||
Shadow(
|
||||
color: Color.fromARGB(122, 0, 0, 0),
|
||||
blurRadius: 5,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (showSendTextMessageInput)
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
color: context.color.surface,
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 10,
|
||||
left: 20,
|
||||
right: 20,
|
||||
top: 10,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: MyInput(
|
||||
dense: true,
|
||||
autofocus: true,
|
||||
controller: textMessageController,
|
||||
hintText: context.lang.chatListDetailInput,
|
||||
onChanged: (value) {
|
||||
setState(() {});
|
||||
},
|
||||
onSubmitted: (value) {
|
||||
setState(() {
|
||||
showSendTextMessageInput = false;
|
||||
showShortReactions = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
MyIconButton(
|
||||
icon: const FaIcon(
|
||||
FontAwesomeIcons.solidPaperPlane,
|
||||
size: 20,
|
||||
),
|
||||
onPressed: () async {
|
||||
if (textMessageController.text.isNotEmpty) {
|
||||
await insertAndSendTextMessage(
|
||||
widget.group.groupId,
|
||||
textMessageController.text,
|
||||
currentMessage!.messageId,
|
||||
);
|
||||
textMessageController.clear();
|
||||
}
|
||||
setState(() {
|
||||
showSendTextMessageInput = false;
|
||||
showShortReactions = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (currentMessage != null)
|
||||
AdditionalMessageContent(currentMessage!),
|
||||
if (currentMedia != null)
|
||||
ReactionButtons(
|
||||
show: showShortReactions,
|
||||
textInputFocused: showSendTextMessageInput,
|
||||
mediaViewerDistanceFromBottom:
|
||||
mediaViewerDistanceFromBottom,
|
||||
groupId: widget.group.groupId,
|
||||
messageId: currentMessage!.messageId,
|
||||
emojiKey: emojiKey,
|
||||
hide: () {
|
||||
setState(() {
|
||||
showShortReactions = false;
|
||||
showSendTextMessageInput = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
Positioned.fill(
|
||||
child: EmojiFloatWidget(key: emojiKey),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (currentMedia != null &&
|
||||
currentMedia?.mediaFile.downloadState != DownloadState.ready)
|
||||
Positioned.fill(child: _loader()),
|
||||
if (canBeSeenUntil != null || progress.value >= 0)
|
||||
Positioned(
|
||||
right: 20,
|
||||
top: 27,
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: ValueListenableBuilder<double>(
|
||||
valueListenable: progress,
|
||||
builder: (context, value, child) {
|
||||
return CircularProgressIndicator(
|
||||
value: value,
|
||||
strokeWidth: 2,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 10,
|
||||
left: showSendTextMessageInput ? 0 : null,
|
||||
right: showSendTextMessageInput ? 0 : 15,
|
||||
child: Text(
|
||||
_currentMediaSender,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: showSendTextMessageInput ? 24 : 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: showSendTextMessageInput
|
||||
? null
|
||||
: const Color.fromARGB(255, 126, 126, 126),
|
||||
shadows: const [
|
||||
Shadow(
|
||||
color: Color.fromARGB(122, 0, 0, 0),
|
||||
blurRadius: 5,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (showSendTextMessageInput)
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
color: context.color.surface,
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 10,
|
||||
left: 20,
|
||||
right: 20,
|
||||
top: 10,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const FaIcon(FontAwesomeIcons.xmark),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
showShortReactions = false;
|
||||
showSendTextMessageInput = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
autofocus: true,
|
||||
controller: textMessageController,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
onChanged: (value) {
|
||||
setState(() {});
|
||||
},
|
||||
onEditingComplete: () {
|
||||
setState(() {
|
||||
showSendTextMessageInput = false;
|
||||
showShortReactions = false;
|
||||
});
|
||||
},
|
||||
decoration: inputTextMessageDeco(
|
||||
context,
|
||||
context.lang.chatListDetailInput,
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const FaIcon(FontAwesomeIcons.solidPaperPlane),
|
||||
onPressed: () async {
|
||||
if (textMessageController.text.isNotEmpty) {
|
||||
await insertAndSendTextMessage(
|
||||
widget.group.groupId,
|
||||
textMessageController.text,
|
||||
currentMessage!.messageId,
|
||||
);
|
||||
textMessageController.clear();
|
||||
}
|
||||
setState(() {
|
||||
showSendTextMessageInput = false;
|
||||
showShortReactions = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (currentMessage != null)
|
||||
AdditionalMessageContent(currentMessage!),
|
||||
if (currentMedia != null)
|
||||
ReactionButtons(
|
||||
show: showShortReactions,
|
||||
textInputFocused: showSendTextMessageInput,
|
||||
mediaViewerDistanceFromBottom: mediaViewerDistanceFromBottom,
|
||||
groupId: widget.group.groupId,
|
||||
messageId: currentMessage!.messageId,
|
||||
emojiKey: emojiKey,
|
||||
hide: () {
|
||||
setState(() {
|
||||
showShortReactions = false;
|
||||
showSendTextMessageInput = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
Positioned.fill(
|
||||
child: EmojiFloatWidget(key: emojiKey),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -56,17 +56,7 @@ class _ReactionButtonsState extends State<ReactionButtons> {
|
|||
void didUpdateWidget(ReactionButtons oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.show != oldWidget.show) {
|
||||
if (widget.show) {
|
||||
_renderAnimations = true;
|
||||
} else {
|
||||
Future.delayed(const Duration(milliseconds: 150), () {
|
||||
if (mounted && !widget.show) {
|
||||
setState(() {
|
||||
_renderAnimations = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
_renderAnimations = widget.show;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -98,7 +88,7 @@ class _ReactionButtonsState extends State<ReactionButtons> {
|
|||
ignoring: !widget.show,
|
||||
child: AnimatedOpacity(
|
||||
opacity: widget.show ? 1.0 : 0.0, // Fade in/out
|
||||
duration: const Duration(milliseconds: 150),
|
||||
duration: Duration(milliseconds: widget.show ? 150 : 50),
|
||||
child: Container(
|
||||
color: widget.show ? Colors.black.withAlpha(0) : Colors.transparent,
|
||||
padding: const EdgeInsets.symmetric(vertical: 32),
|
||||
|
|
|
|||
Loading…
Reference in a new issue