♻️ Make bot management into sheet

This commit is contained in:
2025-11-02 21:04:35 +08:00
parent 7497b77384
commit da2d3f7f17
4 changed files with 340 additions and 246 deletions

View File

@@ -5,8 +5,11 @@ import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/bot.dart'; import 'package:island/models/bot.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/screens/developers/edit_bot.dart';
import 'package:island/screens/developers/new_bot.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:island/widgets/response.dart'; import 'package:island/widgets/response.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -48,12 +51,18 @@ class BotsScreen extends HookConsumerWidget {
const SizedBox(height: 16), const SizedBox(height: 16),
ElevatedButton.icon( ElevatedButton.icon(
onPressed: () { onPressed: () {
context.pushNamed( showModalBottomSheet(
'developerBotNew', context: context,
pathParameters: { isScrollControlled: true,
'name': publisherName, builder:
'projectId': projectId, (context) => SheetScaffold(
}, titleText: 'createBot'.tr(),
child: NewBotScreen(
publisherName: publisherName,
projectId: projectId,
isModal: true,
),
),
); );
}, },
icon: const Icon(Symbols.add), icon: const Icon(Symbols.add),
@@ -74,12 +83,18 @@ class BotsScreen extends HookConsumerWidget {
title: Text('bots').tr().padding(horizontal: 8), title: Text('bots').tr().padding(horizontal: 8),
trailing: IconButton( trailing: IconButton(
onPressed: () { onPressed: () {
context.pushNamed( showModalBottomSheet(
'developerBotNew', context: context,
pathParameters: { isScrollControlled: true,
'name': publisherName, builder:
'projectId': projectId, (context) => SheetScaffold(
}, titleText: 'createBot'.tr(),
child: NewBotScreen(
publisherName: publisherName,
projectId: projectId,
isModal: true,
),
),
); );
}, },
icon: const Icon(Symbols.add), icon: const Icon(Symbols.add),
@@ -93,7 +108,6 @@ class BotsScreen extends HookConsumerWidget {
itemBuilder: (context, index) { itemBuilder: (context, index) {
final bot = data[index]; final bot = data[index];
return Card( return Card(
margin: const EdgeInsets.all(8.0),
child: ListTile( child: ListTile(
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(8.0)), borderRadius: BorderRadius.all(Radius.circular(8.0)),
@@ -140,13 +154,19 @@ class BotsScreen extends HookConsumerWidget {
], ],
onSelected: (value) { onSelected: (value) {
if (value == 'edit') { if (value == 'edit') {
context.pushNamed( showModalBottomSheet(
'developerBotEdit', context: context,
pathParameters: { isScrollControlled: true,
'name': publisherName, builder:
'projectId': projectId, (context) => SheetScaffold(
'id': bot.id, titleText: 'editBot'.tr(),
}, child: EditBotScreen(
publisherName: publisherName,
projectId: projectId,
id: bot.id,
isModal: true,
),
),
); );
} else if (value == 'delete') { } else if (value == 'delete') {
showConfirmAlert( showConfirmAlert(

View File

@@ -38,11 +38,13 @@ class EditBotScreen extends HookConsumerWidget {
final String publisherName; final String publisherName;
final String projectId; final String projectId;
final String? id; final String? id;
final bool isModal;
const EditBotScreen({ const EditBotScreen({
super.key, super.key,
required this.publisherName, required this.publisherName,
required this.projectId, required this.projectId,
this.id, this.id,
this.isModal = false,
}); });
@override @override
@@ -191,230 +193,293 @@ class EditBotScreen extends HookConsumerWidget {
} }
} }
final bodyContent =
botData == null && !isNew
? const Center(child: CircularProgressIndicator())
: botData?.hasError == true && !isNew
? ResponseErrorWidget(
error: botData!.error,
onRetry:
() => ref.invalidate(
botProvider(publisherName, projectId, id!),
),
)
: SingleChildScrollView(
child: Column(
children: [
AspectRatio(
aspectRatio: 16 / 7,
child: Stack(
clipBehavior: Clip.none,
fit: StackFit.expand,
children: [
GestureDetector(
child: Container(
color:
Theme.of(
context,
).colorScheme.surfaceContainerHigh,
child:
background.value != null
? CloudFileWidget(
item: background.value!,
fit: BoxFit.cover,
)
: const SizedBox.shrink(),
),
onTap: () {
setPicture('background');
},
),
Positioned(
left: 20,
bottom: -32,
child: GestureDetector(
child: ProfilePictureWidget(
fileId: picture.value?.id,
radius: 40,
fallbackIcon: Symbols.smart_toy,
),
onTap: () {
setPicture('picture');
},
),
),
],
),
).padding(bottom: 32),
Form(
key: formKey,
child: Column(
children: [
TextFormField(
controller: nameController,
decoration: InputDecoration(
labelText: 'name'.tr(),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
),
),
const SizedBox(height: 16),
TextFormField(
controller: nickController,
decoration: InputDecoration(
labelText: 'nickname'.tr(),
alignLabelWithHint: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
),
),
const SizedBox(height: 16),
TextFormField(
controller: slugController,
decoration: InputDecoration(
labelText: 'slug'.tr(),
helperText: 'slugHint'.tr(),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
),
),
const SizedBox(height: 16),
TextFormField(
controller: bioController,
decoration: InputDecoration(
labelText: 'bio'.tr(),
alignLabelWithHint: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
),
maxLines: 3,
),
const SizedBox(height: 16),
Row(
spacing: 16,
children: [
Expanded(
child: TextFormField(
controller: firstNameController,
decoration: InputDecoration(
labelText: 'firstName'.tr(),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
),
),
),
Expanded(
child: TextFormField(
controller: middleNameController,
decoration: InputDecoration(
labelText: 'middleName'.tr(),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
),
),
),
Expanded(
child: TextFormField(
controller: lastNameController,
decoration: InputDecoration(
labelText: 'lastName'.tr(),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
),
),
),
],
),
const SizedBox(height: 16),
Row(
spacing: 16,
children: [
Expanded(
child: TextFormField(
controller: genderController,
decoration: InputDecoration(
labelText: 'gender'.tr(),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
),
),
),
Expanded(
child: TextFormField(
controller: pronounsController,
decoration: InputDecoration(
labelText: 'pronouns'.tr(),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
),
),
),
],
),
const SizedBox(height: 16),
Row(
spacing: 16,
children: [
Expanded(
child: TextFormField(
controller: locationController,
decoration: InputDecoration(
labelText: 'location'.tr(),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
),
),
),
Expanded(
child: TextFormField(
controller: timeZoneController,
decoration: InputDecoration(
labelText: 'timeZone'.tr(),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
),
),
),
],
),
const SizedBox(height: 16),
GestureDetector(
onTap: () async {
final date = await showDatePicker(
context: context,
initialDate: birthday.value ?? DateTime.now(),
firstDate: DateTime(1900),
lastDate: DateTime.now(),
);
if (date != null) {
birthday.value = date;
}
},
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).dividerColor,
width: 1,
),
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'birthday'.tr(),
style: TextStyle(
color: Theme.of(context).hintColor,
),
),
Text(
birthday.value != null
? DateFormat.yMMMd().format(
birthday.value!,
)
: 'Select a date'.tr(),
),
],
),
),
),
const SizedBox(height: 16),
Align(
alignment: Alignment.centerRight,
child: TextButton.icon(
onPressed: submitting.value ? null : performAction,
label: Text('saveChanges').tr(),
icon: const Icon(Symbols.save),
),
),
],
).padding(all: 24),
),
],
),
);
if (isModal) {
return bodyContent;
}
return AppScaffold( return AppScaffold(
isNoBackground: false, isNoBackground: false,
appBar: AppBar(title: Text(isNew ? 'createBot'.tr() : 'editBot'.tr())), appBar: AppBar(title: Text(isNew ? 'createBot'.tr() : 'editBot'.tr())),
body: body: bodyContent,
botData == null && !isNew
? const Center(child: CircularProgressIndicator())
: botData?.hasError == true && !isNew
? ResponseErrorWidget(
error: botData!.error,
onRetry:
() => ref.invalidate(
botProvider(publisherName, projectId, id!),
),
)
: SingleChildScrollView(
child: Column(
children: [
AspectRatio(
aspectRatio: 16 / 7,
child: Stack(
clipBehavior: Clip.none,
fit: StackFit.expand,
children: [
GestureDetector(
child: Container(
color:
Theme.of(
context,
).colorScheme.surfaceContainerHigh,
child:
background.value != null
? CloudFileWidget(
item: background.value!,
fit: BoxFit.cover,
)
: const SizedBox.shrink(),
),
onTap: () {
setPicture('background');
},
),
Positioned(
left: 20,
bottom: -32,
child: GestureDetector(
child: ProfilePictureWidget(
fileId: picture.value?.id,
radius: 40,
fallbackIcon: Symbols.smart_toy,
),
onTap: () {
setPicture('picture');
},
),
),
],
),
).padding(bottom: 32),
Form(
key: formKey,
child: Column(
children: [
TextFormField(
controller: nameController,
decoration: InputDecoration(labelText: 'name'.tr()),
),
const SizedBox(height: 16),
TextFormField(
controller: nickController,
decoration: InputDecoration(
labelText: 'nickname'.tr(),
alignLabelWithHint: true,
),
),
const SizedBox(height: 16),
TextFormField(
controller: slugController,
decoration: InputDecoration(
labelText: 'slug'.tr(),
helperText: 'slugHint'.tr(),
),
),
const SizedBox(height: 16),
TextFormField(
controller: bioController,
decoration: InputDecoration(
labelText: 'bio'.tr(),
alignLabelWithHint: true,
),
maxLines: 3,
),
const SizedBox(height: 16),
Row(
spacing: 16,
children: [
Expanded(
child: TextFormField(
controller: firstNameController,
decoration: InputDecoration(
labelText: 'firstName'.tr(),
),
),
),
Expanded(
child: TextFormField(
controller: middleNameController,
decoration: InputDecoration(
labelText: 'middleName'.tr(),
),
),
),
Expanded(
child: TextFormField(
controller: lastNameController,
decoration: InputDecoration(
labelText: 'lastName'.tr(),
),
),
),
],
),
const SizedBox(height: 16),
Row(
spacing: 16,
children: [
Expanded(
child: TextFormField(
controller: genderController,
decoration: InputDecoration(
labelText: 'gender'.tr(),
),
),
),
Expanded(
child: TextFormField(
controller: pronounsController,
decoration: InputDecoration(
labelText: 'pronouns'.tr(),
),
),
),
],
),
const SizedBox(height: 16),
Row(
spacing: 16,
children: [
Expanded(
child: TextFormField(
controller: locationController,
decoration: InputDecoration(
labelText: 'location'.tr(),
),
),
),
Expanded(
child: TextFormField(
controller: timeZoneController,
decoration: InputDecoration(
labelText: 'timeZone'.tr(),
),
),
),
],
),
const SizedBox(height: 16),
GestureDetector(
onTap: () async {
final date = await showDatePicker(
context: context,
initialDate: birthday.value ?? DateTime.now(),
firstDate: DateTime(1900),
lastDate: DateTime.now(),
);
if (date != null) {
birthday.value = date;
}
},
child: Container(
padding: const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor,
width: 1,
),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'birthday'.tr(),
style: TextStyle(
color: Theme.of(context).hintColor,
),
),
Text(
birthday.value != null
? DateFormat.yMMMd().format(
birthday.value!,
)
: 'Select a date'.tr(),
),
],
),
),
),
const SizedBox(height: 16),
Align(
alignment: Alignment.centerRight,
child: TextButton.icon(
onPressed:
submitting.value ? null : performAction,
label: Text('saveChanges').tr(),
icon: const Icon(Symbols.save),
),
),
],
).padding(all: 24),
),
],
),
),
); );
} }
} }

View File

@@ -1,14 +1,23 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:island/screens/developers/edit_bot.dart'; import 'package:island/screens/developers/edit_bot.dart';
class NewBotScreen extends StatelessWidget { class NewBotScreen extends StatelessWidget {
final String publisherName; final String publisherName;
final String projectId; final String projectId;
const NewBotScreen({super.key, required this.publisherName, required this.projectId}); final bool isModal;
const NewBotScreen({
super.key,
required this.publisherName,
required this.projectId,
this.isModal = false,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return EditBotScreen(publisherName: publisherName, projectId: projectId); return EditBotScreen(
publisherName: publisherName,
projectId: projectId,
isModal: isModal,
);
} }
} }

View File

@@ -58,8 +58,8 @@ class ProjectDetailView extends HookConsumerWidget {
leading: Container( leading: Container(
width: 256, width: 256,
padding: EdgeInsets.only( padding: EdgeInsets.only(
left: 16, left: 24,
right: 16, right: 24,
bottom: 8, bottom: 8,
top: 2, top: 2,
), ),