mirror of
https://gitlab.com/mysocialportal/relatica
synced 2024-10-18 13:33:32 +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:flutter/material.dart';
|
||||
import 'package:flutter_portal/models/timeline_entry.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../globals.dart';
|
||||
import '../../models/connection.dart';
|
||||
import '../../models/timeline_entry.dart';
|
||||
import '../../routes.dart';
|
||||
import '../../services/connections_manager.dart';
|
||||
import '../../utils/dateutils.dart';
|
||||
import '../../utils/url_opening_utils.dart';
|
||||
|
@ -17,6 +18,10 @@ class StatusHeaderControl extends StatelessWidget {
|
|||
const StatusHeaderControl(
|
||||
{super.key, required this.entry, required this.showActionBar});
|
||||
|
||||
void goToProfile(BuildContext context) {
|
||||
context.pushNamed(ScreenPaths.userProfile, params: {'id': entry.authorId});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final author = getIt<ConnectionsManager>()
|
||||
|
@ -26,18 +31,24 @@ class StatusHeaderControl extends StatelessWidget {
|
|||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CachedNetworkImage(
|
||||
imageUrl: author.avatarUrl.toString(),
|
||||
width: 32.0,
|
||||
GestureDetector(
|
||||
onTap: () => goToProfile(context),
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: author.avatarUrl.toString(),
|
||||
width: 32.0,
|
||||
),
|
||||
),
|
||||
const HorizontalPadding(),
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
author.name,
|
||||
style: Theme.of(context).textTheme.bodyText1,
|
||||
GestureDetector(
|
||||
onTap: () => goToProfile(context),
|
||||
child: Text(
|
||||
author.name,
|
||||
style: Theme.of(context).textTheme.bodyText1,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
|
@ -60,7 +71,7 @@ class StatusHeaderControl extends StatelessWidget {
|
|||
],
|
||||
),
|
||||
if (showActionBar) ...[
|
||||
Expanded(child: SizedBox()),
|
||||
const Expanded(child: SizedBox()),
|
||||
buildActionBar(context),
|
||||
],
|
||||
],
|
||||
|
@ -74,7 +85,7 @@ class StatusHeaderControl extends StatelessWidget {
|
|||
onPressed: () async {
|
||||
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 {
|
||||
final String timelinePath = _typeToTimelinePath(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';
|
||||
if (maxId > 0) {
|
||||
pagingData = '$pagingData&max_id=$maxId';
|
||||
|
@ -104,7 +104,7 @@ class FriendicaClient {
|
|||
final url = '$baseUrl?exclude_replies=true&$pagingData&$timelineQPs';
|
||||
final request = Uri.parse(url);
|
||||
_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(
|
||||
(postsJson) async => postsJson
|
||||
.map((json) => TimelineEntryMastodonExtensions.fromJson(json))
|
||||
|
@ -295,13 +295,14 @@ class FriendicaClient {
|
|||
String _typeToTimelinePath(TimelineIdentifiers type) {
|
||||
switch (type.timeline) {
|
||||
case TimelineType.home:
|
||||
return 'home';
|
||||
return 'timelines/home';
|
||||
case TimelineType.global:
|
||||
return 'public';
|
||||
return 'timelines/public';
|
||||
case TimelineType.local:
|
||||
return 'public';
|
||||
return 'timelines/public';
|
||||
case TimelineType.tag:
|
||||
case TimelineType.profile:
|
||||
return '/accounts/${type.auxData}/statuses';
|
||||
case TimelineType.self:
|
||||
throw UnimplementedError('These types are not supported yet');
|
||||
}
|
||||
|
@ -311,11 +312,11 @@ class FriendicaClient {
|
|||
switch (type.timeline) {
|
||||
case TimelineType.home:
|
||||
case TimelineType.global:
|
||||
case TimelineType.profile:
|
||||
return '';
|
||||
case TimelineType.local:
|
||||
return 'local=true';
|
||||
case TimelineType.tag:
|
||||
case TimelineType.profile:
|
||||
case TimelineType.self:
|
||||
throw UnimplementedError('These types are not supported yet');
|
||||
}
|
||||
|
|
|
@ -17,23 +17,29 @@ class TimelineIdentifiers {
|
|||
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() =>
|
||||
TimelineIdentifiers(timeline: TimelineType.home);
|
||||
|
||||
factory TimelineIdentifiers.profile(String profileId) => TimelineIdentifiers(
|
||||
timeline: TimelineType.profile,
|
||||
auxData: profileId,
|
||||
);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return auxData.isEmpty
|
||||
? 'TimelineIdentifiers{timeline: $timeline)'
|
||||
: '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/sign_in.dart';
|
||||
import 'screens/splash.dart';
|
||||
import 'screens/user_posts_screen.dart';
|
||||
import 'screens/user_profile_screen.dart';
|
||||
import 'services/auth_service.dart';
|
||||
|
||||
class ScreenPaths {
|
||||
|
@ -18,6 +20,8 @@ class ScreenPaths {
|
|||
static String signin = '/signin';
|
||||
static String signup = '/signup';
|
||||
static String settings = '/settings';
|
||||
static String userProfile = '/user_profile';
|
||||
static String userPosts = '/user_posts';
|
||||
}
|
||||
|
||||
bool needAuthChangeInitialized = true;
|
||||
|
@ -130,4 +134,16 @@ final appRouter = GoRouter(
|
|||
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:go_router/go_router.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:provider/provider.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 '../services/timeline_manager.dart';
|
||||
|
||||
class HomeScreen extends StatefulWidget {
|
||||
const HomeScreen({super.key});
|
||||
|
@ -29,7 +27,6 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_logger.finest('Build');
|
||||
final tm = context.watch<TimelineManager>();
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Home'),
|
||||
|
@ -57,55 +54,17 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||
currentType = value!;
|
||||
});
|
||||
}),
|
||||
Expanded(child: buildTimelineComponent(context, tm))
|
||||
Expanded(
|
||||
child: TimelinePanel(
|
||||
timeline: TimelineIdentifiers(
|
||||
timeline: currentType,
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: AppBottomNavBar(
|
||||
bottomNavigationBar: const AppBottomNavBar(
|
||||
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