kopia lustrzana https://gitlab.com/mysocialportal/relatica
Initial implementation of the a user's media screen
rodzic
9913161a3a
commit
5e1a164d06
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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));
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
|
|
Ładowanie…
Reference in New Issue