didvan-app/lib/utils/action_sheet.dart

551 lines
19 KiB
Dart

import 'dart:async';
import 'dart:io';
import 'dart:ui';
import 'package:bot_toast/bot_toast.dart';
import 'package:didvan/config/design_config.dart';
import 'package:didvan/config/theme_data.dart';
import 'package:didvan/constants/assets.dart';
import 'package:didvan/models/enums.dart';
import 'package:didvan/models/view/action_sheet_data.dart';
import 'package:didvan/models/view/alert_data.dart';
import 'package:didvan/views/ai/history_ai_chat_state.dart';
import 'package:didvan/views/widgets/didvan/button.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:didvan/views/widgets/skeleton_image.dart';
import 'package:didvan/views/widgets/state_handlers/empty_state.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class ActionSheetUtils {
final BuildContext context;
ActionSheetUtils(this.context);
MediaQueryData get mediaQueryData {
final ds = MediaQuery.of(context).size;
double width = ds.width;
final shortestSide = ds.shortestSide;
final bool useMobileLayout = shortestSide < 600;
if (kIsWeb && !useMobileLayout) {
width = ds.height * 9 / 16;
}
return MediaQuery.of(context).copyWith(size: Size(width, ds.height));
}
Future<void> showLogoLoadingIndicator() async {
await showDialog(
barrierDismissible: false,
context: context,
builder: (context) => Center(
child: Image.asset(
Assets.loadingAnimation,
width: 160,
height: 160,
),
),
);
}
Future<void> showAlert(AlertData alertData) async {
bool isInit = true;
Color backgroundColor;
Color foregroundColor;
switch (alertData.aLertType) {
case ALertType.info:
backgroundColor = Theme.of(context).colorScheme.focused;
foregroundColor = Theme.of(context).colorScheme.focusedBorder;
break;
case ALertType.success:
backgroundColor = Theme.of(context).colorScheme.successBack;
foregroundColor = Theme.of(context).colorScheme.success;
break;
case ALertType.error:
backgroundColor = Theme.of(context).colorScheme.errorBack;
foregroundColor = Theme.of(context).colorScheme.error;
break;
}
BotToast.showNotification(
backgroundColor: backgroundColor,
title: (cancelFunc) => StatefulBuilder(
builder: (context, setState) {
if (isInit) {
Future.delayed(Duration.zero, () {
setState(() {});
isInit = false;
});
}
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DidvanText(alertData.message, color: foregroundColor),
AnimatedContainer(
duration: const Duration(seconds: 2),
width: isInit ? MediaQuery.of(context).size.width - 32 : 0,
height: 2,
color: foregroundColor,
),
],
);
},
),
);
}
Future<void> showBottomSheet({required ActionSheetData data}) async {
await showModalBottomSheet(
constraints: BoxConstraints(
maxWidth: mediaQueryData.size.width,
),
backgroundColor: data.backgroundColor ?? Colors.transparent,
isScrollControlled: true,
context: context,
builder: (context) => Container(
padding: data.hasPadding
? const EdgeInsets.all(20).copyWith(top: 0)
: EdgeInsets.zero,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(10),
),
),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 20),
Center(
child: Container(
height: 3,
width: 50,
color: Theme.of(context).colorScheme.hint,
),
),
const SizedBox(height: 8),
if (data.title != null)
Row(
children: [
if (data.titleIconWidget != null)
data.titleIconWidget!
else if (data.titleIcon != null)
Icon(
data.titleIcon,
color: data.titleColor ??
Theme.of(context).colorScheme.title,
),
if (data.titleIcon != null || data.titleIconWidget != null)
const SizedBox(width: 8),
DidvanText(
data.title!,
style: Theme.of(context).textTheme.titleMedium,
color: data.titleColor ??
Theme.of(context).colorScheme.title,
)
],
),
const SizedBox(height: 28),
data.content,
const SizedBox(height: 28),
if (!data.withoutButtonMode)
Row(
children: [
if (data.hasDismissButton)
if (data.hasConfirmButton)
Expanded(
flex: 2,
child: DidvanButton(
style: ButtonStyleMode.primary,
onPressed: () {
data.onConfirmed?.call();
pop();
},
title: data.confrimTitle ?? 'تایید',
),
),
if (data.hasDismissButton) const SizedBox(width: 20),
Expanded(
child: DidvanButton(
onPressed: () {
Navigator.of(context).pop();
data.onDismissed?.call();
},
title: data.dismissTitle ?? 'بازگشت',
style: ButtonStyleMode.secondary,
),
),
],
),
],
),
),
),
);
}
Future<void> openDialog(
{required ActionSheetData data,
final bool barrierDismissible = true}) async {
await showDialog(
context: context,
barrierDismissible: barrierDismissible,
builder: (context) => BackdropFilter(
filter: ImageFilter.blur(
sigmaX: data.isBackgroundDropBlur ? 10 : 0,
sigmaY: data.isBackgroundDropBlur ? 10 : 0),
child: Dialog(
backgroundColor:
data.backgroundColor ?? Theme.of(context).colorScheme.surface,
shape: const RoundedRectangleBorder(
borderRadius: DesignConfig.mediumBorderRadius,
),
child: Container(
decoration: BoxDecoration(
borderRadius: DesignConfig.mediumBorderRadius,
color:
data.backgroundColor ?? Theme.of(context).colorScheme.surface,
),
width: mediaQueryData.size.width * 0.8,
padding: const EdgeInsets.fromLTRB(24, 16, 24, 24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (data.title != null)
Row(
mainAxisSize: MainAxisSize.min,
children: [
if (data.titleIconWidget != null)
GestureDetector(
onTap: () => Navigator.of(context).pop(),
child: data.titleIconWidget!,
)
else if (data.titleIcon != null)
GestureDetector(
onTap: () => Navigator.of(context).pop(),
child: Icon(
data.titleIcon,
size: 24,
color: data.titleColor,
),
),
if (data.titleIcon != null ||
data.titleIconWidget != null)
const SizedBox(
width: 4,
),
Expanded(
child: Padding(
padding: const EdgeInsets.only(top: 4.0),
child: DidvanText(
data.title!,
style: Theme.of(context).textTheme.displaySmall,
color: data.titleColor,
fontWeight: FontWeight.bold,
),
),
),
],
),
const SizedBox(
height: 12,
),
data.content,
const SizedBox(
height: 12,
),
Row(
children: [
if (data.hasDismissButton)
Expanded(
child: DidvanButton(
onPressed: () {
data.onDismissed?.call();
pop();
},
title: data.dismissTitle ?? 'بازگشت',
style: ButtonStyleMode.flat,
),
),
if (data.hasDismissButton && data.hasDismissButton)
const SizedBox(
width: 20,
),
if (data.hasDismissButton)
Expanded(
child: DidvanButton(
onPressed: () {
data.onConfirmed?.call();
if (data.hasConfirmButtonClose) {
pop();
}
},
title: data.confrimTitle ?? 'تایید',
),
),
],
),
],
),
),
),
),
);
}
Future<void> botsDialogSelect({
required final BuildContext context,
}) async {
ActionSheetUtils(context).openDialog(
data: ActionSheetData(
hasConfirmButton: false,
hasDismissButton: false,
content: SizedBox(
width: double.infinity,
height: MediaQuery.sizeOf(context).height / 3,
child: Consumer<HistoryAiChatState>(
builder: (context, state, child) {
return state.loadingBots
? Center(
child: Image.asset(
Assets.loadingAnimation,
width: 60,
height: 60,
),
)
: state.bots.isEmpty
? Padding(
padding:
const EdgeInsets.symmetric(horizontal: 12.0),
child: EmptyState(
asset: Assets.emptyResult,
title: 'نتیجه‌ای پیدا نشد',
height: 120,
),
)
: ListView.builder(
itemCount: state.bots.length,
physics: const BouncingScrollPhysics(),
shrinkWrap: true,
itemBuilder: (context, index) {
final bot = state.bots[index];
return InkWell(
onTap: () {
ActionSheetUtils(context).pop();
state.bot = bot;
state.update();
},
child: Container(
alignment: Alignment.center,
padding:
const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
border: index == state.bots.length - 1
? null
: Border(
bottom: BorderSide(
color: Theme.of(context)
.colorScheme
.border,
width: 1))),
child: Row(
children: [
SkeletonImage(
imageUrl: bot.image.toString(),
width: 42,
height: 42,
borderRadius:
BorderRadius.circular(360),
),
const SizedBox(width: 12),
Expanded(
child: DidvanText(
bot.name.toString(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
))
],
),
),
);
});
}),
)));
}
void openInteractiveViewer(BuildContext context, String image, bool isFile) {
showDialog(
context: context,
barrierDismissible: true,
builder: (context) => _RotatableImageViewer(
image: image,
isFile: isFile,
),
);
}
static PopupMenuItem<dynamic> popUpBtns({
required final String value,
required final IconData icon,
final Color? color,
final double? height,
final double? size,
}) {
return PopupMenuItem(
value: value,
height: height ?? 46,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
color: color,
size: size,
),
const SizedBox(
width: 12,
),
DidvanText(
value,
color: color,
fontSize: size,
),
],
),
);
}
void pop() {
DesignConfig.updateSystemUiOverlayStyle();
Navigator.of(context).pop();
}
}
class _RotatableImageViewer extends StatefulWidget {
final String image;
final bool isFile;
const _RotatableImageViewer({
required this.image,
required this.isFile,
});
@override
State<_RotatableImageViewer> createState() => _RotatableImageViewerState();
}
class _RotatableImageViewerState extends State<_RotatableImageViewer>
with SingleTickerProviderStateMixin {
late AnimationController _rotationController;
late Animation<double> _rotationAnimation;
int _rotationQuarters = 0;
@override
void initState() {
super.initState();
_rotationController = AnimationController(
duration: const Duration(milliseconds: 400),
vsync: this,
);
_rotationAnimation = Tween<double>(
begin: 0,
end: 0,
).animate(CurvedAnimation(
parent: _rotationController,
curve: Curves.easeInOutCubic,
));
}
@override
void dispose() {
_rotationController.dispose();
super.dispose();
}
void _rotate() {
final from = _rotationQuarters * 0.25;
_rotationQuarters = (_rotationQuarters + 1) % 4;
final to = _rotationQuarters * 0.25;
_rotationAnimation = Tween<double>(
begin: from,
end: to,
).animate(CurvedAnimation(
parent: _rotationController,
curve: Curves.easeInOutCubic,
));
_rotationController.reset();
_rotationController.forward();
}
@override
Widget build(BuildContext context) {
return Dialog(
backgroundColor: Colors.transparent,
insetPadding: EdgeInsets.zero,
child: Stack(
children: [
Positioned.fill(
child: InteractiveViewer(
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Center(
child: AnimatedBuilder(
animation: _rotationAnimation,
builder: (context, child) {
return Transform.rotate(
angle: _rotationAnimation.value * 2 * 3.14159265359,
child: child,
);
},
child: widget.isFile
? ClipRRect(
borderRadius: DesignConfig.lowBorderRadius,
child: Image.file(File(widget.image)))
: widget.image.startsWith('blob:')
? ClipRRect(
borderRadius: DesignConfig.lowBorderRadius,
child: Image.network(widget.image))
: SkeletonImage(
imageUrl: widget.image,
),
),
),
),
),
),
Positioned(
right: 16,
top: 0,
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).colorScheme.surface,
),
child: const BackButton(),
),
),
Positioned(
left: 16,
top: 0,
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).colorScheme.surface,
),
child: IconButton(
icon: const Icon(Icons.rotate_right),
onPressed: _rotate,
),
),
),
],
),
);
}
}