mirror of
https://gitlab.com/mysocialportal/relatica
synced 2024-10-18 18:13:31 +00:00
Add user profile and posts pages first cuts
This commit is contained in:
parent
ff3e938f70
commit
6f6fe79ac0
8 changed files with 225 additions and 74 deletions
|
@ -1,10 +1,11 @@
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_portal/models/timeline_entry.dart';
|
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
import '../../globals.dart';
|
import '../../globals.dart';
|
||||||
import '../../models/connection.dart';
|
import '../../models/connection.dart';
|
||||||
|
import '../../models/timeline_entry.dart';
|
||||||
|
import '../../routes.dart';
|
||||||
import '../../services/connections_manager.dart';
|
import '../../services/connections_manager.dart';
|
||||||
import '../../utils/dateutils.dart';
|
import '../../utils/dateutils.dart';
|
||||||
import '../../utils/url_opening_utils.dart';
|
import '../../utils/url_opening_utils.dart';
|
||||||
|
@ -17,6 +18,10 @@ class StatusHeaderControl extends StatelessWidget {
|
||||||
const StatusHeaderControl(
|
const StatusHeaderControl(
|
||||||
{super.key, required this.entry, required this.showActionBar});
|
{super.key, required this.entry, required this.showActionBar});
|
||||||
|
|
||||||
|
void goToProfile(BuildContext context) {
|
||||||
|
context.pushNamed(ScreenPaths.userProfile, params: {'id': entry.authorId});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final author = getIt<ConnectionsManager>()
|
final author = getIt<ConnectionsManager>()
|
||||||
|
@ -26,19 +31,25 @@ class StatusHeaderControl extends StatelessWidget {
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
CachedNetworkImage(
|
GestureDetector(
|
||||||
|
onTap: () => goToProfile(context),
|
||||||
|
child: CachedNetworkImage(
|
||||||
imageUrl: author.avatarUrl.toString(),
|
imageUrl: author.avatarUrl.toString(),
|
||||||
width: 32.0,
|
width: 32.0,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
const HorizontalPadding(),
|
const HorizontalPadding(),
|
||||||
Column(
|
Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
GestureDetector(
|
||||||
|
onTap: () => goToProfile(context),
|
||||||
|
child: Text(
|
||||||
author.name,
|
author.name,
|
||||||
style: Theme.of(context).textTheme.bodyText1,
|
style: Theme.of(context).textTheme.bodyText1,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
|
@ -60,7 +71,7 @@ class StatusHeaderControl extends StatelessWidget {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (showActionBar) ...[
|
if (showActionBar) ...[
|
||||||
Expanded(child: SizedBox()),
|
const Expanded(child: SizedBox()),
|
||||||
buildActionBar(context),
|
buildActionBar(context),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
@ -74,7 +85,7 @@ class StatusHeaderControl extends StatelessWidget {
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await _openAction(context);
|
await _openAction(context);
|
||||||
},
|
},
|
||||||
icon: Icon(Icons.launch),
|
icon: const Icon(Icons.launch),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
57
lib/controls/timeline/timeline_panel.dart
Normal file
57
lib/controls/timeline/timeline_panel.dart
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import '../../models/TimelineIdentifiers.dart';
|
||||||
|
import '../../services/timeline_manager.dart';
|
||||||
|
import 'status_control.dart';
|
||||||
|
|
||||||
|
class TimelinePanel extends StatelessWidget {
|
||||||
|
static final _logger = Logger('$TimelinePanel');
|
||||||
|
final TimelineIdentifiers timeline;
|
||||||
|
|
||||||
|
const TimelinePanel({super.key, required this.timeline});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final manager = context.watch<TimelineManager>();
|
||||||
|
final result = manager.getTimeline(timeline);
|
||||||
|
if (result.isFailure) {
|
||||||
|
return Center(child: Text('Error getting timeline: ${result.error}'));
|
||||||
|
}
|
||||||
|
final items = result.value;
|
||||||
|
print('items count = ${items.length}');
|
||||||
|
return RefreshIndicator(
|
||||||
|
onRefresh: () async {
|
||||||
|
await manager.updateTimeline(timeline, TimelineRefreshType.refresh);
|
||||||
|
},
|
||||||
|
child: ListView.separated(
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
if (index == 0) {
|
||||||
|
return TextButton(
|
||||||
|
onPressed: () async => await manager.updateTimeline(
|
||||||
|
timeline, TimelineRefreshType.loadNewer),
|
||||||
|
child: const Text('Load newer posts'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index == items.length + 1) {
|
||||||
|
return TextButton(
|
||||||
|
onPressed: () async => await manager.updateTimeline(
|
||||||
|
timeline, TimelineRefreshType.loadOlder),
|
||||||
|
child: const Text('Load older posts'));
|
||||||
|
}
|
||||||
|
final itemIndex = index - 1;
|
||||||
|
final item = items[itemIndex];
|
||||||
|
_logger.finest(
|
||||||
|
'Building item: $itemIndex: ${item.entry.toShortString()}');
|
||||||
|
return StatusControl(
|
||||||
|
originalItem: item,
|
||||||
|
showActionBar: true,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
separatorBuilder: (context, index) => Divider(),
|
||||||
|
itemCount: items.length + 2,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -91,7 +91,7 @@ class FriendicaClient {
|
||||||
int limit = 20}) async {
|
int limit = 20}) async {
|
||||||
final String timelinePath = _typeToTimelinePath(type);
|
final String timelinePath = _typeToTimelinePath(type);
|
||||||
final String timelineQPs = _typeToTimelineQueryParameters(type);
|
final String timelineQPs = _typeToTimelineQueryParameters(type);
|
||||||
final baseUrl = 'https://$serverName/api/v1/timelines/$timelinePath';
|
final baseUrl = 'https://$serverName/api/v1/$timelinePath';
|
||||||
var pagingData = 'limit=$limit';
|
var pagingData = 'limit=$limit';
|
||||||
if (maxId > 0) {
|
if (maxId > 0) {
|
||||||
pagingData = '$pagingData&max_id=$maxId';
|
pagingData = '$pagingData&max_id=$maxId';
|
||||||
|
@ -104,7 +104,7 @@ class FriendicaClient {
|
||||||
final url = '$baseUrl?exclude_replies=true&$pagingData&$timelineQPs';
|
final url = '$baseUrl?exclude_replies=true&$pagingData&$timelineQPs';
|
||||||
final request = Uri.parse(url);
|
final request = Uri.parse(url);
|
||||||
_logger.finest(() =>
|
_logger.finest(() =>
|
||||||
'Getting home timeline limit $limit sinceId: $sinceId maxId: $maxId : $url');
|
'Getting ${type.toHumanKey()} limit $limit sinceId: $sinceId maxId: $maxId : $url');
|
||||||
return (await _getApiListRequest(request).andThenSuccessAsync(
|
return (await _getApiListRequest(request).andThenSuccessAsync(
|
||||||
(postsJson) async => postsJson
|
(postsJson) async => postsJson
|
||||||
.map((json) => TimelineEntryMastodonExtensions.fromJson(json))
|
.map((json) => TimelineEntryMastodonExtensions.fromJson(json))
|
||||||
|
@ -295,13 +295,14 @@ class FriendicaClient {
|
||||||
String _typeToTimelinePath(TimelineIdentifiers type) {
|
String _typeToTimelinePath(TimelineIdentifiers type) {
|
||||||
switch (type.timeline) {
|
switch (type.timeline) {
|
||||||
case TimelineType.home:
|
case TimelineType.home:
|
||||||
return 'home';
|
return 'timelines/home';
|
||||||
case TimelineType.global:
|
case TimelineType.global:
|
||||||
return 'public';
|
return 'timelines/public';
|
||||||
case TimelineType.local:
|
case TimelineType.local:
|
||||||
return 'public';
|
return 'timelines/public';
|
||||||
case TimelineType.tag:
|
case TimelineType.tag:
|
||||||
case TimelineType.profile:
|
case TimelineType.profile:
|
||||||
|
return '/accounts/${type.auxData}/statuses';
|
||||||
case TimelineType.self:
|
case TimelineType.self:
|
||||||
throw UnimplementedError('These types are not supported yet');
|
throw UnimplementedError('These types are not supported yet');
|
||||||
}
|
}
|
||||||
|
@ -311,11 +312,11 @@ class FriendicaClient {
|
||||||
switch (type.timeline) {
|
switch (type.timeline) {
|
||||||
case TimelineType.home:
|
case TimelineType.home:
|
||||||
case TimelineType.global:
|
case TimelineType.global:
|
||||||
|
case TimelineType.profile:
|
||||||
return '';
|
return '';
|
||||||
case TimelineType.local:
|
case TimelineType.local:
|
||||||
return 'local=true';
|
return 'local=true';
|
||||||
case TimelineType.tag:
|
case TimelineType.tag:
|
||||||
case TimelineType.profile:
|
|
||||||
case TimelineType.self:
|
case TimelineType.self:
|
||||||
throw UnimplementedError('These types are not supported yet');
|
throw UnimplementedError('These types are not supported yet');
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,23 +17,29 @@ class TimelineIdentifiers {
|
||||||
return auxData.isEmpty ? timeline.name : '${timeline.name}_$auxData';
|
return auxData.isEmpty ? timeline.name : '${timeline.name}_$auxData';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
identical(this, other) ||
|
|
||||||
other is TimelineIdentifiers &&
|
|
||||||
runtimeType == other.runtimeType &&
|
|
||||||
timeline == other.timeline;
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => timeline.hashCode;
|
|
||||||
|
|
||||||
factory TimelineIdentifiers.home() =>
|
factory TimelineIdentifiers.home() =>
|
||||||
TimelineIdentifiers(timeline: TimelineType.home);
|
TimelineIdentifiers(timeline: TimelineType.home);
|
||||||
|
|
||||||
|
factory TimelineIdentifiers.profile(String profileId) => TimelineIdentifiers(
|
||||||
|
timeline: TimelineType.profile,
|
||||||
|
auxData: profileId,
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return auxData.isEmpty
|
return auxData.isEmpty
|
||||||
? 'TimelineIdentifiers{timeline: $timeline)'
|
? 'TimelineIdentifiers{timeline: $timeline)'
|
||||||
: 'TimelineIdentifiers{timeline: $timeline, auxData: $auxData}';
|
: 'TimelineIdentifiers{timeline: $timeline, auxData: $auxData}';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is TimelineIdentifiers &&
|
||||||
|
runtimeType == other.runtimeType &&
|
||||||
|
timeline == other.timeline &&
|
||||||
|
auxData == other.auxData;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => timeline.hashCode ^ auxData.hashCode;
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,8 @@ import 'screens/post_screen.dart';
|
||||||
import 'screens/profile_screen.dart';
|
import 'screens/profile_screen.dart';
|
||||||
import 'screens/sign_in.dart';
|
import 'screens/sign_in.dart';
|
||||||
import 'screens/splash.dart';
|
import 'screens/splash.dart';
|
||||||
|
import 'screens/user_posts_screen.dart';
|
||||||
|
import 'screens/user_profile_screen.dart';
|
||||||
import 'services/auth_service.dart';
|
import 'services/auth_service.dart';
|
||||||
|
|
||||||
class ScreenPaths {
|
class ScreenPaths {
|
||||||
|
@ -18,6 +20,8 @@ class ScreenPaths {
|
||||||
static String signin = '/signin';
|
static String signin = '/signin';
|
||||||
static String signup = '/signup';
|
static String signup = '/signup';
|
||||||
static String settings = '/settings';
|
static String settings = '/settings';
|
||||||
|
static String userProfile = '/user_profile';
|
||||||
|
static String userPosts = '/user_posts';
|
||||||
}
|
}
|
||||||
|
|
||||||
bool needAuthChangeInitialized = true;
|
bool needAuthChangeInitialized = true;
|
||||||
|
@ -130,4 +134,16 @@ final appRouter = GoRouter(
|
||||||
EditorScreen(id: state.params['id'] ?? 'Not Found'),
|
EditorScreen(id: state.params['id'] ?? 'Not Found'),
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
|
GoRoute(
|
||||||
|
path: '/user_posts/:id',
|
||||||
|
name: ScreenPaths.userPosts,
|
||||||
|
builder: (context, state) =>
|
||||||
|
UserPostsScreen(userId: state.params['id']!),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/user_profile/:id',
|
||||||
|
name: ScreenPaths.userProfile,
|
||||||
|
builder: (context, state) =>
|
||||||
|
UserProfileScreen(userId: state.params['id']!),
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
import '../controls/app_bottom_nav_bar.dart';
|
import '../controls/app_bottom_nav_bar.dart';
|
||||||
import '../controls/timeline/status_control.dart';
|
import '../controls/timeline/timeline_panel.dart';
|
||||||
import '../models/TimelineIdentifiers.dart';
|
import '../models/TimelineIdentifiers.dart';
|
||||||
import '../services/timeline_manager.dart';
|
|
||||||
|
|
||||||
class HomeScreen extends StatefulWidget {
|
class HomeScreen extends StatefulWidget {
|
||||||
const HomeScreen({super.key});
|
const HomeScreen({super.key});
|
||||||
|
@ -29,7 +27,6 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
_logger.finest('Build');
|
_logger.finest('Build');
|
||||||
final tm = context.watch<TimelineManager>();
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text('Home'),
|
title: Text('Home'),
|
||||||
|
@ -57,55 +54,17 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||||
currentType = value!;
|
currentType = value!;
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
Expanded(child: buildTimelineComponent(context, tm))
|
Expanded(
|
||||||
|
child: TimelinePanel(
|
||||||
|
timeline: TimelineIdentifiers(
|
||||||
|
timeline: currentType,
|
||||||
|
),
|
||||||
|
)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
bottomNavigationBar: AppBottomNavBar(
|
bottomNavigationBar: const AppBottomNavBar(
|
||||||
currentButton: NavBarButtons.home,
|
currentButton: NavBarButtons.home,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildTimelineComponent(BuildContext context, TimelineManager manager) {
|
|
||||||
final result =
|
|
||||||
manager.getTimeline(TimelineIdentifiers(timeline: currentType));
|
|
||||||
if (result.isFailure) {
|
|
||||||
return Center(child: Text('Error getting timeline: ${result.error}'));
|
|
||||||
}
|
|
||||||
final items = result.value;
|
|
||||||
print('items count = ${items.length}');
|
|
||||||
return RefreshIndicator(
|
|
||||||
onRefresh: () async {
|
|
||||||
await manager.updateTimeline(
|
|
||||||
TimelineIdentifiers.home(), TimelineRefreshType.refresh);
|
|
||||||
},
|
|
||||||
child: ListView.separated(
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
if (index == 0) {
|
|
||||||
return TextButton(
|
|
||||||
onPressed: () async => await manager.updateTimeline(
|
|
||||||
TimelineIdentifiers.home(), TimelineRefreshType.loadNewer),
|
|
||||||
child: const Text('Load newer posts'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index == items.length + 1) {
|
|
||||||
return TextButton(
|
|
||||||
onPressed: () async => await manager.updateTimeline(
|
|
||||||
TimelineIdentifiers.home(), TimelineRefreshType.loadOlder),
|
|
||||||
child: const Text('Load older posts'));
|
|
||||||
}
|
|
||||||
final itemIndex = index - 1;
|
|
||||||
final item = items[itemIndex];
|
|
||||||
_logger.finest(
|
|
||||||
'Building item: $itemIndex: ${item.entry.toShortString()}');
|
|
||||||
return StatusControl(
|
|
||||||
originalItem: item,
|
|
||||||
showActionBar: true,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
separatorBuilder: (context, index) => Divider(),
|
|
||||||
itemCount: items.length + 2,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
35
lib/screens/user_posts_screen.dart
Normal file
35
lib/screens/user_posts_screen.dart
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../controls/timeline/timeline_panel.dart';
|
||||||
|
import '../models/TimelineIdentifiers.dart';
|
||||||
|
import '../routes.dart';
|
||||||
|
|
||||||
|
class UserPostsScreen extends StatelessWidget {
|
||||||
|
final String userId;
|
||||||
|
|
||||||
|
const UserPostsScreen({super.key, required this.userId});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('User Posts'),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).popUntil((route) {
|
||||||
|
return route.settings.name == ScreenPaths.home;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.home),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: Center(
|
||||||
|
child: TimelinePanel(
|
||||||
|
timeline: TimelineIdentifiers.profile(userId),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
66
lib/screens/user_profile_screen.dart
Normal file
66
lib/screens/user_profile_screen.dart
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
import '../globals.dart';
|
||||||
|
import '../models/connection.dart';
|
||||||
|
import '../routes.dart';
|
||||||
|
import '../services/connections_manager.dart';
|
||||||
|
import '../utils/url_opening_utils.dart';
|
||||||
|
|
||||||
|
class UserProfileScreen extends StatelessWidget {
|
||||||
|
final String userId;
|
||||||
|
|
||||||
|
const UserProfileScreen({super.key, required this.userId});
|
||||||
|
|
||||||
|
Future<void> openProfileExternal(
|
||||||
|
BuildContext context,
|
||||||
|
Connection connection,
|
||||||
|
) async {
|
||||||
|
final openInBrowser =
|
||||||
|
await showYesNoDialog(context, 'Open profile in browser?');
|
||||||
|
if (openInBrowser == true) {
|
||||||
|
await openUrlStringInSystembrowser(
|
||||||
|
context, connection.profileUrl.toString(), 'Post');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final manager = getIt<ConnectionsManager>();
|
||||||
|
final body = manager.getById(userId).fold(onSuccess: (profile) {
|
||||||
|
return Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
CachedNetworkImage(imageUrl: profile.avatarUrl.toString()),
|
||||||
|
Text(profile.name),
|
||||||
|
Text(profile.status.toString()),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => context.pushNamed(
|
||||||
|
ScreenPaths.userPosts,
|
||||||
|
params: {'id': profile.id},
|
||||||
|
),
|
||||||
|
child: const Text('Posts')),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () async => await openProfileExternal(context, profile),
|
||||||
|
child: const Text('Open In Browser'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}, onError: (error) {
|
||||||
|
return Text('Error getting profile: $error');
|
||||||
|
});
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('Profile'),
|
||||||
|
),
|
||||||
|
body: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Center(
|
||||||
|
child: body,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue