From 83060b362954fc3cc8e80b3c89948cbf847b2944 Mon Sep 17 00:00:00 2001 From: SleepySquash Date: Fri, 22 Jul 2022 11:26:15 +0300 Subject: [PATCH] Corrections --- CHANGELOG.md | 5 +- lib/ui/page/auth/controller.dart | 4 +- lib/ui/page/auth/view.dart | 59 +- .../page/home/page/settings/controller.dart | 4 + lib/ui/page/home/page/settings/view.dart | 63 +- lib/ui/widget/selector.dart | 549 ++++++++++-------- 6 files changed, 382 insertions(+), 302 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7136f1f696..d9ef1dd20f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ All user visible changes to this project will be documented in this file. This p - UI: - User information auto-updating on changes ([#7], [#4]). - Menu: - - Language selection ([#23]). + - Language selection ([#23], [#29]). ### Changed @@ -24,8 +24,7 @@ All user visible changes to this project will be documented in this file. This p - Media panel: - Redesigned desktop interface ([#26]); - Redesigned mobile interface ([#31]). - - Auth page: - - Redesigned `Auth` page ([#29]). + - Redesigned `Auth` page ([#29]). ### Fixed diff --git a/lib/ui/page/auth/controller.dart b/lib/ui/page/auth/controller.dart index b6614f7d9a0..1a4d166fb0d 100644 --- a/lib/ui/page/auth/controller.dart +++ b/lib/ui/page/auth/controller.dart @@ -39,10 +39,10 @@ class AuthController extends GetxController { /// [SMITrigger] triggering the blinking animation. SMITrigger? blink; - /// [GlobalKey] of an animated button used to share it between overlays. + /// [GlobalKey] of the button opening the [Language] selection. final GlobalKey languageKey = GlobalKey(); - /// [Timer] periodically increasing [logoFrame]. + /// [Timer] periodically increasing the [logoFrame]. Timer? _animationTimer; /// Returns user authentication status. diff --git a/lib/ui/page/auth/view.dart b/lib/ui/page/auth/view.dart index 98430f1ab58..5e0a2773810 100644 --- a/lib/ui/page/auth/view.dart +++ b/lib/ui/page/auth/view.dart @@ -57,7 +57,7 @@ class AuthView extends StatelessWidget { ...List.generate(10, (i) => 'assets/images/logo/logo000$i.svg') .map((e) => Offstage(child: SvgLoader.asset(e))) .toList(), - ...List.generate(10, (i) => 'assets/images/logo/logo000$i.svg') + ...List.generate(10, (i) => 'assets/images/logo/head000$i.svg') .map((e) => Offstage(child: SvgLoader.asset(e))) .toList(), const SizedBox(height: 30), @@ -134,37 +134,34 @@ class AuthView extends StatelessWidget { // Language selection popup. Widget language = CupertinoButton( - key: c.languageKey, - child: Text( - '${L10n.chosen.value!.locale.countryCode}, ${L10n.chosen.value!.name}', - style: thin?.copyWith(fontSize: 13, color: primary), - ), - onPressed: () async { - await Selector.show( - context, - items: L10n.languages, - itemBuilder: (Language lang) => Row( - children: [ - Text( - lang.name, - style: thin?.copyWith( - fontSize: 15, - ), - ), - const Spacer(), - Text( - lang.locale.languageCode.toUpperCase(), - style: thin?.copyWith( - fontSize: 15, - ), - ), - ], + key: c.languageKey, + child: Text( + '${L10n.chosen.value!.locale.countryCode}, ${L10n.chosen.value!.name}', + style: thin?.copyWith(fontSize: 13, color: primary), + ), + onPressed: () => Selector.show( + context: context, + buttonKey: c.languageKey, + initial: L10n.chosen.value!, + items: L10n.languages, + onSelected: (l) => L10n.set(l), + debounce: + context.isMobile ? const Duration(milliseconds: 500) : null, + itemBuilder: (Language e) => Row( + children: [ + Text( + e.name, + style: thin?.copyWith(fontSize: 15), + ), + const Spacer(), + Text( + e.locale.languageCode.toUpperCase(), + style: thin?.copyWith(fontSize: 15), ), - initialValue: L10n.chosen.value!, - globalKey: c.languageKey, - onSelect: (selected) => L10n.set(selected), - ); - }); + ], + ), + ), + ); // Footer part of the page. List footer = [ diff --git a/lib/ui/page/home/page/settings/controller.dart b/lib/ui/page/home/page/settings/controller.dart index 1b00e72b223..99ec0f485ad 100644 --- a/lib/ui/page/home/page/settings/controller.dart +++ b/lib/ui/page/home/page/settings/controller.dart @@ -14,6 +14,7 @@ // along with this program. If not, see // . +import 'package:flutter/widgets.dart' show GlobalKey; import 'package:get/get.dart'; import '/domain/model/application_settings.dart'; @@ -24,6 +25,9 @@ import '/l10n/l10n.dart'; class SettingsController extends GetxController { SettingsController(this._settingsRepo); + /// [GlobalKey] of a button opening the [Language] selection. + final GlobalKey languageKey = GlobalKey(); + /// Settings repository, used to update the [ApplicationSettings]. final AbstractSettingsRepository _settingsRepo; diff --git a/lib/ui/page/home/page/settings/view.dart b/lib/ui/page/home/page/settings/view.dart index 3331201b60d..16f5963add2 100644 --- a/lib/ui/page/home/page/settings/view.dart +++ b/lib/ui/page/home/page/settings/view.dart @@ -19,6 +19,8 @@ import 'package:get/get.dart'; import '/l10n/l10n.dart'; import '/routes.dart'; +import '/ui/widget/selector.dart'; +import '/util/platform_utils.dart'; import 'controller.dart'; /// View of the [Routes.settings] page. @@ -52,32 +54,43 @@ class SettingsView extends StatelessWidget { ], ), ), - ListTile( - title: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 200), - child: Obx( - () => DropdownButton( - key: const Key('LanguageDropdown'), - value: L10n.chosen.value, - items: - L10n.languages.map>((e) { - return DropdownMenuItem( - key: Key( - 'Language_${e.locale.languageCode}${e.locale.countryCode}'), - value: e, - child: Text('${e.locale.countryCode}, ${e.name}'), - ); - }).toList(), - onChanged: c.setLocale, - borderRadius: BorderRadius.circular(18), - style: TextStyle( - color: Theme.of(context).colorScheme.primary, - fontSize: 15, - ), - icon: const SizedBox(), - underline: const SizedBox(), - ), + KeyedSubtree( + key: c.languageKey, + child: ListTile( + key: const Key('LanguageDropdown'), + title: Text( + '${L10n.chosen.value!.locale.countryCode}, ${L10n.chosen.value!.name}', ), + onTap: () async { + final TextStyle? thin = context.textTheme.caption + ?.copyWith(color: Colors.black); + await Selector.show( + context: context, + buttonKey: c.languageKey, + alignment: Alignment.bottomCenter, + items: L10n.languages, + initial: L10n.chosen.value!, + onSelected: (l) => L10n.set(l), + debounce: context.isMobile + ? const Duration(milliseconds: 500) + : null, + itemBuilder: (Language e) => Row( + key: Key( + 'Language_${e.locale.languageCode}${e.locale.countryCode}'), + children: [ + Text( + e.name, + style: thin?.copyWith(fontSize: 15), + ), + const Spacer(), + Text( + e.locale.languageCode.toUpperCase(), + style: thin?.copyWith(fontSize: 15), + ), + ], + ), + ); + }, ), ) ], diff --git a/lib/ui/widget/selector.dart b/lib/ui/widget/selector.dart index 318cda12d31..4a12136774c 100644 --- a/lib/ui/widget/selector.dart +++ b/lib/ui/widget/selector.dart @@ -23,78 +23,98 @@ import 'package:get/get.dart'; import '/util/platform_utils.dart'; -/// Class which is responsible for showing popup of items select. +/// Dropdown selecting the provided [items]. +/// +/// Intended to be displayed with the [show] method. class Selector extends StatefulWidget { const Selector({ Key? key, required this.items, required this.itemBuilder, - this.initialValue, - this.onSelect, - this.globalKey, - this.switchingDelayDuration = const Duration(milliseconds: 500), + this.initial, + this.onSelected, + this.buttonKey, + this.alignment = Alignment.topCenter, + this.debounce, + required this.isMobile, }) : super(key: key); - /// Items that are present in this [Selector]; + /// [List] of items to select from. final List items; - /// Initial value of selected item. - final T? initialValue; + /// Initially selected item. + final T? initial; - /// Callback that is called on selection complete. - final Function(T)? onSelect; + /// Callback, called when the provided item is selected. + final void Function(T)? onSelected; - /// Callback that returns view of single item from [items]. + /// Builder building the provided item. final Widget Function(T data) itemBuilder; - /// [GlobalKey] of the parent of this [Selector]. - /// Uses for resolving of popup position. - final GlobalKey? globalKey; + /// [GlobalKey] of an [Object] displaying this [Selector]. + final GlobalKey? buttonKey; - /// Delay that will be spent after value was set and before [onSelect] call. - final Duration switchingDelayDuration; + /// [Alignment] this [Selector] should take relative to the [buttonKey]. + final Alignment alignment; - /// Displays a [Selector] wrapped in a popup depend to the current platform. - static Future show( - BuildContext context, { + /// [Duration] of a debounce effect. + /// + /// No debounce is applied if `null` is provided. + final Duration? debounce; + + /// Indicator whether a mobile design with [CupertinoPicker] should be used. + final bool isMobile; + + /// Displays a [Selector] wrapped in a modal popup. + static Future show({ + required BuildContext context, required List items, required Widget Function(T data) itemBuilder, - GlobalKey? globalKey, - T? initialValue, - void Function(T)? onSelect, + void Function(T)? onSelected, + GlobalKey? buttonKey, + Alignment alignment = Alignment.topCenter, + Duration? debounce, + T? initial, }) { - if (!context.isMobile) { - return showDialog( + bool isMobile = context.isMobile; + if (isMobile) { + return showModalBottomSheet( context: context, barrierColor: kCupertinoModalBarrierColor, + backgroundColor: Colors.white, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(8), + topRight: Radius.circular(8), + ), + ), builder: (context) { return Selector( - globalKey: globalKey, - initialValue: initialValue, + debounce: debounce, + initial: initial, items: items, itemBuilder: itemBuilder, - onSelect: onSelect, + onSelected: onSelected, + buttonKey: buttonKey, + alignment: alignment, + isMobile: isMobile, ); }, ); } else { - return showModalBottomSheet( + return showDialog( context: context, barrierColor: kCupertinoModalBarrierColor, - backgroundColor: Colors.white, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(8), - topRight: Radius.circular(8), - ), - ), builder: (context) { return Selector( - globalKey: globalKey, - initialValue: initialValue, + debounce: debounce, + initial: initial, items: items, itemBuilder: itemBuilder, - onSelect: onSelect, + onSelected: onSelected, + buttonKey: buttonKey, + alignment: alignment, + isMobile: isMobile, ); }, ); @@ -105,28 +125,26 @@ class Selector extends StatefulWidget { State> createState() => _SelectorState(); } -/// State of a [Selector] used to provide [_debounce] worker on mobile. +/// State of a [Selector] maintaining the [_debounce]. class _SelectorState extends State> { - /// Current selected item. + /// Currently selected item. late Rx selected; - /// Prevents instant call of [onSelect] after the user has set - /// [selected] item. + /// [Worker] debouncing the [selected] value, if any debounce is specified. Worker? _debounce; @override void initState() { - selected = Rx(widget.initialValue ?? widget.items.first); + selected = Rx(widget.initial ?? widget.items.first); - if (widget.switchingDelayDuration.inMicroseconds > 0) { + if (widget.debounce != null) { _debounce = debounce( selected, - (T value) { - widget.onSelect?.call(value); - }, - time: widget.switchingDelayDuration, + (T value) => widget.onSelected?.call(value), + time: widget.debounce, ); } + super.initState(); } @@ -138,116 +156,111 @@ class _SelectorState extends State> { @override Widget build(BuildContext context) { - if (!context.isMobile) { - return LayoutBuilder(builder: (context, constraints) { - Offset offset = - Offset(constraints.maxWidth / 2, constraints.maxHeight / 2); - final keyContext = widget.globalKey?.currentContext; - if (keyContext != null) { - final box = keyContext.findRenderObject() as RenderBox?; - offset = box?.localToGlobal(Offset.zero) ?? offset; - offset = Offset( - offset.dx + (box?.size.width ?? 0) / 2, - offset.dy, - ); - } - Widget _button(T item) { - return Padding( - padding: const EdgeInsets.fromLTRB(12, 0, 12, 0), - child: Material( - borderRadius: BorderRadius.circular(8), - color: Colors.white, - child: InkWell( - hoverColor: const Color(0x3363B4FF), - highlightColor: Colors.white.withOpacity(0.1), - borderRadius: BorderRadius.circular(8), - onTap: () => widget.onSelect?.call(item), - child: Align( - alignment: AlignmentDirectional.centerStart, - child: Padding( - padding: const EdgeInsets.all(8), - child: widget.itemBuilder(item), - ), + if (widget.isMobile) { + return _mobile(context); + } else { + return _desktop(context); + } + } + + /// Returns mobile design of this [Selector]. + Widget _mobile(BuildContext context) { + return Container( + height: 12 + 3 + 12 + 14 * 2 + min(widget.items.length * 38, 330) + 12, + margin: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: SafeArea( + top: false, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 12), + Center( + child: Container( + width: 60, + height: 3, + decoration: BoxDecoration( + color: const Color(0xFFCCCCCC), + borderRadius: BorderRadius.circular(12), ), ), ), - ); - } - - return Stack( - children: [ - Positioned( - left: offset.dx - 260 / 2, - bottom: MediaQuery.of(context).size.height - offset.dy, - child: Listener( - onPointerUp: (d) { - Navigator.of(context).pop(); - }, - child: Column( - mainAxisSize: MainAxisSize.min, + const SizedBox(height: 12), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 14), + child: Stack( children: [ - Container( - width: 260, - constraints: const BoxConstraints(maxHeight: 280), - padding: const EdgeInsets.fromLTRB( - 0, - 10, - 0, - 10, + CupertinoPicker( + scrollController: FixedExtentScrollController( + initialItem: widget.initial == null + ? 0 + : widget.items.indexOf(selected.value), ), - decoration: BoxDecoration( - color: CupertinoColors.systemBackground - .resolveFrom(context), - borderRadius: BorderRadius.circular(8), + magnification: 1, + squeeze: 1, + looping: true, + diameterRatio: 100, + useMagnifier: false, + itemExtent: 38, + selectionOverlay: Container( + margin: + const EdgeInsetsDirectional.only(start: 8, end: 8), + decoration: + const BoxDecoration(color: Color(0x3363B4FF)), ), - child: Stack( - children: [ - SingleChildScrollView( - child: Column( - children: widget.items.map(_button).toList(), - ), - ), - if (widget.items.length >= 8) - Positioned.fill( - child: Align( - alignment: Alignment.topCenter, - child: Container( - height: 15, - margin: const EdgeInsets.only(right: 10), - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - Color(0xFFFFFFFF), - Color(0x00FFFFFF), - ], - ), - ), - ), - ), - ), - if (widget.items.length >= 8) - Positioned.fill( - child: Align( - alignment: Alignment.bottomCenter, - child: Container( - height: 15, - margin: const EdgeInsets.only(right: 10), - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - Color(0x00FFFFFF), - Color(0xFFFFFFFF), - ], - ), - ), - ), + onSelectedItemChanged: (int i) { + HapticFeedback.selectionClick(); + selected.value = widget.items[i]; + if (_debounce == null) { + widget.onSelected?.call(selected.value); + } + }, + children: widget.items + .map( + (e) => Center( + child: Padding( + padding: + const EdgeInsets.fromLTRB(46, 0, 29, 0), + child: widget.itemBuilder(e), ), ), - ], + ) + .toList(), + ), + Align( + alignment: Alignment.topCenter, + child: Container( + height: 15, + width: double.infinity, + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0xFFFFFFFF), + Color(0x00FFFFFF), + ], + ), + ), + ), + ), + Align( + alignment: Alignment.bottomCenter, + child: Container( + height: 15, + width: double.infinity, + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0x00FFFFFF), + Color(0xFFFFFFFF), + ], + ), + ), ), ), ], @@ -255,108 +268,162 @@ class _SelectorState extends State> { ), ), ], - ); - }); - } else { - return Container( - height: min(widget.items.length * (65), 330), - margin: EdgeInsets.only( - bottom: MediaQuery.of(context).viewInsets.bottom, ), - child: SafeArea( - top: false, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(height: 12), - Center( - child: Container( - width: 60, - height: 3, - decoration: BoxDecoration( - color: const Color(0xFFCCCCCC), - borderRadius: BorderRadius.circular(12), - ), + ), + ); + } + + /// Returns desktop design of this [Selector]. + Widget _desktop(BuildContext context) { + return LayoutBuilder(builder: (context, constraints) { + double? left, right; + double? top, bottom; + + Offset offset = + Offset(constraints.maxWidth / 2, constraints.maxHeight / 2); + final keyContext = widget.buttonKey?.currentContext; + if (keyContext != null) { + final box = keyContext.findRenderObject() as RenderBox?; + offset = box?.localToGlobal(Offset.zero) ?? offset; + + if (widget.alignment == Alignment.topCenter) { + offset = Offset( + offset.dx + (box?.size.width ?? 0) / 2, + offset.dy, + ); + + left = offset.dx - 260 / 2; + bottom = MediaQuery.of(context).size.height - offset.dy; + } else if (widget.alignment == Alignment.bottomCenter) { + offset = Offset( + offset.dx + (box?.size.width ?? 0) / 2, + offset.dy + (box?.size.height ?? 0), + ); + + left = offset.dx - 260 / 2; + top = offset.dy; + } else { + offset = Offset( + offset.dx + (box?.size.width ?? 0) / 2, + offset.dy + (box?.size.height ?? 0) / 2, + ); + + left = offset.dx - 260 / 2; + top = offset.dy; + } + } + + // Builds the provided [item]. + Widget _button(T item) { + return Padding( + padding: const EdgeInsets.fromLTRB(12, 0, 12, 0), + child: Material( + borderRadius: BorderRadius.circular(8), + color: Colors.white, + child: InkWell( + hoverColor: const Color(0x3363B4FF), + highlightColor: Colors.white.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + onTap: () { + selected.value = item; + if (_debounce == null) { + widget.onSelected?.call(selected.value); + } + }, + child: Align( + alignment: AlignmentDirectional.centerStart, + child: Padding( + padding: const EdgeInsets.all(8), + child: widget.itemBuilder(item), ), ), - const SizedBox(height: 12), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 14), - child: Stack( - children: [ - CupertinoPicker( - scrollController: FixedExtentScrollController( - initialItem: widget.initialValue == null - ? 0 - : widget.items.indexOf(selected.value)), - magnification: 1, - squeeze: 1, - looping: true, - diameterRatio: 100, - useMagnifier: false, - itemExtent: 38, - selectionOverlay: Container( - margin: const EdgeInsetsDirectional.only( - start: 8, end: 8), - decoration: - const BoxDecoration(color: Color(0x3363B4FF)), + ), + ), + ); + } + + return Stack( + children: [ + Positioned( + left: left, + right: right, + top: top, + bottom: bottom, + child: Listener( + onPointerUp: (d) => Navigator.of(context).pop(), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 260, + constraints: const BoxConstraints(maxHeight: 280), + padding: const EdgeInsets.fromLTRB( + 0, + 10, + 0, + 10, + ), + decoration: BoxDecoration( + color: + CupertinoColors.systemBackground.resolveFrom(context), + borderRadius: BorderRadius.circular(8), + ), + child: Stack( + children: [ + SingleChildScrollView( + child: Column( + children: widget.items.map(_button).toList(), ), - onSelectedItemChanged: (int i) { - HapticFeedback.selectionClick(); - selected.value = widget.items[i]; - }, - children: widget.items - .map((item) => Center( - child: Container( - padding: const EdgeInsets.fromLTRB( - 46, 0, 29, 0), - child: widget.itemBuilder(item), - ), - )) - .toList()), - Align( - alignment: Alignment.topCenter, - child: Container( - height: 15, - width: double.infinity, - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - Color(0xFFFFFFFF), - Color(0x00FFFFFF), - ], + ), + if (widget.items.length >= 8) + Positioned.fill( + child: Align( + alignment: Alignment.topCenter, + child: Container( + height: 15, + margin: const EdgeInsets.only(right: 10), + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0xFFFFFFFF), + Color(0x00FFFFFF), + ], + ), + ), + ), ), ), - ), - ), - Align( - alignment: Alignment.bottomCenter, - child: Container( - height: 15, - width: double.infinity, - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - Color(0x00FFFFFF), - Color(0xFFFFFFFF), - ], + if (widget.items.length >= 8) + Positioned.fill( + child: Align( + alignment: Alignment.bottomCenter, + child: Container( + height: 15, + margin: const EdgeInsets.only(right: 10), + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0x00FFFFFF), + Color(0xFFFFFFFF), + ], + ), + ), + ), ), ), - ), - ), - ], + ], + ), ), - ), + ], ), - ], + ), ), - ), + ], ); - } + }); } }