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(
|
GoRoute(
|
||||||
path: 'media_viewer',
|
path: 'media_viewer',
|
||||||
builder: (context, state) {
|
pageBuilder: (context, state) {
|
||||||
final group = state.extra! as Group;
|
final group = state.extra! as Group;
|
||||||
return MediaViewerView(group);
|
return MediaViewerView.buildPage(
|
||||||
|
key: state.pageKey,
|
||||||
|
group: group,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
GoRoute(
|
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.autofocus = false,
|
||||||
this.errorText,
|
this.errorText,
|
||||||
this.obscureText = false,
|
this.obscureText = false,
|
||||||
|
this.dense = false,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -31,6 +32,7 @@ class MyInput extends StatefulWidget {
|
||||||
final bool autofocus;
|
final bool autofocus;
|
||||||
final String? errorText;
|
final String? errorText;
|
||||||
final bool obscureText;
|
final bool obscureText;
|
||||||
|
final bool dense;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<MyInput> createState() => _MyInputState();
|
State<MyInput> createState() => _MyInputState();
|
||||||
|
|
@ -165,14 +167,15 @@ class _MyInputState extends State<MyInput> with SingleTickerProviderStateMixin {
|
||||||
color: isDark ? Colors.white : Colors.black87,
|
color: isDark ? Colors.white : Colors.black87,
|
||||||
),
|
),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
|
isDense: widget.dense,
|
||||||
hintText: widget.hintText,
|
hintText: widget.hintText,
|
||||||
hintStyle: TextStyle(
|
hintStyle: TextStyle(
|
||||||
color: inputHintColor,
|
color: inputHintColor,
|
||||||
),
|
),
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: inputFillColor,
|
fillColor: inputFillColor,
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
contentPadding: EdgeInsets.symmetric(
|
||||||
vertical: 18,
|
vertical: widget.dense ? 14 : 18,
|
||||||
horizontal: 24,
|
horizontal: 24,
|
||||||
),
|
),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
|
|
|
||||||
|
|
@ -95,14 +95,10 @@ class _ChatMediaEntryState extends State<ChatMediaEntry> {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
await Navigator.push(
|
await Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MediaViewerView.buildPage<void>(
|
||||||
builder: (context) {
|
group: widget.group,
|
||||||
return MediaViewerView(
|
initialMessage: widget.message,
|
||||||
widget.group,
|
).createRoute(context),
|
||||||
initialMessage: widget.message,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
} else if (widget.mediaService.mediaFile.downloadState ==
|
} else if (widget.mediaService.mediaFile.downloadState ==
|
||||||
DownloadState.pending) {
|
DownloadState.pending) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:clock/clock.dart';
|
import 'package:clock/clock.dart';
|
||||||
import 'package:drift/drift.dart' show Value;
|
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/log.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/visual/components/animate_icon.comp.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/helpers/media_view_sizing.helper.dart';
|
||||||
import 'package:twonly/src/visual/loader/three_rotating_dots.loader.dart';
|
import 'package:twonly/src/visual/loader/three_rotating_dots.loader.dart';
|
||||||
import 'package:twonly/src/visual/views/camera/camera_send_to.view.dart';
|
import 'package:twonly/src/visual/views/camera/camera_send_to.view.dart';
|
||||||
|
|
@ -41,6 +43,30 @@ class MediaViewerView extends StatefulWidget {
|
||||||
|
|
||||||
final Message? initialMessage;
|
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
|
@override
|
||||||
State<MediaViewerView> createState() => _MediaViewerViewState();
|
State<MediaViewerView> createState() => _MediaViewerViewState();
|
||||||
}
|
}
|
||||||
|
|
@ -81,6 +107,9 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
final HashSet<String> _alreadyOpenedMediaIds = HashSet();
|
final HashSet<String> _alreadyOpenedMediaIds = HashSet();
|
||||||
|
|
||||||
bool _isTransitioning = false;
|
bool _isTransitioning = false;
|
||||||
|
bool _isZoomed = false;
|
||||||
|
late PageController _verticalPager;
|
||||||
|
final ValueNotifier<double> _backdropOpacityNotifier = ValueNotifier(1);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
|
@ -91,6 +120,11 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
allMediaFiles = [widget.initialMessage!];
|
allMediaFiles = [widget.initialMessage!];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_verticalPager = PageController(initialPage: 1);
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (mounted) _verticalPager.addListener(_onVerticalScrollUpdated);
|
||||||
|
});
|
||||||
|
|
||||||
asyncLoadNextMedia(true);
|
asyncLoadNextMedia(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,9 +152,29 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
);
|
);
|
||||||
textMessageController.dispose();
|
textMessageController.dispose();
|
||||||
|
|
||||||
|
_verticalPager
|
||||||
|
..removeListener(_onVerticalScrollUpdated)
|
||||||
|
..dispose();
|
||||||
|
_backdropOpacityNotifier.dispose();
|
||||||
|
|
||||||
super.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() {
|
void _disposeVideoController() {
|
||||||
final listener = _videoListener;
|
final listener = _videoListener;
|
||||||
final controller = videoController;
|
final controller = videoController;
|
||||||
|
|
@ -549,30 +603,18 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
if (currentMedia != null &&
|
if (currentMedia != null &&
|
||||||
!currentMedia!.mediaFile.requiresAuthentication &&
|
!currentMedia!.mediaFile.requiresAuthentication &&
|
||||||
currentMedia!.mediaFile.displayLimitInMilliseconds == null)
|
currentMedia!.mediaFile.displayLimitInMilliseconds == null)
|
||||||
OutlinedButton(
|
MyIconButton(
|
||||||
style: OutlinedButton.styleFrom(
|
variant: MyIconButtonVariant.secondary,
|
||||||
iconColor: imageSaved
|
|
||||||
? Theme.of(context).colorScheme.outline
|
|
||||||
: Theme.of(context).colorScheme.primary,
|
|
||||||
foregroundColor: imageSaved
|
|
||||||
? Theme.of(context).colorScheme.outline
|
|
||||||
: Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
|
||||||
onPressed: (currentMedia == null) ? null : onPressedSaveToGallery,
|
onPressed: (currentMedia == null) ? null : onPressedSaveToGallery,
|
||||||
child: Row(
|
icon: imageSaving
|
||||||
children: [
|
? const SizedBox(
|
||||||
if (imageSaving)
|
width: 16,
|
||||||
const SizedBox(
|
height: 16,
|
||||||
width: 10,
|
child: CircularProgressIndicator.adaptive(strokeWidth: 2),
|
||||||
height: 10,
|
|
||||||
child: CircularProgressIndicator.adaptive(strokeWidth: 1),
|
|
||||||
)
|
)
|
||||||
else
|
: imageSaved
|
||||||
imageSaved
|
? const Icon(Icons.check)
|
||||||
? const Icon(Icons.check)
|
: const FaIcon(FontAwesomeIcons.floppyDisk, size: 20),
|
||||||
: const FaIcon(FontAwesomeIcons.floppyDisk),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
IconButton(
|
IconButton(
|
||||||
|
|
@ -614,23 +656,21 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
IconButton.outlined(
|
MyIconButton(
|
||||||
icon: const FaIcon(FontAwesomeIcons.message),
|
variant: MyIconButtonVariant.secondary,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
displayShortReactions();
|
displayShortReactions();
|
||||||
setState(() {
|
setState(() {
|
||||||
showSendTextMessageInput = true;
|
showSendTextMessageInput = true;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
style: ButtonStyle(
|
icon: const FaIcon(
|
||||||
padding: WidgetStateProperty.all<EdgeInsets>(
|
FontAwesomeIcons.message,
|
||||||
const EdgeInsets.symmetric(vertical: 10, horizontal: 20),
|
size: 20,
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
IconButton.outlined(
|
MyIconButton(
|
||||||
icon: const FaIcon(FontAwesomeIcons.camera),
|
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
nextMediaTimer?.cancel();
|
nextMediaTimer?.cancel();
|
||||||
progressTimer?.cancel();
|
progressTimer?.cancel();
|
||||||
|
|
@ -651,11 +691,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
await videoController?.play();
|
await videoController?.play();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
style: ButtonStyle(
|
icon: const FaIcon(FontAwesomeIcons.camera, size: 24),
|
||||||
padding: WidgetStateProperty.all<EdgeInsets>(
|
|
||||||
const EdgeInsets.symmetric(vertical: 10, horizontal: 20),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
@ -688,221 +724,256 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: SafeArea(
|
backgroundColor: Colors.transparent,
|
||||||
child: Stack(
|
body: ValueListenableBuilder<double>(
|
||||||
fit: StackFit.expand,
|
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: [
|
children: [
|
||||||
if (_showDownloadingLoader) _loader(),
|
const SizedBox.expand(),
|
||||||
if ((currentMedia != null || videoController != null) &&
|
SafeArea(
|
||||||
(canBeSeenUntil == null || progress.value >= 0))
|
child: Stack(
|
||||||
GestureDetector(
|
fit: StackFit.expand,
|
||||||
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(
|
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
if (_showDownloadingLoader) _loader(),
|
||||||
icon: const Icon(Icons.close, size: 30),
|
if ((currentMedia != null || videoController != null) &&
|
||||||
color: Colors.white,
|
(canBeSeenUntil == null || progress.value >= 0))
|
||||||
onPressed: () => Navigator.pop(context),
|
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) {
|
void didUpdateWidget(ReactionButtons oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
if (widget.show != oldWidget.show) {
|
if (widget.show != oldWidget.show) {
|
||||||
if (widget.show) {
|
_renderAnimations = widget.show;
|
||||||
_renderAnimations = true;
|
|
||||||
} else {
|
|
||||||
Future.delayed(const Duration(milliseconds: 150), () {
|
|
||||||
if (mounted && !widget.show) {
|
|
||||||
setState(() {
|
|
||||||
_renderAnimations = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -98,7 +88,7 @@ class _ReactionButtonsState extends State<ReactionButtons> {
|
||||||
ignoring: !widget.show,
|
ignoring: !widget.show,
|
||||||
child: AnimatedOpacity(
|
child: AnimatedOpacity(
|
||||||
opacity: widget.show ? 1.0 : 0.0, // Fade in/out
|
opacity: widget.show ? 1.0 : 0.0, // Fade in/out
|
||||||
duration: const Duration(milliseconds: 150),
|
duration: Duration(milliseconds: widget.show ? 150 : 50),
|
||||||
child: Container(
|
child: Container(
|
||||||
color: widget.show ? Colors.black.withAlpha(0) : Colors.transparent,
|
color: widget.show ? Colors.black.withAlpha(0) : Colors.transparent,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 32),
|
padding: const EdgeInsets.symmetric(vertical: 32),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue