From 73748c89b9fd97b58acd7e963968a9c420174368 Mon Sep 17 00:00:00 2001 From: William Verhaeghe Date: Wed, 29 Jan 2025 17:44:06 +0100 Subject: [PATCH] Added a new Fortune wheel (#14) * Added a new Fortune wheel * Fix missing import * Fix issues blocking from running on web * Updated spinner --- .fvm/fvm_config.json | 3 ++ .fvmrc | 2 +- .vscode/settings.json | 2 +- lib/repo/remote_config.dart | 7 ++- lib/screen/login/login_screen.dart | 7 +-- .../raffle/raffle_winner_picker_screen.dart | 16 ++++-- lib/theme/theme_duration.dart | 3 +- .../raffle_winner_picker_viewmodel.dart | 27 ++++++---- lib/widget/raffle/add_participant_dialog.dart | 2 + lib/widget/raffle/custom_fortune_wheel.dart | 52 ++++++------------- pubspec.lock | 36 ++++++++----- pubspec.yaml | 4 +- 12 files changed, 87 insertions(+), 74 deletions(-) create mode 100644 .fvm/fvm_config.json diff --git a/.fvm/fvm_config.json b/.fvm/fvm_config.json new file mode 100644 index 0000000..c0b314b --- /dev/null +++ b/.fvm/fvm_config.json @@ -0,0 +1,3 @@ +{ + "flutterSdkVersion": "3.27.3" +} \ No newline at end of file diff --git a/.fvmrc b/.fvmrc index d1af5d5..74c2c15 100644 --- a/.fvmrc +++ b/.fvmrc @@ -1,4 +1,4 @@ { - "flutter": "3.27.1", + "flutter": "3.27.3", "flavors": {} } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 432ab17..ddca12c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,5 @@ { - "dart.flutterSdkPath": ".fvm/versions/3.27.1", + "dart.flutterSdkPath": ".fvm/versions/3.27.3", "dart.sdkPath": ".fvm/flutter_sdk/bin/cache/dart-sdk", "dart.lineLength": 180, "search.exclude": { diff --git a/lib/repo/remote_config.dart b/lib/repo/remote_config.dart index 3821089..80146da 100644 --- a/lib/repo/remote_config.dart +++ b/lib/repo/remote_config.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:firebase_remote_config/firebase_remote_config.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter_belgium/model/data/remote_config/remote_config_data.dart'; import 'package:flutter_belgium/util/flavor/flavor_config.dart'; import 'package:impaktfull_architecture/impaktfull_architecture.dart'; @@ -34,7 +35,11 @@ class AppRemoteConfigRepository extends ImpaktfullRemoteConfigRepository getDefault() async => RemoteConfigData( latestVersionCode: 1, minVersionCode: 1, - updateUrl: Platform.isAndroid ? 'https://play.google.com/store/apps/details?id=be.flutterbelgium.app' : 'https://apps.apple.com/us/app/flutter-belgium/id6479450596', + updateUrl: kIsWeb + ? '' + : Platform.isAndroid + ? 'https://play.google.com/store/apps/details?id=be.flutterbelgium.app' + : 'https://apps.apple.com/us/app/flutter-belgium/id6479450596', adminIds: [], ); diff --git a/lib/screen/login/login_screen.dart b/lib/screen/login/login_screen.dart index 8c88c6e..f8b23fc 100644 --- a/lib/screen/login/login_screen.dart +++ b/lib/screen/login/login_screen.dart @@ -1,13 +1,14 @@ import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_belgium/di/injectable.dart'; import 'package:flutter_belgium/model/data/login/login_type.dart'; import 'package:flutter_belgium/theme/theme_assets.dart'; import 'package:flutter_belgium/viewmodel/login/login_viewmodel.dart'; +import 'package:flutter_belgium/widget/provider/provider_widget.dart'; import 'package:flutter_belgium/widget/social_login/social_login_button.dart'; import 'package:flutter_navigation_generator_annotations/flutter_navigation_generator_annotations.dart'; -import 'package:flutter_belgium/di/injectable.dart'; -import 'package:flutter_belgium/widget/provider/provider_widget.dart'; import 'package:impaktfull_architecture/impaktfull_architecture.dart'; @FlutterRoute( @@ -45,7 +46,7 @@ class LoginScreen extends StatelessWidget { onTap: viewModel.onLoginTapped, loginType: LoginType.github, ), - if (Platform.isIOS) ...[ + if (!kIsWeb && Platform.isIOS) ...[ SocialLoginButton( onTap: viewModel.onLoginTapped, loginType: LoginType.apple, diff --git a/lib/screen/raffle/raffle_winner_picker_screen.dart b/lib/screen/raffle/raffle_winner_picker_screen.dart index c24552b..c004f4e 100644 --- a/lib/screen/raffle/raffle_winner_picker_screen.dart +++ b/lib/screen/raffle/raffle_winner_picker_screen.dart @@ -1,26 +1,31 @@ import 'package:flutter/widgets.dart'; +import 'package:flutter_belgium/di/injectable.dart'; import 'package:flutter_belgium/theme/theme_colors.dart'; import 'package:flutter_belgium/viewmodel/raffle/raffle_winner_picker_viewmodel.dart'; import 'package:flutter_belgium/widget/general/button.dart'; +import 'package:flutter_belgium/widget/provider/provider_widget.dart'; import 'package:flutter_belgium/widget/raffle/custom_confetti.dart'; import 'package:flutter_belgium/widget/raffle/custom_fortune_wheel.dart'; import 'package:flutter_navigation_generator_annotations/flutter_navigation_generator_annotations.dart'; -import 'package:flutter_belgium/di/injectable.dart'; -import 'package:flutter_belgium/widget/provider/provider_widget.dart'; import 'package:impaktfull_architecture/impaktfull_architecture.dart'; @FlutterRoute( navigationType: NavigationType.push, ) -class RaffleWinnerPickerScreen extends StatelessWidget { +class RaffleWinnerPickerScreen extends StatefulWidget { const RaffleWinnerPickerScreen({ super.key, }); + @override + State createState() => _RaffleWinnerPickerScreenState(); +} + +class _RaffleWinnerPickerScreenState extends State with SingleTickerProviderStateMixin { @override Widget build(BuildContext context) { return ProviderWidget( - create: () => getIt()..init(), + create: () => getIt()..init(this), builder: (context, viewModel) => ImpaktfullUiScreen( child: Builder(builder: (context) { if (viewModel.hasInactiveRaffle) { @@ -57,7 +62,8 @@ class RaffleWinnerPickerScreen extends StatelessWidget { ); } return CustomFortuneWheel( - selected: viewModel.selectedIndexStream, + winnerIndex: viewModel.raffleWinnerIndex, + animation: viewModel.raffleAnimation, participants: viewModel.participants, ); }, diff --git a/lib/theme/theme_duration.dart b/lib/theme/theme_duration.dart index 3d54353..fd87b8d 100644 --- a/lib/theme/theme_duration.dart +++ b/lib/theme/theme_duration.dart @@ -2,7 +2,6 @@ class ThemeDuration { const ThemeDuration._(); static const confettiDuration = Duration(seconds: 5); - static const raffleWheelDuration = Duration(seconds: 3); - static const nextRoundDelayDuration = Duration(seconds: 5); + static const raffleWheelDuration = Duration(seconds: 10); } diff --git a/lib/viewmodel/raffle/raffle_winner_picker_viewmodel.dart b/lib/viewmodel/raffle/raffle_winner_picker_viewmodel.dart index 67c548d..985dbd2 100644 --- a/lib/viewmodel/raffle/raffle_winner_picker_viewmodel.dart +++ b/lib/viewmodel/raffle/raffle_winner_picker_viewmodel.dart @@ -2,11 +2,13 @@ import 'dart:async'; import 'dart:math'; import 'package:confetti/confetti.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_belgium/model/data/raffle/participant.dart'; import 'package:flutter_belgium/model/data/raffle/raffle.dart'; import 'package:flutter_belgium/navigator/main_navigator.dart'; import 'package:flutter_belgium/repo/raffle/raffle_repo.dart'; import 'package:flutter_belgium/theme/theme_duration.dart'; +import 'package:flutter_crazy_fortune_wheel/flutter_crazy_fortune_wheel.dart'; import 'package:impaktfull_architecture/impaktfull_architecture.dart'; @injectable @@ -15,7 +17,9 @@ class RaffleWinnerPickerViewModel extends ChangeNotifierEx { final MainNavigator _mainNavigator; final _confettiController = ConfettiController(); - final _selectedIndexStreamController = StreamController.broadcast(); + late final AnimationController _raffleAnimationController; + late final Animation _raffleAnimation; + int _raffleWinnerIndex = 0; StreamSubscription? _subscription; Raffle? _raffle; @@ -27,7 +31,9 @@ class RaffleWinnerPickerViewModel extends ChangeNotifierEx { ConfettiController get confettiController => _confettiController; - Stream get selectedIndexStream => _selectedIndexStreamController.stream; + Animation get raffleAnimation => _raffleAnimation; + + int get raffleWinnerIndex => _raffleWinnerIndex; int get minRequiredParticipants => 2; @@ -46,7 +52,9 @@ class RaffleWinnerPickerViewModel extends ChangeNotifierEx { this._mainNavigator, ); - void init() { + void init(TickerProvider vsync) { + _raffleAnimationController = AnimationController(vsync: vsync, duration: ThemeDuration.raffleWheelDuration); + _raffleAnimation = CurvedAnimation(parent: _raffleAnimationController, curve: FortuneWheelCurve()); _subscription?.cancel(); _subscription = _raffleRepository.getRaffle().listen((raffle) { final winnerIds = raffle?.winners.map((e) => e.userUid) ?? []; @@ -61,7 +69,7 @@ class RaffleWinnerPickerViewModel extends ChangeNotifierEx { @override void dispose() { _subscription?.cancel(); - _selectedIndexStreamController.close(); + _raffleAnimationController.dispose(); super.dispose(); } @@ -78,18 +86,17 @@ class RaffleWinnerPickerViewModel extends ChangeNotifierEx { } _winner = null; _lockedParticipants = participants; + _raffleWinnerIndex = Random.secure().nextInt(participants.length); notifyListeners(); - final winnerIndex = Random().nextInt(participants.length); - final winner = participants[winnerIndex]; - _selectedIndexStreamController.add(winnerIndex); - await Future.delayed(ThemeDuration.raffleWheelDuration); + final winner = participants[_raffleWinnerIndex]; + await _raffleAnimationController.forward(from: 0); _raffleRepository.setWinner(raffleId: raffleId, winner: winner); _winner = winner; notifyListeners(); _confettiController.play(); await Future.delayed(ThemeDuration.confettiDuration); _confettiController.stop(); - await Future.delayed(ThemeDuration.nextRoundDelayDuration); + _raffleAnimationController.reset(); _winner = null; _lockedParticipants = null; notifyListeners(); @@ -107,7 +114,7 @@ class RaffleWinnerPickerViewModel extends ChangeNotifierEx { Future onAddParticipantTapped() async { final docId = _raffle?.id; if (docId == null) { - _mainNavigator.showErrorMessage('Failed to make raffle active (no raffle available)'); + _mainNavigator.showErrorMessage('Failed to add participant (no raffle available)'); return; } final name = await _mainNavigator.goToAddParticipantDialog(); diff --git a/lib/widget/raffle/add_participant_dialog.dart b/lib/widget/raffle/add_participant_dialog.dart index ab7f489..2777d82 100644 --- a/lib/widget/raffle/add_participant_dialog.dart +++ b/lib/widget/raffle/add_participant_dialog.dart @@ -40,7 +40,9 @@ class _AddParticipantDialogState extends State { ], child: TextField( controller: textController, + onSubmitted: (_) => Navigator.of(context).pop(textController.text), cursorColor: ThemeColors.primary, + focusNode: FocusNode()..requestFocus(), decoration: InputDecoration( hintText: localization.dialogRaffleNewParticipantInputName, focusedBorder: UnderlineInputBorder( diff --git a/lib/widget/raffle/custom_fortune_wheel.dart b/lib/widget/raffle/custom_fortune_wheel.dart index 2bc93f7..3bc574d 100644 --- a/lib/widget/raffle/custom_fortune_wheel.dart +++ b/lib/widget/raffle/custom_fortune_wheel.dart @@ -1,58 +1,38 @@ -import 'dart:math'; - import 'package:flutter/material.dart'; import 'package:flutter_belgium/model/data/raffle/participant.dart'; import 'package:flutter_belgium/theme/theme_colors.dart'; -import 'package:flutter_belgium/theme/theme_duration.dart'; -import 'package:flutter_fortune_wheel/flutter_fortune_wheel.dart'; +import 'package:flutter_crazy_fortune_wheel/flutter_crazy_fortune_wheel.dart'; class CustomFortuneWheel extends StatelessWidget { - final Stream selected; + final int winnerIndex; + final Animation animation; final List participants; const CustomFortuneWheel({ - required this.selected, + required this.winnerIndex, + required this.animation, required this.participants, super.key, }); @override Widget build(BuildContext context) { - return FortuneWheel( - selected: selected, - animateFirst: false, - duration: ThemeDuration.raffleWheelDuration, - rotationCount: Random().nextInt(10) + 20, - indicators: const [ - FortuneIndicator( - alignment: Alignment.topCenter, - child: TriangleIndicator( - color: ThemeColors.primary, - ), - ), - ], - physics: CircularPanPhysics( - duration: const Duration(seconds: 1), - curve: Curves.decelerate, - ), - items: [ - for (final participant in participants) - FortuneItem( - child: Text( + return RandomWheel( + animation: animation, + winnerIndex: winnerIndex, + wheelType: WheelType.values[participants.length % WheelType.values.length], + children: participants + .map( + (participant) => Text( participant.name, - style: TextStyle( + style: const TextStyle( color: ThemeColors.primary, - fontSize: participants.length > 30 ? 16 : 24, + fontSize: 24, fontWeight: FontWeight.bold, ), ), - style: const FortuneItemStyle( - color: ThemeColors.primaryUltraLight, // <-- custom circle slice fill color - borderColor: ThemeColors.primary, - borderWidth: 2, - ), - ), - ], + ) + .toList(), ); } } diff --git a/pubspec.lock b/pubspec.lock index 8507c5b..3e997b1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -435,22 +435,14 @@ packages: description: flutter source: sdk version: "0.0.0" - flutter_fortune_wheel: + flutter_crazy_fortune_wheel: dependency: "direct main" description: - name: flutter_fortune_wheel - sha256: "8f93144fab448ec95579d320e77dcf189b71bc363545985e41d3ba57fa42664e" + name: flutter_crazy_fortune_wheel + sha256: a6b299961a4bed63a3244462bd2512296f78bd37170a350022c8c332d5fcf13f url: "https://pub.dev" source: hosted - version: "1.3.2" - flutter_hooks: - dependency: transitive - description: - name: flutter_hooks - sha256: cde36b12f7188c85286fba9b38cc5a902e7279f36dd676967106c041dc9dde70 - url: "https://pub.dev" - source: hosted - version: "0.20.5" + version: "1.0.1" flutter_lints: dependency: "direct dev" description: @@ -536,6 +528,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" + flutter_shader_snap: + dependency: transitive + description: + name: flutter_shader_snap + sha256: a97fd2767391ca49dce96b15643aafe34dd3823da5b996e18bf06c3f7f02b513 + url: "https://pub.dev" + source: hosted + version: "0.0.3" + flutter_shaders: + dependency: transitive + description: + name: flutter_shaders + sha256: "34794acadd8275d971e02df03afee3dee0f98dbfb8c4837082ad0034f612a3e2" + url: "https://pub.dev" + source: hosted + version: "0.1.3" flutter_svg: dependency: transitive description: @@ -582,10 +590,10 @@ packages: dependency: transitive description: name: glob - sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" google_identity_services_web: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index d0b5c7a..3a83bfb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,7 +14,7 @@ dependencies: firebase_remote_config: ^5.3.0 flutter: sdk: flutter - flutter_fortune_wheel: ^1.3.2 + flutter_crazy_fortune_wheel: ^1.0.1 flutter_localizations: sdk: flutter flutter_navigation_generator_annotations: ^2.1.0 @@ -51,6 +51,8 @@ flutter: - family: Ubuntu fonts: - asset: assets/font/ubuntu/ubuntu_regular.ttf + shaders: + - packages/flutter_crazy_fortune_wheel/shaders/sliced_wheel_shader.frag impaktfull_translations: api_key: caca1cf8-fd65-428e-bd5b-8f5e2a3fdf23