Initial implementation of the a user's media screen

accessibility-and-i18n
Hank Grabowski 2024-12-24 13:39:33 -05:00
rodzic 9913161a3a
commit 5e1a164d06
7 zmienionych plików z 527 dodań i 5 usunięć

Wyświetl plik

@ -6,6 +6,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../../globals.dart';
import '../../models/auth/profile.dart';
import '../../models/exec_error.dart';
import '../../models/networking/paged_response.dart';
import '../../models/networking/paging_data.dart';
import '../../models/timeline_entry.dart';
import '../../models/timeline_identifiers.dart';
@ -47,6 +48,40 @@ Future<Result<List<TimelineEntry>, ExecError>> timeline(
return result.execErrorCast();
}
@riverpod
Future<Result<PagedResponse<List<TimelineEntry>>, ExecError>>
userMediaTimelineClient(
Ref ref,
Profile profile,
String accountId, {
required PagingData page,
}) async {
final type =
TimelineIdentifiers(timeline: TimelineType.profile, auxData: accountId);
ref.read(timelineLoadingStatusProvider(profile, type));
Future.microtask(
() async =>
ref.read(timelineLoadingStatusProvider(profile, type).notifier).begin(),
);
final String timelineQPs = _typeToTimelineQueryParameters(type);
final baseUrl =
'https://${profile.serverName}/api/v1/accounts/$accountId/statuses';
final url =
'$baseUrl?only_media=true&${page.toQueryParameters()}&$timelineQPs';
final request = Uri.parse(url);
_logger.info(
() => 'Getting ${type.toHumanKey()} media only with paging data $page');
final result = await ref
.read(getApiListRequestProvider(profile, request).future)
.transformAsync((response) async {
final entries = await _timelineEntriesFromJson(ref, profile, response.data);
return response.map((_) => entries);
});
ref.read(timelineLoadingStatusProvider(profile, type).notifier).end();
return result.execErrorCast();
}
Future<List<TimelineEntry>> _timelineEntriesFromJson(
Ref ref,
Profile profile,

Wyświetl plik

@ -194,5 +194,177 @@ class _TimelineProviderElement extends AutoDisposeFutureProviderElement<
@override
PagingData get page => (origin as TimelineProvider).page;
}
String _$userMediaTimelineClientHash() =>
r'e4a21707ca98374e69ad289412f8accea32e367d';
/// See also [userMediaTimelineClient].
@ProviderFor(userMediaTimelineClient)
const userMediaTimelineClientProvider = UserMediaTimelineClientFamily();
/// See also [userMediaTimelineClient].
class UserMediaTimelineClientFamily extends Family<
AsyncValue<Result<PagedResponse<List<TimelineEntry>>, ExecError>>> {
/// See also [userMediaTimelineClient].
const UserMediaTimelineClientFamily();
/// See also [userMediaTimelineClient].
UserMediaTimelineClientProvider call(
Profile profile,
String accountId, {
required PagingData page,
}) {
return UserMediaTimelineClientProvider(
profile,
accountId,
page: page,
);
}
@override
UserMediaTimelineClientProvider getProviderOverride(
covariant UserMediaTimelineClientProvider provider,
) {
return call(
provider.profile,
provider.accountId,
page: provider.page,
);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'userMediaTimelineClientProvider';
}
/// See also [userMediaTimelineClient].
class UserMediaTimelineClientProvider extends AutoDisposeFutureProvider<
Result<PagedResponse<List<TimelineEntry>>, ExecError>> {
/// See also [userMediaTimelineClient].
UserMediaTimelineClientProvider(
Profile profile,
String accountId, {
required PagingData page,
}) : this._internal(
(ref) => userMediaTimelineClient(
ref as UserMediaTimelineClientRef,
profile,
accountId,
page: page,
),
from: userMediaTimelineClientProvider,
name: r'userMediaTimelineClientProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$userMediaTimelineClientHash,
dependencies: UserMediaTimelineClientFamily._dependencies,
allTransitiveDependencies:
UserMediaTimelineClientFamily._allTransitiveDependencies,
profile: profile,
accountId: accountId,
page: page,
);
UserMediaTimelineClientProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.profile,
required this.accountId,
required this.page,
}) : super.internal();
final Profile profile;
final String accountId;
final PagingData page;
@override
Override overrideWith(
FutureOr<Result<PagedResponse<List<TimelineEntry>>, ExecError>> Function(
UserMediaTimelineClientRef provider)
create,
) {
return ProviderOverride(
origin: this,
override: UserMediaTimelineClientProvider._internal(
(ref) => create(ref as UserMediaTimelineClientRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
profile: profile,
accountId: accountId,
page: page,
),
);
}
@override
AutoDisposeFutureProviderElement<
Result<PagedResponse<List<TimelineEntry>>, ExecError>> createElement() {
return _UserMediaTimelineClientProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is UserMediaTimelineClientProvider &&
other.profile == profile &&
other.accountId == accountId &&
other.page == page;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, profile.hashCode);
hash = _SystemHash.combine(hash, accountId.hashCode);
hash = _SystemHash.combine(hash, page.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin UserMediaTimelineClientRef on AutoDisposeFutureProviderRef<
Result<PagedResponse<List<TimelineEntry>>, ExecError>> {
/// The parameter `profile` of this provider.
Profile get profile;
/// The parameter `accountId` of this provider.
String get accountId;
/// The parameter `page` of this provider.
PagingData get page;
}
class _UserMediaTimelineClientProviderElement
extends AutoDisposeFutureProviderElement<
Result<PagedResponse<List<TimelineEntry>>, ExecError>>
with UserMediaTimelineClientRef {
_UserMediaTimelineClientProviderElement(super.provider);
@override
Profile get profile => (origin as UserMediaTimelineClientProvider).profile;
@override
String get accountId => (origin as UserMediaTimelineClientProvider).accountId;
@override
PagingData get page => (origin as UserMediaTimelineClientProvider).page;
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

Wyświetl plik

@ -1,10 +1,15 @@
import 'package:logging/logging.dart';
import 'package:result_monad/result_monad.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:stack_trace/stack_trace.dart';
import '../models/auth/profile.dart';
import '../models/exec_error.dart';
import '../models/media_attachment.dart';
import '../models/networking/paging_data.dart';
import '../models/timeline.dart';
import '../models/timeline_identifiers.dart';
import '../riverpod_controllers/networking/friendica_timelines_client_services.dart';
import 'entry_tree_item_services.dart';
part 'timeline_services.g.dart';
@ -147,3 +152,41 @@ class TimelineManager extends _$TimelineManager {
return hadItem;
}
}
@riverpod
class UserMediaTimeline extends _$UserMediaTimeline {
static const limit = 50;
var nextPage = const PagingData(limit: limit);
var media = <MediaAttachment>[];
@override
Future<Result<List<MediaAttachment>, ExecError>> build(
Profile profile, String accountId) async {
await updateTimeline(reset: true, withNotification: false);
return Result.ok(media);
}
Future<Result<List<MediaAttachment>, ExecError>> updateTimeline(
{bool reset = true, bool withNotification = true}) async {
if (reset) {
nextPage = const PagingData(limit: limit);
media.clear();
}
final result = await ref.watch(
userMediaTimelineClientProvider(profile, accountId, page: nextPage)
.future);
return result
.withResult((result) {
for (final entries in result.data) {
media.addAll(entries.mediaAttachments);
}
nextPage = result.next!;
if (withNotification) {
ref.notifyListeners();
}
})
.transform((_) => media)
.execErrorCast();
}
}

Wyświetl plik

@ -340,5 +340,176 @@ class _TimelineManagerProviderElement
TimelineIdentifiers get timelineId =>
(origin as TimelineManagerProvider).timelineId;
}
String _$userMediaTimelineHash() => r'f13ba417b1d5550392f973af9e86c099efd60491';
abstract class _$UserMediaTimeline extends BuildlessAutoDisposeAsyncNotifier<
Result<List<MediaAttachment>, ExecError>> {
late final Profile profile;
late final String accountId;
FutureOr<Result<List<MediaAttachment>, ExecError>> build(
Profile profile,
String accountId,
);
}
/// See also [UserMediaTimeline].
@ProviderFor(UserMediaTimeline)
const userMediaTimelineProvider = UserMediaTimelineFamily();
/// See also [UserMediaTimeline].
class UserMediaTimelineFamily
extends Family<AsyncValue<Result<List<MediaAttachment>, ExecError>>> {
/// See also [UserMediaTimeline].
const UserMediaTimelineFamily();
/// See also [UserMediaTimeline].
UserMediaTimelineProvider call(
Profile profile,
String accountId,
) {
return UserMediaTimelineProvider(
profile,
accountId,
);
}
@override
UserMediaTimelineProvider getProviderOverride(
covariant UserMediaTimelineProvider provider,
) {
return call(
provider.profile,
provider.accountId,
);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'userMediaTimelineProvider';
}
/// See also [UserMediaTimeline].
class UserMediaTimelineProvider extends AutoDisposeAsyncNotifierProviderImpl<
UserMediaTimeline, Result<List<MediaAttachment>, ExecError>> {
/// See also [UserMediaTimeline].
UserMediaTimelineProvider(
Profile profile,
String accountId,
) : this._internal(
() => UserMediaTimeline()
..profile = profile
..accountId = accountId,
from: userMediaTimelineProvider,
name: r'userMediaTimelineProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$userMediaTimelineHash,
dependencies: UserMediaTimelineFamily._dependencies,
allTransitiveDependencies:
UserMediaTimelineFamily._allTransitiveDependencies,
profile: profile,
accountId: accountId,
);
UserMediaTimelineProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.profile,
required this.accountId,
}) : super.internal();
final Profile profile;
final String accountId;
@override
FutureOr<Result<List<MediaAttachment>, ExecError>> runNotifierBuild(
covariant UserMediaTimeline notifier,
) {
return notifier.build(
profile,
accountId,
);
}
@override
Override overrideWith(UserMediaTimeline Function() create) {
return ProviderOverride(
origin: this,
override: UserMediaTimelineProvider._internal(
() => create()
..profile = profile
..accountId = accountId,
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
profile: profile,
accountId: accountId,
),
);
}
@override
AutoDisposeAsyncNotifierProviderElement<UserMediaTimeline,
Result<List<MediaAttachment>, ExecError>> createElement() {
return _UserMediaTimelineProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is UserMediaTimelineProvider &&
other.profile == profile &&
other.accountId == accountId;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, profile.hashCode);
hash = _SystemHash.combine(hash, accountId.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin UserMediaTimelineRef on AutoDisposeAsyncNotifierProviderRef<
Result<List<MediaAttachment>, ExecError>> {
/// The parameter `profile` of this provider.
Profile get profile;
/// The parameter `accountId` of this provider.
String get accountId;
}
class _UserMediaTimelineProviderElement
extends AutoDisposeAsyncNotifierProviderElement<UserMediaTimeline,
Result<List<MediaAttachment>, ExecError>> with UserMediaTimelineRef {
_UserMediaTimelineProviderElement(super.provider);
@override
Profile get profile => (origin as UserMediaTimelineProvider).profile;
@override
String get accountId => (origin as UserMediaTimelineProvider).accountId;
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

Wyświetl plik

@ -28,6 +28,7 @@ import 'screens/settings_screen.dart';
import 'screens/sign_in.dart';
import 'screens/splash.dart';
import 'screens/tags_timeline_screen.dart';
import 'screens/user_media_screen.dart';
import 'screens/user_posts_screen.dart';
import 'screens/user_profile_screen.dart';
@ -50,6 +51,7 @@ class ScreenPaths {
static String signup = '/signup';
static String userProfile = '/user_profile';
static String userPosts = '/user_posts';
static String userMedia = '/user_media';
static String likes = '/likes';
static String reshares = '/reshares';
static String explore = '/explore';
@ -271,6 +273,12 @@ final routes = [
builder: (context, state) =>
UserPostsScreen(userId: state.pathParameters['id']!),
),
GoRoute(
path: '/user_media/:id',
name: ScreenPaths.userMedia,
builder: (context, state) =>
UserMediaScreen(userId: state.pathParameters['id']!),
),
GoRoute(
path: '/likes/:id',
name: ScreenPaths.likes,

Wyświetl plik

@ -0,0 +1,85 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../controls/async_value_widget.dart';
import '../controls/error_message_widget.dart';
import '../controls/media_attachment_viewer_control.dart';
import '../controls/standard_appbar.dart';
import '../models/timeline_identifiers.dart';
import '../riverpod_controllers/account_services.dart';
import '../riverpod_controllers/networking/network_status_services.dart';
import '../riverpod_controllers/timeline_services.dart';
class UserMediaScreen extends ConsumerStatefulWidget {
final String userId;
const UserMediaScreen({super.key, required this.userId});
@override
ConsumerState<UserMediaScreen> createState() => _UserMediaScreenState();
}
class _UserMediaScreenState extends ConsumerState<UserMediaScreen> {
static const thumbnailDimension = 350.0;
@override
Widget build(BuildContext context) {
final profile = ref.watch(activeProfileProvider);
final timeline = TimelineIdentifiers.profile(widget.userId);
final loading = ref.watch(timelineLoadingStatusProvider(profile, timeline));
return Scaffold(
appBar: StandardAppBar.build(
context,
'User Posts',
actions: [],
),
body: Center(
child: Column(
children: [
if (loading) const LinearProgressIndicator(),
Expanded(
child: AsyncValueWidget(
ref.watch(userMediaTimelineProvider(profile, widget.userId)),
valueBuilder: (_, __, result) {
return result.fold(
onSuccess: (media) {
if (media.isEmpty) {
return const ErrorMessageWidget(
message: 'No media for this user');
}
return GridView.builder(
itemCount: media.length,
padding: const EdgeInsets.all(5.0),
gridDelegate:
const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: thumbnailDimension),
itemBuilder: (context, index) {
if (index == media.length - 1) {
ref
.read(userMediaTimelineProvider(
profile, widget.userId)
.notifier)
.updateTimeline(
reset: false,
withNotification: true,
);
}
return Padding(
padding: const EdgeInsets.all(2.0),
child: MediaAttachmentViewerControl(
attachments: [media[index]], index: 0),
);
});
},
onError: (error) =>
ErrorMessageWidget(message: error.message));
}),
),
],
),
),
);
}
}

Wyświetl plik

@ -81,11 +81,19 @@ class _UserProfileScreenState extends ConsumerState<UserProfileScreen> {
runSpacing: 10.0,
children: [
ElevatedButton(
onPressed: () => context.pushNamed(
ScreenPaths.userPosts,
pathParameters: {'id': connectionProfile.id},
),
child: const Text('Posts')),
onPressed: () => context.pushNamed(
ScreenPaths.userPosts,
pathParameters: {'id': connectionProfile.id},
),
child: const Text('Posts'),
),
ElevatedButton(
onPressed: () => context.pushNamed(
ScreenPaths.userMedia,
pathParameters: {'id': connectionProfile.id},
),
child: const Text('Media'),
),
ElevatedButton(
onPressed: () async =>
await openProfileExternal(context, connectionProfile),