From ead270f993df72ceeebf6bfe553ef69e0ccd9464 Mon Sep 17 00:00:00 2001 From: Tien Do Nam Date: Wed, 28 Dec 2022 23:01:39 +0100 Subject: [PATCH] feat: add more animations --- assets/i18n/strings.i18n.json | 4 ++ assets/i18n/strings_de.i18n.json | 4 ++ lib/pages/send_page.dart | 55 +++++++++++-------- lib/pages/tabs/receive_tab.dart | 33 ++++++++--- lib/pages/tabs/send_tab.dart | 9 ++- .../animations/initial_fade_transition.dart | 46 ++++++++++++++++ lib/widget/dialogs/no_files_dialog.dart | 22 ++++++++ 7 files changed, 142 insertions(+), 31 deletions(-) create mode 100644 lib/widget/animations/initial_fade_transition.dart create mode 100644 lib/widget/dialogs/no_files_dialog.dart diff --git a/assets/i18n/strings.i18n.json b/assets/i18n/strings.i18n.json index 13dede83..8dcd7918 100644 --- a/assets/i18n/strings.i18n.json +++ b/assets/i18n/strings.i18n.json @@ -107,6 +107,10 @@ "title": "Changelog" }, "dialogs": { + "noFiles": { + "title": "No file selected", + "content": "Please select at least one file." + }, "cancelSession": { "title": "Cancel file transfer", "content": "Do you really want to cancel the file transfer?" diff --git a/assets/i18n/strings_de.i18n.json b/assets/i18n/strings_de.i18n.json index 1898caec..8ddda0bb 100644 --- a/assets/i18n/strings_de.i18n.json +++ b/assets/i18n/strings_de.i18n.json @@ -107,6 +107,10 @@ "title": "Changelog" }, "dialogs": { + "noFiles": { + "title": "Keine Datei ausgewählt", + "content": "Bitte wähle mindestens eine Datei aus." + }, "cancelSession": { "title": "Dateiübertragung abbrechen", "content": "Möchtest du wirklich die Dateiübertragung abbrechen?" diff --git a/lib/pages/send_page.dart b/lib/pages/send_page.dart index abc15d02..ef67753b 100644 --- a/lib/pages/send_page.dart +++ b/lib/pages/send_page.dart @@ -6,6 +6,7 @@ import 'package:localsend_app/model/session_status.dart'; import 'package:localsend_app/provider/device_info_provider.dart'; import 'package:localsend_app/provider/network/send_provider.dart'; import 'package:localsend_app/util/sleep.dart'; +import 'package:localsend_app/widget/animations/initial_fade_transition.dart'; import 'package:localsend_app/widget/list_tile/device_list_tile.dart'; import 'package:routerino/routerino.dart'; @@ -81,7 +82,11 @@ class _SendPageState extends ConsumerState { ), ), const SizedBox(height: 20), - const Icon(Icons.arrow_downward), + const InitialFadeTransition( + duration: Duration(milliseconds: 300), + delay: Duration(milliseconds: 400), + child: Icon(Icons.arrow_downward), + ), const SizedBox(height: 20), Hero( tag: 'device-${(sendState?.target ?? _targetDevice)?.ip}', @@ -93,28 +98,34 @@ class _SendPageState extends ConsumerState { ), ), if (sendState != null) - ...[ - if (sendState.status == SessionStatus.waiting) - Padding( - padding: const EdgeInsets.only(bottom: 20), - child: Text(t.sendPage.waiting, textAlign: TextAlign.center), - ) - else if (sendState.status == SessionStatus.declined) - Padding( - padding: const EdgeInsets.only(bottom: 20), - child: Text(t.sendPage.rejected, style: const TextStyle(color: Colors.orange), textAlign: TextAlign.center), - ), - Center( - child: ElevatedButton.icon( - onPressed: () { - _cancel(); - context.pop(); - }, - icon: Icon(sendState.status == SessionStatus.declined ? Icons.check_circle : Icons.close), - label: Text(sendState.status == SessionStatus.declined ? t.general.close : t.general.cancel), - ), + InitialFadeTransition( + duration: const Duration(milliseconds: 300), + delay: const Duration(milliseconds: 400), + child: Column( + children: [ + if (sendState.status == SessionStatus.waiting) + Padding( + padding: const EdgeInsets.only(bottom: 20), + child: Text(t.sendPage.waiting, textAlign: TextAlign.center), + ) + else if (sendState.status == SessionStatus.declined) + Padding( + padding: const EdgeInsets.only(bottom: 20), + child: Text(t.sendPage.rejected, style: const TextStyle(color: Colors.orange), textAlign: TextAlign.center), + ), + Center( + child: ElevatedButton.icon( + onPressed: () { + _cancel(); + context.pop(); + }, + icon: Icon(sendState.status == SessionStatus.declined ? Icons.check_circle : Icons.close), + label: Text(sendState.status == SessionStatus.declined ? t.general.close : t.general.cancel), + ), + ), + ], ), - ], + ), ], ), ), diff --git a/lib/pages/tabs/receive_tab.dart b/lib/pages/tabs/receive_tab.dart index a8b583c3..ce4c4fd2 100644 --- a/lib/pages/tabs/receive_tab.dart +++ b/lib/pages/tabs/receive_tab.dart @@ -6,6 +6,7 @@ import 'package:localsend_app/provider/network_info_provider.dart'; import 'package:localsend_app/provider/network/server_provider.dart'; import 'package:localsend_app/provider/settings_provider.dart'; import 'package:localsend_app/util/ip_helper.dart'; +import 'package:localsend_app/widget/animations/initial_fade_transition.dart'; import 'package:localsend_app/widget/rotating_widget.dart'; class ReceiveTab extends ConsumerStatefulWidget { @@ -15,11 +16,16 @@ class ReceiveTab extends ConsumerStatefulWidget { ConsumerState createState() => _ReceiveTagState(); } -class _ReceiveTagState extends ConsumerState { +class _ReceiveTagState extends ConsumerState with AutomaticKeepAliveClientMixin { bool _advanced = false; + @override + bool get wantKeepAlive => true; + @override Widget build(BuildContext context) { + super.build(context); + final settings = ref.watch(settingsProvider); final networkInfo = ref.watch(networkInfoProvider); final serverState = ref.watch(serverProvider); @@ -35,15 +41,26 @@ class _ReceiveTagState extends ConsumerState { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - RotatingWidget( - duration: const Duration(seconds: 15), - spinning: serverState != null, - child: Assets.img.logo512.image(width: 200), + InitialFadeTransition( + duration: const Duration(milliseconds: 300), + delay: const Duration(milliseconds: 200), + child: RotatingWidget( + duration: const Duration(seconds: 15), + spinning: serverState != null, + child: SizedBox( + height: 200, + child: Assets.img.logo512.image(height: 200), + ), + ), ), Text(serverState?.alias ?? settings.alias, style: const TextStyle(fontSize: 48)), - Text( - serverState == null ? t.general.offline : '#${networkInfo?.localIp?.visualId ?? '?'}', - style: const TextStyle(fontSize: 24), + InitialFadeTransition( + duration: const Duration(milliseconds: 300), + delay: const Duration(milliseconds: 500), + child: Text( + serverState == null ? t.general.offline : '#${networkInfo?.localIp?.visualId ?? '?'}', + style: const TextStyle(fontSize: 24), + ), ), ], ), diff --git a/lib/pages/tabs/send_tab.dart b/lib/pages/tabs/send_tab.dart index 0833ff3f..30a8bf71 100644 --- a/lib/pages/tabs/send_tab.dart +++ b/lib/pages/tabs/send_tab.dart @@ -12,6 +12,7 @@ import 'package:localsend_app/provider/settings_provider.dart'; import 'package:localsend_app/util/file_size_helper.dart'; import 'package:localsend_app/widget/big_button.dart'; import 'package:localsend_app/widget/dialogs/add_file_dialog.dart'; +import 'package:localsend_app/widget/dialogs/no_files_dialog.dart'; import 'package:localsend_app/widget/list_tile/device_list_tile.dart'; import 'package:localsend_app/widget/rotating_widget.dart'; import 'package:routerino/routerino.dart'; @@ -211,9 +212,15 @@ class _SendTabState extends ConsumerState { child: DeviceListTile( device: device, onTap: () { + final files = ref.read(selectedFilesProvider); + if (files.isEmpty) { + context.pushBottomSheet(() => const NoFilesDialog()); + return; + } + ref.read(sendProvider.notifier).startSession( target: device, - files: ref.read(selectedFilesProvider), + files: files, ); }, ), diff --git a/lib/widget/animations/initial_fade_transition.dart b/lib/widget/animations/initial_fade_transition.dart new file mode 100644 index 00000000..fc4fd241 --- /dev/null +++ b/lib/widget/animations/initial_fade_transition.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:localsend_app/util/sleep.dart'; + +class InitialFadeTransition extends StatefulWidget { + final Widget child; + final Duration duration; + final Duration delay; + + const InitialFadeTransition({ + required this.child, + required this.duration, + this.delay = Duration.zero, + Key? key, + }) : super(key: key); + + @override + State createState() => _InitialFadeTransitionState(); +} + +class _InitialFadeTransitionState extends State { + + double _opacity = 0; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) async { + await sleepAsync(widget.delay.inMilliseconds); + if (!mounted) { + return; + } + setState(() { + _opacity = 1; + }); + }); + } + + @override + Widget build(BuildContext context) { + return AnimatedOpacity( + opacity: _opacity, + duration: widget.duration, + child: widget.child, + ); + } +} diff --git a/lib/widget/dialogs/no_files_dialog.dart b/lib/widget/dialogs/no_files_dialog.dart new file mode 100644 index 00000000..70f5ed4f --- /dev/null +++ b/lib/widget/dialogs/no_files_dialog.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:localsend_app/gen/strings.g.dart'; +import 'package:localsend_app/widget/dialogs/custom_bottom_sheet.dart'; +import 'package:routerino/routerino.dart'; + +class NoFilesDialog extends StatelessWidget { + const NoFilesDialog({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return CustomBottomSheet( + title: t.dialogs.noFiles.title, + description: t.dialogs.noFiles.content, + child: Center( + child: ElevatedButton( + onPressed: () => context.popUntilRoot(), + child: Text(t.general.close), + ), + ), + ); + } +}