diff --git a/packages/mottai_flutter_app/lib/auth/ui/auth_dependent_builder.dart b/packages/mottai_flutter_app/lib/auth/ui/auth_dependent_builder.dart index 0e46ed8b..8bbc50cc 100644 --- a/packages/mottai_flutter_app/lib/auth/ui/auth_dependent_builder.dart +++ b/packages/mottai_flutter_app/lib/auth/ui/auth_dependent_builder.dart @@ -5,7 +5,7 @@ import '../auth.dart'; /// Firebase に Auth にサインイン済みの場合にのみ [onAuthenticated] で渡した /// ウィジェットを表示する。 -/// その最、サインイン済みのユーザーの `userId` が使用できる。 +/// その際、サインイン済みのユーザーの `userId` が使用できる。 class AuthDependentBuilder extends ConsumerWidget { const AuthDependentBuilder({ super.key, @@ -13,11 +13,11 @@ class AuthDependentBuilder extends ConsumerWidget { this.onUnAuthenticated, }); - /// サインイン済みの場合に表示されるウィジェットを `userId` とともに返す - /// ビルダー関数。 + /// Firebase に Auth にサインイン済みの場合に表示されるウィジェットを `userId` とともに + /// 返すビルダー関数。 final Widget Function(String userId) onAuthenticated; - /// サインインアウトの場合に表示されるウィジェットを返すビルダー関数(任意)。 + /// Firebase Auth にサインインしていない場合に表示されるウィジェットを返すビルダー関数(任意)。 /// 渡さなければ共通の [_SignedOut] ウィジェットが表示される。 final Widget Function()? onUnAuthenticated; @@ -43,3 +43,39 @@ class _SignedOut extends StatelessWidget { return const Center(child: Text('ログインしてください。')); } } + +/// Firebase Auth にサインイン済みであり、そのユーザーが指定した [userId] に一致する場合 +/// にのみ [onUserAuthenticated] で渡したウィジェットを表示する。 +/// その際、サインイン済みのユーザーの [userId] が使用できる。 +class UserAuthDependentBuilder extends ConsumerWidget { + const UserAuthDependentBuilder({ + super.key, + required this.userId, + required this.onUserAuthenticated, + this.onUserUnAuthenticated, + }); + + /// Firebase Auth にサインイン済みであり、そのユーザーが指定した [userId] に一致する場合 + /// に表示されるウィジェットを [userId] とともに返すビルダー関数。 + final Widget Function(String userId) onUserAuthenticated; + + /// Firebase Auth にサインインしていない、またはサインイン済みでもそのユーザーが指定した + /// [userId] に一致しない場合表示されるウィジェットを返すビルダー関数(任意)。 + final Widget Function()? onUserUnAuthenticated; + + /// 表示するユーザーの uid. + final String userId; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final userId = ref.watch(userIdProvider); + if (userId == null || userId != this.userId) { + if (onUserUnAuthenticated != null) { + return onUserUnAuthenticated!(); + } else { + return const SizedBox(); + } + } + return onUserAuthenticated(userId); + } +} diff --git a/packages/mottai_flutter_app/lib/development/image_picker/ui/image_picker_sample.dart b/packages/mottai_flutter_app/lib/development/image_picker/ui/image_picker_sample.dart index 2fe0e934..872cd70c 100644 --- a/packages/mottai_flutter_app/lib/development/image_picker/ui/image_picker_sample.dart +++ b/packages/mottai_flutter_app/lib/development/image_picker/ui/image_picker_sample.dart @@ -4,6 +4,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:dart_flutter_common/dart_flutter_common.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import '../../../scaffold_messenger_controller.dart'; @@ -42,7 +43,7 @@ class _ImagePickerSamplePageState extends ConsumerState { ), body: ListView( children: [ - const SizedBox(height: 60), + const Gap(60), const Center(child: Text('1 枚の画像を選択')), if (_pickedImageFromGallery == null) GestureDetector( @@ -67,7 +68,7 @@ class _ImagePickerSamplePageState extends ConsumerState { child: Image.file(_pickedImageFromGallery!), ), ), - const SizedBox(height: 60), + const Gap(60), const Center(child: Text('1 枚の画像を撮影して選択')), if (_pickedImageFromCamera == null) GestureDetector( @@ -92,7 +93,7 @@ class _ImagePickerSamplePageState extends ConsumerState { child: Image.file(_pickedImageFromCamera!), ), ), - const SizedBox(height: 60), + const Gap(60), const Center(child: Text('複数の画像を選択')), if (_pickedImagesFromGallery.isEmpty) GestureDetector( diff --git a/packages/mottai_flutter_app/lib/host/ui/host.dart b/packages/mottai_flutter_app/lib/host/ui/host.dart index e7351e0b..766c0699 100644 --- a/packages/mottai_flutter_app/lib/host/ui/host.dart +++ b/packages/mottai_flutter_app/lib/host/ui/host.dart @@ -6,9 +6,9 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import '../../auth/ui/auth_dependent_builder.dart'; import '../../job/job.dart'; import '../../user/host.dart'; -import '../../user/ui/identity_dependent_builder.dart'; import '../../user/ui/user_mode.dart'; import '../../user/user_mode.dart'; import 'create_or_update_host.dart'; @@ -74,8 +74,8 @@ class HostPageBody extends ConsumerWidget { overflow: TextOverflow.ellipsis, ), ), - IdentityDependentBuilder( - buildForIdentity: () { + UserAuthDependentBuilder( + onUserAuthenticated: (userId) { return CircleAvatar( backgroundColor: Theme.of(context).focusColor, child: IconButton( @@ -90,22 +90,22 @@ class HostPageBody extends ConsumerWidget { ), ); }, - targetUserId: userId, + userId: userId, ), ], ), - IdentityDependentBuilder( - buildForIdentity: () { - return const Column( + UserAuthDependentBuilder( + userId: userId, + onUserAuthenticated: (_) { + return Column( children: [ - Gap(16), - UserModeSection(), + const Gap(16), + UserModeSection(userId: userId), ], ); }, - targetUserId: userId, ), - const SizedBox(height: 24), + const Gap(24), // TODO 自己紹介をDBに追加する Section( title: '自己紹介', @@ -116,7 +116,7 @@ class HostPageBody extends ConsumerWidget { 神奈川県小田原市で農家や漁師をしています。夏の時期にレモンの収穫のお手伝いをしてくれる方を募集しています。こんな感じでここには自己紹介文を表示する。表示するのは最大 8 行表くらいでいいだろうか。あいうえお、かきくけこ、さしすせそ、たちつてと、なにぬねの、はひふへほ、まみむめも、やゆよ、わをん、あいうえお、かきくけこ、さしすせそ、たちつてと、なにぬねの、はひふへほ、まみむめも、やゆよ...''', ), ), - const SizedBox(height: 24), + const Gap(24), Section( title: 'ホストタイプ', titleStyle: Theme.of(context).textTheme.titleLarge, @@ -145,7 +145,7 @@ class HostPageBody extends ConsumerWidget { child: Text('通信に失敗しました。'), ), ), - const SizedBox(height: 24), + const Gap(24), Section( title: '公開する場所・住所', titleStyle: Theme.of(context).textTheme.titleLarge, @@ -155,62 +155,14 @@ class HostPageBody extends ConsumerWidget { children: [ Text(''' 農場や主な作業場所などの、公開される場所・住所です。ワーカーは地図上から近所や興味がある地域のホストを探します。必ずしも正確で細かい住所である必要はありません。'''), - SizedBox(height: 12), + Gap(12), // TODO ここは後でデータを取得する Text('神奈川県小田原市石322 (hostLocation.address)'), ], ), ), - IdentityDependentBuilder( - buildForIdentity: () { - return Column( - children: [ - const SizedBox( - height: 12, - ), - Section( - titleBottomMargin: 4, - title: 'ユーザーモード', - content: Text( - currentUserMode == UserMode.host - ? ''' -ホストとしてアプリを使用します。あなたが募集するお手伝いに興味があるワーカーとやりとりをして、お手伝いを受け入れるモードです。''' - : '''ワーカーとしてアプリを使用します。興味のあるホストやお手伝いを探して、お手伝いに応募するモードです。''', - ), - ), - SegmentedButton( - style: ButtonStyle( - shape: - MaterialStateProperty.all( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - ), - segments: const >[ - ButtonSegment( - value: UserMode.host, - label: Text('ホスト'), - ), - ButtonSegment( - value: UserMode.worker, - label: Text('ワーカー'), - ), - ], - selected: {currentUserMode}, - onSelectionChanged: (newSelection) { - ref - .read(userModeStateProvider.notifier) - .update((_) => newSelection.first); - }, - ), - ], - ); - }, - targetUserId: userId, - ), - - const SizedBox(height: 24), + UserModeSection(userId: userId), + const Gap(24), // TODO 自己紹介をDBに追加する const Section( titleBottomMargin: 4, @@ -220,7 +172,7 @@ class HostPageBody extends ConsumerWidget { 神奈川県小田原市で農家や漁師をしています。夏の時期にレモンの収穫のお手伝いをしてくれる方を募集しています。こんな感じでここには自己紹介文を表示する。表示するのは最大 8 行表くらいでいいだろうか。あいうえお、かきくけこ、さしすせそ、たちつてと、なにぬねの、はひふへほ、まみむめも、やゆよ、わをん、あいうえお、かきくけこ、さしすせそ、たちつてと、なにぬねの、はひふへほ、まみむめも、やゆよ...''', ), ), - const SizedBox(height: 24), + const Gap(24), const Section( titleBottomMargin: 4, title: 'ホストタイプ', @@ -248,7 +200,7 @@ class HostPageBody extends ConsumerWidget { child: Text('通信に失敗しました。'), ), ), - const SizedBox(height: 24), + const Gap(24), const Section( titleBottomMargin: 4, title: '公開する場所・住所', @@ -257,7 +209,7 @@ class HostPageBody extends ConsumerWidget { children: [ Text(''' 農場や主な作業場所などの、公開される場所・住所です。ワーカーは地図上から近所や興味がある地域のホストを探します。必ずしも正確で細かい住所である必要はありません。'''), - SizedBox(height: 12), + Gap(12), // TODO ここは後でデータを取得する Text('神奈川県小田原市石322 (hostLocation.address)'), ], @@ -298,8 +250,9 @@ class HostPageBody extends ConsumerWidget { ), ), ), - IdentityDependentBuilder( - buildForIdentity: () { + UserAuthDependentBuilder( + userId: userId, + onUserAuthenticated: (_) { return Column( children: [ const Divider(height: 36), @@ -314,7 +267,7 @@ class HostPageBody extends ConsumerWidget { FontAwesomeIcons.google, size: 30, ), - SizedBox(width: 10), + Gap(10), Text('Google'), // TODO google連携済みかどうかで出し分けられるようにする Expanded( @@ -325,14 +278,14 @@ class HostPageBody extends ConsumerWidget { ), ], ), - SizedBox(height: 12), + Gap(12), Row( children: [ FaIcon( FontAwesomeIcons.apple, size: 40, ), - SizedBox(width: 10), + Gap(10), Text('Apple'), // TODO apple連携済みかどうかで出し分けられるようにする Expanded( @@ -343,7 +296,7 @@ class HostPageBody extends ConsumerWidget { ), ], ), - SizedBox(height: 12), + Gap(12), Row( children: [ FaIcon( @@ -351,7 +304,7 @@ class HostPageBody extends ConsumerWidget { color: Color(0xff06c755), size: 30, ), - SizedBox(width: 10), + Gap(10), Text('LINE'), // TODO line連携済みかどうかで出し分けられるようにする Expanded( @@ -368,7 +321,6 @@ class HostPageBody extends ConsumerWidget { ], ); }, - targetUserId: userId, ), const Gap(32), ], diff --git a/packages/mottai_flutter_app/lib/map/ui/map.dart b/packages/mottai_flutter_app/lib/map/ui/map.dart index 83caa9bd..441b5e78 100644 --- a/packages/mottai_flutter_app/lib/map/ui/map.dart +++ b/packages/mottai_flutter_app/lib/map/ui/map.dart @@ -279,6 +279,7 @@ class _HostLocationPageView extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + // TODO: 表示する [HostLocation] がない場合の UI を考える。 return PageView.builder( controller: PageController(viewportFraction: _viewportFraction), itemCount: readHostLocations.length, diff --git a/packages/mottai_flutter_app/lib/root/ui/root.dart b/packages/mottai_flutter_app/lib/root/ui/root.dart index d2ccd611..dbc6551c 100644 --- a/packages/mottai_flutter_app/lib/root/ui/root.dart +++ b/packages/mottai_flutter_app/lib/root/ui/root.dart @@ -98,7 +98,10 @@ class _DrawerChild extends ConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(packageInfo.packageName), + Text( + '${packageInfo.packageName} ' + '(${packageInfo.version}+${packageInfo.buildNumber})', + ), if (ref.watch(isHostProvider)) ...[ const Gap(8), Text( diff --git a/packages/mottai_flutter_app/lib/user/ui/identity_dependent_builder.dart b/packages/mottai_flutter_app/lib/user/ui/identity_dependent_builder.dart deleted file mode 100644 index bd2011f8..00000000 --- a/packages/mottai_flutter_app/lib/user/ui/identity_dependent_builder.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; - -import '../../auth/auth.dart'; - -/// サインイン済みのユーザーの userId と -/// 表示するユーザー情報の userIdが一致するかどうかで -/// ウィジェットの出し分けを行う -class IdentityDependentBuilder extends ConsumerWidget { - const IdentityDependentBuilder({ - super.key, - required this.buildForIdentity, - this.buildForOthers, - required this.targetUserId, - }); - - /// userIdが一致する場合に表示されるウィジェット - /// ビルダー関数。 - final Widget Function() buildForIdentity; - - /// userIdが不一致の場合に表示されるウィジェット - /// /// 渡さなければ [SizedBox] ウィジェットが表示される。 - final Widget Function()? buildForOthers; - - /// 表示するユーザーの userId - final String targetUserId; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final userId = ref.watch(userIdProvider); - if (userId == targetUserId) { - return buildForIdentity(); - } - if (buildForOthers != null) { - return buildForOthers!(); - } - return const SizedBox(); - } -} diff --git a/packages/mottai_flutter_app/lib/user/ui/user_mode.dart b/packages/mottai_flutter_app/lib/user/ui/user_mode.dart index 9c446a64..b28fa7bf 100644 --- a/packages/mottai_flutter_app/lib/user/ui/user_mode.dart +++ b/packages/mottai_flutter_app/lib/user/ui/user_mode.dart @@ -3,57 +3,72 @@ import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import '../../auth/ui/auth_dependent_builder.dart'; import '../user_mode.dart'; /// ワーカーページ、ホストページなどで使用する、[UserMode] を選択する [Section]. class UserModeSection extends ConsumerWidget { - const UserModeSection({super.key}); + const UserModeSection({required this.userId, super.key}); + + final String userId; @override Widget build(BuildContext context, WidgetRef ref) { final userMode = ref.watch(userModeStateProvider); // TODO:「自分かどうか Builder」を使用する。 - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Section( - titleBottomMargin: 8, - title: 'ユーザーモード', - titleStyle: Theme.of(context).textTheme.titleLarge, - content: Text( - userMode == UserMode.host - ? ''' -ホストとしてアプリを使用します。あなたが募集するお手伝いに興味があるワーカーとやりとりをして、お手伝いを受け入れるモードです。''' - : 'ワーカーとしてアプリを使用します。興味のあるホストやお手伝いを探して、お手伝いに応募するモードです。', - ), - ), - const Gap(8), - SegmentedButton( - style: ButtonStyle( - shape: MaterialStateProperty.all( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - ), - segments: const >[ - ButtonSegment( - value: UserMode.host, - label: Text('ホスト'), - ), - ButtonSegment( - value: UserMode.worker, - label: Text('ワーカー'), + return UserAuthDependentBuilder( + userId: userId, + onUserAuthenticated: (userId) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Section( + titleBottomMargin: 8, + title: 'ユーザーモード', + titleStyle: Theme.of(context).textTheme.titleLarge, + content: Text(_userModeDescription(userMode)), ), + const Gap(8), + SegmentedButton( + style: ButtonStyle( + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ), + segments: const >[ + ButtonSegment( + value: UserMode.host, + label: Text('ホスト'), + ), + ButtonSegment( + value: UserMode.worker, + label: Text('ワーカー'), + ), + ], + selected: {userMode}, + onSelectionChanged: (newSelection) { + ref + .read(userModeStateProvider.notifier) + .update((_) => newSelection.first); + }, + ) ], - selected: {userMode}, - onSelectionChanged: (newSelection) { - ref - .read(userModeStateProvider.notifier) - .update((_) => newSelection.first); - }, - ) - ], + ); + }, ); } } + +String _userModeDescription(UserMode userMode) { + switch (userMode) { + case UserMode.worker: + return 'ワーカーとしてアプリを使用します。' + '興味のあるホストやお手伝いを探して、お手伝いに応募するモードです。'; + case UserMode.host: + return 'ホストとしてアプリを使用します。' + 'あなたが募集するお手伝いに興味があるワーカーとやりとりをして、' + 'お手伝いを受け入れるモードです。'; + } +} diff --git a/packages/mottai_flutter_app/lib/worker/ui/worker.dart b/packages/mottai_flutter_app/lib/worker/ui/worker.dart index 52d04c9e..c42dc1dc 100644 --- a/packages/mottai_flutter_app/lib/worker/ui/worker.dart +++ b/packages/mottai_flutter_app/lib/worker/ui/worker.dart @@ -5,8 +5,8 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import '../../auth/ui/auth_dependent_builder.dart'; import '../../host/ui/create_or_update_host.dart'; -import '../../user/ui/identity_dependent_builder.dart'; import '../../user/ui/user_mode.dart'; import '../../user/user.dart'; import '../../user/worker.dart'; @@ -71,8 +71,9 @@ class WorkerPageBody extends ConsumerWidget { overflow: TextOverflow.ellipsis, ), ), - IdentityDependentBuilder( - buildForIdentity: () { + UserAuthDependentBuilder( + userId: userId, + onUserAuthenticated: (_) { return CircleAvatar( backgroundColor: Theme.of(context).focusColor, child: IconButton( @@ -84,20 +85,19 @@ class WorkerPageBody extends ConsumerWidget { ), ); }, - targetUserId: userId, ), ], ), - IdentityDependentBuilder( - buildForIdentity: () { - return const Column( + UserAuthDependentBuilder( + userId: userId, + onUserAuthenticated: (_) { + return Column( children: [ - Gap(16), - UserModeSection(), + const Gap(16), + UserModeSection(userId: userId), ], ); }, - targetUserId: userId, ), const Gap(16), // TODO 自己紹介をDBに追加する @@ -125,8 +125,9 @@ class WorkerPageBody extends ConsumerWidget { 'https://www.kaku-ichi.co.jp/media/wp-content/uploads/2020/02/20200226001.jpg', ), ), - IdentityDependentBuilder( - buildForIdentity: () { + UserAuthDependentBuilder( + userId: userId, + onUserAuthenticated: (userId) { return Column( children: [ const Divider( @@ -143,7 +144,7 @@ class WorkerPageBody extends ConsumerWidget { FontAwesomeIcons.google, size: 30, ), - SizedBox(width: 10), + Gap(10), Text('Google'), // TODO google連携済みかどうかで出し分けられるようにする Expanded( @@ -154,14 +155,14 @@ class WorkerPageBody extends ConsumerWidget { ), ], ), - SizedBox(height: 12), + Gap(12), Row( children: [ FaIcon( FontAwesomeIcons.apple, size: 40, ), - SizedBox(width: 10), + Gap(10), Text('Apple'), // TODO apple連携済みかどうかで出し分けられるようにする Expanded( @@ -172,7 +173,7 @@ class WorkerPageBody extends ConsumerWidget { ), ], ), - SizedBox(height: 12), + Gap(12), Row( children: [ FaIcon( @@ -180,7 +181,7 @@ class WorkerPageBody extends ConsumerWidget { color: Color(0xff06c755), size: 30, ), - SizedBox(width: 10), + Gap(10), Text('LINE'), // TODO line連携済みかどうかで出し分けられるようにする Expanded( @@ -220,7 +221,6 @@ class WorkerPageBody extends ConsumerWidget { ], ); }, - targetUserId: userId, ), const Gap(32), ],