From 273ee006027450fe369480c7fca834c7c7154d76 Mon Sep 17 00:00:00 2001 From: RikitoNoto <56541594+RikitoNoto@users.noreply.github.com> Date: Sat, 12 Aug 2023 14:58:20 +0900 Subject: [PATCH 01/15] =?UTF-8?q?#119=20=E4=BB=95=E4=BA=8B=E7=B7=A8?= =?UTF-8?q?=E9=9B=86=E3=83=9A=E3=83=BC=E3=82=B8=E3=81=AE=E6=A7=8B=E6=88=90?= =?UTF-8?q?=E3=82=92=E4=BD=9C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/development_items.dart | 7 + .../lib/job/ui/job_update.dart | 193 ++++++++++++ .../mottai_flutter_app/lib/router/router.dart | 9 +- .../lib/router/router.gr.dart | 293 +++++++++++------- 4 files changed, 378 insertions(+), 124 deletions(-) create mode 100644 packages/mottai_flutter_app/lib/job/ui/job_update.dart diff --git a/packages/mottai_flutter_app/lib/development/development_items/ui/development_items.dart b/packages/mottai_flutter_app/lib/development/development_items/ui/development_items.dart index ba166010..9da64c1a 100644 --- a/packages/mottai_flutter_app/lib/development/development_items/ui/development_items.dart +++ b/packages/mottai_flutter_app/lib/development/development_items/ui/development_items.dart @@ -11,6 +11,7 @@ import '../../../chat/ui/chat_rooms.dart'; import '../../../host/ui/create_or_update_host.dart'; import '../../../host/ui/host.dart'; import '../../../job/ui/job_detail.dart'; +import '../../../job/ui/job_update.dart'; import '../../../map/ui/map.dart'; import '../../../package_info.dart'; import '../../../push_notification/firebase_messaging.dart'; @@ -92,6 +93,12 @@ class _DevelopmentItemsPageState extends ConsumerState { JobDetailPage.location(jobId: 'PYRsrMSOApEgZ6lzMuUK'), ), ), + ListTile( + title: const Text('仕事情報編集ページ'), + onTap: () => context.router.pushNamed( + JobUpdatePage.location(jobId: 'PYRsrMSOApEgZ6lzMuUK'), + ), + ), ListTile( title: const Text('チャットルーム一覧ページ(StreamProvider、未既読管理)'), onTap: () => context.router.pushNamed(ChatRoomsPage.location), diff --git a/packages/mottai_flutter_app/lib/job/ui/job_update.dart b/packages/mottai_flutter_app/lib/job/ui/job_update.dart new file mode 100644 index 00000000..0c7ae767 --- /dev/null +++ b/packages/mottai_flutter_app/lib/job/ui/job_update.dart @@ -0,0 +1,193 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:dart_flutter_common/dart_flutter_common.dart'; +import 'package:firebase_common/firebase_common.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import '../job.dart'; + +//TODO: jobのhostIdとログイン中のhostIdを比較する。 + +/// 仕事情報更新ページ。 +@RoutePage() +class JobUpdatePage extends ConsumerWidget { + const JobUpdatePage({ + @PathParam('jobId') required this.jobId, + super.key, + }); + + static const path = '/jobs/:jobId/update'; + + /// [JobUpdatePage] に遷移する際に `context.router.pushNamed` で指定する文字列。 + static String location({required String jobId}) => '/jobs/$jobId/update'; + + final String jobId; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Scaffold( + appBar: AppBar(title: const Text('お手伝い募集内容を入力')), + // Jobの読み込み状態によって表示を変更 + body: ref.watch(jobFutureProvider(jobId)).when( + data: (job) { + if (job == null) { + return const Center(child: Text('お手伝いが存在していません。')); + } + return _JobUpdate( + job: job, + ); + }, + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, stackTrace) => const Center( + child: Text('通信に失敗しました。'), + ), + ), + ); + } +} + +class _JobUpdate extends ConsumerWidget { + const _JobUpdate({ + required this.job, + }); + + final ReadJob job; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // if (readHost.imageUrl.isNotEmpty) + // Center( + // child: LimitedBox( + // maxHeight: 300, + // child: Image.network(readHost.imageUrl, fit: BoxFit.cover), + // ), + // ), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 24, + ), + child: Form( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const _TextInputSection( + title: 'お手伝いのタイトル', + description: 'お手伝いのタイトルを最大2行程度で入力してください。', + maxLines: 2, + defaultDisplayLines: 2, + ), + const _TextInputSection( + title: 'お手伝いの場所', + description: + 'お手伝いを行う場所(農場や作業場所など)を入力してください。作業内容や曜日によって複数の場所の可能性がある場合は、それも入力してください。', + ), + const _TextInputSection( + title: 'お手伝いの内容', + description: + 'お手伝いの作業内容、作業時間帯やその他の情報をできるだけ詳しくを入力してください。お手伝い可能な曜日や時間帯、時期や季節が限られている場合や、その他に事前にお知らせするべき条件や情報などがあれば、その内容も入力してください。', + defaultDisplayLines: 10, + ), + const _TextInputSection( + title: '持ち物', + description: + 'お手伝いに必要な服装や持ち物などを書いてください。特に必要ない場合や貸出を行う場合はその内容も入力してください。', + ), + const _TextInputSection( + title: '報酬', + description: 'お手伝いをしてくれたワーカーにお渡しする報酬(食べ物など)を入力してください。', + ), + _TextInputSection( + title: 'アクセス', + description: + 'お手伝いの場所までのアクセス方法について補足説明をしてください。最寄りの駅やバス停まで送迎ができる場合などは、その内容も入力してください。', + choices: { + for (final v in AccessType.values) v: v.label, + }, + ), + const _TextInputSection( + title: 'ひとこと', + description: + 'お手伝いを検討してくれるワーカーの方が、ぜひお手伝いをしてみたくなるようひとことや、募集するお手伝いの魅力を入力しましょう!', + defaultDisplayLines: 5, + ), + Padding( + padding: const EdgeInsets.only(top: 16, bottom: 32), + child: Center( + child: ElevatedButton( + onPressed: () {}, // TODO: 未実装 + child: const Text('この内容で登録する'), + ), + ), + ), + ], + ), + ), + ), + ], + ), + ); + } +} + +/// タイトルと説明、テキストフィールドからなるセクション +/// [Section]を使用し、contentにフィールドを与えている +class _TextInputSection extends StatelessWidget { + const _TextInputSection({ + required this.title, + this.description, + this.maxLines, + this.defaultDisplayLines = 1, + this.choices, + }); + + /// セクションのタイトル。 + final String title; + + /// セクションの説明。 + final String? description; + + /// テキストフィールドの最大行数 + final int? maxLines; + + /// 初期表示時のテキストフィールドの行数 + final int defaultDisplayLines; + + /// テキストフィールドの下に表示する選択肢 + /// 選択された際の値がkeyで、表示する値がvalueの[Map]で受け取る。 + final Map? choices; + + @override + Widget build(BuildContext context) { + return Section( + title: title, + titleStyle: Theme.of(context).textTheme.titleLarge, + description: description, + descriptionStyle: Theme.of(context).textTheme.bodyMedium, + sectionPadding: const EdgeInsets.only(bottom: 32), + content: Column( + children: [ + TextFormField( + // onSaved: , + + maxLines: maxLines, + decoration: InputDecoration( + hintText: List.filled(defaultDisplayLines - 1, '\n').join(), + border: const OutlineInputBorder(), + ), + ), + if (choices != null) + SelectableChips( + allItems: choices!.keys, + labels: choices!, + enabledItems: choices!.keys, + ) + ], + ), + ); + } +} diff --git a/packages/mottai_flutter_app/lib/router/router.dart b/packages/mottai_flutter_app/lib/router/router.dart index e61f817e..7f5eaf57 100644 --- a/packages/mottai_flutter_app/lib/router/router.dart +++ b/packages/mottai_flutter_app/lib/router/router.dart @@ -16,6 +16,7 @@ import '../development/web_link/ui/web_link_stub.dart'; import '../host/ui/create_or_update_host.dart'; import '../host/ui/host.dart'; import '../job/ui/job_detail.dart'; +import '../job/ui/job_update.dart'; import '../map/ui/map.dart'; import '../worker/ui/create_or_update_worker.dart'; import '../worker/ui/worker.dart'; @@ -50,10 +51,10 @@ class AppRouter extends $AppRouter { path: HostPage.path, page: HostRoute.page, ), - // AutoRoute( - // path: CreateOrUpdateJobPage.path, - // page: CreateOrUpdateJobRoute.page, - // ), + AutoRoute( + path: JobUpdatePage.path, + page: JobUpdateRoute.page, + ), // AutoRoute( // path: UserPage.path, // page: UserRoute.page, diff --git a/packages/mottai_flutter_app/lib/router/router.gr.dart b/packages/mottai_flutter_app/lib/router/router.gr.dart index 77f6ca4e..bea30a2c 100644 --- a/packages/mottai_flutter_app/lib/router/router.gr.dart +++ b/packages/mottai_flutter_app/lib/router/router.gr.dart @@ -8,8 +8,8 @@ // coverage:ignore-file // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'package:auto_route/auto_route.dart' as _i21; -import 'package:flutter/material.dart' as _i22; +import 'package:auto_route/auto_route.dart' as _i22; +import 'package:flutter/material.dart' as _i23; import 'package:mottai_flutter_app/chat/ui/chat_room.dart' as _i1; import 'package:mottai_flutter_app/chat/ui/chat_rooms.dart' as _i2; import 'package:mottai_flutter_app/development/color/ui/color.dart' as _i3; @@ -28,30 +28,31 @@ import 'package:mottai_flutter_app/development/image_picker/ui/image_picker_samp import 'package:mottai_flutter_app/development/in_review/ui/in_review.dart' as _i13; import 'package:mottai_flutter_app/development/sample_todo/ui/sample_todos.dart' - as _i17; -import 'package:mottai_flutter_app/development/sign_in/ui/sign_in.dart' as _i18; + as _i18; +import 'package:mottai_flutter_app/development/sign_in/ui/sign_in.dart' as _i19; import 'package:mottai_flutter_app/development/web_link/ui/web_link_stub.dart' - as _i19; + as _i20; import 'package:mottai_flutter_app/host/ui/create_or_update_host.dart' as _i4; import 'package:mottai_flutter_app/host/ui/host.dart' as _i10; import 'package:mottai_flutter_app/job/ui/job_detail.dart' as _i14; -import 'package:mottai_flutter_app/map/ui/map.dart' as _i15; -import 'package:mottai_flutter_app/root/ui/root.dart' as _i16; +import 'package:mottai_flutter_app/job/ui/job_update.dart' as _i15; +import 'package:mottai_flutter_app/map/ui/map.dart' as _i16; +import 'package:mottai_flutter_app/root/ui/root.dart' as _i17; import 'package:mottai_flutter_app/worker/ui/create_or_update_worker.dart' as _i5; -import 'package:mottai_flutter_app/worker/ui/worker.dart' as _i20; +import 'package:mottai_flutter_app/worker/ui/worker.dart' as _i21; -abstract class $AppRouter extends _i21.RootStackRouter { +abstract class $AppRouter extends _i22.RootStackRouter { $AppRouter({super.navigatorKey}); @override - final Map pagesMap = { + final Map pagesMap = { ChatRoomRoute.name: (routeData) { final pathParams = routeData.inheritedPathParams; final args = routeData.argsAs( orElse: () => ChatRoomRouteArgs( chatRoomId: pathParams.getString('chatRoomId'))); - return _i21.AutoRoutePage( + return _i22.AutoRoutePage( routeData: routeData, child: _i1.ChatRoomPage( chatRoomId: args.chatRoomId, @@ -60,13 +61,13 @@ abstract class $AppRouter extends _i21.RootStackRouter { ); }, ChatRoomsRoute.name: (routeData) { - return _i21.AutoRoutePage( + return _i22.AutoRoutePage( routeData: routeData, child: const _i2.ChatRoomsPage(), ); }, ColorRoute.name: (routeData) { - return _i21.AutoRoutePage( + return _i22.AutoRoutePage( routeData: routeData, child: const _i3.ColorPage(), ); @@ -78,7 +79,7 @@ abstract class $AppRouter extends _i21.RootStackRouter { userId: pathParams.getString('userId'), actionType: pathParams.getString('actionType'), )); - return _i21.AutoRoutePage( + return _i22.AutoRoutePage( routeData: routeData, child: _i4.CreateOrUpdateHostPage( userId: args.userId, @@ -92,7 +93,7 @@ abstract class $AppRouter extends _i21.RootStackRouter { final args = routeData.argsAs( orElse: () => CreateOrUpdateWorkerRouteArgs( userId: pathParams.getString('userId'))); - return _i21.AutoRoutePage( + return _i22.AutoRoutePage( routeData: routeData, child: _i5.CreateOrUpdateWorkerPage( userId: args.userId, @@ -101,25 +102,25 @@ abstract class $AppRouter extends _i21.RootStackRouter { ); }, DevelopmentItemsRoute.name: (routeData) { - return _i21.AutoRoutePage( + return _i22.AutoRoutePage( routeData: routeData, child: const _i6.DevelopmentItemsPage(), ); }, FirebaseStorageSampleRoute.name: (routeData) { - return _i21.AutoRoutePage( + return _i22.AutoRoutePage( routeData: routeData, child: const _i7.FirebaseStorageSamplePage(), ); }, ForceUpdateSampleRoute.name: (routeData) { - return _i21.AutoRoutePage( + return _i22.AutoRoutePage( routeData: routeData, child: const _i8.ForceUpdateSamplePage(), ); }, GenericImagesRoute.name: (routeData) { - return _i21.AutoRoutePage( + return _i22.AutoRoutePage( routeData: routeData, child: const _i9.GenericImagesPage(), ); @@ -128,7 +129,7 @@ abstract class $AppRouter extends _i21.RootStackRouter { final pathParams = routeData.inheritedPathParams; final args = routeData.argsAs( orElse: () => HostRouteArgs(userId: pathParams.getString('userId'))); - return _i21.AutoRoutePage( + return _i22.AutoRoutePage( routeData: routeData, child: _i10.HostPage( userId: args.userId, @@ -137,19 +138,19 @@ abstract class $AppRouter extends _i21.RootStackRouter { ); }, ImageDetailViewStubRoute.name: (routeData) { - return _i21.AutoRoutePage( + return _i22.AutoRoutePage( routeData: routeData, child: const _i11.ImageDetailViewStubPage(), ); }, ImagePickerSampleRoute.name: (routeData) { - return _i21.AutoRoutePage( + return _i22.AutoRoutePage( routeData: routeData, child: const _i12.ImagePickerSamplePage(), ); }, InReviewRoute.name: (routeData) { - return _i21.AutoRoutePage( + return _i22.AutoRoutePage( routeData: routeData, child: const _i13.InReviewPage(), ); @@ -159,7 +160,7 @@ abstract class $AppRouter extends _i21.RootStackRouter { final args = routeData.argsAs( orElse: () => JobDetailRouteArgs(jobId: pathParams.getString('jobId'))); - return _i21.AutoRoutePage( + return _i22.AutoRoutePage( routeData: routeData, child: _i14.JobDetailPage( jobId: args.jobId, @@ -167,34 +168,47 @@ abstract class $AppRouter extends _i21.RootStackRouter { ), ); }, + JobUpdateRoute.name: (routeData) { + final pathParams = routeData.inheritedPathParams; + final args = routeData.argsAs( + orElse: () => + JobUpdateRouteArgs(jobId: pathParams.getString('jobId'))); + return _i22.AutoRoutePage( + routeData: routeData, + child: _i15.JobUpdatePage( + jobId: args.jobId, + key: args.key, + ), + ); + }, MapRoute.name: (routeData) { - return _i21.AutoRoutePage( + return _i22.AutoRoutePage( routeData: routeData, - child: const _i15.MapPage(), + child: const _i16.MapPage(), ); }, RootRoute.name: (routeData) { - return _i21.AutoRoutePage( + return _i22.AutoRoutePage( routeData: routeData, - child: const _i16.RootPage(), + child: const _i17.RootPage(), ); }, SampleTodosRoute.name: (routeData) { - return _i21.AutoRoutePage( + return _i22.AutoRoutePage( routeData: routeData, - child: const _i17.SampleTodosPage(), + child: const _i18.SampleTodosPage(), ); }, SignInSampleRoute.name: (routeData) { - return _i21.AutoRoutePage( + return _i22.AutoRoutePage( routeData: routeData, - child: const _i18.SignInSamplePage(), + child: const _i19.SignInSamplePage(), ); }, WebLinkStubRoute.name: (routeData) { - return _i21.AutoRoutePage( + return _i22.AutoRoutePage( routeData: routeData, - child: const _i19.WebLinkStubPage(), + child: const _i20.WebLinkStubPage(), ); }, WorkerRoute.name: (routeData) { @@ -202,9 +216,9 @@ abstract class $AppRouter extends _i21.RootStackRouter { final args = routeData.argsAs( orElse: () => WorkerRouteArgs(userId: pathParams.getString('userId'))); - return _i21.AutoRoutePage( + return _i22.AutoRoutePage( routeData: routeData, - child: _i20.WorkerPage( + child: _i21.WorkerPage( userId: args.userId, key: args.key, ), @@ -215,11 +229,11 @@ abstract class $AppRouter extends _i21.RootStackRouter { /// generated route for /// [_i1.ChatRoomPage] -class ChatRoomRoute extends _i21.PageRouteInfo { +class ChatRoomRoute extends _i22.PageRouteInfo { ChatRoomRoute({ required String chatRoomId, - _i22.Key? key, - List<_i21.PageRouteInfo>? children, + _i23.Key? key, + List<_i22.PageRouteInfo>? children, }) : super( ChatRoomRoute.name, args: ChatRoomRouteArgs( @@ -232,8 +246,8 @@ class ChatRoomRoute extends _i21.PageRouteInfo { static const String name = 'ChatRoomRoute'; - static const _i21.PageInfo page = - _i21.PageInfo(name); + static const _i22.PageInfo page = + _i22.PageInfo(name); } class ChatRoomRouteArgs { @@ -244,7 +258,7 @@ class ChatRoomRouteArgs { final String chatRoomId; - final _i22.Key? key; + final _i23.Key? key; @override String toString() { @@ -254,8 +268,8 @@ class ChatRoomRouteArgs { /// generated route for /// [_i2.ChatRoomsPage] -class ChatRoomsRoute extends _i21.PageRouteInfo { - const ChatRoomsRoute({List<_i21.PageRouteInfo>? children}) +class ChatRoomsRoute extends _i22.PageRouteInfo { + const ChatRoomsRoute({List<_i22.PageRouteInfo>? children}) : super( ChatRoomsRoute.name, initialChildren: children, @@ -263,13 +277,13 @@ class ChatRoomsRoute extends _i21.PageRouteInfo { static const String name = 'ChatRoomsRoute'; - static const _i21.PageInfo page = _i21.PageInfo(name); + static const _i22.PageInfo page = _i22.PageInfo(name); } /// generated route for /// [_i3.ColorPage] -class ColorRoute extends _i21.PageRouteInfo { - const ColorRoute({List<_i21.PageRouteInfo>? children}) +class ColorRoute extends _i22.PageRouteInfo { + const ColorRoute({List<_i22.PageRouteInfo>? children}) : super( ColorRoute.name, initialChildren: children, @@ -277,18 +291,18 @@ class ColorRoute extends _i21.PageRouteInfo { static const String name = 'ColorRoute'; - static const _i21.PageInfo page = _i21.PageInfo(name); + static const _i22.PageInfo page = _i22.PageInfo(name); } /// generated route for /// [_i4.CreateOrUpdateHostPage] class CreateOrUpdateHostRoute - extends _i21.PageRouteInfo { + extends _i22.PageRouteInfo { CreateOrUpdateHostRoute({ required String userId, required String actionType, - _i22.Key? key, - List<_i21.PageRouteInfo>? children, + _i23.Key? key, + List<_i22.PageRouteInfo>? children, }) : super( CreateOrUpdateHostRoute.name, args: CreateOrUpdateHostRouteArgs( @@ -305,8 +319,8 @@ class CreateOrUpdateHostRoute static const String name = 'CreateOrUpdateHostRoute'; - static const _i21.PageInfo page = - _i21.PageInfo(name); + static const _i22.PageInfo page = + _i22.PageInfo(name); } class CreateOrUpdateHostRouteArgs { @@ -320,7 +334,7 @@ class CreateOrUpdateHostRouteArgs { final String actionType; - final _i22.Key? key; + final _i23.Key? key; @override String toString() { @@ -331,11 +345,11 @@ class CreateOrUpdateHostRouteArgs { /// generated route for /// [_i5.CreateOrUpdateWorkerPage] class CreateOrUpdateWorkerRoute - extends _i21.PageRouteInfo { + extends _i22.PageRouteInfo { CreateOrUpdateWorkerRoute({ required String userId, - _i22.Key? key, - List<_i21.PageRouteInfo>? children, + _i23.Key? key, + List<_i22.PageRouteInfo>? children, }) : super( CreateOrUpdateWorkerRoute.name, args: CreateOrUpdateWorkerRouteArgs( @@ -348,8 +362,8 @@ class CreateOrUpdateWorkerRoute static const String name = 'CreateOrUpdateWorkerRoute'; - static const _i21.PageInfo page = - _i21.PageInfo(name); + static const _i22.PageInfo page = + _i22.PageInfo(name); } class CreateOrUpdateWorkerRouteArgs { @@ -360,7 +374,7 @@ class CreateOrUpdateWorkerRouteArgs { final String userId; - final _i22.Key? key; + final _i23.Key? key; @override String toString() { @@ -370,8 +384,8 @@ class CreateOrUpdateWorkerRouteArgs { /// generated route for /// [_i6.DevelopmentItemsPage] -class DevelopmentItemsRoute extends _i21.PageRouteInfo { - const DevelopmentItemsRoute({List<_i21.PageRouteInfo>? children}) +class DevelopmentItemsRoute extends _i22.PageRouteInfo { + const DevelopmentItemsRoute({List<_i22.PageRouteInfo>? children}) : super( DevelopmentItemsRoute.name, initialChildren: children, @@ -379,13 +393,13 @@ class DevelopmentItemsRoute extends _i21.PageRouteInfo { static const String name = 'DevelopmentItemsRoute'; - static const _i21.PageInfo page = _i21.PageInfo(name); + static const _i22.PageInfo page = _i22.PageInfo(name); } /// generated route for /// [_i7.FirebaseStorageSamplePage] -class FirebaseStorageSampleRoute extends _i21.PageRouteInfo { - const FirebaseStorageSampleRoute({List<_i21.PageRouteInfo>? children}) +class FirebaseStorageSampleRoute extends _i22.PageRouteInfo { + const FirebaseStorageSampleRoute({List<_i22.PageRouteInfo>? children}) : super( FirebaseStorageSampleRoute.name, initialChildren: children, @@ -393,13 +407,13 @@ class FirebaseStorageSampleRoute extends _i21.PageRouteInfo { static const String name = 'FirebaseStorageSampleRoute'; - static const _i21.PageInfo page = _i21.PageInfo(name); + static const _i22.PageInfo page = _i22.PageInfo(name); } /// generated route for /// [_i8.ForceUpdateSamplePage] -class ForceUpdateSampleRoute extends _i21.PageRouteInfo { - const ForceUpdateSampleRoute({List<_i21.PageRouteInfo>? children}) +class ForceUpdateSampleRoute extends _i22.PageRouteInfo { + const ForceUpdateSampleRoute({List<_i22.PageRouteInfo>? children}) : super( ForceUpdateSampleRoute.name, initialChildren: children, @@ -407,13 +421,13 @@ class ForceUpdateSampleRoute extends _i21.PageRouteInfo { static const String name = 'ForceUpdateSampleRoute'; - static const _i21.PageInfo page = _i21.PageInfo(name); + static const _i22.PageInfo page = _i22.PageInfo(name); } /// generated route for /// [_i9.GenericImagesPage] -class GenericImagesRoute extends _i21.PageRouteInfo { - const GenericImagesRoute({List<_i21.PageRouteInfo>? children}) +class GenericImagesRoute extends _i22.PageRouteInfo { + const GenericImagesRoute({List<_i22.PageRouteInfo>? children}) : super( GenericImagesRoute.name, initialChildren: children, @@ -421,16 +435,16 @@ class GenericImagesRoute extends _i21.PageRouteInfo { static const String name = 'GenericImagesRoute'; - static const _i21.PageInfo page = _i21.PageInfo(name); + static const _i22.PageInfo page = _i22.PageInfo(name); } /// generated route for /// [_i10.HostPage] -class HostRoute extends _i21.PageRouteInfo { +class HostRoute extends _i22.PageRouteInfo { HostRoute({ required String userId, - _i22.Key? key, - List<_i21.PageRouteInfo>? children, + _i23.Key? key, + List<_i22.PageRouteInfo>? children, }) : super( HostRoute.name, args: HostRouteArgs( @@ -443,8 +457,8 @@ class HostRoute extends _i21.PageRouteInfo { static const String name = 'HostRoute'; - static const _i21.PageInfo page = - _i21.PageInfo(name); + static const _i22.PageInfo page = + _i22.PageInfo(name); } class HostRouteArgs { @@ -455,7 +469,7 @@ class HostRouteArgs { final String userId; - final _i22.Key? key; + final _i23.Key? key; @override String toString() { @@ -465,8 +479,8 @@ class HostRouteArgs { /// generated route for /// [_i11.ImageDetailViewStubPage] -class ImageDetailViewStubRoute extends _i21.PageRouteInfo { - const ImageDetailViewStubRoute({List<_i21.PageRouteInfo>? children}) +class ImageDetailViewStubRoute extends _i22.PageRouteInfo { + const ImageDetailViewStubRoute({List<_i22.PageRouteInfo>? children}) : super( ImageDetailViewStubRoute.name, initialChildren: children, @@ -474,13 +488,13 @@ class ImageDetailViewStubRoute extends _i21.PageRouteInfo { static const String name = 'ImageDetailViewStubRoute'; - static const _i21.PageInfo page = _i21.PageInfo(name); + static const _i22.PageInfo page = _i22.PageInfo(name); } /// generated route for /// [_i12.ImagePickerSamplePage] -class ImagePickerSampleRoute extends _i21.PageRouteInfo { - const ImagePickerSampleRoute({List<_i21.PageRouteInfo>? children}) +class ImagePickerSampleRoute extends _i22.PageRouteInfo { + const ImagePickerSampleRoute({List<_i22.PageRouteInfo>? children}) : super( ImagePickerSampleRoute.name, initialChildren: children, @@ -488,13 +502,13 @@ class ImagePickerSampleRoute extends _i21.PageRouteInfo { static const String name = 'ImagePickerSampleRoute'; - static const _i21.PageInfo page = _i21.PageInfo(name); + static const _i22.PageInfo page = _i22.PageInfo(name); } /// generated route for /// [_i13.InReviewPage] -class InReviewRoute extends _i21.PageRouteInfo { - const InReviewRoute({List<_i21.PageRouteInfo>? children}) +class InReviewRoute extends _i22.PageRouteInfo { + const InReviewRoute({List<_i22.PageRouteInfo>? children}) : super( InReviewRoute.name, initialChildren: children, @@ -502,16 +516,16 @@ class InReviewRoute extends _i21.PageRouteInfo { static const String name = 'InReviewRoute'; - static const _i21.PageInfo page = _i21.PageInfo(name); + static const _i22.PageInfo page = _i22.PageInfo(name); } /// generated route for /// [_i14.JobDetailPage] -class JobDetailRoute extends _i21.PageRouteInfo { +class JobDetailRoute extends _i22.PageRouteInfo { JobDetailRoute({ required String jobId, - _i22.Key? key, - List<_i21.PageRouteInfo>? children, + _i23.Key? key, + List<_i22.PageRouteInfo>? children, }) : super( JobDetailRoute.name, args: JobDetailRouteArgs( @@ -524,8 +538,8 @@ class JobDetailRoute extends _i21.PageRouteInfo { static const String name = 'JobDetailRoute'; - static const _i21.PageInfo page = - _i21.PageInfo(name); + static const _i22.PageInfo page = + _i22.PageInfo(name); } class JobDetailRouteArgs { @@ -536,7 +550,7 @@ class JobDetailRouteArgs { final String jobId; - final _i22.Key? key; + final _i23.Key? key; @override String toString() { @@ -545,9 +559,48 @@ class JobDetailRouteArgs { } /// generated route for -/// [_i15.MapPage] -class MapRoute extends _i21.PageRouteInfo { - const MapRoute({List<_i21.PageRouteInfo>? children}) +/// [_i15.JobUpdatePage] +class JobUpdateRoute extends _i22.PageRouteInfo { + JobUpdateRoute({ + required String jobId, + _i23.Key? key, + List<_i22.PageRouteInfo>? children, + }) : super( + JobUpdateRoute.name, + args: JobUpdateRouteArgs( + jobId: jobId, + key: key, + ), + rawPathParams: {'jobId': jobId}, + initialChildren: children, + ); + + static const String name = 'JobUpdateRoute'; + + static const _i22.PageInfo page = + _i22.PageInfo(name); +} + +class JobUpdateRouteArgs { + const JobUpdateRouteArgs({ + required this.jobId, + this.key, + }); + + final String jobId; + + final _i23.Key? key; + + @override + String toString() { + return 'JobUpdateRouteArgs{jobId: $jobId, key: $key}'; + } +} + +/// generated route for +/// [_i16.MapPage] +class MapRoute extends _i22.PageRouteInfo { + const MapRoute({List<_i22.PageRouteInfo>? children}) : super( MapRoute.name, initialChildren: children, @@ -555,13 +608,13 @@ class MapRoute extends _i21.PageRouteInfo { static const String name = 'MapRoute'; - static const _i21.PageInfo page = _i21.PageInfo(name); + static const _i22.PageInfo page = _i22.PageInfo(name); } /// generated route for -/// [_i16.RootPage] -class RootRoute extends _i21.PageRouteInfo { - const RootRoute({List<_i21.PageRouteInfo>? children}) +/// [_i17.RootPage] +class RootRoute extends _i22.PageRouteInfo { + const RootRoute({List<_i22.PageRouteInfo>? children}) : super( RootRoute.name, initialChildren: children, @@ -569,13 +622,13 @@ class RootRoute extends _i21.PageRouteInfo { static const String name = 'RootRoute'; - static const _i21.PageInfo page = _i21.PageInfo(name); + static const _i22.PageInfo page = _i22.PageInfo(name); } /// generated route for -/// [_i17.SampleTodosPage] -class SampleTodosRoute extends _i21.PageRouteInfo { - const SampleTodosRoute({List<_i21.PageRouteInfo>? children}) +/// [_i18.SampleTodosPage] +class SampleTodosRoute extends _i22.PageRouteInfo { + const SampleTodosRoute({List<_i22.PageRouteInfo>? children}) : super( SampleTodosRoute.name, initialChildren: children, @@ -583,13 +636,13 @@ class SampleTodosRoute extends _i21.PageRouteInfo { static const String name = 'SampleTodosRoute'; - static const _i21.PageInfo page = _i21.PageInfo(name); + static const _i22.PageInfo page = _i22.PageInfo(name); } /// generated route for -/// [_i18.SignInSamplePage] -class SignInSampleRoute extends _i21.PageRouteInfo { - const SignInSampleRoute({List<_i21.PageRouteInfo>? children}) +/// [_i19.SignInSamplePage] +class SignInSampleRoute extends _i22.PageRouteInfo { + const SignInSampleRoute({List<_i22.PageRouteInfo>? children}) : super( SignInSampleRoute.name, initialChildren: children, @@ -597,13 +650,13 @@ class SignInSampleRoute extends _i21.PageRouteInfo { static const String name = 'SignInSampleRoute'; - static const _i21.PageInfo page = _i21.PageInfo(name); + static const _i22.PageInfo page = _i22.PageInfo(name); } /// generated route for -/// [_i19.WebLinkStubPage] -class WebLinkStubRoute extends _i21.PageRouteInfo { - const WebLinkStubRoute({List<_i21.PageRouteInfo>? children}) +/// [_i20.WebLinkStubPage] +class WebLinkStubRoute extends _i22.PageRouteInfo { + const WebLinkStubRoute({List<_i22.PageRouteInfo>? children}) : super( WebLinkStubRoute.name, initialChildren: children, @@ -611,16 +664,16 @@ class WebLinkStubRoute extends _i21.PageRouteInfo { static const String name = 'WebLinkStubRoute'; - static const _i21.PageInfo page = _i21.PageInfo(name); + static const _i22.PageInfo page = _i22.PageInfo(name); } /// generated route for -/// [_i20.WorkerPage] -class WorkerRoute extends _i21.PageRouteInfo { +/// [_i21.WorkerPage] +class WorkerRoute extends _i22.PageRouteInfo { WorkerRoute({ required String userId, - _i22.Key? key, - List<_i21.PageRouteInfo>? children, + _i23.Key? key, + List<_i22.PageRouteInfo>? children, }) : super( WorkerRoute.name, args: WorkerRouteArgs( @@ -633,8 +686,8 @@ class WorkerRoute extends _i21.PageRouteInfo { static const String name = 'WorkerRoute'; - static const _i21.PageInfo page = - _i21.PageInfo(name); + static const _i22.PageInfo page = + _i22.PageInfo(name); } class WorkerRouteArgs { @@ -645,7 +698,7 @@ class WorkerRouteArgs { final String userId; - final _i22.Key? key; + final _i23.Key? key; @override String toString() { From 959249fdc22523947ef96c0599ebcf11905c67e0 Mon Sep 17 00:00:00 2001 From: RikitoNoto <56541594+RikitoNoto@users.noreply.github.com> Date: Sat, 12 Aug 2023 18:05:26 +0900 Subject: [PATCH 02/15] =?UTF-8?q?#119=20job=E3=81=ABimageUrl=E3=81=AE?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lib/src/firestore_documents/job.dart | 3 +++ .../src/firestore_documents/job.flutterfire_gen.dart | 10 ++++++++++ 2 files changed, 13 insertions(+) diff --git a/packages/firebase_common/lib/src/firestore_documents/job.dart b/packages/firebase_common/lib/src/firestore_documents/job.dart index 2a06a7cf..ee922d4e 100644 --- a/packages/firebase_common/lib/src/firestore_documents/job.dart +++ b/packages/firebase_common/lib/src/firestore_documents/job.dart @@ -17,6 +17,7 @@ class Job { required this.belongings, required this.reward, this.comment = '', + this.imageUrl = '', this.createdAt = const ServerTimestamp(), this.updatedAt = const ServerTimestamp(), }); @@ -45,6 +46,8 @@ class Job { final String comment; + final String imageUrl; + // TODO: やや冗長になってしまっているのは、flutterfire_gen と // flutterfire_json_converters の作りのため。それらのパッケージが更新されたら // この実装も変更する。 diff --git a/packages/firebase_common/lib/src/firestore_documents/job.flutterfire_gen.dart b/packages/firebase_common/lib/src/firestore_documents/job.flutterfire_gen.dart index 6a1cc092..897e6c87 100644 --- a/packages/firebase_common/lib/src/firestore_documents/job.flutterfire_gen.dart +++ b/packages/firebase_common/lib/src/firestore_documents/job.flutterfire_gen.dart @@ -17,6 +17,7 @@ class ReadJob { required this.belongings, required this.reward, required this.comment, + required this.imageUrl, required this.createdAt, required this.updatedAt, }); @@ -43,6 +44,8 @@ class ReadJob { final String comment; + final String imageUrl; + final SealedTimestamp createdAt; final SealedTimestamp updatedAt; @@ -63,6 +66,7 @@ class ReadJob { belongings: json['belongings'] as String? ?? '', reward: json['reward'] as String? ?? '', comment: json['comment'] as String? ?? '', + imageUrl: json['imageUrl'] as String? ?? '', createdAt: json['createdAt'] == null ? const ServerTimestamp() : sealedTimestampConverter.fromJson(json['createdAt'] as Object), @@ -94,6 +98,7 @@ class CreateJob { required this.belongings, required this.reward, this.comment = '', + this.imageUrl = '', this.createdAt = const ServerTimestamp(), this.updatedAt = const ServerTimestamp(), }); @@ -107,6 +112,7 @@ class CreateJob { final String belongings; final String reward; final String comment; + final String imageUrl; final SealedTimestamp createdAt; final SealedTimestamp updatedAt; @@ -121,6 +127,7 @@ class CreateJob { 'belongings': belongings, 'reward': reward, 'comment': comment, + 'imageUrl': imageUrl, 'createdAt': sealedTimestampConverter.toJson(createdAt), 'updatedAt': alwaysUseServerTimestampSealedTimestampConverter.toJson(updatedAt), @@ -139,6 +146,7 @@ class UpdateJob { this.belongings, this.reward, this.comment, + this.imageUrl, this.createdAt, this.updatedAt = const ServerTimestamp(), }); @@ -152,6 +160,7 @@ class UpdateJob { final String? belongings; final String? reward; final String? comment; + final String? imageUrl; final SealedTimestamp? createdAt; final SealedTimestamp? updatedAt; @@ -167,6 +176,7 @@ class UpdateJob { if (belongings != null) 'belongings': belongings, if (reward != null) 'reward': reward, if (comment != null) 'comment': comment, + if (imageUrl != null) 'imageUrl': imageUrl, if (createdAt != null) 'createdAt': sealedTimestampConverter.toJson(createdAt!), 'updatedAt': updatedAt == null From dcbe67e78a09b4b2c5dfb484951687e0cb07bc99 Mon Sep 17 00:00:00 2001 From: RikitoNoto <56541594+RikitoNoto@users.noreply.github.com> Date: Sun, 13 Aug 2023 01:22:06 +0900 Subject: [PATCH 03/15] =?UTF-8?q?#119=20=E3=81=8A=E6=89=8B=E4=BC=9D?= =?UTF-8?q?=E3=81=84=E5=8B=9F=E9=9B=86=E3=83=9A=E3=83=BC=E3=82=B8=EF=BC=9A?= =?UTF-8?q?=E7=94=BB=E5=83=8F=E3=82=92=E8=A1=A8=E7=A4=BA=E3=81=99=E3=82=8B?= =?UTF-8?q?=E3=82=88=E3=81=86=E3=81=AB=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lib/job/ui/job_update.dart | 34 +++++++++++++++---- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/packages/mottai_flutter_app/lib/job/ui/job_update.dart b/packages/mottai_flutter_app/lib/job/ui/job_update.dart index 0c7ae767..835eaa7e 100644 --- a/packages/mottai_flutter_app/lib/job/ui/job_update.dart +++ b/packages/mottai_flutter_app/lib/job/ui/job_update.dart @@ -4,6 +4,7 @@ import 'package:firebase_common/firebase_common.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import '../../development/firebase_storage/ui/firebase_storage_controller.dart'; import '../job.dart'; //TODO: jobのhostIdとログイン中のhostIdを比較する。 @@ -55,17 +56,36 @@ class _JobUpdate extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final controller = ref.watch(firebaseStorageControllerProvider); + + late final Widget imageWidget; + // 画像が選択されている場合は画像を表示 + // 選択されていない場合は画像アイコンを表示 + if (job.imageUrl != '') { + imageWidget = GenericImage.rectangle( + showDetailOnTap: false, + imageUrl: job.imageUrl, + height: 300, + width: null, + ); + } else { + imageWidget = Container( + height: 300, + decoration: BoxDecoration( + border: Border.all(color: Colors.black38), + ), + child: const Center(child: Icon(Icons.image)), + ); + } + return SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // if (readHost.imageUrl.isNotEmpty) - // Center( - // child: LimitedBox( - // maxHeight: 300, - // child: Image.network(readHost.imageUrl, fit: BoxFit.cover), - // ), - // ), + GestureDetector( + onTap: controller.pickImageFromGallery, + child: imageWidget, + ), Padding( padding: const EdgeInsets.symmetric( vertical: 8, From ed775b0eec66976ac43d6cd63f08331f2dd96105 Mon Sep 17 00:00:00 2001 From: RikitoNoto <56541594+RikitoNoto@users.noreply.github.com> Date: Sun, 13 Aug 2023 06:08:23 +0900 Subject: [PATCH 04/15] =?UTF-8?q?#119=20=E4=BF=9D=E5=AD=98=E5=87=A6?= =?UTF-8?q?=E7=90=86=E3=81=AE=E4=BD=9C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lib/src/firestore_repositories/job.dart | 30 ++++++ packages/mottai_flutter_app/lib/job/job.dart | 26 +++++ .../lib/job/ui/job_controller.dart | 63 +++++++++++++ .../lib/job/ui/job_update.dart | 94 ++++++++++++++----- 4 files changed, 192 insertions(+), 21 deletions(-) create mode 100644 packages/mottai_flutter_app/lib/job/ui/job_controller.dart diff --git a/packages/firebase_common/lib/src/firestore_repositories/job.dart b/packages/firebase_common/lib/src/firestore_repositories/job.dart index c8f85b15..b05ae739 100644 --- a/packages/firebase_common/lib/src/firestore_repositories/job.dart +++ b/packages/firebase_common/lib/src/firestore_repositories/job.dart @@ -1,3 +1,5 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; + import '../firestore_documents/job.dart'; class JobRepository { @@ -12,4 +14,32 @@ class JobRepository { _query.fetchDocuments( queryBuilder: (query) => query.where('hostId', isEqualTo: userId), ); + + /// [Job] の情報を更新する。 + Future update({ + required String jobId, + String? title, + String? content, + String? place, + Set? accessTypes, + String? accessDescription, + String? belongings, + String? reward, + String? comment, + String? imageUrl, + }) => + _query.update( + jobId: jobId, + updateJob: UpdateJob( + title: title, + content: content, + place: place, + accessTypes: accessTypes, + accessDescription: accessDescription, + belongings: belongings, + reward: reward, + comment: comment, + imageUrl: imageUrl, + ), + ); } diff --git a/packages/mottai_flutter_app/lib/job/job.dart b/packages/mottai_flutter_app/lib/job/job.dart index f2ddd941..55873940 100644 --- a/packages/mottai_flutter_app/lib/job/job.dart +++ b/packages/mottai_flutter_app/lib/job/job.dart @@ -33,4 +33,30 @@ class JobService { /// 指定したユーザーの [Job] を全件取得する。 Future> fetchUserJobs({required String userId}) => _jobRepository.fetchUserJobs(userId: userId); + + /// [Job] の情報を更新する。 + Future update({ + required String jobId, + String? title, + String? content, + String? place, + Set? accessTypes, + String? accessDescription, + String? belongings, + String? reward, + String? comment, + String? imageUrl, + }) => + _jobRepository.update( + jobId: jobId, + title: title, + content: content, + place: place, + accessTypes: accessTypes, + accessDescription: accessDescription, + belongings: belongings, + reward: reward, + comment: comment, + imageUrl: imageUrl, + ); } diff --git a/packages/mottai_flutter_app/lib/job/ui/job_controller.dart b/packages/mottai_flutter_app/lib/job/ui/job_controller.dart new file mode 100644 index 00000000..cc7808b7 --- /dev/null +++ b/packages/mottai_flutter_app/lib/job/ui/job_controller.dart @@ -0,0 +1,63 @@ +import 'dart:io'; + +import 'package:firebase_common/firebase_common.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import '../../image/firebase_storage.dart'; +import '../job.dart'; + +final jobControllerProvider = Provider.autoDispose( + (ref) => JobController( + jobService: ref.watch(jobServiceProvider), + firebaseStorageService: ref.watch(firebaseStorageServiceProvider), + ), +); + +class JobController { + const JobController({ + required JobService jobService, + required FirebaseStorageService firebaseStorageService, + }) : _jobService = jobService, + _firebaseStorageService = firebaseStorageService; + + /// storageの画像を保存するフォルダのパス + static const _storagePath = 'jobs'; + + final JobService _jobService; + final FirebaseStorageService _firebaseStorageService; + + /// [Job] を更新する。 + Future updateJob({ + required String jobId, + String? title, + String? content, + String? place, + Set? accessTypes, + String? accessDescription, + String? belongings, + String? reward, + String? comment, + File? imageFile, + }) async { + String? imageUrl; + if (imageFile != null) { + final imagePath = '$_storagePath/$jobId-${DateTime.now()}.jpg'; + imageUrl = await _firebaseStorageService.upload( + path: imagePath, + resource: FirebaseStorageFile(imageFile), + ); + } + await _jobService.update( + jobId: jobId, + title: title, + content: content, + place: place, + accessTypes: accessTypes, + accessDescription: accessDescription, + belongings: belongings, + reward: reward, + comment: comment, + imageUrl: imageUrl, + ); + } +} diff --git a/packages/mottai_flutter_app/lib/job/ui/job_update.dart b/packages/mottai_flutter_app/lib/job/ui/job_update.dart index 835eaa7e..c2eedfc5 100644 --- a/packages/mottai_flutter_app/lib/job/ui/job_update.dart +++ b/packages/mottai_flutter_app/lib/job/ui/job_update.dart @@ -4,8 +4,10 @@ import 'package:firebase_common/firebase_common.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import '../../development/firebase_storage/firebase_storage.dart'; import '../../development/firebase_storage/ui/firebase_storage_controller.dart'; import '../job.dart'; +import 'job_controller.dart'; //TODO: jobのhostIdとログイン中のhostIdを比較する。 @@ -56,25 +58,56 @@ class _JobUpdate extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final controller = ref.watch(firebaseStorageControllerProvider); + final firebaseStorageController = + ref.watch(firebaseStorageControllerProvider); + final pickedImageFile = ref.watch(pickedImageFileStateProvider); + final controller = ref.watch(jobControllerProvider); + final titleController = TextEditingController(text: job.title); + final locationController = TextEditingController(text: job.place); + final contentController = TextEditingController(text: job.content); + final belongingsController = TextEditingController(text: job.belongings); + final rewardController = TextEditingController(text: job.reward); + final accessDiscriptionController = + TextEditingController(text: job.accessDescription); + final commentController = TextEditingController(text: job.comment); + + final imageUrlProvider = StateProvider.autoDispose( + (ref) => job.imageUrl, + ); late final Widget imageWidget; + + if (pickedImageFile != null) { + imageWidget = GestureDetector( + onTap: firebaseStorageController.pickImageFromGallery, + child: SizedBox( + height: 300, + child: Center( + child: Image.file(pickedImageFile), + ), + ), + ); + } // 画像が選択されている場合は画像を表示 // 選択されていない場合は画像アイコンを表示 - if (job.imageUrl != '') { + else if (ref.watch(imageUrlProvider) != '') { imageWidget = GenericImage.rectangle( + onTap: firebaseStorageController.pickImageFromGallery, showDetailOnTap: false, - imageUrl: job.imageUrl, + imageUrl: pickedImageFile?.path ?? ref.watch(imageUrlProvider), height: 300, width: null, ); } else { - imageWidget = Container( - height: 300, - decoration: BoxDecoration( - border: Border.all(color: Colors.black38), + imageWidget = GestureDetector( + onTap: firebaseStorageController.pickImageFromGallery, + child: Container( + height: 300, + decoration: BoxDecoration( + border: Border.all(color: Colors.black38), + ), + child: const Center(child: Icon(Icons.image)), ), - child: const Center(child: Icon(Icons.image)), ); } @@ -82,10 +115,7 @@ class _JobUpdate extends ConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - GestureDetector( - onTap: controller.pickImageFromGallery, - child: imageWidget, - ), + imageWidget, Padding( padding: const EdgeInsets.symmetric( vertical: 8, @@ -95,51 +125,70 @@ class _JobUpdate extends ConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const _TextInputSection( + _TextInputSection( title: 'お手伝いのタイトル', description: 'お手伝いのタイトルを最大2行程度で入力してください。', maxLines: 2, defaultDisplayLines: 2, + controller: titleController, ), - const _TextInputSection( + _TextInputSection( title: 'お手伝いの場所', description: 'お手伝いを行う場所(農場や作業場所など)を入力してください。作業内容や曜日によって複数の場所の可能性がある場合は、それも入力してください。', + controller: locationController, ), - const _TextInputSection( + _TextInputSection( title: 'お手伝いの内容', description: 'お手伝いの作業内容、作業時間帯やその他の情報をできるだけ詳しくを入力してください。お手伝い可能な曜日や時間帯、時期や季節が限られている場合や、その他に事前にお知らせするべき条件や情報などがあれば、その内容も入力してください。', defaultDisplayLines: 10, + controller: contentController, ), - const _TextInputSection( + _TextInputSection( title: '持ち物', description: 'お手伝いに必要な服装や持ち物などを書いてください。特に必要ない場合や貸出を行う場合はその内容も入力してください。', + controller: belongingsController, ), - const _TextInputSection( + _TextInputSection( title: '報酬', description: 'お手伝いをしてくれたワーカーにお渡しする報酬(食べ物など)を入力してください。', + controller: rewardController, ), _TextInputSection( title: 'アクセス', description: 'お手伝いの場所までのアクセス方法について補足説明をしてください。最寄りの駅やバス停まで送迎ができる場合などは、その内容も入力してください。', + controller: accessDiscriptionController, choices: { for (final v in AccessType.values) v: v.label, }, ), - const _TextInputSection( + _TextInputSection( title: 'ひとこと', description: 'お手伝いを検討してくれるワーカーの方が、ぜひお手伝いをしてみたくなるようひとことや、募集するお手伝いの魅力を入力しましょう!', defaultDisplayLines: 5, + controller: commentController, ), Padding( padding: const EdgeInsets.only(top: 16, bottom: 32), child: Center( child: ElevatedButton( - onPressed: () {}, // TODO: 未実装 + onPressed: () { + controller.updateJob( + jobId: job.jobId, + title: titleController.text, + place: locationController.text, + content: contentController.text, + belongings: belongingsController.text, + reward: rewardController.text, + accessDescription: accessDiscriptionController.text, + comment: commentController.text, + imageFile: pickedImageFile, + ); + }, child: const Text('この内容で登録する'), ), ), @@ -163,6 +212,7 @@ class _TextInputSection extends StatelessWidget { this.maxLines, this.defaultDisplayLines = 1, this.choices, + this.controller, }); /// セクションのタイトル。 @@ -181,6 +231,9 @@ class _TextInputSection extends StatelessWidget { /// 選択された際の値がkeyで、表示する値がvalueの[Map]で受け取る。 final Map? choices; + /// テキストフィールドのコントローラー + final TextEditingController? controller; + @override Widget build(BuildContext context) { return Section( @@ -192,8 +245,7 @@ class _TextInputSection extends StatelessWidget { content: Column( children: [ TextFormField( - // onSaved: , - + controller: controller, maxLines: maxLines, decoration: InputDecoration( hintText: List.filled(defaultDisplayLines - 1, '\n').join(), From 448bd3adda6bed8303343d8dfabfde1658135afb Mon Sep 17 00:00:00 2001 From: RikitoNoto <56541594+RikitoNoto@users.noreply.github.com> Date: Sun, 13 Aug 2023 16:05:50 +0900 Subject: [PATCH 05/15] =?UTF-8?q?#119=20=E7=B7=A8=E9=9B=86=E6=A8=A9?= =?UTF-8?q?=E9=99=90=E3=81=AE=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mottai_flutter_app/lib/job/ui/job_update.dart | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/mottai_flutter_app/lib/job/ui/job_update.dart b/packages/mottai_flutter_app/lib/job/ui/job_update.dart index c2eedfc5..0c46ccef 100644 --- a/packages/mottai_flutter_app/lib/job/ui/job_update.dart +++ b/packages/mottai_flutter_app/lib/job/ui/job_update.dart @@ -6,11 +6,10 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import '../../development/firebase_storage/firebase_storage.dart'; import '../../development/firebase_storage/ui/firebase_storage_controller.dart'; +import '../../user/user.dart' as my_user; import '../job.dart'; import 'job_controller.dart'; -//TODO: jobのhostIdとログイン中のhostIdを比較する。 - /// 仕事情報更新ページ。 @RoutePage() class JobUpdatePage extends ConsumerWidget { @@ -36,13 +35,20 @@ class JobUpdatePage extends ConsumerWidget { if (job == null) { return const Center(child: Text('お手伝いが存在していません。')); } + // UrlのhostIdとログイン中のユーザーのhostIdが違う場合 + if (job.hostId != + ref.watch(my_user.hostStreamProvider).valueOrNull?.hostId) { + return const Center( + child: Text('編集の権限がありません。'), + ); + } return _JobUpdate( job: job, ); }, loading: () => const Center(child: CircularProgressIndicator()), error: (error, stackTrace) => const Center( - child: Text('通信に失敗しました。'), + child: Text('仕事情報が取得できませんでした。'), ), ), ); From e47c4debe0a4e699669abc42bbecb3d31c272375 Mon Sep 17 00:00:00 2001 From: RikitoNoto <56541594+RikitoNoto@users.noreply.github.com> Date: Sun, 13 Aug 2023 19:46:45 +0900 Subject: [PATCH 06/15] =?UTF-8?q?#119=20=E3=83=A6=E3=83=BC=E3=82=B6?= =?UTF-8?q?=E3=83=BCid=E3=81=A8job.hostId=E3=82=92=E6=AF=94=E8=BC=83?= =?UTF-8?q?=E3=81=99=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/mottai_flutter_app/lib/job/ui/job_update.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/mottai_flutter_app/lib/job/ui/job_update.dart b/packages/mottai_flutter_app/lib/job/ui/job_update.dart index 0c46ccef..4d0a6e2e 100644 --- a/packages/mottai_flutter_app/lib/job/ui/job_update.dart +++ b/packages/mottai_flutter_app/lib/job/ui/job_update.dart @@ -4,9 +4,9 @@ import 'package:firebase_common/firebase_common.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import '../../auth/auth.dart'; import '../../development/firebase_storage/firebase_storage.dart'; import '../../development/firebase_storage/ui/firebase_storage_controller.dart'; -import '../../user/user.dart' as my_user; import '../job.dart'; import 'job_controller.dart'; @@ -36,8 +36,7 @@ class JobUpdatePage extends ConsumerWidget { return const Center(child: Text('お手伝いが存在していません。')); } // UrlのhostIdとログイン中のユーザーのhostIdが違う場合 - if (job.hostId != - ref.watch(my_user.hostStreamProvider).valueOrNull?.hostId) { + if (job.hostId != ref.watch(userIdProvider)) { return const Center( child: Text('編集の権限がありません。'), ); From 98560d216fa93659083b1dd5fbd9a19b4efe9237 Mon Sep 17 00:00:00 2001 From: RikitoNoto <56541594+RikitoNoto@users.noreply.github.com> Date: Sun, 13 Aug 2023 21:04:40 +0900 Subject: [PATCH 07/15] =?UTF-8?q?#119=20job=E6=96=B0=E8=A6=8F=E7=99=BB?= =?UTF-8?q?=E9=8C=B2=E3=83=9A=E3=83=BC=E3=82=B8=E3=81=AE=E4=BD=9C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lib/src/firestore_repositories/job.dart | 28 ++ .../ui/development_items.dart | 7 + packages/mottai_flutter_app/lib/job/job.dart | 26 ++ .../lib/job/ui/job_controller.dart | 62 +++- .../lib/job/ui/job_create.dart | 26 ++ .../lib/job/ui/job_form.dart | 242 +++++++++++++++ .../lib/job/ui/job_update.dart | 223 +------------- .../mottai_flutter_app/lib/router/router.dart | 13 +- .../lib/router/router.gr.dart | 287 ++++++++++-------- 9 files changed, 550 insertions(+), 364 deletions(-) create mode 100644 packages/mottai_flutter_app/lib/job/ui/job_create.dart create mode 100644 packages/mottai_flutter_app/lib/job/ui/job_form.dart diff --git a/packages/firebase_common/lib/src/firestore_repositories/job.dart b/packages/firebase_common/lib/src/firestore_repositories/job.dart index b05ae739..a5c8a469 100644 --- a/packages/firebase_common/lib/src/firestore_repositories/job.dart +++ b/packages/firebase_common/lib/src/firestore_repositories/job.dart @@ -15,6 +15,34 @@ class JobRepository { queryBuilder: (query) => query.where('hostId', isEqualTo: userId), ); + /// [Job] の情報を作成する。 + Future create({ + required String hostId, + required String title, + required String content, + required String place, + required Set accessTypes, + required String accessDescription, + required String belongings, + required String reward, + required String comment, + required String imageUrl, + }) => + _query.add( + createJob: CreateJob( + hostId: hostId, + title: title, + content: content, + place: place, + accessTypes: accessTypes, + accessDescription: accessDescription, + belongings: belongings, + reward: reward, + comment: comment, + imageUrl: imageUrl, + ), + ); + /// [Job] の情報を更新する。 Future update({ required String jobId, diff --git a/packages/mottai_flutter_app/lib/development/development_items/ui/development_items.dart b/packages/mottai_flutter_app/lib/development/development_items/ui/development_items.dart index 9da64c1a..3b0a3dbd 100644 --- a/packages/mottai_flutter_app/lib/development/development_items/ui/development_items.dart +++ b/packages/mottai_flutter_app/lib/development/development_items/ui/development_items.dart @@ -10,6 +10,7 @@ import '../../../chat/ui/chat_room.dart'; import '../../../chat/ui/chat_rooms.dart'; import '../../../host/ui/create_or_update_host.dart'; import '../../../host/ui/host.dart'; +import '../../../job/ui/job_create.dart'; import '../../../job/ui/job_detail.dart'; import '../../../job/ui/job_update.dart'; import '../../../map/ui/map.dart'; @@ -93,6 +94,12 @@ class _DevelopmentItemsPageState extends ConsumerState { JobDetailPage.location(jobId: 'PYRsrMSOApEgZ6lzMuUK'), ), ), + ListTile( + title: const Text('仕事情報作成ページ'), + onTap: () => context.router.pushNamed( + JobCreatePage.location(), + ), + ), ListTile( title: const Text('仕事情報編集ページ'), onTap: () => context.router.pushNamed( diff --git a/packages/mottai_flutter_app/lib/job/job.dart b/packages/mottai_flutter_app/lib/job/job.dart index 55873940..b1d52fe7 100644 --- a/packages/mottai_flutter_app/lib/job/job.dart +++ b/packages/mottai_flutter_app/lib/job/job.dart @@ -34,6 +34,32 @@ class JobService { Future> fetchUserJobs({required String userId}) => _jobRepository.fetchUserJobs(userId: userId); + /// [Job] の情報を作成する。 + Future create({ + required String hostId, + required String title, + required String content, + required String place, + required Set accessTypes, + required String accessDescription, + required String belongings, + required String reward, + required String comment, + required String imageUrl, + }) => + _jobRepository.create( + hostId: hostId, + title: title, + content: content, + place: place, + accessTypes: accessTypes, + accessDescription: accessDescription, + belongings: belongings, + reward: reward, + comment: comment, + imageUrl: imageUrl, + ); + /// [Job] の情報を更新する。 Future update({ required String jobId, diff --git a/packages/mottai_flutter_app/lib/job/ui/job_controller.dart b/packages/mottai_flutter_app/lib/job/ui/job_controller.dart index cc7808b7..134c2c08 100644 --- a/packages/mottai_flutter_app/lib/job/ui/job_controller.dart +++ b/packages/mottai_flutter_app/lib/job/ui/job_controller.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:firebase_common/firebase_common.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import '../../auth/auth.dart'; import '../../image/firebase_storage.dart'; import '../job.dart'; @@ -10,6 +11,7 @@ final jobControllerProvider = Provider.autoDispose( (ref) => JobController( jobService: ref.watch(jobServiceProvider), firebaseStorageService: ref.watch(firebaseStorageServiceProvider), + userId: ref.watch(userIdProvider), ), ); @@ -17,14 +19,53 @@ class JobController { const JobController({ required JobService jobService, required FirebaseStorageService firebaseStorageService, + String? userId, }) : _jobService = jobService, - _firebaseStorageService = firebaseStorageService; + _firebaseStorageService = firebaseStorageService, + _userId = userId; /// storageの画像を保存するフォルダのパス static const _storagePath = 'jobs'; final JobService _jobService; final FirebaseStorageService _firebaseStorageService; + final String? _userId; + + //TODO: 必須入力のバリデーション + /// [Job] の情報を作成する。 + Future create({ + required String title, + required String content, + required String place, + required Set accessTypes, + required String accessDescription, + required String belongings, + required String reward, + required String comment, + File? imageFile, + }) async { + final userId = _userId; + if (userId == null) { + return; + } + + var imageUrl = ''; + if (imageFile != null) { + imageUrl = await _uploadImage(imageFile); + } + await _jobService.create( + hostId: userId, + title: title, + content: content, + place: place, + accessTypes: accessTypes, + accessDescription: accessDescription, + belongings: belongings, + reward: reward, + comment: comment, + imageUrl: imageUrl, + ); + } /// [Job] を更新する。 Future updateJob({ @@ -39,13 +80,14 @@ class JobController { String? comment, File? imageFile, }) async { + final userId = _userId; + if (userId == null) { + return; + } + String? imageUrl; if (imageFile != null) { - final imagePath = '$_storagePath/$jobId-${DateTime.now()}.jpg'; - imageUrl = await _firebaseStorageService.upload( - path: imagePath, - resource: FirebaseStorageFile(imageFile), - ); + imageUrl = await _uploadImage(imageFile); } await _jobService.update( jobId: jobId, @@ -60,4 +102,12 @@ class JobController { imageUrl: imageUrl, ); } + + Future _uploadImage(File imageFile) { + final imagePath = '$_storagePath/$_userId-${DateTime.now()}.jpg'; + return _firebaseStorageService.upload( + path: imagePath, + resource: FirebaseStorageFile(imageFile), + ); + } } diff --git a/packages/mottai_flutter_app/lib/job/ui/job_create.dart b/packages/mottai_flutter_app/lib/job/ui/job_create.dart new file mode 100644 index 00000000..1b52991a --- /dev/null +++ b/packages/mottai_flutter_app/lib/job/ui/job_create.dart @@ -0,0 +1,26 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import 'job_form.dart'; + +/// 仕事情報更新ページ。 +@RoutePage() +class JobCreatePage extends ConsumerWidget { + const JobCreatePage({ + super.key, + }); + + static const path = '/jobs/create'; + + /// [JobCreatePage] に遷移する際に `context.router.pushNamed` で指定する文字列。 + static String location() => '/jobs/create'; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Scaffold( + appBar: AppBar(title: const Text('お手伝い募集内容を入力')), + body: const JobForm(), + ); + } +} diff --git a/packages/mottai_flutter_app/lib/job/ui/job_form.dart b/packages/mottai_flutter_app/lib/job/ui/job_form.dart new file mode 100644 index 00000000..be3ba9cd --- /dev/null +++ b/packages/mottai_flutter_app/lib/job/ui/job_form.dart @@ -0,0 +1,242 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:dart_flutter_common/dart_flutter_common.dart'; +import 'package:firebase_common/firebase_common.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import '../../development/firebase_storage/firebase_storage.dart'; +import '../../development/firebase_storage/ui/firebase_storage_controller.dart'; +import 'job_controller.dart'; + +class JobForm extends ConsumerWidget { + const JobForm({ + this.job, + super.key, + }); + + final ReadJob? job; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final firebaseStorageController = + ref.watch(firebaseStorageControllerProvider); + final pickedImageFile = ref.watch(pickedImageFileStateProvider); + final controller = ref.watch(jobControllerProvider); + final titleController = TextEditingController(text: job?.title); + final locationController = TextEditingController(text: job?.place); + final contentController = TextEditingController(text: job?.content); + final belongingsController = TextEditingController(text: job?.belongings); + final rewardController = TextEditingController(text: job?.reward); + final accessDiscriptionController = + TextEditingController(text: job?.accessDescription); + final commentController = TextEditingController(text: job?.comment); + + final imageUrlProvider = StateProvider.autoDispose( + (ref) => job?.imageUrl ?? '', + ); + final jobId = job?.jobId; + + late final Widget imageWidget; + + if (pickedImageFile != null) { + imageWidget = GestureDetector( + onTap: firebaseStorageController.pickImageFromGallery, + child: SizedBox( + height: 300, + child: Center( + child: Image.file(pickedImageFile), + ), + ), + ); + } + // 画像が選択されている場合は画像を表示 + // 選択されていない場合は画像アイコンを表示 + else if (ref.watch(imageUrlProvider) != '') { + imageWidget = GenericImage.rectangle( + onTap: firebaseStorageController.pickImageFromGallery, + showDetailOnTap: false, + imageUrl: pickedImageFile?.path ?? ref.watch(imageUrlProvider), + height: 300, + width: null, + ); + } else { + imageWidget = GestureDetector( + onTap: firebaseStorageController.pickImageFromGallery, + child: Container( + height: 300, + decoration: BoxDecoration( + border: Border.all(color: Colors.black38), + ), + child: const Center(child: Icon(Icons.image)), + ), + ); + } + + return SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + imageWidget, + Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 24, + ), + child: Form( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _TextInputSection( + title: 'お手伝いのタイトル', + description: 'お手伝いのタイトルを最大2行程度で入力してください。', + maxLines: 2, + defaultDisplayLines: 2, + controller: titleController, + ), + _TextInputSection( + title: 'お手伝いの場所', + description: + 'お手伝いを行う場所(農場や作業場所など)を入力してください。作業内容や曜日によって複数の場所の可能性がある場合は、それも入力してください。', + controller: locationController, + ), + _TextInputSection( + title: 'お手伝いの内容', + description: + 'お手伝いの作業内容、作業時間帯やその他の情報をできるだけ詳しくを入力してください。お手伝い可能な曜日や時間帯、時期や季節が限られている場合や、その他に事前にお知らせするべき条件や情報などがあれば、その内容も入力してください。', + defaultDisplayLines: 10, + controller: contentController, + ), + _TextInputSection( + title: '持ち物', + description: + 'お手伝いに必要な服装や持ち物などを書いてください。特に必要ない場合や貸出を行う場合はその内容も入力してください。', + controller: belongingsController, + ), + _TextInputSection( + title: '報酬', + description: 'お手伝いをしてくれたワーカーにお渡しする報酬(食べ物など)を入力してください。', + controller: rewardController, + ), + _TextInputSection( + title: 'アクセス', + description: + 'お手伝いの場所までのアクセス方法について補足説明をしてください。最寄りの駅やバス停まで送迎ができる場合などは、その内容も入力してください。', + controller: accessDiscriptionController, + choices: { + for (final v in AccessType.values) v: v.label, + }, + ), + _TextInputSection( + title: 'ひとこと', + description: + 'お手伝いを検討してくれるワーカーの方が、ぜひお手伝いをしてみたくなるようひとことや、募集するお手伝いの魅力を入力しましょう!', + defaultDisplayLines: 5, + controller: commentController, + ), + Padding( + padding: const EdgeInsets.only(top: 16, bottom: 32), + child: Center( + child: ElevatedButton( + onPressed: () { + if (jobId != null) { + controller.updateJob( + jobId: jobId, + title: titleController.text, + place: locationController.text, + content: contentController.text, + belongings: belongingsController.text, + reward: rewardController.text, + accessDescription: + accessDiscriptionController.text, + comment: commentController.text, + imageFile: pickedImageFile, + ); + } else { + controller.create( + title: titleController.text, + place: locationController.text, + content: contentController.text, + belongings: belongingsController.text, + reward: rewardController.text, + accessDescription: + accessDiscriptionController.text, + comment: commentController.text, + imageFile: pickedImageFile, + accessTypes: {AccessType.busAvailable}, + ); + } + }, + child: const Text('この内容で登録する'), + ), + ), + ), + ], + ), + ), + ), + ], + ), + ); + } +} + +/// タイトルと説明、テキストフィールドからなるセクション +/// [Section]を使用し、contentにフィールドを与えている +class _TextInputSection extends StatelessWidget { + const _TextInputSection({ + required this.title, + this.description, + this.maxLines, + this.defaultDisplayLines = 1, + this.choices, + this.controller, + }); + + /// セクションのタイトル。 + final String title; + + /// セクションの説明。 + final String? description; + + /// テキストフィールドの最大行数 + final int? maxLines; + + /// 初期表示時のテキストフィールドの行数 + final int defaultDisplayLines; + + /// テキストフィールドの下に表示する選択肢 + /// 選択された際の値がkeyで、表示する値がvalueの[Map]で受け取る。 + final Map? choices; + + /// テキストフィールドのコントローラー + final TextEditingController? controller; + + @override + Widget build(BuildContext context) { + return Section( + title: title, + titleStyle: Theme.of(context).textTheme.titleLarge, + description: description, + descriptionStyle: Theme.of(context).textTheme.bodyMedium, + sectionPadding: const EdgeInsets.only(bottom: 32), + content: Column( + children: [ + TextFormField( + controller: controller, + maxLines: maxLines, + decoration: InputDecoration( + hintText: List.filled(defaultDisplayLines - 1, '\n').join(), + border: const OutlineInputBorder(), + ), + ), + if (choices != null) + SelectableChips( + allItems: choices!.keys, + labels: choices!, + enabledItems: choices!.keys, + ) + ], + ), + ); + } +} diff --git a/packages/mottai_flutter_app/lib/job/ui/job_update.dart b/packages/mottai_flutter_app/lib/job/ui/job_update.dart index 4d0a6e2e..d02157ca 100644 --- a/packages/mottai_flutter_app/lib/job/ui/job_update.dart +++ b/packages/mottai_flutter_app/lib/job/ui/job_update.dart @@ -1,14 +1,10 @@ import 'package:auto_route/auto_route.dart'; -import 'package:dart_flutter_common/dart_flutter_common.dart'; -import 'package:firebase_common/firebase_common.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import '../../auth/auth.dart'; -import '../../development/firebase_storage/firebase_storage.dart'; -import '../../development/firebase_storage/ui/firebase_storage_controller.dart'; import '../job.dart'; -import 'job_controller.dart'; +import 'job_form.dart'; /// 仕事情報更新ページ。 @RoutePage() @@ -41,7 +37,7 @@ class JobUpdatePage extends ConsumerWidget { child: Text('編集の権限がありません。'), ); } - return _JobUpdate( + return JobForm( job: job, ); }, @@ -53,218 +49,3 @@ class JobUpdatePage extends ConsumerWidget { ); } } - -class _JobUpdate extends ConsumerWidget { - const _JobUpdate({ - required this.job, - }); - - final ReadJob job; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final firebaseStorageController = - ref.watch(firebaseStorageControllerProvider); - final pickedImageFile = ref.watch(pickedImageFileStateProvider); - final controller = ref.watch(jobControllerProvider); - final titleController = TextEditingController(text: job.title); - final locationController = TextEditingController(text: job.place); - final contentController = TextEditingController(text: job.content); - final belongingsController = TextEditingController(text: job.belongings); - final rewardController = TextEditingController(text: job.reward); - final accessDiscriptionController = - TextEditingController(text: job.accessDescription); - final commentController = TextEditingController(text: job.comment); - - final imageUrlProvider = StateProvider.autoDispose( - (ref) => job.imageUrl, - ); - - late final Widget imageWidget; - - if (pickedImageFile != null) { - imageWidget = GestureDetector( - onTap: firebaseStorageController.pickImageFromGallery, - child: SizedBox( - height: 300, - child: Center( - child: Image.file(pickedImageFile), - ), - ), - ); - } - // 画像が選択されている場合は画像を表示 - // 選択されていない場合は画像アイコンを表示 - else if (ref.watch(imageUrlProvider) != '') { - imageWidget = GenericImage.rectangle( - onTap: firebaseStorageController.pickImageFromGallery, - showDetailOnTap: false, - imageUrl: pickedImageFile?.path ?? ref.watch(imageUrlProvider), - height: 300, - width: null, - ); - } else { - imageWidget = GestureDetector( - onTap: firebaseStorageController.pickImageFromGallery, - child: Container( - height: 300, - decoration: BoxDecoration( - border: Border.all(color: Colors.black38), - ), - child: const Center(child: Icon(Icons.image)), - ), - ); - } - - return SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - imageWidget, - Padding( - padding: const EdgeInsets.symmetric( - vertical: 8, - horizontal: 24, - ), - child: Form( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _TextInputSection( - title: 'お手伝いのタイトル', - description: 'お手伝いのタイトルを最大2行程度で入力してください。', - maxLines: 2, - defaultDisplayLines: 2, - controller: titleController, - ), - _TextInputSection( - title: 'お手伝いの場所', - description: - 'お手伝いを行う場所(農場や作業場所など)を入力してください。作業内容や曜日によって複数の場所の可能性がある場合は、それも入力してください。', - controller: locationController, - ), - _TextInputSection( - title: 'お手伝いの内容', - description: - 'お手伝いの作業内容、作業時間帯やその他の情報をできるだけ詳しくを入力してください。お手伝い可能な曜日や時間帯、時期や季節が限られている場合や、その他に事前にお知らせするべき条件や情報などがあれば、その内容も入力してください。', - defaultDisplayLines: 10, - controller: contentController, - ), - _TextInputSection( - title: '持ち物', - description: - 'お手伝いに必要な服装や持ち物などを書いてください。特に必要ない場合や貸出を行う場合はその内容も入力してください。', - controller: belongingsController, - ), - _TextInputSection( - title: '報酬', - description: 'お手伝いをしてくれたワーカーにお渡しする報酬(食べ物など)を入力してください。', - controller: rewardController, - ), - _TextInputSection( - title: 'アクセス', - description: - 'お手伝いの場所までのアクセス方法について補足説明をしてください。最寄りの駅やバス停まで送迎ができる場合などは、その内容も入力してください。', - controller: accessDiscriptionController, - choices: { - for (final v in AccessType.values) v: v.label, - }, - ), - _TextInputSection( - title: 'ひとこと', - description: - 'お手伝いを検討してくれるワーカーの方が、ぜひお手伝いをしてみたくなるようひとことや、募集するお手伝いの魅力を入力しましょう!', - defaultDisplayLines: 5, - controller: commentController, - ), - Padding( - padding: const EdgeInsets.only(top: 16, bottom: 32), - child: Center( - child: ElevatedButton( - onPressed: () { - controller.updateJob( - jobId: job.jobId, - title: titleController.text, - place: locationController.text, - content: contentController.text, - belongings: belongingsController.text, - reward: rewardController.text, - accessDescription: accessDiscriptionController.text, - comment: commentController.text, - imageFile: pickedImageFile, - ); - }, - child: const Text('この内容で登録する'), - ), - ), - ), - ], - ), - ), - ), - ], - ), - ); - } -} - -/// タイトルと説明、テキストフィールドからなるセクション -/// [Section]を使用し、contentにフィールドを与えている -class _TextInputSection extends StatelessWidget { - const _TextInputSection({ - required this.title, - this.description, - this.maxLines, - this.defaultDisplayLines = 1, - this.choices, - this.controller, - }); - - /// セクションのタイトル。 - final String title; - - /// セクションの説明。 - final String? description; - - /// テキストフィールドの最大行数 - final int? maxLines; - - /// 初期表示時のテキストフィールドの行数 - final int defaultDisplayLines; - - /// テキストフィールドの下に表示する選択肢 - /// 選択された際の値がkeyで、表示する値がvalueの[Map]で受け取る。 - final Map? choices; - - /// テキストフィールドのコントローラー - final TextEditingController? controller; - - @override - Widget build(BuildContext context) { - return Section( - title: title, - titleStyle: Theme.of(context).textTheme.titleLarge, - description: description, - descriptionStyle: Theme.of(context).textTheme.bodyMedium, - sectionPadding: const EdgeInsets.only(bottom: 32), - content: Column( - children: [ - TextFormField( - controller: controller, - maxLines: maxLines, - decoration: InputDecoration( - hintText: List.filled(defaultDisplayLines - 1, '\n').join(), - border: const OutlineInputBorder(), - ), - ), - if (choices != null) - SelectableChips( - allItems: choices!.keys, - labels: choices!, - enabledItems: choices!.keys, - ) - ], - ), - ); - } -} diff --git a/packages/mottai_flutter_app/lib/router/router.dart b/packages/mottai_flutter_app/lib/router/router.dart index 7f5eaf57..76fdeb86 100644 --- a/packages/mottai_flutter_app/lib/router/router.dart +++ b/packages/mottai_flutter_app/lib/router/router.dart @@ -15,6 +15,7 @@ import '../development/sign_in/ui/sign_in.dart'; import '../development/web_link/ui/web_link_stub.dart'; import '../host/ui/create_or_update_host.dart'; import '../host/ui/host.dart'; +import '../job/ui/job_create.dart'; import '../job/ui/job_detail.dart'; import '../job/ui/job_update.dart'; import '../map/ui/map.dart'; @@ -39,10 +40,18 @@ class AppRouter extends $AppRouter { path: ChatRoomPage.path, page: ChatRoomRoute.page, ), + AutoRoute( + path: JobCreatePage.path, + page: JobCreateRoute.page, + ), AutoRoute( path: JobDetailPage.path, page: JobDetailRoute.page, ), + AutoRoute( + path: JobUpdatePage.path, + page: JobUpdateRoute.page, + ), AutoRoute( path: WorkerPage.path, page: WorkerRoute.page, @@ -51,10 +60,6 @@ class AppRouter extends $AppRouter { path: HostPage.path, page: HostRoute.page, ), - AutoRoute( - path: JobUpdatePage.path, - page: JobUpdateRoute.page, - ), // AutoRoute( // path: UserPage.path, // page: UserRoute.page, diff --git a/packages/mottai_flutter_app/lib/router/router.gr.dart b/packages/mottai_flutter_app/lib/router/router.gr.dart index bea30a2c..3f33de9d 100644 --- a/packages/mottai_flutter_app/lib/router/router.gr.dart +++ b/packages/mottai_flutter_app/lib/router/router.gr.dart @@ -8,8 +8,8 @@ // coverage:ignore-file // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'package:auto_route/auto_route.dart' as _i22; -import 'package:flutter/material.dart' as _i23; +import 'package:auto_route/auto_route.dart' as _i23; +import 'package:flutter/material.dart' as _i24; import 'package:mottai_flutter_app/chat/ui/chat_room.dart' as _i1; import 'package:mottai_flutter_app/chat/ui/chat_rooms.dart' as _i2; import 'package:mottai_flutter_app/development/color/ui/color.dart' as _i3; @@ -28,31 +28,32 @@ import 'package:mottai_flutter_app/development/image_picker/ui/image_picker_samp import 'package:mottai_flutter_app/development/in_review/ui/in_review.dart' as _i13; import 'package:mottai_flutter_app/development/sample_todo/ui/sample_todos.dart' - as _i18; -import 'package:mottai_flutter_app/development/sign_in/ui/sign_in.dart' as _i19; + as _i19; +import 'package:mottai_flutter_app/development/sign_in/ui/sign_in.dart' as _i20; import 'package:mottai_flutter_app/development/web_link/ui/web_link_stub.dart' - as _i20; + as _i21; import 'package:mottai_flutter_app/host/ui/create_or_update_host.dart' as _i4; import 'package:mottai_flutter_app/host/ui/host.dart' as _i10; -import 'package:mottai_flutter_app/job/ui/job_detail.dart' as _i14; -import 'package:mottai_flutter_app/job/ui/job_update.dart' as _i15; -import 'package:mottai_flutter_app/map/ui/map.dart' as _i16; -import 'package:mottai_flutter_app/root/ui/root.dart' as _i17; +import 'package:mottai_flutter_app/job/ui/job_create.dart' as _i14; +import 'package:mottai_flutter_app/job/ui/job_detail.dart' as _i15; +import 'package:mottai_flutter_app/job/ui/job_update.dart' as _i16; +import 'package:mottai_flutter_app/map/ui/map.dart' as _i17; +import 'package:mottai_flutter_app/root/ui/root.dart' as _i18; import 'package:mottai_flutter_app/worker/ui/create_or_update_worker.dart' as _i5; -import 'package:mottai_flutter_app/worker/ui/worker.dart' as _i21; +import 'package:mottai_flutter_app/worker/ui/worker.dart' as _i22; -abstract class $AppRouter extends _i22.RootStackRouter { +abstract class $AppRouter extends _i23.RootStackRouter { $AppRouter({super.navigatorKey}); @override - final Map pagesMap = { + final Map pagesMap = { ChatRoomRoute.name: (routeData) { final pathParams = routeData.inheritedPathParams; final args = routeData.argsAs( orElse: () => ChatRoomRouteArgs( chatRoomId: pathParams.getString('chatRoomId'))); - return _i22.AutoRoutePage( + return _i23.AutoRoutePage( routeData: routeData, child: _i1.ChatRoomPage( chatRoomId: args.chatRoomId, @@ -61,13 +62,13 @@ abstract class $AppRouter extends _i22.RootStackRouter { ); }, ChatRoomsRoute.name: (routeData) { - return _i22.AutoRoutePage( + return _i23.AutoRoutePage( routeData: routeData, child: const _i2.ChatRoomsPage(), ); }, ColorRoute.name: (routeData) { - return _i22.AutoRoutePage( + return _i23.AutoRoutePage( routeData: routeData, child: const _i3.ColorPage(), ); @@ -79,7 +80,7 @@ abstract class $AppRouter extends _i22.RootStackRouter { userId: pathParams.getString('userId'), actionType: pathParams.getString('actionType'), )); - return _i22.AutoRoutePage( + return _i23.AutoRoutePage( routeData: routeData, child: _i4.CreateOrUpdateHostPage( userId: args.userId, @@ -93,7 +94,7 @@ abstract class $AppRouter extends _i22.RootStackRouter { final args = routeData.argsAs( orElse: () => CreateOrUpdateWorkerRouteArgs( userId: pathParams.getString('userId'))); - return _i22.AutoRoutePage( + return _i23.AutoRoutePage( routeData: routeData, child: _i5.CreateOrUpdateWorkerPage( userId: args.userId, @@ -102,25 +103,25 @@ abstract class $AppRouter extends _i22.RootStackRouter { ); }, DevelopmentItemsRoute.name: (routeData) { - return _i22.AutoRoutePage( + return _i23.AutoRoutePage( routeData: routeData, child: const _i6.DevelopmentItemsPage(), ); }, FirebaseStorageSampleRoute.name: (routeData) { - return _i22.AutoRoutePage( + return _i23.AutoRoutePage( routeData: routeData, child: const _i7.FirebaseStorageSamplePage(), ); }, ForceUpdateSampleRoute.name: (routeData) { - return _i22.AutoRoutePage( + return _i23.AutoRoutePage( routeData: routeData, child: const _i8.ForceUpdateSamplePage(), ); }, GenericImagesRoute.name: (routeData) { - return _i22.AutoRoutePage( + return _i23.AutoRoutePage( routeData: routeData, child: const _i9.GenericImagesPage(), ); @@ -129,7 +130,7 @@ abstract class $AppRouter extends _i22.RootStackRouter { final pathParams = routeData.inheritedPathParams; final args = routeData.argsAs( orElse: () => HostRouteArgs(userId: pathParams.getString('userId'))); - return _i22.AutoRoutePage( + return _i23.AutoRoutePage( routeData: routeData, child: _i10.HostPage( userId: args.userId, @@ -138,31 +139,37 @@ abstract class $AppRouter extends _i22.RootStackRouter { ); }, ImageDetailViewStubRoute.name: (routeData) { - return _i22.AutoRoutePage( + return _i23.AutoRoutePage( routeData: routeData, child: const _i11.ImageDetailViewStubPage(), ); }, ImagePickerSampleRoute.name: (routeData) { - return _i22.AutoRoutePage( + return _i23.AutoRoutePage( routeData: routeData, child: const _i12.ImagePickerSamplePage(), ); }, InReviewRoute.name: (routeData) { - return _i22.AutoRoutePage( + return _i23.AutoRoutePage( routeData: routeData, child: const _i13.InReviewPage(), ); }, + JobCreateRoute.name: (routeData) { + return _i23.AutoRoutePage( + routeData: routeData, + child: const _i14.JobCreatePage(), + ); + }, JobDetailRoute.name: (routeData) { final pathParams = routeData.inheritedPathParams; final args = routeData.argsAs( orElse: () => JobDetailRouteArgs(jobId: pathParams.getString('jobId'))); - return _i22.AutoRoutePage( + return _i23.AutoRoutePage( routeData: routeData, - child: _i14.JobDetailPage( + child: _i15.JobDetailPage( jobId: args.jobId, key: args.key, ), @@ -173,42 +180,42 @@ abstract class $AppRouter extends _i22.RootStackRouter { final args = routeData.argsAs( orElse: () => JobUpdateRouteArgs(jobId: pathParams.getString('jobId'))); - return _i22.AutoRoutePage( + return _i23.AutoRoutePage( routeData: routeData, - child: _i15.JobUpdatePage( + child: _i16.JobUpdatePage( jobId: args.jobId, key: args.key, ), ); }, MapRoute.name: (routeData) { - return _i22.AutoRoutePage( + return _i23.AutoRoutePage( routeData: routeData, - child: const _i16.MapPage(), + child: const _i17.MapPage(), ); }, RootRoute.name: (routeData) { - return _i22.AutoRoutePage( + return _i23.AutoRoutePage( routeData: routeData, - child: const _i17.RootPage(), + child: const _i18.RootPage(), ); }, SampleTodosRoute.name: (routeData) { - return _i22.AutoRoutePage( + return _i23.AutoRoutePage( routeData: routeData, - child: const _i18.SampleTodosPage(), + child: const _i19.SampleTodosPage(), ); }, SignInSampleRoute.name: (routeData) { - return _i22.AutoRoutePage( + return _i23.AutoRoutePage( routeData: routeData, - child: const _i19.SignInSamplePage(), + child: const _i20.SignInSamplePage(), ); }, WebLinkStubRoute.name: (routeData) { - return _i22.AutoRoutePage( + return _i23.AutoRoutePage( routeData: routeData, - child: const _i20.WebLinkStubPage(), + child: const _i21.WebLinkStubPage(), ); }, WorkerRoute.name: (routeData) { @@ -216,9 +223,9 @@ abstract class $AppRouter extends _i22.RootStackRouter { final args = routeData.argsAs( orElse: () => WorkerRouteArgs(userId: pathParams.getString('userId'))); - return _i22.AutoRoutePage( + return _i23.AutoRoutePage( routeData: routeData, - child: _i21.WorkerPage( + child: _i22.WorkerPage( userId: args.userId, key: args.key, ), @@ -229,11 +236,11 @@ abstract class $AppRouter extends _i22.RootStackRouter { /// generated route for /// [_i1.ChatRoomPage] -class ChatRoomRoute extends _i22.PageRouteInfo { +class ChatRoomRoute extends _i23.PageRouteInfo { ChatRoomRoute({ required String chatRoomId, - _i23.Key? key, - List<_i22.PageRouteInfo>? children, + _i24.Key? key, + List<_i23.PageRouteInfo>? children, }) : super( ChatRoomRoute.name, args: ChatRoomRouteArgs( @@ -246,8 +253,8 @@ class ChatRoomRoute extends _i22.PageRouteInfo { static const String name = 'ChatRoomRoute'; - static const _i22.PageInfo page = - _i22.PageInfo(name); + static const _i23.PageInfo page = + _i23.PageInfo(name); } class ChatRoomRouteArgs { @@ -258,7 +265,7 @@ class ChatRoomRouteArgs { final String chatRoomId; - final _i23.Key? key; + final _i24.Key? key; @override String toString() { @@ -268,8 +275,8 @@ class ChatRoomRouteArgs { /// generated route for /// [_i2.ChatRoomsPage] -class ChatRoomsRoute extends _i22.PageRouteInfo { - const ChatRoomsRoute({List<_i22.PageRouteInfo>? children}) +class ChatRoomsRoute extends _i23.PageRouteInfo { + const ChatRoomsRoute({List<_i23.PageRouteInfo>? children}) : super( ChatRoomsRoute.name, initialChildren: children, @@ -277,13 +284,13 @@ class ChatRoomsRoute extends _i22.PageRouteInfo { static const String name = 'ChatRoomsRoute'; - static const _i22.PageInfo page = _i22.PageInfo(name); + static const _i23.PageInfo page = _i23.PageInfo(name); } /// generated route for /// [_i3.ColorPage] -class ColorRoute extends _i22.PageRouteInfo { - const ColorRoute({List<_i22.PageRouteInfo>? children}) +class ColorRoute extends _i23.PageRouteInfo { + const ColorRoute({List<_i23.PageRouteInfo>? children}) : super( ColorRoute.name, initialChildren: children, @@ -291,18 +298,18 @@ class ColorRoute extends _i22.PageRouteInfo { static const String name = 'ColorRoute'; - static const _i22.PageInfo page = _i22.PageInfo(name); + static const _i23.PageInfo page = _i23.PageInfo(name); } /// generated route for /// [_i4.CreateOrUpdateHostPage] class CreateOrUpdateHostRoute - extends _i22.PageRouteInfo { + extends _i23.PageRouteInfo { CreateOrUpdateHostRoute({ required String userId, required String actionType, - _i23.Key? key, - List<_i22.PageRouteInfo>? children, + _i24.Key? key, + List<_i23.PageRouteInfo>? children, }) : super( CreateOrUpdateHostRoute.name, args: CreateOrUpdateHostRouteArgs( @@ -319,8 +326,8 @@ class CreateOrUpdateHostRoute static const String name = 'CreateOrUpdateHostRoute'; - static const _i22.PageInfo page = - _i22.PageInfo(name); + static const _i23.PageInfo page = + _i23.PageInfo(name); } class CreateOrUpdateHostRouteArgs { @@ -334,7 +341,7 @@ class CreateOrUpdateHostRouteArgs { final String actionType; - final _i23.Key? key; + final _i24.Key? key; @override String toString() { @@ -345,11 +352,11 @@ class CreateOrUpdateHostRouteArgs { /// generated route for /// [_i5.CreateOrUpdateWorkerPage] class CreateOrUpdateWorkerRoute - extends _i22.PageRouteInfo { + extends _i23.PageRouteInfo { CreateOrUpdateWorkerRoute({ required String userId, - _i23.Key? key, - List<_i22.PageRouteInfo>? children, + _i24.Key? key, + List<_i23.PageRouteInfo>? children, }) : super( CreateOrUpdateWorkerRoute.name, args: CreateOrUpdateWorkerRouteArgs( @@ -362,8 +369,8 @@ class CreateOrUpdateWorkerRoute static const String name = 'CreateOrUpdateWorkerRoute'; - static const _i22.PageInfo page = - _i22.PageInfo(name); + static const _i23.PageInfo page = + _i23.PageInfo(name); } class CreateOrUpdateWorkerRouteArgs { @@ -374,7 +381,7 @@ class CreateOrUpdateWorkerRouteArgs { final String userId; - final _i23.Key? key; + final _i24.Key? key; @override String toString() { @@ -384,8 +391,8 @@ class CreateOrUpdateWorkerRouteArgs { /// generated route for /// [_i6.DevelopmentItemsPage] -class DevelopmentItemsRoute extends _i22.PageRouteInfo { - const DevelopmentItemsRoute({List<_i22.PageRouteInfo>? children}) +class DevelopmentItemsRoute extends _i23.PageRouteInfo { + const DevelopmentItemsRoute({List<_i23.PageRouteInfo>? children}) : super( DevelopmentItemsRoute.name, initialChildren: children, @@ -393,13 +400,13 @@ class DevelopmentItemsRoute extends _i22.PageRouteInfo { static const String name = 'DevelopmentItemsRoute'; - static const _i22.PageInfo page = _i22.PageInfo(name); + static const _i23.PageInfo page = _i23.PageInfo(name); } /// generated route for /// [_i7.FirebaseStorageSamplePage] -class FirebaseStorageSampleRoute extends _i22.PageRouteInfo { - const FirebaseStorageSampleRoute({List<_i22.PageRouteInfo>? children}) +class FirebaseStorageSampleRoute extends _i23.PageRouteInfo { + const FirebaseStorageSampleRoute({List<_i23.PageRouteInfo>? children}) : super( FirebaseStorageSampleRoute.name, initialChildren: children, @@ -407,13 +414,13 @@ class FirebaseStorageSampleRoute extends _i22.PageRouteInfo { static const String name = 'FirebaseStorageSampleRoute'; - static const _i22.PageInfo page = _i22.PageInfo(name); + static const _i23.PageInfo page = _i23.PageInfo(name); } /// generated route for /// [_i8.ForceUpdateSamplePage] -class ForceUpdateSampleRoute extends _i22.PageRouteInfo { - const ForceUpdateSampleRoute({List<_i22.PageRouteInfo>? children}) +class ForceUpdateSampleRoute extends _i23.PageRouteInfo { + const ForceUpdateSampleRoute({List<_i23.PageRouteInfo>? children}) : super( ForceUpdateSampleRoute.name, initialChildren: children, @@ -421,13 +428,13 @@ class ForceUpdateSampleRoute extends _i22.PageRouteInfo { static const String name = 'ForceUpdateSampleRoute'; - static const _i22.PageInfo page = _i22.PageInfo(name); + static const _i23.PageInfo page = _i23.PageInfo(name); } /// generated route for /// [_i9.GenericImagesPage] -class GenericImagesRoute extends _i22.PageRouteInfo { - const GenericImagesRoute({List<_i22.PageRouteInfo>? children}) +class GenericImagesRoute extends _i23.PageRouteInfo { + const GenericImagesRoute({List<_i23.PageRouteInfo>? children}) : super( GenericImagesRoute.name, initialChildren: children, @@ -435,16 +442,16 @@ class GenericImagesRoute extends _i22.PageRouteInfo { static const String name = 'GenericImagesRoute'; - static const _i22.PageInfo page = _i22.PageInfo(name); + static const _i23.PageInfo page = _i23.PageInfo(name); } /// generated route for /// [_i10.HostPage] -class HostRoute extends _i22.PageRouteInfo { +class HostRoute extends _i23.PageRouteInfo { HostRoute({ required String userId, - _i23.Key? key, - List<_i22.PageRouteInfo>? children, + _i24.Key? key, + List<_i23.PageRouteInfo>? children, }) : super( HostRoute.name, args: HostRouteArgs( @@ -457,8 +464,8 @@ class HostRoute extends _i22.PageRouteInfo { static const String name = 'HostRoute'; - static const _i22.PageInfo page = - _i22.PageInfo(name); + static const _i23.PageInfo page = + _i23.PageInfo(name); } class HostRouteArgs { @@ -469,7 +476,7 @@ class HostRouteArgs { final String userId; - final _i23.Key? key; + final _i24.Key? key; @override String toString() { @@ -479,8 +486,8 @@ class HostRouteArgs { /// generated route for /// [_i11.ImageDetailViewStubPage] -class ImageDetailViewStubRoute extends _i22.PageRouteInfo { - const ImageDetailViewStubRoute({List<_i22.PageRouteInfo>? children}) +class ImageDetailViewStubRoute extends _i23.PageRouteInfo { + const ImageDetailViewStubRoute({List<_i23.PageRouteInfo>? children}) : super( ImageDetailViewStubRoute.name, initialChildren: children, @@ -488,13 +495,13 @@ class ImageDetailViewStubRoute extends _i22.PageRouteInfo { static const String name = 'ImageDetailViewStubRoute'; - static const _i22.PageInfo page = _i22.PageInfo(name); + static const _i23.PageInfo page = _i23.PageInfo(name); } /// generated route for /// [_i12.ImagePickerSamplePage] -class ImagePickerSampleRoute extends _i22.PageRouteInfo { - const ImagePickerSampleRoute({List<_i22.PageRouteInfo>? children}) +class ImagePickerSampleRoute extends _i23.PageRouteInfo { + const ImagePickerSampleRoute({List<_i23.PageRouteInfo>? children}) : super( ImagePickerSampleRoute.name, initialChildren: children, @@ -502,13 +509,13 @@ class ImagePickerSampleRoute extends _i22.PageRouteInfo { static const String name = 'ImagePickerSampleRoute'; - static const _i22.PageInfo page = _i22.PageInfo(name); + static const _i23.PageInfo page = _i23.PageInfo(name); } /// generated route for /// [_i13.InReviewPage] -class InReviewRoute extends _i22.PageRouteInfo { - const InReviewRoute({List<_i22.PageRouteInfo>? children}) +class InReviewRoute extends _i23.PageRouteInfo { + const InReviewRoute({List<_i23.PageRouteInfo>? children}) : super( InReviewRoute.name, initialChildren: children, @@ -516,16 +523,30 @@ class InReviewRoute extends _i22.PageRouteInfo { static const String name = 'InReviewRoute'; - static const _i22.PageInfo page = _i22.PageInfo(name); + static const _i23.PageInfo page = _i23.PageInfo(name); +} + +/// generated route for +/// [_i14.JobCreatePage] +class JobCreateRoute extends _i23.PageRouteInfo { + const JobCreateRoute({List<_i23.PageRouteInfo>? children}) + : super( + JobCreateRoute.name, + initialChildren: children, + ); + + static const String name = 'JobCreateRoute'; + + static const _i23.PageInfo page = _i23.PageInfo(name); } /// generated route for -/// [_i14.JobDetailPage] -class JobDetailRoute extends _i22.PageRouteInfo { +/// [_i15.JobDetailPage] +class JobDetailRoute extends _i23.PageRouteInfo { JobDetailRoute({ required String jobId, - _i23.Key? key, - List<_i22.PageRouteInfo>? children, + _i24.Key? key, + List<_i23.PageRouteInfo>? children, }) : super( JobDetailRoute.name, args: JobDetailRouteArgs( @@ -538,8 +559,8 @@ class JobDetailRoute extends _i22.PageRouteInfo { static const String name = 'JobDetailRoute'; - static const _i22.PageInfo page = - _i22.PageInfo(name); + static const _i23.PageInfo page = + _i23.PageInfo(name); } class JobDetailRouteArgs { @@ -550,7 +571,7 @@ class JobDetailRouteArgs { final String jobId; - final _i23.Key? key; + final _i24.Key? key; @override String toString() { @@ -559,12 +580,12 @@ class JobDetailRouteArgs { } /// generated route for -/// [_i15.JobUpdatePage] -class JobUpdateRoute extends _i22.PageRouteInfo { +/// [_i16.JobUpdatePage] +class JobUpdateRoute extends _i23.PageRouteInfo { JobUpdateRoute({ required String jobId, - _i23.Key? key, - List<_i22.PageRouteInfo>? children, + _i24.Key? key, + List<_i23.PageRouteInfo>? children, }) : super( JobUpdateRoute.name, args: JobUpdateRouteArgs( @@ -577,8 +598,8 @@ class JobUpdateRoute extends _i22.PageRouteInfo { static const String name = 'JobUpdateRoute'; - static const _i22.PageInfo page = - _i22.PageInfo(name); + static const _i23.PageInfo page = + _i23.PageInfo(name); } class JobUpdateRouteArgs { @@ -589,7 +610,7 @@ class JobUpdateRouteArgs { final String jobId; - final _i23.Key? key; + final _i24.Key? key; @override String toString() { @@ -598,9 +619,9 @@ class JobUpdateRouteArgs { } /// generated route for -/// [_i16.MapPage] -class MapRoute extends _i22.PageRouteInfo { - const MapRoute({List<_i22.PageRouteInfo>? children}) +/// [_i17.MapPage] +class MapRoute extends _i23.PageRouteInfo { + const MapRoute({List<_i23.PageRouteInfo>? children}) : super( MapRoute.name, initialChildren: children, @@ -608,13 +629,13 @@ class MapRoute extends _i22.PageRouteInfo { static const String name = 'MapRoute'; - static const _i22.PageInfo page = _i22.PageInfo(name); + static const _i23.PageInfo page = _i23.PageInfo(name); } /// generated route for -/// [_i17.RootPage] -class RootRoute extends _i22.PageRouteInfo { - const RootRoute({List<_i22.PageRouteInfo>? children}) +/// [_i18.RootPage] +class RootRoute extends _i23.PageRouteInfo { + const RootRoute({List<_i23.PageRouteInfo>? children}) : super( RootRoute.name, initialChildren: children, @@ -622,13 +643,13 @@ class RootRoute extends _i22.PageRouteInfo { static const String name = 'RootRoute'; - static const _i22.PageInfo page = _i22.PageInfo(name); + static const _i23.PageInfo page = _i23.PageInfo(name); } /// generated route for -/// [_i18.SampleTodosPage] -class SampleTodosRoute extends _i22.PageRouteInfo { - const SampleTodosRoute({List<_i22.PageRouteInfo>? children}) +/// [_i19.SampleTodosPage] +class SampleTodosRoute extends _i23.PageRouteInfo { + const SampleTodosRoute({List<_i23.PageRouteInfo>? children}) : super( SampleTodosRoute.name, initialChildren: children, @@ -636,13 +657,13 @@ class SampleTodosRoute extends _i22.PageRouteInfo { static const String name = 'SampleTodosRoute'; - static const _i22.PageInfo page = _i22.PageInfo(name); + static const _i23.PageInfo page = _i23.PageInfo(name); } /// generated route for -/// [_i19.SignInSamplePage] -class SignInSampleRoute extends _i22.PageRouteInfo { - const SignInSampleRoute({List<_i22.PageRouteInfo>? children}) +/// [_i20.SignInSamplePage] +class SignInSampleRoute extends _i23.PageRouteInfo { + const SignInSampleRoute({List<_i23.PageRouteInfo>? children}) : super( SignInSampleRoute.name, initialChildren: children, @@ -650,13 +671,13 @@ class SignInSampleRoute extends _i22.PageRouteInfo { static const String name = 'SignInSampleRoute'; - static const _i22.PageInfo page = _i22.PageInfo(name); + static const _i23.PageInfo page = _i23.PageInfo(name); } /// generated route for -/// [_i20.WebLinkStubPage] -class WebLinkStubRoute extends _i22.PageRouteInfo { - const WebLinkStubRoute({List<_i22.PageRouteInfo>? children}) +/// [_i21.WebLinkStubPage] +class WebLinkStubRoute extends _i23.PageRouteInfo { + const WebLinkStubRoute({List<_i23.PageRouteInfo>? children}) : super( WebLinkStubRoute.name, initialChildren: children, @@ -664,16 +685,16 @@ class WebLinkStubRoute extends _i22.PageRouteInfo { static const String name = 'WebLinkStubRoute'; - static const _i22.PageInfo page = _i22.PageInfo(name); + static const _i23.PageInfo page = _i23.PageInfo(name); } /// generated route for -/// [_i21.WorkerPage] -class WorkerRoute extends _i22.PageRouteInfo { +/// [_i22.WorkerPage] +class WorkerRoute extends _i23.PageRouteInfo { WorkerRoute({ required String userId, - _i23.Key? key, - List<_i22.PageRouteInfo>? children, + _i24.Key? key, + List<_i23.PageRouteInfo>? children, }) : super( WorkerRoute.name, args: WorkerRouteArgs( @@ -686,8 +707,8 @@ class WorkerRoute extends _i22.PageRouteInfo { static const String name = 'WorkerRoute'; - static const _i22.PageInfo page = - _i22.PageInfo(name); + static const _i23.PageInfo page = + _i23.PageInfo(name); } class WorkerRouteArgs { @@ -698,7 +719,7 @@ class WorkerRouteArgs { final String userId; - final _i23.Key? key; + final _i24.Key? key; @override String toString() { From 386b71c515f05b24db4b947a5576421773094ede Mon Sep 17 00:00:00 2001 From: RikitoNoto <56541594+RikitoNoto@users.noreply.github.com> Date: Sun, 13 Aug 2023 21:47:14 +0900 Subject: [PATCH 08/15] =?UTF-8?q?#119=20=E3=82=BB=E3=82=AF=E3=82=B7?= =?UTF-8?q?=E3=83=A7=E3=83=B3=E3=81=8C=E5=85=A5=E5=8A=9B=E5=BF=85=E9=A0=88?= =?UTF-8?q?=E3=81=8B=E8=A1=A8=E3=81=99=E3=83=90=E3=83=83=E3=82=B8=E3=81=AE?= =?UTF-8?q?=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lib/src/widgets/section.dart | 17 +++++++-- .../lib/job/ui/job_form.dart | 37 ++++++++++++++++++- 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/packages/dart_flutter_common/lib/src/widgets/section.dart b/packages/dart_flutter_common/lib/src/widgets/section.dart index ee4807e9..682d8658 100644 --- a/packages/dart_flutter_common/lib/src/widgets/section.dart +++ b/packages/dart_flutter_common/lib/src/widgets/section.dart @@ -9,6 +9,7 @@ class Section extends StatelessWidget { /// [Section] を作成する。 const Section({ required this.title, + this.titleBadge, this.titleStyle, this.titleMaxLines = 1, this.titleBottomMargin = 16, @@ -24,6 +25,9 @@ class Section extends StatelessWidget { /// セクションのタイトル。 final String title; + /// タイトルの横に配置されるバッジ + final Widget? titleBadge; + /// [title] のスタイル。 final TextStyle? titleStyle; @@ -58,10 +62,15 @@ class Section extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - title, - style: titleStyle, - maxLines: titleMaxLines, + Row( + children: [ + Text( + title, + style: titleStyle, + maxLines: titleMaxLines, + ), + if (titleBadge != null) titleBadge!, + ], ), SizedBox(height: titleBottomMargin), if (description != null) ...[ diff --git a/packages/mottai_flutter_app/lib/job/ui/job_form.dart b/packages/mottai_flutter_app/lib/job/ui/job_form.dart index be3ba9cd..05b75ce7 100644 --- a/packages/mottai_flutter_app/lib/job/ui/job_form.dart +++ b/packages/mottai_flutter_app/lib/job/ui/job_form.dart @@ -1,4 +1,3 @@ -import 'package:auto_route/auto_route.dart'; import 'package:dart_flutter_common/dart_flutter_common.dart'; import 'package:firebase_common/firebase_common.dart'; import 'package:flutter/material.dart'; @@ -92,12 +91,14 @@ class JobForm extends ConsumerWidget { maxLines: 2, defaultDisplayLines: 2, controller: titleController, + isRequired: true, ), _TextInputSection( title: 'お手伝いの場所', description: 'お手伝いを行う場所(農場や作業場所など)を入力してください。作業内容や曜日によって複数の場所の可能性がある場合は、それも入力してください。', controller: locationController, + isRequired: true, ), _TextInputSection( title: 'お手伝いの内容', @@ -105,17 +106,20 @@ class JobForm extends ConsumerWidget { 'お手伝いの作業内容、作業時間帯やその他の情報をできるだけ詳しくを入力してください。お手伝い可能な曜日や時間帯、時期や季節が限られている場合や、その他に事前にお知らせするべき条件や情報などがあれば、その内容も入力してください。', defaultDisplayLines: 10, controller: contentController, + isRequired: true, ), _TextInputSection( title: '持ち物', description: 'お手伝いに必要な服装や持ち物などを書いてください。特に必要ない場合や貸出を行う場合はその内容も入力してください。', controller: belongingsController, + isRequired: true, ), _TextInputSection( title: '報酬', description: 'お手伝いをしてくれたワーカーにお渡しする報酬(食べ物など)を入力してください。', controller: rewardController, + isRequired: true, ), _TextInputSection( title: 'アクセス', @@ -190,6 +194,7 @@ class _TextInputSection extends StatelessWidget { this.defaultDisplayLines = 1, this.choices, this.controller, + this.isRequired = false, }); /// セクションのタイトル。 @@ -211,10 +216,17 @@ class _TextInputSection extends StatelessWidget { /// テキストフィールドのコントローラー final TextEditingController? controller; + /// 必須入力か否か + final bool isRequired; + @override Widget build(BuildContext context) { return Section( title: title, + titleBadge: Padding( + padding: const EdgeInsets.only(left: 8), + child: _OptionalBadge(isRequired: isRequired), + ), titleStyle: Theme.of(context).textTheme.titleLarge, description: description, descriptionStyle: Theme.of(context).textTheme.bodyMedium, @@ -240,3 +252,26 @@ class _TextInputSection extends StatelessWidget { ); } } + +/// 入力が必須か任意かを表すバッジ +/// [isRequired] の値によって、必須か任意の文字を選択して返す。 +class _OptionalBadge extends StatelessWidget { + const _OptionalBadge({this.isRequired = false}); + + /// 必須か否か + final bool isRequired; + + @override + Widget build(BuildContext context) { + if (isRequired) { + return const Badge( + label: Text('必須'), + ); + } else { + return const Badge( + label: Text('任意'), + backgroundColor: Colors.grey, + ); + } + } +} From 088987aa28fb9aea16b72dfa9c241072c9f7e4fe Mon Sep 17 00:00:00 2001 From: RikitoNoto <56541594+RikitoNoto@users.noreply.github.com> Date: Sun, 13 Aug 2023 22:03:08 +0900 Subject: [PATCH 09/15] =?UTF-8?q?#119=20=E5=BF=85=E9=A0=88=E5=85=A5?= =?UTF-8?q?=E5=8A=9B=E3=81=AE=E3=83=90=E3=83=AA=E3=83=87=E3=83=BC=E3=82=B7?= =?UTF-8?q?=E3=83=A7=E3=83=B3=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mottai_flutter_app/lib/job/ui/job_create.dart | 2 +- .../mottai_flutter_app/lib/job/ui/job_form.dart | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/mottai_flutter_app/lib/job/ui/job_create.dart b/packages/mottai_flutter_app/lib/job/ui/job_create.dart index 1b52991a..0aa515c0 100644 --- a/packages/mottai_flutter_app/lib/job/ui/job_create.dart +++ b/packages/mottai_flutter_app/lib/job/ui/job_create.dart @@ -20,7 +20,7 @@ class JobCreatePage extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { return Scaffold( appBar: AppBar(title: const Text('お手伝い募集内容を入力')), - body: const JobForm(), + body: JobForm(), ); } } diff --git a/packages/mottai_flutter_app/lib/job/ui/job_form.dart b/packages/mottai_flutter_app/lib/job/ui/job_form.dart index 05b75ce7..46edfcd7 100644 --- a/packages/mottai_flutter_app/lib/job/ui/job_form.dart +++ b/packages/mottai_flutter_app/lib/job/ui/job_form.dart @@ -8,12 +8,13 @@ import '../../development/firebase_storage/ui/firebase_storage_controller.dart'; import 'job_controller.dart'; class JobForm extends ConsumerWidget { - const JobForm({ + JobForm({ this.job, super.key, }); final ReadJob? job; + final formKey = GlobalKey(); @override Widget build(BuildContext context, WidgetRef ref) { @@ -82,6 +83,7 @@ class JobForm extends ConsumerWidget { horizontal: 24, ), child: Form( + key: formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -142,6 +144,11 @@ class JobForm extends ConsumerWidget { child: Center( child: ElevatedButton( onPressed: () { + final isValidate = formKey.currentState?.validate(); + if (!(isValidate ?? true)) { + return; + } + if (jobId != null) { controller.updateJob( jobId: jobId, @@ -236,6 +243,12 @@ class _TextInputSection extends StatelessWidget { TextFormField( controller: controller, maxLines: maxLines, + validator: (value) { + if (isRequired && value == '') { + return '入力してください。'; + } + return null; + }, decoration: InputDecoration( hintText: List.filled(defaultDisplayLines - 1, '\n').join(), border: const OutlineInputBorder(), From 960068f783a5efc8ce70bf0e59909e2dd3856981 Mon Sep 17 00:00:00 2001 From: RikitoNoto <56541594+RikitoNoto@users.noreply.github.com> Date: Mon, 14 Aug 2023 03:58:18 +0900 Subject: [PATCH 10/15] =?UTF-8?q?#119=20AccessType=E3=82=92=E9=81=B8?= =?UTF-8?q?=E6=8A=9E=E3=81=A7=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?= =?UTF-8?q?=E8=A8=AD=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lib/src/widgets/selectable_chips.dart | 20 ++++++++--- .../lib/job/ui/job_controller.dart | 4 +++ .../lib/job/ui/job_form.dart | 36 +++++++++++++++++-- 3 files changed, 52 insertions(+), 8 deletions(-) diff --git a/packages/dart_flutter_common/lib/src/widgets/selectable_chips.dart b/packages/dart_flutter_common/lib/src/widgets/selectable_chips.dart index b7c1df7a..a46a7ab0 100644 --- a/packages/dart_flutter_common/lib/src/widgets/selectable_chips.dart +++ b/packages/dart_flutter_common/lib/src/widgets/selectable_chips.dart @@ -16,6 +16,7 @@ class SelectableChips extends StatelessWidget { this.direction = Axis.horizontal, this.alignment = WrapAlignment.start, this.crossAxisAlignment = WrapCrossAlignment.start, + this.onTap, super.key, }); @@ -43,6 +44,9 @@ class SelectableChips extends StatelessWidget { /// [direction] と垂直方向の並び方。 final WrapCrossAlignment crossAxisAlignment; + /// チップが押されたときのコールバック + final void Function(T item)? onTap; + @override Widget build(BuildContext context) { return Wrap( @@ -59,6 +63,7 @@ class SelectableChips extends StatelessWidget { child: _SwitchingChip( label: labels[item] ?? '', isEnabled: enabledItems.contains(item), + onTap: () => onTap?.call(item), ), ), ) @@ -71,18 +76,23 @@ class _SwitchingChip extends StatelessWidget { const _SwitchingChip({ required this.label, this.isEnabled = false, + this.onTap, }); final bool isEnabled; final String label; + final void Function()? onTap; @override Widget build(BuildContext context) { - return InputChip( - onPressed: () => {}, - isEnabled: isEnabled, - side: isEnabled ? BorderSide.none : null, - label: Text(label), + return GestureDetector( + onTap: onTap, + child: InputChip( + onPressed: onTap, // コールバックを設定しないとデザインが変わるので、設定している + isEnabled: isEnabled, + side: isEnabled ? BorderSide.none : null, + label: Text(label), + ), ); } } diff --git a/packages/mottai_flutter_app/lib/job/ui/job_controller.dart b/packages/mottai_flutter_app/lib/job/ui/job_controller.dart index 134c2c08..ed6a6dd1 100644 --- a/packages/mottai_flutter_app/lib/job/ui/job_controller.dart +++ b/packages/mottai_flutter_app/lib/job/ui/job_controller.dart @@ -7,6 +7,10 @@ import '../../auth/auth.dart'; import '../../image/firebase_storage.dart'; import '../job.dart'; +/// 現在選択中の [AccessType] を保持する [StateProvider]. +final selectedAccessTypeStateProvider = StateProvider>( + (_) => [AccessType.busAvailable]); + final jobControllerProvider = Provider.autoDispose( (ref) => JobController( jobService: ref.watch(jobServiceProvider), diff --git a/packages/mottai_flutter_app/lib/job/ui/job_form.dart b/packages/mottai_flutter_app/lib/job/ui/job_form.dart index 46edfcd7..188d1393 100644 --- a/packages/mottai_flutter_app/lib/job/ui/job_form.dart +++ b/packages/mottai_flutter_app/lib/job/ui/job_form.dart @@ -131,6 +131,19 @@ class JobForm extends ConsumerWidget { choices: { for (final v in AccessType.values) v: v.label, }, + enableChoices: ref.watch(selectedAccessTypeStateProvider), + onChoiceSelected: (item) { + final selectedList = ref + .read(selectedAccessTypeStateProvider.notifier) + .state; + if (selectedList.contains(item)) { + selectedList.remove(item); + } else { + selectedList.add(item as AccessType); + } + ref.read(selectedAccessTypeStateProvider.notifier).state = + List.from(selectedList); + }, ), _TextInputSection( title: 'ひとこと', @@ -149,6 +162,10 @@ class JobForm extends ConsumerWidget { return; } + final accessTypes = ref + .watch(selectedAccessTypeStateProvider) + .toSet(); + if (jobId != null) { controller.updateJob( jobId: jobId, @@ -161,6 +178,7 @@ class JobForm extends ConsumerWidget { accessDiscriptionController.text, comment: commentController.text, imageFile: pickedImageFile, + accessTypes: accessTypes, ); } else { controller.create( @@ -173,7 +191,7 @@ class JobForm extends ConsumerWidget { accessDiscriptionController.text, comment: commentController.text, imageFile: pickedImageFile, - accessTypes: {AccessType.busAvailable}, + accessTypes: accessTypes, ); } }, @@ -200,9 +218,14 @@ class _TextInputSection extends StatelessWidget { this.maxLines, this.defaultDisplayLines = 1, this.choices, + this.enableChoices, + this.onChoiceSelected, this.controller, this.isRequired = false, - }); + }) : assert( + (choices != null) == (enableChoices != null), + 'choicesとenableChoicesのどちらかのみをnullにすることはできません。', + ); /// セクションのタイトル。 final String title; @@ -220,6 +243,12 @@ class _TextInputSection extends StatelessWidget { /// 選択された際の値がkeyで、表示する値がvalueの[Map]で受け取る。 final Map? choices; + /// [choices] の有効な値のリスト + final List? enableChoices; + + /// [choices] が選択された際のコールバック + final void Function(dynamic item)? onChoiceSelected; + /// テキストフィールドのコントローラー final TextEditingController? controller; @@ -258,7 +287,8 @@ class _TextInputSection extends StatelessWidget { SelectableChips( allItems: choices!.keys, labels: choices!, - enabledItems: choices!.keys, + enabledItems: enableChoices!, + onTap: onChoiceSelected, ) ], ), From 5a710618ec800326ff34cd69f1d84a56adfad20c Mon Sep 17 00:00:00 2001 From: RikitoNoto <56541594+RikitoNoto@users.noreply.github.com> Date: Mon, 14 Aug 2023 22:42:38 +0900 Subject: [PATCH 11/15] =?UTF-8?q?#119=20=E4=BA=A4=E9=80=9A=E6=89=8B?= =?UTF-8?q?=E6=AE=B5=E3=81=AE=E7=B7=A8=E9=9B=86=E5=87=A6=E7=90=86=E4=BD=9C?= =?UTF-8?q?=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lib/job/ui/job_controller.dart | 5 -- .../lib/job/ui/job_form.dart | 63 ++++++++++--------- 2 files changed, 35 insertions(+), 33 deletions(-) diff --git a/packages/mottai_flutter_app/lib/job/ui/job_controller.dart b/packages/mottai_flutter_app/lib/job/ui/job_controller.dart index ed6a6dd1..c373fc0f 100644 --- a/packages/mottai_flutter_app/lib/job/ui/job_controller.dart +++ b/packages/mottai_flutter_app/lib/job/ui/job_controller.dart @@ -7,10 +7,6 @@ import '../../auth/auth.dart'; import '../../image/firebase_storage.dart'; import '../job.dart'; -/// 現在選択中の [AccessType] を保持する [StateProvider]. -final selectedAccessTypeStateProvider = StateProvider>( - (_) => [AccessType.busAvailable]); - final jobControllerProvider = Provider.autoDispose( (ref) => JobController( jobService: ref.watch(jobServiceProvider), @@ -35,7 +31,6 @@ class JobController { final FirebaseStorageService _firebaseStorageService; final String? _userId; - //TODO: 必須入力のバリデーション /// [Job] の情報を作成する。 Future create({ required String title, diff --git a/packages/mottai_flutter_app/lib/job/ui/job_form.dart b/packages/mottai_flutter_app/lib/job/ui/job_form.dart index 188d1393..318fae6b 100644 --- a/packages/mottai_flutter_app/lib/job/ui/job_form.dart +++ b/packages/mottai_flutter_app/lib/job/ui/job_form.dart @@ -7,35 +7,50 @@ import '../../development/firebase_storage/firebase_storage.dart'; import '../../development/firebase_storage/ui/firebase_storage_controller.dart'; import 'job_controller.dart'; -class JobForm extends ConsumerWidget { - JobForm({ +class JobForm extends ConsumerStatefulWidget { + const JobForm({ this.job, super.key, }); final ReadJob? job; + + @override + JobFormState createState() => JobFormState(); +} + +class JobFormState extends ConsumerState { final formKey = GlobalKey(); + final List _selectedAccessTypes = []; @override - Widget build(BuildContext context, WidgetRef ref) { + void initState() { + super.initState(); + if (widget.job != null) { + _selectedAccessTypes.addAll(widget.job!.accessTypes.toList()); + } + } + + @override + Widget build(BuildContext context) { final firebaseStorageController = ref.watch(firebaseStorageControllerProvider); final pickedImageFile = ref.watch(pickedImageFileStateProvider); final controller = ref.watch(jobControllerProvider); - final titleController = TextEditingController(text: job?.title); - final locationController = TextEditingController(text: job?.place); - final contentController = TextEditingController(text: job?.content); - final belongingsController = TextEditingController(text: job?.belongings); - final rewardController = TextEditingController(text: job?.reward); + final titleController = TextEditingController(text: widget.job?.title); + final locationController = TextEditingController(text: widget.job?.place); + final contentController = TextEditingController(text: widget.job?.content); + final belongingsController = + TextEditingController(text: widget.job?.belongings); + final rewardController = TextEditingController(text: widget.job?.reward); final accessDiscriptionController = - TextEditingController(text: job?.accessDescription); - final commentController = TextEditingController(text: job?.comment); + TextEditingController(text: widget.job?.accessDescription); + final commentController = TextEditingController(text: widget.job?.comment); final imageUrlProvider = StateProvider.autoDispose( - (ref) => job?.imageUrl ?? '', + (ref) => widget.job?.imageUrl ?? '', ); - final jobId = job?.jobId; - + final jobId = widget.job?.jobId; late final Widget imageWidget; if (pickedImageFile != null) { @@ -131,18 +146,14 @@ class JobForm extends ConsumerWidget { choices: { for (final v in AccessType.values) v: v.label, }, - enableChoices: ref.watch(selectedAccessTypeStateProvider), + enableChoices: _selectedAccessTypes, onChoiceSelected: (item) { - final selectedList = ref - .read(selectedAccessTypeStateProvider.notifier) - .state; - if (selectedList.contains(item)) { - selectedList.remove(item); + if (_selectedAccessTypes.contains(item)) { + _selectedAccessTypes.remove(item); } else { - selectedList.add(item as AccessType); + _selectedAccessTypes.add(item as AccessType); } - ref.read(selectedAccessTypeStateProvider.notifier).state = - List.from(selectedList); + setState(() {}); }, ), _TextInputSection( @@ -162,10 +173,6 @@ class JobForm extends ConsumerWidget { return; } - final accessTypes = ref - .watch(selectedAccessTypeStateProvider) - .toSet(); - if (jobId != null) { controller.updateJob( jobId: jobId, @@ -178,7 +185,7 @@ class JobForm extends ConsumerWidget { accessDiscriptionController.text, comment: commentController.text, imageFile: pickedImageFile, - accessTypes: accessTypes, + accessTypes: _selectedAccessTypes.toSet(), ); } else { controller.create( @@ -191,7 +198,7 @@ class JobForm extends ConsumerWidget { accessDiscriptionController.text, comment: commentController.text, imageFile: pickedImageFile, - accessTypes: accessTypes, + accessTypes: _selectedAccessTypes.toSet(), ); } }, From a0a9dc15df92decb6aee42c9f7a31f2d28a7d801 Mon Sep 17 00:00:00 2001 From: RikitoNoto <56541594+RikitoNoto@users.noreply.github.com> Date: Mon, 14 Aug 2023 23:14:22 +0900 Subject: [PATCH 12/15] =?UTF-8?q?#119=20=E4=BA=A4=E9=80=9A=E6=89=8B?= =?UTF-8?q?=E6=AE=B5=E3=82=92=E7=B7=A8=E9=9B=86=E3=81=99=E3=82=8B=E3=81=A8?= =?UTF-8?q?=E3=80=81=E5=85=A5=E5=8A=9B=E4=B8=AD=E3=81=AE=E5=86=85=E5=AE=B9?= =?UTF-8?q?=E3=81=8C=E6=B6=88=E3=81=88=E3=82=8B=E4=B8=8D=E5=85=B7=E5=90=88?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lib/job/ui/job_form.dart | 91 +++++++++++-------- 1 file changed, 55 insertions(+), 36 deletions(-) diff --git a/packages/mottai_flutter_app/lib/job/ui/job_form.dart b/packages/mottai_flutter_app/lib/job/ui/job_form.dart index 318fae6b..7f5a3a29 100644 --- a/packages/mottai_flutter_app/lib/job/ui/job_form.dart +++ b/packages/mottai_flutter_app/lib/job/ui/job_form.dart @@ -20,12 +20,44 @@ class JobForm extends ConsumerStatefulWidget { } class JobFormState extends ConsumerState { + /// フォームのグローバルキー final formKey = GlobalKey(); + + /// 選択中のアクセスタイプ final List _selectedAccessTypes = []; + /// [Job.title] のテキストフィールド用コントローラー + late final TextEditingController _titleController; + + /// [Job.place] のテキストフィールド用コントローラー + late final TextEditingController _locationController; + + /// [Job.content] のテキストフィールド用コントローラー + late final TextEditingController _contentController; + + /// [Job.belongings] のテキストフィールド用コントローラー + late final TextEditingController _belongingsController; + + /// [Job.reward] のテキストフィールド用コントローラー + late final TextEditingController _rewardController; + + /// [Job.accessDescription] のテキストフィールド用コントローラー + late final TextEditingController _accessDiscriptionController; + + /// [Job.comment] のテキストフィールド用コントローラー + late final TextEditingController _commentController; + @override void initState() { super.initState(); + _titleController = TextEditingController(text: widget.job?.title); + _locationController = TextEditingController(text: widget.job?.place); + _contentController = TextEditingController(text: widget.job?.content); + _belongingsController = TextEditingController(text: widget.job?.belongings); + _rewardController = TextEditingController(text: widget.job?.reward); + _accessDiscriptionController = + TextEditingController(text: widget.job?.accessDescription); + _commentController = TextEditingController(text: widget.job?.comment); if (widget.job != null) { _selectedAccessTypes.addAll(widget.job!.accessTypes.toList()); } @@ -37,19 +69,6 @@ class JobFormState extends ConsumerState { ref.watch(firebaseStorageControllerProvider); final pickedImageFile = ref.watch(pickedImageFileStateProvider); final controller = ref.watch(jobControllerProvider); - final titleController = TextEditingController(text: widget.job?.title); - final locationController = TextEditingController(text: widget.job?.place); - final contentController = TextEditingController(text: widget.job?.content); - final belongingsController = - TextEditingController(text: widget.job?.belongings); - final rewardController = TextEditingController(text: widget.job?.reward); - final accessDiscriptionController = - TextEditingController(text: widget.job?.accessDescription); - final commentController = TextEditingController(text: widget.job?.comment); - - final imageUrlProvider = StateProvider.autoDispose( - (ref) => widget.job?.imageUrl ?? '', - ); final jobId = widget.job?.jobId; late final Widget imageWidget; @@ -66,11 +85,11 @@ class JobFormState extends ConsumerState { } // 画像が選択されている場合は画像を表示 // 選択されていない場合は画像アイコンを表示 - else if (ref.watch(imageUrlProvider) != '') { + else if (widget.job != null && widget.job?.imageUrl != '') { imageWidget = GenericImage.rectangle( onTap: firebaseStorageController.pickImageFromGallery, showDetailOnTap: false, - imageUrl: pickedImageFile?.path ?? ref.watch(imageUrlProvider), + imageUrl: pickedImageFile?.path ?? widget.job!.imageUrl, height: 300, width: null, ); @@ -107,14 +126,14 @@ class JobFormState extends ConsumerState { description: 'お手伝いのタイトルを最大2行程度で入力してください。', maxLines: 2, defaultDisplayLines: 2, - controller: titleController, + controller: _titleController, isRequired: true, ), _TextInputSection( title: 'お手伝いの場所', description: 'お手伝いを行う場所(農場や作業場所など)を入力してください。作業内容や曜日によって複数の場所の可能性がある場合は、それも入力してください。', - controller: locationController, + controller: _locationController, isRequired: true, ), _TextInputSection( @@ -122,27 +141,27 @@ class JobFormState extends ConsumerState { description: 'お手伝いの作業内容、作業時間帯やその他の情報をできるだけ詳しくを入力してください。お手伝い可能な曜日や時間帯、時期や季節が限られている場合や、その他に事前にお知らせするべき条件や情報などがあれば、その内容も入力してください。', defaultDisplayLines: 10, - controller: contentController, + controller: _contentController, isRequired: true, ), _TextInputSection( title: '持ち物', description: 'お手伝いに必要な服装や持ち物などを書いてください。特に必要ない場合や貸出を行う場合はその内容も入力してください。', - controller: belongingsController, + controller: _belongingsController, isRequired: true, ), _TextInputSection( title: '報酬', description: 'お手伝いをしてくれたワーカーにお渡しする報酬(食べ物など)を入力してください。', - controller: rewardController, + controller: _rewardController, isRequired: true, ), _TextInputSection( title: 'アクセス', description: 'お手伝いの場所までのアクセス方法について補足説明をしてください。最寄りの駅やバス停まで送迎ができる場合などは、その内容も入力してください。', - controller: accessDiscriptionController, + controller: _accessDiscriptionController, choices: { for (final v in AccessType.values) v: v.label, }, @@ -161,7 +180,7 @@ class JobFormState extends ConsumerState { description: 'お手伝いを検討してくれるワーカーの方が、ぜひお手伝いをしてみたくなるようひとことや、募集するお手伝いの魅力を入力しましょう!', defaultDisplayLines: 5, - controller: commentController, + controller: _commentController, ), Padding( padding: const EdgeInsets.only(top: 16, bottom: 32), @@ -176,27 +195,27 @@ class JobFormState extends ConsumerState { if (jobId != null) { controller.updateJob( jobId: jobId, - title: titleController.text, - place: locationController.text, - content: contentController.text, - belongings: belongingsController.text, - reward: rewardController.text, + title: _titleController.text, + place: _locationController.text, + content: _contentController.text, + belongings: _belongingsController.text, + reward: _rewardController.text, accessDescription: - accessDiscriptionController.text, - comment: commentController.text, + _accessDiscriptionController.text, + comment: _commentController.text, imageFile: pickedImageFile, accessTypes: _selectedAccessTypes.toSet(), ); } else { controller.create( - title: titleController.text, - place: locationController.text, - content: contentController.text, - belongings: belongingsController.text, - reward: rewardController.text, + title: _titleController.text, + place: _locationController.text, + content: _contentController.text, + belongings: _belongingsController.text, + reward: _rewardController.text, accessDescription: - accessDiscriptionController.text, - comment: commentController.text, + _accessDiscriptionController.text, + comment: _commentController.text, imageFile: pickedImageFile, accessTypes: _selectedAccessTypes.toSet(), ); From 614a527b448f6fa6fff84c6abb665e14c1a6fda7 Mon Sep 17 00:00:00 2001 From: RikitoNoto <56541594+RikitoNoto@users.noreply.github.com> Date: Tue, 15 Aug 2023 15:15:45 +0900 Subject: [PATCH 13/15] =?UTF-8?q?#119=20lint=E5=AF=BE=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../job.flutterfire_gen.dart | 2 -- .../lib/src/firestore_repositories/job.dart | 2 -- .../lib/job/ui/job_create.dart | 2 +- .../lib/job/ui/job_form.dart | 21 ++++++++++--------- 4 files changed, 12 insertions(+), 15 deletions(-) diff --git a/packages/firebase_common/lib/src/firestore_documents/job.flutterfire_gen.dart b/packages/firebase_common/lib/src/firestore_documents/job.flutterfire_gen.dart index ae344619..51a4e2ca 100644 --- a/packages/firebase_common/lib/src/firestore_documents/job.flutterfire_gen.dart +++ b/packages/firebase_common/lib/src/firestore_documents/job.flutterfire_gen.dart @@ -18,7 +18,6 @@ class ReadJob { required this.accessDescription, required this.accessTypes, required this.comment, - required this.imageUrl, required this.createdAt, required this.updatedAt, }); @@ -138,7 +137,6 @@ class UpdateJob { this.accessDescription, this.accessTypes, this.comment, - this.imageUrl, this.createdAt, }); diff --git a/packages/firebase_common/lib/src/firestore_repositories/job.dart b/packages/firebase_common/lib/src/firestore_repositories/job.dart index 3d9eacfa..8139e0d2 100644 --- a/packages/firebase_common/lib/src/firestore_repositories/job.dart +++ b/packages/firebase_common/lib/src/firestore_repositories/job.dart @@ -1,5 +1,3 @@ -import 'package:cloud_firestore/cloud_firestore.dart'; - import '../firestore_documents/job.dart'; class JobRepository { diff --git a/packages/mottai_flutter_app/lib/job/ui/job_create.dart b/packages/mottai_flutter_app/lib/job/ui/job_create.dart index 0aa515c0..1b52991a 100644 --- a/packages/mottai_flutter_app/lib/job/ui/job_create.dart +++ b/packages/mottai_flutter_app/lib/job/ui/job_create.dart @@ -20,7 +20,7 @@ class JobCreatePage extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { return Scaffold( appBar: AppBar(title: const Text('お手伝い募集内容を入力')), - body: JobForm(), + body: const JobForm(), ); } } diff --git a/packages/mottai_flutter_app/lib/job/ui/job_form.dart b/packages/mottai_flutter_app/lib/job/ui/job_form.dart index 7f5a3a29..8779a719 100644 --- a/packages/mottai_flutter_app/lib/job/ui/job_form.dart +++ b/packages/mottai_flutter_app/lib/job/ui/job_form.dart @@ -131,23 +131,24 @@ class JobFormState extends ConsumerState { ), _TextInputSection( title: 'お手伝いの場所', - description: - 'お手伝いを行う場所(農場や作業場所など)を入力してください。作業内容や曜日によって複数の場所の可能性がある場合は、それも入力してください。', + description: 'お手伝いを行う場所(農場や作業場所など)を入力してください。' + '作業内容や曜日によって複数の場所の可能性がある場合は、それも入力してください。', controller: _locationController, isRequired: true, ), _TextInputSection( title: 'お手伝いの内容', - description: - 'お手伝いの作業内容、作業時間帯やその他の情報をできるだけ詳しくを入力してください。お手伝い可能な曜日や時間帯、時期や季節が限られている場合や、その他に事前にお知らせするべき条件や情報などがあれば、その内容も入力してください。', + description: 'お手伝いの作業内容、作業時間帯やその他の情報をできるだけ詳しくを入力してください。' + 'お手伝い可能な曜日や時間帯、時期や季節が限られている場合や、' + 'その他に事前にお知らせするべき条件や情報などがあれば、その内容も入力してください。', defaultDisplayLines: 10, controller: _contentController, isRequired: true, ), _TextInputSection( title: '持ち物', - description: - 'お手伝いに必要な服装や持ち物などを書いてください。特に必要ない場合や貸出を行う場合はその内容も入力してください。', + description: 'お手伝いに必要な服装や持ち物などを書いてください。' + '特に必要ない場合や貸出を行う場合はその内容も入力してください。', controller: _belongingsController, isRequired: true, ), @@ -159,8 +160,8 @@ class JobFormState extends ConsumerState { ), _TextInputSection( title: 'アクセス', - description: - 'お手伝いの場所までのアクセス方法について補足説明をしてください。最寄りの駅やバス停まで送迎ができる場合などは、その内容も入力してください。', + description: 'お手伝いの場所までのアクセス方法について補足説明をしてください。' + '最寄りの駅やバス停まで送迎ができる場合などは、その内容も入力してください。', controller: _accessDiscriptionController, choices: { for (final v in AccessType.values) v: v.label, @@ -177,8 +178,8 @@ class JobFormState extends ConsumerState { ), _TextInputSection( title: 'ひとこと', - description: - 'お手伝いを検討してくれるワーカーの方が、ぜひお手伝いをしてみたくなるようひとことや、募集するお手伝いの魅力を入力しましょう!', + description: 'お手伝いを検討してくれるワーカーの方が、ぜひお手伝いをしてみたくなるようひとことや、' + '募集するお手伝いの魅力を入力しましょう!', defaultDisplayLines: 5, controller: _commentController, ), From d61f377b7fb48476d27ec58bf5976b4c885ee78f Mon Sep 17 00:00:00 2001 From: Kosuke Saigusa Date: Tue, 15 Aug 2023 21:14:55 +0900 Subject: [PATCH 14/15] chore: add doc comment --- packages/mottai_flutter_app/lib/job/job.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/mottai_flutter_app/lib/job/job.dart b/packages/mottai_flutter_app/lib/job/job.dart index 3f80d352..abe5b85b 100644 --- a/packages/mottai_flutter_app/lib/job/job.dart +++ b/packages/mottai_flutter_app/lib/job/job.dart @@ -105,6 +105,8 @@ class JobService { comment: comment, imageUrl: imageUrl, ); + + /// 指定したユーザーの [Job] を全件取得する。 Future> fetchUserJobs({required String hostId}) => _jobRepository.fetchUserJobs(hostId: hostId); From 7e4eb16ec3f5ad5ef5b436ab5ab091e717a2c88b Mon Sep 17 00:00:00 2001 From: Kosuke Saigusa Date: Tue, 15 Aug 2023 22:15:33 +0900 Subject: [PATCH 15/15] refactor: refactor job update/create related codes --- .../ui/development_items.dart | 4 +- .../lib/job/ui/job_controller.dart | 25 +-- .../lib/job/ui/job_create.dart | 10 +- .../lib/job/ui/job_detail.dart | 44 ++-- .../lib/job/ui/job_form.dart | 192 ++++++++++-------- .../lib/job/ui/job_update.dart | 45 ++-- 6 files changed, 175 insertions(+), 145 deletions(-) diff --git a/packages/mottai_flutter_app/lib/development/development_items/ui/development_items.dart b/packages/mottai_flutter_app/lib/development/development_items/ui/development_items.dart index e5299ee8..d73cd0ec 100644 --- a/packages/mottai_flutter_app/lib/development/development_items/ui/development_items.dart +++ b/packages/mottai_flutter_app/lib/development/development_items/ui/development_items.dart @@ -64,9 +64,7 @@ class DevelopmentItemsPage extends ConsumerWidget { ), ListTile( title: const Text('仕事情報作成ページ'), - onTap: () => context.router.pushNamed( - JobCreatePage.location(), - ), + onTap: () => context.router.pushNamed(JobCreatePage.location), ), ListTile( title: const Text('仕事情報編集ページ'), diff --git a/packages/mottai_flutter_app/lib/job/ui/job_controller.dart b/packages/mottai_flutter_app/lib/job/ui/job_controller.dart index c373fc0f..7c3e3e35 100644 --- a/packages/mottai_flutter_app/lib/job/ui/job_controller.dart +++ b/packages/mottai_flutter_app/lib/job/ui/job_controller.dart @@ -3,15 +3,15 @@ import 'dart:io'; import 'package:firebase_common/firebase_common.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import '../../auth/auth.dart'; import '../../image/firebase_storage.dart'; import '../job.dart'; -final jobControllerProvider = Provider.autoDispose( - (ref) => JobController( +final jobControllerProvider = + Provider.family.autoDispose( + (ref, userId) => JobController( jobService: ref.watch(jobServiceProvider), firebaseStorageService: ref.watch(firebaseStorageServiceProvider), - userId: ref.watch(userIdProvider), + userId: userId, ), ); @@ -19,7 +19,7 @@ class JobController { const JobController({ required JobService jobService, required FirebaseStorageService firebaseStorageService, - String? userId, + required String userId, }) : _jobService = jobService, _firebaseStorageService = firebaseStorageService, _userId = userId; @@ -29,10 +29,11 @@ class JobController { final JobService _jobService; final FirebaseStorageService _firebaseStorageService; - final String? _userId; + final String _userId; /// [Job] の情報を作成する。 Future create({ + required String hostId, required String title, required String content, required String place, @@ -43,17 +44,12 @@ class JobController { required String comment, File? imageFile, }) async { - final userId = _userId; - if (userId == null) { - return; - } - var imageUrl = ''; if (imageFile != null) { imageUrl = await _uploadImage(imageFile); } await _jobService.create( - hostId: userId, + hostId: hostId, title: title, content: content, place: place, @@ -79,11 +75,6 @@ class JobController { String? comment, File? imageFile, }) async { - final userId = _userId; - if (userId == null) { - return; - } - String? imageUrl; if (imageFile != null) { imageUrl = await _uploadImage(imageFile); diff --git a/packages/mottai_flutter_app/lib/job/ui/job_create.dart b/packages/mottai_flutter_app/lib/job/ui/job_create.dart index 1b52991a..08488a74 100644 --- a/packages/mottai_flutter_app/lib/job/ui/job_create.dart +++ b/packages/mottai_flutter_app/lib/job/ui/job_create.dart @@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import '../../auth/ui/auth_dependent_builder.dart'; import 'job_form.dart'; /// 仕事情報更新ページ。 @@ -14,13 +15,18 @@ class JobCreatePage extends ConsumerWidget { static const path = '/jobs/create'; /// [JobCreatePage] に遷移する際に `context.router.pushNamed` で指定する文字列。 - static String location() => '/jobs/create'; + static const location = path; @override Widget build(BuildContext context, WidgetRef ref) { return Scaffold( appBar: AppBar(title: const Text('お手伝い募集内容を入力')), - body: const JobForm(), + // TODO:なるんさんが実装中の「本人かどうかビルダー」に後で書き換える。 + body: AuthDependentBuilder( + onAuthenticated: (hostId) { + return JobForm.create(hostId: hostId); + }, + ), ); } } diff --git a/packages/mottai_flutter_app/lib/job/ui/job_detail.dart b/packages/mottai_flutter_app/lib/job/ui/job_detail.dart index 4cc00190..2bed5e87 100644 --- a/packages/mottai_flutter_app/lib/job/ui/job_detail.dart +++ b/packages/mottai_flutter_app/lib/job/ui/job_detail.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import '../../auth/auth.dart'; import '../../auth/ui/auth_dependent_builder.dart'; import '../../chat/chat_rooms.dart'; import '../../chat/ui/chat_room.dart'; @@ -12,6 +13,7 @@ import '../../scaffold_messenger_controller.dart'; import '../../user/host.dart'; import '../../user/user_mode.dart'; import '../job.dart'; +import 'job_update.dart'; import 'start_chat_with_host_controller.dart'; /// 仕事詳細ページ。 @@ -32,16 +34,28 @@ class JobDetailPage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final userId = ref.watch(userIdProvider); + final jobAsyncValue = ref.watch(jobFutureProvider(jobId)); + final isLoading = jobAsyncValue.isLoading; + final job = ref.watch(jobFutureProvider(jobId)).valueOrNull; return Scaffold( - appBar: AppBar(title: const Text('お手伝い募集')), - body: ref.watch(jobFutureProvider(jobId)).when( - data: (job) { - if (job == null) { - return const Center( - child: Text('お手伝いの情報が見つかりませんでした。'), - ); - } - return ref.watch(hostFutureProvider(job.hostId)).when( + appBar: AppBar( + title: const Text('お手伝い募集'), + // TODO:なるんさんが実装中の「本人かどうかビルダー」に後で書き換える。 + actions: [ + if (!isLoading && userId != null && job != null) + IconButton( + onPressed: () => context.router + .pushNamed(JobUpdatePage.location(jobId: jobId)), + icon: const Icon(Icons.edit), + ), + ], + ), + body: isLoading + ? const Center(child: CircularProgressIndicator()) + : job == null + ? const Text('お手伝い募集の情報が見つかりませんでした。') + : ref.watch(hostFutureProvider(job.hostId)).when( data: (readHost) { if (readHost == null) { return const Center( @@ -52,16 +66,10 @@ class JobDetailPage extends ConsumerWidget { }, loading: () => const Center(child: CircularProgressIndicator()), - error: (error, stackTrace) => const Center( - child: Text('通信に失敗しました。'), + error: (_, __) => const Center( + child: Text('ホスト情報の取得に失敗しました。'), ), - ); - }, - loading: () => const Center(child: CircularProgressIndicator()), - error: (error, stackTrace) => const Center( - child: Text('お手伝いの情報の取得に失敗しました。'), - ), - ), + ), ); } } diff --git a/packages/mottai_flutter_app/lib/job/ui/job_form.dart b/packages/mottai_flutter_app/lib/job/ui/job_form.dart index 8779a719..9963881a 100644 --- a/packages/mottai_flutter_app/lib/job/ui/job_form.dart +++ b/packages/mottai_flutter_app/lib/job/ui/job_form.dart @@ -1,19 +1,34 @@ import 'package:dart_flutter_common/dart_flutter_common.dart'; import 'package:firebase_common/firebase_common.dart'; import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import '../../development/firebase_storage/firebase_storage.dart'; import '../../development/firebase_storage/ui/firebase_storage_controller.dart'; import 'job_controller.dart'; +/// - `create` の場合、ログイン済みの `hostId`(ユーザー ID) +/// - `update` の場合、更新対象の [Job] とその本人であることが確認された `hostId`(ユーザー ID) +/// +/// を受け取り、それに応じた [Job] の作成または更新を行うフォーム。 class JobForm extends ConsumerStatefulWidget { - const JobForm({ - this.job, + const JobForm.create({ + required String hostId, super.key, - }); + }) : _hostId = hostId, + _job = null; + + const JobForm.update({ + required String hostId, + required ReadJob job, + super.key, + }) : _hostId = hostId, + _job = job; + + final ReadJob? _job; - final ReadJob? job; + final String _hostId; @override JobFormState createState() => JobFormState(); @@ -42,24 +57,28 @@ class JobFormState extends ConsumerState { late final TextEditingController _rewardController; /// [Job.accessDescription] のテキストフィールド用コントローラー - late final TextEditingController _accessDiscriptionController; + late final TextEditingController _accessDescriptionController; /// [Job.comment] のテキストフィールド用コントローラー late final TextEditingController _commentController; + /// 画像の高さ。 + static const double _imageHeight = 300; + @override void initState() { super.initState(); - _titleController = TextEditingController(text: widget.job?.title); - _locationController = TextEditingController(text: widget.job?.place); - _contentController = TextEditingController(text: widget.job?.content); - _belongingsController = TextEditingController(text: widget.job?.belongings); - _rewardController = TextEditingController(text: widget.job?.reward); - _accessDiscriptionController = - TextEditingController(text: widget.job?.accessDescription); - _commentController = TextEditingController(text: widget.job?.comment); - if (widget.job != null) { - _selectedAccessTypes.addAll(widget.job!.accessTypes.toList()); + _titleController = TextEditingController(text: widget._job?.title); + _locationController = TextEditingController(text: widget._job?.place); + _contentController = TextEditingController(text: widget._job?.content); + _belongingsController = + TextEditingController(text: widget._job?.belongings); + _rewardController = TextEditingController(text: widget._job?.reward); + _accessDescriptionController = + TextEditingController(text: widget._job?.accessDescription); + _commentController = TextEditingController(text: widget._job?.comment); + if (widget._job != null) { + _selectedAccessTypes.addAll(widget._job!.accessTypes.toList()); } } @@ -68,54 +87,44 @@ class JobFormState extends ConsumerState { final firebaseStorageController = ref.watch(firebaseStorageControllerProvider); final pickedImageFile = ref.watch(pickedImageFileStateProvider); - final controller = ref.watch(jobControllerProvider); - final jobId = widget.job?.jobId; - late final Widget imageWidget; - - if (pickedImageFile != null) { - imageWidget = GestureDetector( - onTap: firebaseStorageController.pickImageFromGallery, - child: SizedBox( - height: 300, - child: Center( - child: Image.file(pickedImageFile), - ), - ), - ); - } - // 画像が選択されている場合は画像を表示 - // 選択されていない場合は画像アイコンを表示 - else if (widget.job != null && widget.job?.imageUrl != '') { - imageWidget = GenericImage.rectangle( - onTap: firebaseStorageController.pickImageFromGallery, - showDetailOnTap: false, - imageUrl: pickedImageFile?.path ?? widget.job!.imageUrl, - height: 300, - width: null, - ); - } else { - imageWidget = GestureDetector( - onTap: firebaseStorageController.pickImageFromGallery, - child: Container( - height: 300, - decoration: BoxDecoration( - border: Border.all(color: Colors.black38), - ), - child: const Center(child: Icon(Icons.image)), - ), - ); - } - + final controller = ref.watch(jobControllerProvider(widget._hostId)); + final jobId = widget._job?.jobId; return SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - imageWidget, - Padding( - padding: const EdgeInsets.symmetric( - vertical: 8, - horizontal: 24, + if (pickedImageFile != null) + GestureDetector( + onTap: firebaseStorageController.pickImageFromGallery, + child: SizedBox( + height: _imageHeight, + child: Center( + child: Image.file(pickedImageFile), + ), + ), + ) + else if ((widget._job?.imageUrl ?? '').isNotEmpty) + GenericImage.rectangle( + onTap: firebaseStorageController.pickImageFromGallery, + showDetailOnTap: false, + imageUrl: pickedImageFile?.path ?? widget._job!.imageUrl, + height: _imageHeight, + width: null, + ) + else + GestureDetector( + onTap: firebaseStorageController.pickImageFromGallery, + child: Container( + height: _imageHeight, + decoration: BoxDecoration( + border: Border.all(color: Colors.black38), + ), + child: const Center(child: Icon(Icons.image)), + ), ), + const Gap(32), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), child: Form( key: formKey, child: Column( @@ -158,20 +167,20 @@ class JobFormState extends ConsumerState { controller: _rewardController, isRequired: true, ), - _TextInputSection( + _TextInputSection.withChoice( title: 'アクセス', description: 'お手伝いの場所までのアクセス方法について補足説明をしてください。' '最寄りの駅やバス停まで送迎ができる場合などは、その内容も入力してください。', - controller: _accessDiscriptionController, + controller: _accessDescriptionController, choices: { for (final v in AccessType.values) v: v.label, }, - enableChoices: _selectedAccessTypes, + enabledChoices: _selectedAccessTypes, onChoiceSelected: (item) { if (_selectedAccessTypes.contains(item)) { _selectedAccessTypes.remove(item); } else { - _selectedAccessTypes.add(item as AccessType); + _selectedAccessTypes.add(item); } setState(() {}); }, @@ -202,20 +211,21 @@ class JobFormState extends ConsumerState { belongings: _belongingsController.text, reward: _rewardController.text, accessDescription: - _accessDiscriptionController.text, + _accessDescriptionController.text, comment: _commentController.text, imageFile: pickedImageFile, accessTypes: _selectedAccessTypes.toSet(), ); } else { controller.create( + hostId: widget._hostId, title: _titleController.text, place: _locationController.text, content: _contentController.text, belongings: _belongingsController.text, reward: _rewardController.text, accessDescription: - _accessDiscriptionController.text, + _accessDescriptionController.text, comment: _commentController.text, imageFile: pickedImageFile, accessTypes: _selectedAccessTypes.toSet(), @@ -236,23 +246,33 @@ class JobFormState extends ConsumerState { } } -/// タイトルと説明、テキストフィールドからなるセクション -/// [Section]を使用し、contentにフィールドを与えている -class _TextInputSection extends StatelessWidget { +/// タイトルと説明、テキストフィールドからなるセクション。 +/// [Section] を使用し、`Section.content` にフィールドを与えている。 +class _TextInputSection extends StatelessWidget { + /// テキスト入力のみをさせる通常の [_TextInputSection] を作成する。 const _TextInputSection({ required this.title, this.description, this.maxLines, this.defaultDisplayLines = 1, - this.choices, - this.enableChoices, - this.onChoiceSelected, this.controller, this.isRequired = false, - }) : assert( - (choices != null) == (enableChoices != null), - 'choicesとenableChoicesのどちらかのみをnullにすることはできません。', - ); + }) : choices = const {}, + enabledChoices = const [], + onChoiceSelected = null; + + /// テキスト入力と選択肢を併せて入力させる [_TextInputSection] を作成する。 + const _TextInputSection.withChoice({ + required this.title, + this.description, + this.maxLines, + this.defaultDisplayLines = 1, + required this.choices, + required this.enabledChoices, + required this.onChoiceSelected, + this.controller, + this.isRequired = false, + }); /// セクションのタイトル。 final String title; @@ -268,13 +288,13 @@ class _TextInputSection extends StatelessWidget { /// テキストフィールドの下に表示する選択肢 /// 選択された際の値がkeyで、表示する値がvalueの[Map]で受け取る。 - final Map? choices; + final Map choices; /// [choices] の有効な値のリスト - final List? enableChoices; + final List enabledChoices; /// [choices] が選択された際のコールバック - final void Function(dynamic item)? onChoiceSelected; + final void Function(T item)? onChoiceSelected; /// テキストフィールドのコントローラー final TextEditingController? controller; @@ -295,12 +315,13 @@ class _TextInputSection extends StatelessWidget { descriptionStyle: Theme.of(context).textTheme.bodyMedium, sectionPadding: const EdgeInsets.only(bottom: 32), content: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ TextFormField( controller: controller, maxLines: maxLines, validator: (value) { - if (isRequired && value == '') { + if (isRequired && (value ?? '').isEmpty) { return '入力してください。'; } return null; @@ -310,13 +331,16 @@ class _TextInputSection extends StatelessWidget { border: const OutlineInputBorder(), ), ), - if (choices != null) - SelectableChips( - allItems: choices!.keys, - labels: choices!, - enabledItems: enableChoices!, + if (choices.isNotEmpty) ...[ + const Gap(16), + SelectableChips( + allItems: choices.keys, + labels: choices, + runSpacing: 8, + enabledItems: enabledChoices, onTap: onChoiceSelected, - ) + ), + ], ], ), ); diff --git a/packages/mottai_flutter_app/lib/job/ui/job_update.dart b/packages/mottai_flutter_app/lib/job/ui/job_update.dart index d02157ca..fbc7a6e3 100644 --- a/packages/mottai_flutter_app/lib/job/ui/job_update.dart +++ b/packages/mottai_flutter_app/lib/job/ui/job_update.dart @@ -2,7 +2,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import '../../auth/auth.dart'; +import '../../auth/ui/auth_dependent_builder.dart'; import '../job.dart'; import 'job_form.dart'; @@ -25,27 +25,30 @@ class JobUpdatePage extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { return Scaffold( appBar: AppBar(title: const Text('お手伝い募集内容を入力')), - // Jobの読み込み状態によって表示を変更 - body: ref.watch(jobFutureProvider(jobId)).when( - data: (job) { - if (job == null) { - return const Center(child: Text('お手伝いが存在していません。')); - } - // UrlのhostIdとログイン中のユーザーのhostIdが違う場合 - if (job.hostId != ref.watch(userIdProvider)) { - return const Center( - child: Text('編集の権限がありません。'), - ); - } - return JobForm( - job: job, + // TODO:なるんさんが実装中の「本人かどうかビルダー」に後で書き換える。 + // すると 「編集の権限がありません。」のチェックはその時点で済んでいることになる。 + body: AuthDependentBuilder( + onAuthenticated: (hostId) { + return ref.watch(jobFutureProvider(jobId)).when( + data: (job) { + if (job == null) { + return const Center(child: Text('お手伝いが存在していません。')); + } + // UrlのhostIdとログイン中のユーザーのhostIdが違う場合 + if (job.hostId != hostId) { + return const Center( + child: Text('編集の権限がありません。'), + ); + } + return JobForm.update(hostId: hostId, job: job); + }, + loading: () => const Center(child: CircularProgressIndicator()), + error: (_, __) => const Center( + child: Text('仕事情報が取得できませんでした。'), + ), ); - }, - loading: () => const Center(child: CircularProgressIndicator()), - error: (error, stackTrace) => const Center( - child: Text('仕事情報が取得できませんでした。'), - ), - ), + }, + ), ); } }